[Node.js]auth-token

이정원·2022년 8월 17일
0

초기설정

.env.example파일을 .env로 바꾼 후 패스워드를 넣어주고 ACCESS_SECRET, REFRESH_SECRET에 아무 값이나 넣어준다.
mkcert로 받은 인증서를 server-token폴더에 복사 해준다.
그리고 client의 정상적인 작동을 위해서는 client-token폴더에 있는 package.json파일의 script의 start부분의 fill_me_in부분을 인증서 경로로 바꿔줘야한다.

"scripts": {
    "start": "HTTPS=true SSL_CRT_FILE=../server-token/(인증서)SSL_KEY_FILE=../server-token/(인증서)react-scripts start",
    "build": "react-scripts build",
    "test": "./node_modules/.bin/mocha --require ./node_modules/@babel/register '__tests__/client.test.js'  --reporter  ./node_modules/mocha-multi-reporters --reporter-options  configFile=multi-reporters.json || true",
    "report": "./node_modules/.bin/mocha --require @babel/register '__tests__/client.test.js' --reporter @mochajs/json-file-reporter",
    "submit": "codestates"
  }

서버구현

req로부터 받은 userId, password가 DB에 존재하는지 확인한다.
일치하는 유저가 없을 경우:
로그인 요청을 거절한다.
일치하는 유저가 있을 경우:
필요한 데이터를 담은 두 종류의 JWT(access, refresh)를 생성한다.
생성한 JWT를 적절한 방법으로 반환한다.
access token은 클라이언트에서 react state로 다루고 있다.
refresh token은 클라이언트의 쿠키에서 다루고 있다.
라는 조건으로 코드를 작성해야했다.
구글링과 유어클래스에 나온 토큰 생성 방법을 통하여 코드를 작성하였다.
login.js

const { Users } = require("../../models");
const jwt = require('jsonwebtoken');

module.exports = async (req, res) => {
    // TODO: urclass의 가이드를 참고하여 POST /login 구현에 필요한 로직을 작성하세요.
    // HINT: auth-session과 마찬가지로, sequelize를 사용해 DB를 조회하세요.

    const data = await Users.findOne({
        where: { userId: req.body.userId, password: req.body.password },
    });
    if(!data){
        res.status(404).send({"data": null, "message": "not authorized"})
    }else{
        const payload = {
            id : data.id,
            userId : data.userId,
            email : data.email,
            createdAt : data.createdAt,
            updatedAt : data.updatedAt
        }
        const accessToken = jwt.sign(payload, process.env.ACCESS_SECRET, { expiresIn: '1d'});
        const refreshToken = jwt.sign(payload, process.env.REFRESH_SECRET, {expiresIn: '2d'});

        res.cookie('refreshToken', refreshToken)
        res.status(200).send({"data": {"accessToken": accessToken}, "message" : "ok"})

    }
};

authorization header에 담긴 토큰이 서버에서 생성한 JWT인지 확인합니다.
서버에서 생성한 유효한 토큰일 경우, 유효하지 않은 토큰일 경우 각각 다른 응답을 반환한다.
이 조건으로 accesstokenrequest.js파일을 작성하였다.
헤더를 콘솔로 찍어보면 authorization에 토큰 값이 들어온다 이거를 이용하여 코드를 작성할 수 있다.
accesstokenrequest.js

const { Users } = require('../../models');
const jwt = require('jsonwebtoken');

module.exports = (req, res) => {
  if(!req.headers.authorization){
    res.status(404).send({"data": null, "message" : "invalid access token"})
  }else{
    const authorization = req.headers['authorization'];
    const token = authorization.split(' ')[1];
    const data = jwt.verify(token, process.env.ACCESS_SECRET);
    
    if(!data){
      res.status(400).send({"data": null, "message": "invalid access token"})
    }else{
      res.status(200).send({
        data:{
          userInfo:{
            id: data.id,
            userId: data.userId,
            email: data.email,
            createdAt: data.createdAt,
            updatedAt: data.updatedAt
          }
        }, message: 'ok'
      })
    }
  }
  // TODO: urclass의 가이드를 참고하여 GET /accesstokenrequest 구현에 필요한 로직을 작성하세요.
};

