sprint - token_based Authentictaion

FeelSoo·2022년 5월 17일
0

CodeStates

목록 보기
37/43
post-thumbnail

https://jwt.io/ --- JSON WEB TOKEN 공식 문서




초기 셋팅

1. env 파일 생성 >> MYSQL DB 생성

2. cer.pem & key.pem 서버 디렉토리로 복사




server-token/controllers/users/accessTokenRequest.js

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

module.exports = (req, res) => {
   // TODO: urclass의 가이드를 참고하여 GET /accesstokenrequest 구현에 필요한 로직을 작성하세요.
   
   // authorization header에 담긴 토큰이 서버에서 생성한 JWT인지 확인합니다.
   
  //  서버에서 생성한 유효한 토큰일 경우, 유효하지 않은 토큰일 경우 각각 다른 응답을 반환합니다.
   

   // console.log("요청확인", req.headers);
  // console.log("토큰 있는가?", req.headers.authorization);

  //로그인 성공했으면 이미 쿠키에 토큰을 가지고 있을것
  //하지만 토큰이 없으면 쳐내기
  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); //올바른 토큰인지 검증
    // console.log(data)
    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'
      })
    }

  }
};



server-token/controllers/users/login.js

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

module.exports = async(req, res) => {
  // TODO: urclass의 가이드를 참고하여 POST /login 구현에 필요한 로직을 작성하세요.

  //request로부터 받은 userId, password와 일치하는 유저가 DB에 존재하는지 확인합니다.

//  일치하는 유저가 없을 경우:

/// 로그인 요청을 거절합니다.

/// 일치하는 유저가 있을 경우:

//  필요한 데이터를 담은 두 종류의 JWT(access, refresh)를 생성합니다.

//  생성한 JWT를 적절한 방법으로 반환합니다.

////  access token은 클라이언트에서 react state로 다루고 있습니다.

//  refresh token은 클라이언트의 쿠키에서 다루고 있습니다.


 //보안을 철저히 하기 위해 쿠키 셋팅을 해줌
  //쿠키 탈취 방지를 위해 쿠키 유효기간도 셋팅
  const cookieSetUp = {
    sameSite: "none",
    httpOnly: true,
    secure: true,
    maxAge: 5 * 60 * 1000,
  };


  const userInfo = await Users.findOne({
    where: { userId: req.body.userId, password: req.body.password },
  });
  if(!userInfo){
    res.status(404).send({ "data": null, "message": "not authorized" });
  }else{
    const payload = {
      id : userInfo.id ,
      userId : userInfo.userId,
      email: userInfo.email,
      createdAt: userInfo.createdAt,
      updatedAt: userInfo.updatedAt
    }

    const accessToken = jwt.sign(payload, process.env.ACCESS_SECRET, { expiresIn: '1d' }); // 유효기간 1일
    const refreshToken = jwt.sign(payload, process.env.REFRESH_SECRET, { expiresIn: '2d' }); // 2일

    res.cookie('refreshToken', refreshToken)  //쿠키에 리프레시 토큰, 데이터에 액세스 토큰 넣어서 보냄
    res.status(200).send({"data": { "accessToken": accessToken }, "message": "ok"})
  }
};



server-token/controllers/users/refreshTokenRequest.js

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


module.exports = (req, res) => {
  // TODO: urclass의 가이드를 참고하여 GET /refreshtokenrequest 구현에 필요한 로직을 작성하세요.

  //요청에 담긴 refresh token이 유효하다면 새로운 access token을 발급해줌과 동시에 유저가 요청한 정보를 반환합니다.
  
//요청에 담긴 refresh token이 유효하지 않거나, 조작된 토큰일 경우 각각 다른 응답을 반환합니다.

  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" });

 // 토큰이 유효한데 해독한 데이터가 db의 정보와 같은지 
  }else{
    const data = jwt.verify(isRefreshToken, process.env.REFRESH_SECRET);

    if(!data){
      res.status(400).send({ "data": null, "message": "refresh token has been tempered" })
      
 // 정보가 같다면 payload에 데이터와 
 // accessToken을 새로 생성해서 보내줘야 한다
    }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'
      })
    }

  }
};



client-token/package.json

..... 중략

"scripts": {
    "start": "HTTPS=true SSL_CRT_FILE='../server-token/cert.pem' SSL_KEY_FILE='../server-token/key.pem' 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"
  },
  
  ..... 중략



client-token/src/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'>
        {/* 
        TODO: isLogin 상태에 따라 Mypage 혹은 Login 컴포넌트를 렌더해야합니다.
        알맞은 컴포넌트를 렌더링하는것은 물론, 올바른 props전달하도록 작성하세요.
        */}

        {isLogin ? 
        <Mypage 
          accessToken={this.state.accessToken}
          issueAccessToken={this.issueAccessToken}
          /> : 
        <Login
          loginHandler={this.loginHandler}
          issueAccessToken={this.issueAccessToken}
        />}
      </div>
    );
  }
}

export default App;



client-token/src/components/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() {
    /*
    TODO: Login 컴포넌트가 가지고 있는 state를 이용해 로그인을 구현합니다.
    로그인을 담당하는 api endpoint에 요청을 보내고, 받은 데이터로 상위 컴포넌트 App의 state를 변경하세요.
    초기 App:
    state = { isLogin: false, accessToken: "" }
    로그인 요청 후 App:
    state = { isLogin: true, accessToken: 서버에_요청하여_받은_access_token }
    */
    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)
    } )
    //  data: { data: { accessToken: 'fakeAccessToken' } }
  }

  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;



client-token/src/components/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() {
    /* 
    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에 담아 요청을 보내야 합니다. 
    */
    axios.get('https://localhost:4000/accesstokenrequest', {headers: {authorization: `Bearer ${this.props.accessToken}`}})
    .then(res => {
      this.setState({
        ...res.data.data.userInfo
      })
    })
  }

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

0개의 댓글