[node.js] JWT(json web token) 적용해보자

장일규·2022년 6월 12일
2
post-thumbnail

JWT

JSON(json-web-token)은 stateless authorization 즉, 상태를 저장하지 않는 인증 방법이다.

로그인을 구현하는 방식 중 서버에서 생성된 JWT-token을 사용자에 로컬 스토리지인 클라이언트단에 보관한다.

JWT는 .을 기준으로 header, payload, signature 3개에 영역으로 나뉜다.

Header

{
  "alg": "서명 시 사용하는 알고리즘",
  "typ": "타입"
}

헤더(Header)에는 서명 생성 암호화를 위해 어떤 알고리즘을 사용할지에 대한 정보가 들어가 있다.
일반적으로 많이 사용되는 HS256 암호화 알고리즘을 사용하여 암호화 하고 있다.

Payload

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

토큰에 담을 Claim정보를 포함하고 있다.

Claim은 Registered Claim, Public Claim, Private Claim 세 종류가 있다.

  • Registrered Claim : 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터들
    • iss: 토큰 발급자(issuer)
    • sub: 토큰 제목(subject)
    • aud: 토큰 대상자(audience)
    • exp: 토큰 만료 시간(expiration), NumericData 형식
    • nbf: 토큰 활성일(not before), 이 날이 지나기 전의 토큰은 활성화 안됌
    • iat: 토큰 발급 시간(issued at): 토큰 발급 이후의 경과 시간을 알 수 있음
    • jti: JWT 토큰 식별자(JWT ID), 중복 방지를 위해 사용하며 일회용 토큰(Access Token)등에 사용된다.

signature

시그니처(signature)에는 위의 헤더(header)와 페이로드(Paylaod)를 합친 문자열을 서명한 값이다. 서명은 헤더의 alg에 정의된 알고리즘과 secret key를 이용해 생성하고 Base64 URL-Safe로 인코딩한다.
secret key를 포함해서 암호화가 되어있다.

Step 0 — Express JS 앱을 설정

(a) 새 디렉토리를 생성하고 다음 노드 모듈을 설치합니다.

$ mkdir json-web-token
$ cd json-web-token

$ npm init -y

$ npm install express mongoose http-status-codes
$ npm install nodemon dotenv -D

(b) MongoDB연동을 위한 connect.js파일을 생성합니다.

const mongoose = require("mongoose");

const connectDB = (url) => {
  return mongoose.connect(url);
};

module.exports = connectDB;

(c) app.js파일을 생성합니다.

// basic
require('dotenv').config();

// db
const connectDB = require('./db/connect');

// server
const express = require('express');
const app = express();

// utility middleware
app.use(express.json());

// start server
const port = process.env.PORT || 3000;

const start = async () => {
    try {
        await connectDB(process.env.MONGO_URI);
        app.listen(port, () => {
            console.log(`server is listening on port ${port}`);
        });
    } catch (e) {
        console.log(`error has occured ${e}`);
    }
}

start();

(d) .env파일을 생성합니다.

MONGO_URI = mongodb+srv://ilkyu:<password>@blog-code.obkbq.mongodb.net/?retryWrites=true&w=majority
PORT = 3000

(e) nodemon을 사용하여 node서버를 실행할 수 있습니다.

nodemon app.js

Step 1 — 새로운 회원 등록

MongoDB와 mongoose를 사용하여 회원을 저장합니다.

NOTE:
MongoDB 무료로 사용하기(MongoDB Atlas)
스키마 정의하고 모듈화 하기

(a) app.js에 authRouter 미들웨어 선언

const authRouter = require("./routes/auth");
app.use("/api/v1/auth", authRouter);

(b) routes폴더 생성 후 auth.js파일 생성

const express = require("express");
const router = express.Router();

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

router.post("/register", registerUser);

module.exports = router;

(c) bcryptjs로 사용자 비밀번호를 암호화를 참고하여 비밀번호를 암호화하여 DB에 저장한다.

(c-1) bcryptjs라이브러리를 설치한다.

const bcrypt = require("bcryptjs");

(c-2) 회원 정보를 DB에 저장하기 전에 실행되는 함수이다.
회원에 평문 비밀번호를 bcrypt라이브러리로 암호화하여 저장한다.

UserSchema.pre('save', async function () {
    if (!this.isModified('password')) {
        return;
    }
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
});

(d) controllers폴더 생성 후 auth.js파일 생성

require('dotenv').config();

const { StatusCodes } =require('http-status-codes');
const User = require('../models/User');

const registerUser = async (req, res) => {
    const { gender, name, email, password } = req.body;
    if (!gender, !name, !email, !password) {
        throw new BadRequestError('please provide all information');
    }

    const user = await User.create(req.body);

    res.status(StatusCodes.CREATED)
    .json(user);
};

module.exports = { registerUser };

(e) Postman에서 "http://localhost:3000/api/v1/auth/register" 를 호출하여 위 코드를 테스트하고 본문으로 다음 내용을 전송할 수 있습니다.

Step 2 JWT 환경변수 세팅

(a) Encryption key에서 Security level에서 암호화값을 가져온다.