요청에 담긴 refresh token이 유효하면 새로운 access token을 발급해 줌과 동시에 유저가 요청한 정보를 반환한다.
요청에 담긴 refresh token이 유효하지 않거나, 조작된 토큰일 경우 각각 다른 응답을 반환한다.
req.cookies로 리프레시 토큰이 들어오고 있으니 req.cookies.refreshToken을 이용해서 토큰값이 있는지 확인하고, 토큰이 유효한지, 해독한 payload가 db의 값과 같은지 확인하여 코드를 작성하면 된다.
refreshTokenRequest.js

const { Users } = require('../../models');
const jwt = require('jsonwebtoken');

module.exports = (req, res) => {
  const isRefreshToken = req.cookies.refreshToken
  if(!isRefreshToken){
    res.status(400).send({"data": null, "message": "refresh token not provided"});
  }else if(isRefreshToken === 'invalidtoken'){
    res.status(400).send({"data": null, "message": "invalid refresh token, please log in again"});

  }else{
    const data = jwt.verify(isRefreshToken, process.env.REFRESH_SECRET);
    if(!data){
      res.status(400).send({"data": null, "message": "refresh token has been tempered"});

    }else{
      const payload = {
        id: data.id,
        userId: data.userId,
        email: data.email,
        createdAt: data.createdAt,
        updatedAt: data.updatedAt
      }
      const accessToken = jwt.sign(payload, process.env.ACCESS_SECRET, {expiresIn: '1d'});

      res.status(200).send({
        "data": {
          accessToken:  accessToken,
          userInfo: payload
        }, message: 'ok'
      })
    }
  }
  // TODO: urclass의 가이드를 참고하여 GET /refreshtokenrequest 구현에 필요한 로직을 작성하세요.
};

클라이언트 구현

삼항연산자로 로그인 상태에 따라 마이페이지, 로그인페이지로 렌더링 해주고, 핸드러를 이용하여 로그인 상태 변동 함수와 토큰의 값을 변경시키는 함수를 작성한다.
app.js

import React, { Component } from "react";

import Login from "./components/Login";
import Mypage from "./components/Mypage";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isLogin: false,
      accessToken: "",
    };

    this.loginHandler = this.loginHandler.bind(this);
    this.issueAccessToken = this.issueAccessToken.bind(this);
  }

  
  loginHandler(data) {
    this.setState({isLogin: true})
  }

  issueAccessToken(token) {
    this.setState({accessToken: token})
  }

  render() {
    const { isLogin } = this.state;
    return (
      <div className='App'>
        {isLogin ? 
        <Mypage
        accessToken={this.state.accessToken}
        issueAccessToken={this.issueAccessToken}/> :
        <Login
        loginHandler={this.loginHandler}
        issueAccessToken={this.issueAccessToken}/>
       }
      </div>
    );
  }
}

export default App;

로그인 버튼 클릭시 localhost:4000주소로 POST 요청을 보내야 하고, state에 유저아이디와 패스워드를 저장해야 한다. POST요청에 유저아이디와 패스워드 정보를 포함시키려면 this.state를 통해 값을 저장하고, .then을 이용하여 로그인시 isLogin과 accessToken의 상태를 변경할 수 있다.
Login.js

import axios from "axios";
import React, { Component } from "react";

