HTTPS - 토큰 인증 기반 실습(Server)

김도영·2022년 6월 8일
0

사전 작업

  1. 환경 변수 설정
    .env 파일 생성 후 데이터 베이스 관련 환경변수 설정

  2. https 사설 인증서 발급 및 디렉토리에 복사

// mkcert 설치
sudo apt install libnss3-tools
wget -O mkcert https://github.com/FiloSottile/mkcert/releases/download/v1.4.3/mkcert-v1.4.3-linux-amd64
chmod +x mkcert
sudo cp mkcert /usr/local/bin/

// 인증서 생성
mkcert -install
mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 ::1
// 127.0.0.1(IPv4), ::1(IPv6)

  1. jwt라이브러리를 사용해 토큰을 생성하는 방법
const jwt = require('jsonwebtoken');
const token = jwt.sign(토큰에 담을 값, ACCESS_SECRET, { 옵션1:, ...});

서버 구현

index.js

require("dotenv").config();
const fs = require("fs");
const https = require("https");
const cors = require("cors");
const cookieParser = require("cookie-parser");

const express = require("express");
const app = express();

const controllers = require("./controllers");

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(
  cors({
    origin: ["https://localhost:3000"],
    credentials: true,
    methods: ["GET", "POST", "OPTIONS"],
  })
);
app.use(cookieParser());
app.post("/login", controllers.login);
app.get("/accesstokenrequest", controllers.accessTokenRequest);
app.get("/refreshtokenrequest", controllers.refreshTokenRequest);

const HTTPS_PORT = process.env.HTTPS_PORT || 4000;

// 인증서 파일들이 존재하는 경우에만 https 프로토콜을 사용하는 서버를 실행
// 만약 인증서 파일이 존재하지 않는경우, http 프로토콜을 사용하는 서버를 실행
// 파일 존재여부를 확인하는 폴더는 서버 폴더의 package.json이 위치한 곳
let server;
if(fs.existsSync("./key.pem") && fs.existsSync("./cert.pem")){

  const privateKey = fs.readFileSync(__dirname + "/key.pem", "utf8");
  const certificate = fs.readFileSync(__dirname + "/cert.pem", "utf8");
  const credentials = { key: privateKey, cert: certificate };

  server = https.createServer(credentials, app);
  server.listen(HTTPS_PORT, () => console.log("server runnning"));

} else {
  server = app.listen(HTTPS_PORT)
}
module.exports = server;

controllers/users/login.js

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

module.exports = async(req, res) => {
  // 로그인에 필요한 정보를 HTTP 요청의 body에 담아 전송
  const userInfo = await Users.findOne({
    where: { userId: req.body.userId, password: req.body.password }
  })
// 요청한 userId, password와 일치하는 유저가 DB에 존재하는지 확인
  if (!userInfo) { // 일치하는 유저가 없을 경우
    res.status(400).send({ data: null, message: 'not authorized' });
  } else { // 일치하는 유저가 있을 경우 : 필요한 데이터를 payload에 담아 JWT token을 생성
    // access token, refresh token 두 가지를 생성
    // access token/refresh token은 각각 다른 비밀키를 생성
    // 환경 변수에 저장된 ACCESS_SECRET, REFRESH_SECRET 값을 사용
    const payload = {
      id: userInfo.id,
      userId: userInfo.userId,
      email: userInfo.userId,
      createdAt: userInfo.createdAt,
      updatedAt: userInfo.updatedAt
    }

    const accessToken = jwt.sign( payload, process.env.ACCESS_SECRET, { expiresIn: '30s' } );
    const refreshToken = jwt.sign( payload, process.env.REFRESH_SECRET, { expiresIn: '2d' } );
	
    // 생성된 refreshToken을 쿠키에 담는다.
    res.cookie('refreshToken', refreshToken)
    // 클라이언트에 json객체 반환
    res.status(200).json({ data: { 'accessToken': accessToken }, message: 'ok' } )
  }
};

controller/users/accesstokenrequest.js

해당 스크립트에 나온 헤더를 콘솔로 찍어보면 authorization에 토큰 값이 들어오는 것을 확인할 수 있다.

{
  host: '127.0.0.1:4000',
  'accept-encoding': 'gzip, deflate',
  authorization: 'Bearer token...',
}

JWT라이브러리를 사용해 토큰을 verify하는 방법

const jwt = require('jsonwebtoken');
const authorization = req.headers['authorization'];
const token = authorization.split(' ')[1];
const data = jwt.verify(token, process.env.ACCESS_SECRET);

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

module.exports = (req, res) => {
  // accesstokenrequest 구현에 필요한 로직을 작성
  // Authorizaton header에 담은 access token이 유효한지,
  // 서버가 가지고 있는 비밀키로 생성한 토큰이 맞는지 확인
  
  // Authorization header에 토큰이 담겨 있지 않거나, 해독할 수 없는 토큰이면 json객체 반환
  if (!req.headers.authorization) {
    res.status(400).send({ data: null, message: 'invalid access token' })
  } else { // 토큰 verify
    const authorization = req.headers['authorization'];
    const token = authorization.split(' ')[1];
    const data = jwt.verify(token, process.env.ACCESS_SECRET);

    // JWT를 해독하여 얻은 payload안의 값으로 DB에 유저를 조회
    if (!data) { // 일치하지 않은 경우
      res.status(400).send({ data: null, message: 'invalid access token' })
    } else { // 일치하는 경우, 필요한 데이터를 응답에 담아 반환
      res.status(200).json( {
        data: {
          userInfo: {
            id: data.id,
            userId: data.userId,
            email: data.email,
            createdAt: data.createdAt,
            updatedAt: data.updatedAt
          }
        }, message: 'ok'
      })
    }
  }
};

controller/users/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: 'not data' })
    } else { // 정보가 일치하면 payload에 새로운 데이터와 accesstoken을 생성해서 보내준다.
        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: '30s' } );

        res.status(200).json({ data: {'accessToken': accessToken, userInfo: payload}, message: 'ok' })
      }
  }
};
profile
Blockchain Developer

0개의 댓글