9y$B&E(H+MbQeThWmZq4t7w!z%C*F-J@NcRfUjXn2r5u8x/A?D(G+KbPdSgVkYp3

(b) .env환경변수에 저장

JWT토큰 인증을 위한 SECRET KEY가 필요함.

JWT_ACCESS_SECRET: 암호화값
JWT_ACCESS_VERSION: JWT버전
JWT_ACCESS_LIFETIME: JWT유효기간

JWT_ACCESS_SECRET=9y$B&E(H+MbQeThWmZq4t7w!z%C*F-J@NcRfUjXn2r5u8x/A?D(G+KbPdSgVkYp3
JWT_ACCESS_VERSION=0.0.0
JWT_ACCESS_LIFETIME=1h

(c) JWT발급을 위한 createAccessJWT메소드

jwt.sign(
Token에 넣을 데이터,
Secret Key,
JWT Options,
Callback Func
)

UserSchema.methods.createAccessJWT = function () {
  return jwt.sign(
    {
      // JWT 에 담을 내용
      userId: this._id,
      role: this.role,
    },
    // 암호화값
    `${process.env.JWT_ACCESS_SECRET}.${process.env.JWT_ACCESS_VERSION}`,
    // JWT 유효기간
    { expiresIn: process.env.JWT_ACCESS_LIFETIME }
  );
};

Step 3 사용자를 인증

Step1에서 사용자를 등록하였고, 이메일비밀번호로 로그인을 할 수 있는

"http://localhost:3000/api/v1/auth/loginUser" 엔드포인트를 만든 후 사용자 인증이 완료되면 액세스 토큰을 사용자에게 전달할 것이다.

(a) routes폴더에 auth.js파일에 다음과 같이 HTTP POST요청으로 /login엔드포인트로 설정한다.

router.post("/login", loginUser);

(b) controllers폴더에 auth.js파일에 /login 엔드포인트에 대한 콜백 핸들러가 실행된다.

const loginUser = async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) {
    throw new BadRequestError("email and password missing");
  }
  const user = await User.findOne({ email });
  const isPasswordCorrect = await user.comparePassword(password);
  if (!isPasswordCorrect) {
    throw new UnauthenticatedError("invalid credentials");
  }
  const accessToken = user.createAccessJWT();
  res.status(StatusCodes.OK).setHeader("accesstoken", accessToken).json(user);
};

(c) 다음과 같이 /auth/login 엔드포인트로 로그인 인증API를 테스트 할 수 있다.

클라이언트에 이메일/비밀번호가 올바른지 사용자 인증을 거친다.

사용자가 인증되면 서버에서는 클라이언트에게 "access token"값을 보낸다.

Postman을 보면 위와 같이 Headers부분에 JWT-token값이 저장된다.

JWT_ACCESS_SECRET을 사용하는 모든 서버에 대해 인증된 API 호출을 수행한다.

1시간 후 "access token"은 만료되고 사용자에게 "401" 메시지가 표시된다.

jwt사이트에서 accesstoken에 값을 Encoded에 입력하면 Output값인 Decoded에 HEADER, PAYLOAD에 정보는 다음과 같다.

Step 4 유효한 accessToken을 사용하여 인증된 자원에 접근하기 위한 회원 수정 API만들기

사용자가 로그인 후 자신의 회원 정보를 수정하기 위해서는 로그인 정보가 유지되어야 한다.
JWT token이 유효할 경우에 접근을 허용하는 /api/v1/users API를 만들어보자.

(a) 📁routes/users.js 파일을 생성

HTTP PATCH로 호출 시 updateUser가 실행된다.

// import
const updateUser = require("../controllers/users");

  router.route("/:id").patch(updateUser);

module.exports = router;

(b) 📁controllers/users.js 파일에 다음 로직 추가

const updateUser = async (req, res) => {
  const { id } = req.params;
  const { email, password, name, gender, introduction } = req.body;
  const user = await User.findByIdAndUpdate(
    id,
    {
      email,
      password,
      name,
      gender,
      introduction,
    },
    { new: true }
  );
  res.status(StatusCodes.OK).json(user);
};

Step 5 유효한 accessToken을 사용하여 인증된 자원에 접근하기

(a) app.js파일에 authentication미들웨어와 users라우터를 추가한다.

app.use()에서 두 번째 매개 변수로 추가된 auth메서드는 '미들웨어'로 처리되고, app.use()메서드가 실행되기 전에 auth메서드가 실행이 된다.

// authentication middlewares
const auth = require('./middleware/authentication');

const userRouter = require('./routes/users');
app.use('/api/v1/users', auth, userRouter);

(b) 📁middleware/authentication.js 파일을 생성하고, 로직은 다음과 같다.

require('dotenv').config();
const jwt = require('jsonwebtoken');
const { UnauthenciatedError } = require('../errors');

const auth = async (req, res, next) => {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        throw new UnauthenciatedError('invalid authentication');
    }
    
    // bearer 토큰 분리
    const access = authHeader.split(' ')[1]
    try {
        const payload = jwt.verify(
            access,
            `${process.env.JWT_ACCESS_SECRET}.${process.env.JWT_ACCESS_VERSION}`
        );

        req.user = {
            user: payload.userId,
            role: payload.role,
        };
        next();
    } catch (error) {
        // 버전이 변경되거나, 시크릿이 바뀐경우 
        throw new UnauthenticatedError(error.message);
    }
};

module.exports = auth;

req.headers.authorization에 결과는 다음과 같다.

JWT 또는 OAuth에 대한 토큰을 사용 시에는 Bearer인증 타입에 종류를 사용한다.


Ref

0개의 댓글