class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      userId: "",
      password: "",
    };
    this.inputHandler = this.inputHandler.bind(this);
    this.loginRequestHandler = this.loginRequestHandler.bind(this);
  }

  inputHandler(e) {
    this.setState({ [e.target.name]: e.target.value });
  }

  loginRequestHandler() {
    axios.post('https://localhost:4000/login', {
      userId: this.state.userId,
      password: this.state.password
    }).then(res => {
      this.props.issueAccessToken(res.data.data.accessToken)
      this.props.loginHandler(res.data.data.accessToken)
    })
   
   
  }

  render() {
    return (
      <div className='loginContainer'>
        <div className='inputField'>
          <div>Username</div>
          <input
            name='userId'
            onChange={(e) => this.inputHandler(e)}
            value={this.state.userId}
            type='text'
          />
        </div>
        <div className='inputField'>
          <div>Password</div>
          <input
            name='password'
            onChange={(e) => this.inputHandler(e)}
            value={this.state.password}
            type='password'
          />
        </div>
        <div className='loginBtnContainer'>
          <button onClick={this.loginRequestHandler} className='loginBtn'>
            JWT Login
          </button>
        </div>
      </div>
    );
  }
}

export default Login;

Login.js와 같이 axios를 이용하여 작성하되, 이번엔 GET요청을 보내야한다. GET요청을 보낼 주소를 지정하고 this.setState로 Mypage의 상태를 변경시켜 요청이 성공했을 때의 정보들을 업데이트 시킨다.
Mypage.js

import axios from "axios";
import React, { Component } from "react";

class Mypage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      userId: "",
      email: "",
      createdAt: "",
    };
    this.accessTokenRequest = this.accessTokenRequest.bind(this);
    this.refreshTokenRequest = this.refreshTokenRequest.bind(this);
  }

  accessTokenRequest() {
    axios.get('https://localhost:4000/accesstokenrequest', {headers: {authorization: `Bearer ${this.props.accessToken}`}})
    .then(res => {
      this.setState({
        ...res.data.data.userInfo
      })
    })
    /* 
    TODO: 상위 컴포넌트인 App에서 받은 props를 이용해 accessTokenRequest 메소드를 구현합니다.
    access token을 처리할 수 있는 api endpoint에 요청을 보내고, 받은 데이터로 Mypage 컴포넌트의 state (userId, email, createdAt)를 변경하세요
    초기 Mypage:
    state = { userId: "", email: "", createdAt: "" }
    accessTokenRequest 후 Mypage:
    state = { userId: "특정유저id", email: "특정유저email", created: "특정유저createdAt" }
    
    ** 주의사항 **
    App 컴포넌트에서 내려받은 accessToken props를 authorization header에 담아 요청을 보내야 합니다. 
    */
   
  }

  refreshTokenRequest() {
    axios.get('https://localhost:4000/refreshtokenrequest')
    .then(res => {
      this.setState({
        ...res.data.data.userInfo
      })
      this.props.issueAccessToken(res.data.data.accessToken)
    })
    /*
    TODO: 쿠키에 담겨져 있는 refreshToken을 이용하여 refreshTokenRequest 메소드를 구현합니다.
    refresh token을 처리할 수 있는 api endpoint에 요청을 보내고, 받은 데이터로 2가지를 구현합니다.
    1. Mypage 컴포넌트의 state(userId, email, createdAt)를 변경
    2. 상위 컴포넌트 App의 state에 accessToken을 받은 새 토큰으로 교환
    */
   
  }

  render() {
    const { userId, email, createdAt } = this.state;
    return (
      <div className='mypageContainer'>
        <div className='title'>Mypage</div>
        <hr />
        <br />
        <br />
        <div>
          안녕하세요. <span className='name'>{userId ? userId : "Guest"}</span>님! jwt 로그인이
          완료되었습니다.
        </div>
        <br />
        <br />
        <div className='item'>
          <span className='item'>나의 이메일: </span> {email}
        </div>
        <div className='item'>
          <span className='item'>나의 아이디 생성일: </span> {createdAt}
        </div>
        <br />
        <br />
        <div className='btnContainer'>
          <button className='tokenBtn red' onClick={this.accessTokenRequest}>
            access token request
          </button>
          <button className='tokenBtn navy' onClick={this.refreshTokenRequest}>
            refresh token request
          </button>
        </div>
      </div>
    );
  }
}

export default Mypage;
profile
Study.log

0개의 댓글