Access Token과 Refresh Token을 발급해보자.

 

# yarn을 이용해 프로젝트를 초기화합니다.
yarn init -y

# express, jsonwebtoken, cookie-parser 패키지를 설치합니다.
yarn add express jsonwebtoken cookie-parser
  • package.json에 "type":"module" 추가

 

app.js ( 기본적인 틀 )

// app.js

import express from 'express';
import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser';

const app = express();
const PORT = 3019;

// 비밀 키는 외부에 노출되면 안되겠죠? 그렇기 때문에, .env 파일을 이용해 비밀 키를 관리해야함
const ACCESS_TOKEN_SECRET_KEY = `HangHae99`; // Access Token의 비밀 키를 정의한다.
const REFRESH_TOKEN_SECRET_KEY = `Sparta`; // Refresh Token의 비밀 키를 정의한다.

app.use(express.json());
app.use(cookieParser());

app.get('/', (req, res) => {
  return res.status(200).send('Hello Token!');
});

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

 

 

Refresh Token과 Access Token을 발급하는 API 만들어보기

POST /tokens 의 주소를 갖는 API 생성

// app.js

import express from 'express';
import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser';

const app = express();
const PORT = 3019;

// 비밀 키는 외부에 노출되면 안되겠죠? 그렇기 때문에, .env 파일을 이용해 비밀 키를 관리해야함
const ACCESS_TOKEN_SECRET_KEY = `HangHae99`; // Access Token의 비밀 키를 정의한다.
const REFRESH_TOKEN_SECRET_KEY = `Sparta`; // Refresh Token의 비밀 키를 정의한다.

app.use(express.json());
app.use(cookieParser());

app.get('/', (req, res) => {
    return res.status(200).send('Hello Token!');
});

let tokenStorage = {}; // Refresh Token을 저장할 객체

/** Access Token, Refresh Token 발급 API **/
app.post('/tokens', (req, res) => {
    // id 전달
    const { id } = req.body;

    // accessToken 발급
    const accessToken = createAccessToken(id);
    // refreshToken 발급
    const refreshToken = createRefreshToken(id);

    // Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다.
    tokenStorage[refreshToken] = {
        id: id, // 사용자에게 전달받은 ID를 저장합니다.
        ip: req.ip, // 사용자의 IP 정보를 저장합니다.

        // 사용자의 User Agent 정보를 저장합니다.
        // 특정 클라이언트가 어떤 방식으로 서버에 요청했는지를 알수 있는 정보
        // 예를 들어 firefox를 이용했는지, chrome을 이용했는지, 모바일로 chrome을 실행했는지 등        
        userAgent: req.headers['user-agent'], 
    };

    //console.log(tokenStorage);

    res.cookie('accessToken', accessToken); // Access Token을 Cookie에 전달한다.
    res.cookie('refreshToken', refreshToken); // Refresh Token을 Cookie에 전달한다.

    return res
        .status(200)
        .json({ message: 'Token이 정상적으로 발급되었습니다.' });
});

// Access Token을 생성하는 함수
function createAccessToken(id) {
    const accessToken = jwt.sign(
        { id: id }, // JWT 데이터
        ACCESS_TOKEN_SECRET_KEY, // Access Token의 비밀 키
        { expiresIn: '10s' }, // Access Token이 10초 뒤에 만료되도록 설정한다.
    );

    return accessToken;
}

// Refresh Token을 생성하는 함수
function createRefreshToken(id) {
    const refreshToken = jwt.sign(
        { id: id }, // JWT 데이터
        REFRESH_TOKEN_SECRET_KEY, // Refresh Token의 비밀 키
        { expiresIn: '7d' }, // Refresh Token이 7일 뒤에 만료되도록 설정한다.
    );

    return refreshToken;
}

app.listen(PORT, () => {
    console.log(PORT, '포트로 서버가 열렸어요!');
});

 


 

Refresh Token의 정보를 어디서 관리해야할까?

위 예시에서는 tokenStorage 라는 변수에서 관리했지만, 이는 인 메모리 방식을 사용하기 때문에 서버가 재시작 또는 종료될 경우 모든 정보가 사라지게 된다.

따라서 별도의 테이블에서 Refresh Token을 저장하고 관리해야한다. 

이렇게 하면, Refresh Token 검증 작업을 MySQL과 같은 데이터베이스를 조회함과 동시에 함께 처리할 수 있다.

 

Refresh Tokens Table

Name 타입 ( Type ) NULL default 비고
tokenId ( PK ) INTEGER NOT NULL AUTO_INCREMENT 토큰의 기본 키
userId ( FK ) INTEGER NOT NULL   사용자의 기본 키
token STRING NOT NULL   리프레시 토큰
expiresAt DATETIME NOT NULL   만료 날짜
createdAt DATETIME NOT NULL 현재 시간 생성 날짜
  • 이 외에도 ip 또는 user-Agent와 같은 정보를 추가할 수 있다.

 

Access Token 검증 API

서버에서 발급받은 Access Token을 검증하는 GET /tokens/validate API를 만들어보자.

 

// app. js

...

/** 엑세스 토큰 검증 API **/
app.get('/tokens/validate', (req, res) => {
  const accessToken = req.cookies.accessToken;

  if (!accessToken) {
    return res
      .status(400)
      .json({ errorMessage: 'Access Token이 존재하지 않습니다.' });
  }

  const payload = validateToken(accessToken, ACCESS_TOKEN_SECRET_KEY);
  if (!payload) {
    return res
      .status(401)
      .json({ errorMessage: 'Access Token이 유효하지 않습니다.' });
  }

  const { id } = payload;
  return res.json({
    message: `${id}의 Payload를 가진 Token이 성공적으로 인증되었습니다.`,
  });
});

// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
  try {
    const payload = jwt.verify(token, secretKey);
    return payload;
  } catch (error) {
    return null;
  }
}

 

ValidateToken 함수 추가 설명

ValidaeToken 함수는 제공된 토큰이 유효한지 여부를 검증하는 역할을 담당한다.

  • secretkey를 전달받아, 서버에서 검증할 비밀 키를 설정한다.
  • Access Token 이나 Refresh Token이 우리가 발급한 것인지 검증한다.
  • Access Token 이나 Refresh Token의 만료 여부를 검증한다.

사용자가 Cookie를 전달할 때, Access Token이 없다면 에러가 발생한다.

  • { "message": "Access Token이 존재하지 않습니다." }

사용자가 전달한 Access Token이 유효하지 않을 경우 에러가 발생한다.

  • { "message": "Access Token이 유효하지 않습니다." }

 

사용자가 Access Token을 이용해 API를 호출하면, 해당 토큰의 상태에 따라 적절한 응답 ( Response )이 반환되는 것을 확인 할 수 있다.

Access Token이 정상적으로 인증되면, 응답에서 페이로드 값을 확인할 수 있다.

 


 

Refresh Token으로 Access Token을 재발급하는 API 

서버에서 발급받은 Access Token을 검증하는 POST /tokens/refresh API를 만들어보자.

// app. js

...

/** 리프레시 토큰 검증 API **/
app.post('/tokens/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  if (!refreshToken)
    return res
      .status(400)
      .json({ errorMessage: 'Refresh Token이 존재하지 않습니다.' });

  const payload = validateToken(refreshToken, REFRESH_TOKEN_SECRET_KEY);
  if (!payload) {
    return res
      .status(401)
      .json({ errorMessage: 'Refresh Token이 유효하지 않습니다.' });
  }

  const userInfo = tokenStorage[refreshToken];
  if (!userInfo)
    return res.status(419).json({
      errorMessage: 'Refresh Token의 정보가 서버에 존재하지 않습니다.',
    });

  const newAccessToken = createAccessToken(userInfo.id);

  res.cookie('accessToken', newAccessToken);
  return res.json({ message: 'Access Token을 새롭게 발급하였습니다.' });
});

// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
  try {
    const payload = jwt.verify(token, secretKey);
    return payload;
  } catch (error) {
    return null;
  }
}

 

사용자가 Cookie를 전달할 때, Refresh Token이 없다면 에러가 발생한다.

  • { "message": "Refresh Token이 존재하지 않습니다." }

사용자가 전달한 Refresh Token이 유효하지 않을 경우 에러가 발생한다.

  • { "message": "Refresh Token이 유효하지 않습니다." }

Refresh Token이 유효하지만, 서버에 해당 토큰 정보가 없을 경우 에러가 발생한다.

  • { "message": "Refresh Token의 정보가 서버에 존재하지 않습니다." }

사용자가 Refresh Token을 이용해 API를 호출하면, 해당 토큰의 상태에 따라 적절한 응답 ( Response )이 반환되는 것을 확인할 수 있다.

만약, Refresh Token이 정상적으로 인증되면, 사용자에게 Access Token을 담은 Cookie를 전달하는 것을 확인할 수 있다.

 


 

 

Express.js를 이용한 게시판 프로젝트를 만들기 전에, 우선 app.js에 기본적인 토대를 만들고 간단한 API를 구현해보자.

 

모든 Router에서 Prisma를 사용할 수 있도록 utils/prisma/index.js 파일을 생성한다.

 

app.js 초기화

// src/app.js

import express from 'express';
import cookieParser from 'cookie-parser';

const app = express();
const PORT = 3018;

app.use(express.json());
app.use(cookieParser());

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

 

Primsa 초기화

// src/utils/prisma/index.js

import { PrismaClient } from '@prisma/client';

export const prisma = new PrismaClient({
  // Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해줍니다.
  log: ['query', 'info', 'warn', 'error'],

  // 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해줍니다.
  errorFormat: 'pretty',
}); // PrismaClient 인스턴스를 생성합니다.

 

프로젝트 구성

.
├── package.json
├── prisma
│   └── schema.prisma
├── src
│   ├── app.js
│   └── utils
│       └── prisma
│           └── index.js
└── yarn.lock

 

회원가입 API

회원가입 API 비즈니스 로직

  1. email, password, name, age, gender, profileImage를 body로 전달받는다.
  2. 동일한 email을 가진 사용자가 있는지 확인한다.
  3. Users 테이블에 email, password를 이용해 사용자를 생성한다.
  4. UserInfos 테이블에 name, age, gender, profileImage를 이용해 사용자 정보를 생성한다.

회원가입 API사용자사용자 정보가 1:1 관계를 가진 것을 바탕으로 비즈니스 로직이 구현된다.

사용자 정보사용자가 존재하지 않을 경우 생성될 수 없으므로, 전달받은 인자값을 이용해 사용자사용자 정보 순서대로 회원가입을 진행해야한다.

routes/users.router.js 파일을 생성하고, UsersRouter를 app.js 전역 미들웨어에 등록 후 회원가입 API를 구현해보자.

 

UsersRouter를 등록한 app.js

// app.js

import express from 'express';
import cookieParser from 'cookie-parser';
import UsersRouter from './routes/users.router.js';

const app = express();
const PORT = 3018;

app.use(express.json());
app.use(cookieParser());
app.use('/api', [UsersRouter]);

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

 

회원가입 API

// src/routes/users.router.js

import express from 'express';
import { prisma } from '../utils/prisma/index.js';

const router = express.Router();

/** 사용자 회원가입 API **/
router.post('/sign-up', async (req, res, next) => {
  const { email, password, name, age, gender, profileImage } = req.body;
  const isExistUser = await prisma.users.findFirst({
    where: {
      email,
    },
  });

  if (isExistUser) {
    return res.status(409).json({ message: '이미 존재하는 이메일입니다.' });
  }

  // Users 테이블에 사용자를 추가합니다.
  const user = await prisma.users.create({
    data: { email, password },
  });
  // UserInfos 테이블에 사용자 정보를 추가합니다.
  const userInfo = await prisma.userInfos.create({
    data: {
      userId: user.userId, // 생성한 유저의 userId를 바탕으로 사용자 정보를 생성합니다.
      name,
      age,
      gender: gender.toUpperCase(), // 성별을 대문자로 변환합니다.
      profileImage,
    },
  });

  return res.status(201).json({ message: '회원가입이 완료되었습니다.' });
});

export default router;

 

사용자만 생성되고, 사용자 정보 생성에 실패할 경우

현재는 Users 테이블에 사용자를 생성하고, UserInfos 테이블에 userId를 이용해 사용자의 정보를 저장한다.

만약, 사용자 정보 저장에 실패하게 될 경우 Users 테이블에는 사용자가 존재하지만, UserInfos 테이블의 사용자 정보는 저장되지 않는 상태로 남아있는다.

이런 문제를 해결하기 위해 MySQL과 같은 RDBMS에서는 트랜잭션 ( Transaction )이라는 개념이 도입됐는데, Prisma 또한 동일하게 사용할 수 있기 때문에 다음 글에서 작성해보도록 하겠다.

 

bcrypt ( 단방향 암호화, 복호화 )

2024.09.09 - [Javascript] - [Javascript] bcrypt ( 단방향 암호화, 복호화 )

 

[Javascript] bcrypt ( 단방향 암호화, 복호화 )

일반적으로, 사용자의 비밀번호를 데이터베이스에 저장할 때, 보안을 위해 비밀번호를 평문으로 저장하지 않고 암호화해 저장한다. bcrypt 모듈은 입력받은 데이터를 특정 암호화 알고리즘을 이

program-yam.tistory.com

 

회원가입 API Bcrypt 리팩토링

// src/routes/users.router.js

import bcrypt from 'bcrypt';

/** 사용자 회원가입 API 리팩토링**/
router.post('/sign-up', async (req, res, next) => {
  const { email, password, name, age, gender, profileImage } = req.body;
  const isExistUser = await prisma.users.findFirst({
    where: {
      email,
    },
  });

  if (isExistUser) {
    return res.status(409).json({ message: '이미 존재하는 이메일입니다.' });
  }

  // 사용자 비밀번호를 암호화합니다.
  const hashedPassword = await bcrypt.hash(password, 10);

  // Users 테이블에 사용자를 추가합니다.
  const user = await prisma.users.create({
    data: {
      email,
      password: hashedPassword, // 암호화된 비밀번호를 저장합니다.
    },
  });

  // UserInfos 테이블에 사용자 정보를 추가합니다.
  const userInfo = await prisma.userInfos.create({
    data: {
      userId: user.userId, // 생성한 유저의 userId를 바탕으로 사용자 정보를 생성합니다.
      name,
      age,
      gender: gender.toUpperCase(), // 성별을 대문자로 변환합니다.
      profileImage,
    },
  });

  return res.status(201).json({ message: '회원가입이 완료되었습니다.' });
});

 

회원가입 API 리팩토링이 완료되었으면, API Client를 이용해 해당 API를 호출해보자.

Users 테이블의 password 항목이 이전과 다르게 암호화된 값으로 작성되어 있을 것이다.

 

암호화된 사용자의 비밀번호


 

로그인 API

로그인 API는 bcrypt로 암호화된 사용자 비밀번호를 인증하는 방식이다.

클라이언트가 제공한 비밀번호와 데이터베이스에 저장된 암호화된 비밀번호를 검증해 구현해보자.

 

로그인 API 비즈니스 로직

  1. email, password를 body로 전달받는다.
  2. 전달 받은 email에 해당하는 사용자가 있는지 확인한다.
  3. 전달 받은 password와 데이터베이스의 저장된 password를 bcrypt를 이용해 검증한다.
  4. 로그인에 성공하면, 사용자에게 JWT를 발급한다.

로그인 API는 클라이언트가 전달한 정보를 바탕으로 사용자를 확인한다. 클라이언트로부터 받은 email과 password를 사용해 데이터베이스에 저장된 사용자를 검증하고, 검증에 성공하면 JWT를 담고있는 쿠키를 생성해 반환한다.

클라이언트는 로그인 이후 요청부터 쿠키를 함께 보내면, 이를 통해 서버에서는 해당 사용자를 식별하고, 인증 ( Authentication ), 인가 ( Authorization ) 과정을 거친다.

 

로그인 API

// src/routes/users.route.js

import jwt from 'jsonwebtoken';

/** 로그인 API **/
router.post('/sign-in', async (req, res, next) => {
  const { email, password } = req.body;
  const user = await prisma.users.findFirst({ where: { email } });

  if (!user)
    return res.status(401).json({ message: '존재하지 않는 이메일입니다.' });
  // 입력받은 사용자의 비밀번호와 데이터베이스에 저장된 비밀번호를 비교합니다.
  else if (!(await bcrypt.compare(password, user.password)))
    return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });

  // 로그인에 성공하면, 사용자의 userId를 바탕으로 토큰을 생성합니다.
  const token = jwt.sign(
    {
      userId: user.userId,
    },
    'custom-secret-key',
  );

  // authotization 쿠키에 Berer 토큰 형식으로 JWT를 저장합니다.
  res.cookie('authorization', `Bearer ${token}`);
  return res.status(200).json({ message: '로그인 성공' });
});

 

발급한 쿠키의 모습

 


사용자 인증 미들웨어

사용자 인증 미들웨어는 클라이언트로 부터 전달받은 쿠키를 검증하는 작업을 수행한다.

클라이언트가 제공한 쿠키에 담겨있는 JWT를 이용해 사용자를 조회하도록 구현해보자.

 

사용자 인증 미들웨어 비즈니스 로직

  1. 클라이언트로 부터 쿠키 ( Cookie )를 전달받는다.
  2. 쿠키 ( Cookie )가 Bearer 토큰 형식인지 확인한다.
  3. 서버에서 발급한 JWT가 맞는지 검증한다.
  4. JWT의 userId를 이용해 사용자를 조회한다.
  5. req.user에 조회된 사용자 정보를 할당한다.
  6. 다음 미들웨어를 실행한다.

사용자 인증 미들웨어는 클라이언트가 전달한 쿠키를 바탕으로 사용자를 검증한다. 이 과정에서 토큰이 만료되진 않았는지, 토큰의 형식은 일치하는지, 서버가 발급한 토큰이 맞는지 등 다양한 검증을 수행해 사용자의 권한을 확인한다.

이렇게, JWT를 통해 사용자를 인증하는 것을 인증 ( Authentication )이라고 부른다.

클라이언트는 인증 과정을 전부 통과하였을 때에 로그인된 사용자만 사용할 수 있는 게시글 작성 API, 사용자 정보 조회 API와 같은 권한이 필요한 API를 사용할 수 있게 된다. 이 사용자의 권한을 확인하는 과정을 인가 ( Authorization ) 과정이라고 부른다. ( 사용자 인증 미들웨어는 인증 기능만 제공한다. 이후 관리자 권한, 등급별 권한이 필요하면, 별도의 인가 과정을 추가해야한다 )

 

사용자 인증 미들웨어

// src/middlewares/auth.middleware.js

import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';

export default async function (req, res, next) {
  try {
    const { authorization } = req.cookies;
    if (!authorization) throw new Error('토큰이 존재하지 않습니다.');

	// Bearer이 tokenType에 들어가고, %20을 건너 뛴 후에 이후 내용이 token으로 들어간다.
    const [tokenType, token] = authorization.split(' ');

    if (tokenType !== 'Bearer')
      throw new Error('토큰 타입이 일치하지 않습니다.');

    const decodedToken = jwt.verify(token, 'custom-secret-key');
    const userId = decodedToken.userId;

    const user = await prisma.users.findFirst({
      where: { userId: +userId },
    });
    if (!user) {
      res.clearCookie('authorization');
      throw new Error('토큰 사용자가 존재하지 않습니다.');
    }

    // req.user에 사용자 정보를 저장합니다.
    req.user = user;

    next();
  } catch (error) {
    res.clearCookie('authorization');

    // 토큰이 만료되었거나, 조작되었을 때, 에러 메시지를 다르게 출력합니다.
    switch (error.name) {
      case 'TokenExpiredError':
        return res.status(401).json({ message: '토큰이 만료되었습니다.' });
      case 'JsonWebTokenError':
        return res.status(401).json({ message: '토큰이 조작되었습니다.' });
      default:
        return res
          .status(401)
          .json({ message: error.message ?? '비정상적인 요청입니다.' });
    }
  }
}

 

사용자 정보 조회 API

사용자 정보 조회 API는 일반적으로 다른 사용자의 정보를 조회하기 위해 사용한다.

 

사용자 정보 조회 API 비즈니스 로직

  1. 클라이언트가 로그인된 사용자인지 검증한다.
  2. 사용자를 조회할 때, 1:1 관계를 맺고 있는 Users와 UserInfos 테이블을 조회한다.
  3. 조회한 사용자의 상세한 정보를 클라이언트에게 반환한다.

사용자 정보 조회 API는 단순히 Users 테이블 하나만 조회를 하는 것이 아니라, UserInfos 테이블을 함께 조회한다.

그렇기 때문에, 각각의 테이블을 1번씩 조회하게 되어 총 2번의 조회를 하게되는 문제가 발생한다.

이런 문제를 해결하기 위해 Prisma에서는 중첩 select 라는 문법을 제공한다.

 

사용자 정보 조회 API

// src/routes/users.route.js

/** 사용자 조회 API **/
router.get('/users', authMiddleware, async (req, res, next) => {
  const { userId } = req.user;

  const user = await prisma.users.findFirst({
    where: { userId: +userId },
    select: {
      userId: true,
      email: true,
      createdAt: true,
      updatedAt: true,
      userInfos: {
        // 1:1 관계를 맺고있는 UserInfos 테이블을 조회합니다.
        select: {
          name: true,
          age: true,
          gender: true,
          profileImage: true,
        },
      },
    },
  });

  return res.status(200).json({ data: user });
});

 

Prisma 에서 연관 관계 테이블을 조회하는 방법

Prisma의 select 문법은 Query 메서드의 옵션으로 사용할 수 있다.

await prisma.users.findFirst({
  where: { userId: +userId },
  select: {
    userId: true,
    email: true,
    createdAt: true,
    updatedAt: true,
    userInfos: {
      // 1:1 관계를 맺고있는 UserInfos 테이블을 조회합니다.
      select: {
        name: true,
        age: true,
        gender: true,
        profileImage: true,
      },
    },
  },
});
  • select 내에 또다른 select가 있는데, 이를 중첩 select 문법이라 한다.
  • 중첩 select 는 SQL의 JOIN과 동일한 역할을 수행한다.
  • 중첩 select 문법을 사용하기 위해서는 Prisma model에서 @relation() 과 같이 관계가 설정이 되어야 한다.
  • @relation() 으로 Prisma는 현재 모델에서 참조하는 외래 키를 인식하고, SQL을 생성할 수 있게 된다.
  • 현재 테이블과 연관된 테이블의 모든 컬럼을 조회하고 싶으면, include 문법으로도 조회할 수 있다.

중첩 select 문법과 include 문법에 대한 자세한 정보 : https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries

 

Relation queries (Concepts) | Prisma Documentation

Prisma Client provides convenient queries for working with relations, such as a fluent API, nested writes (transactions), nested reads and relation filters.

www.prisma.io

 

JWT 사용방법

Node.js에서 제일 사용량이 많은 jsonwebtoken 라이브러리를 사용하자

 

# yarn을 이용해 프로젝트를 초기화합니다.
yarn init -y

# jsonwebtoken, express 라이브러리를 설치합니다.
yarn add jsonwebtoken express
  • yarn을 이용해 생성된 package.json 파일에서 type을 module로 변경하자 ( 프로젝트에서 ES6 모듈을 사용할수 있도록 설정해주는 옵션 )

JSON 데이터를 암호화

  • jsonwebtoken 라이브러리의 sign 메서드를 사용해 JWT를 생성한다.
import jwt from 'jsonwebtoken';

const token = jwt.sign({ myPayloadData: 1234 }, 'mysecretkey');
console.log(token); // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJteVBheWxvYWREYXRhIjoxMjM0LCJpYXQiOjE2OTA4NzM4ODV9.YUmYY9aef9HOO8f2d6Umh2gtWRXJjDkzjm5FPhsQEA0
  • sign 메서드는 첫 번째 인자로 Payload 데이터를, 두 번째 인자로 비밀 키를 받아 JWT를 생성한다. ( 여기서, Payload는 문자열 뿐만 아니라, 객체도 할당할 수 있다 )

JWT로 만든 토큰 모습

 

JSON 데이터를 복호화

  • jsonwebtoken 라이브러리의 decode 메서드를 사용해  JWT를 복호화한다.
import jwt from 'jsonwebtoken';

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJteVBheWxvYWREYXRhIjoxMjM0LCJpYXQiOjE2OTA4NzM4ODV9.YUmYY9aef9HOO8f2d6Umh2gtWRXJjDkzjm5FPhsQEA0";
const decodedValue = jwt.decode(token);

console.log(decodedValue); // { myPayloadData: 1234, iat: 1690873885 }
  • JWT는 누구나 복호화가 가능하다. 하지만 검증을 통해 변조가 되지 않은 데이터인지 알 수 있다.

복호화 된 JWT 출력

 

복호화가 아닌, 변조되지 않은 데이터 검증해보기

  • jsonwebtoken 라이브러리의 verify 메서드를 사용해 JWT를 검증한다.
import jwt from 'jsonwebtoken';

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJteVBheWxvYWREYXRhIjoxMjM0LCJpYXQiOjE2OTA4NzM4ODV9.YUmYY9aef9HOO8f2d6Umh2gtWRXJjDkzjm5FPhsQEA0";
const decodedValueByVerify = jwt.verify(token, "mysecretkey");

console.log(decodedValueByVerify); // { myPayloadData: 1234, iat: 1690873885 }
  • JWT가 변조되지 않았고, 올바른 비밀 키로 서명되었는지를 검증하는 중
  • 검증에 실패하면 에러가 발생한다.

잘못된 비밀키를 입력해서 데이터를 검증해보기

  • 잘못된 비밀 키를 이용해 JWT를 검증하면, 에러가 발생한다.
import jwt from 'jsonwebtoken';

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJteVBheWxvYWREYXRhIjoxMjM0LCJpYXQiOjE2OTA4NzM4ODV9.YUmYY9aef9HOO8f2d6Umh2gtWRXJjDkzjm5FPhsQEA0";
const decodedValueByVerify = jwt.verify(token, "secretkey");

console.log(decodedValueByVerify);

// JsonWebTokenError: invalid signature
  • mysecretkey를 secretkey로 변경해서 실행해보자 ( 그러면 아래와 같은 에러가 발생하게되어, 검증에 실패했다는 것을 확인할 수 있다. )

Invalid Signature 에러 발생


JWT를 써야 하는 이유

JWT는 두 가지 중요한 특징을 가지고 있다.

  • JWT가 인증 서버에서 발급되었는지 위변조 여부를 확인할 수 있다.
  • 누구든지 JWT 내부에 들어있는 정보를 확인할 수 있다. ( 복호화 )

만약 JWT를 사용하지 않은 상태에서 사용자 로그인을 구현하려고 하면 어떻게 될까?

 

JWT를 적용하지 않은 로그인 API를 만들어보자

import express from 'express';
const app = express();

app.post('/login', function (req, res, next) {
  const user = { // 사용자 정보
    userId: 203, // 사용자의 고유 아이디 (Primary key)
    email: "archepro84@gmail.com", // 사용자의 이메일
    name: "이용우", // 사용자의 이름
  }

  res.cookie('sparta', user);  // sparta 라는 이름을 가진 쿠키에 user 객체를 할당합니다.
  return res.status(200).end();
});

app.listen(5002, () => {
  console.log(5002, "번호로 서버가 켜졌어요!");
});
  • 사용자의 정보가 sparta 이름을 가진 쿠키에 할당된다.
  • 쿠키의 속성값이나 만료 시간을 클라이언트가 언제든지 수정할 수 있다
  • 쿠키의 위변조 여부를 확인 할 수 없다.

 

JWT를 적용한 로그인 API를 만들어보자

import express from 'express';
import JWT from 'jsonwebtoken';

const app = express();

app.post('/login', (req, res) => {
  // 사용자 정보
  const user = {
    userId: 203,
    email: 'archepro84@gmail.com',
    name: '이용우',
  };

  // 사용자 정보를 JWT로 생성
  const userJWT = JWT.sign(
    user, // user 변수의 데이터를 payload에 할당
    'secretOrPrivateKey', // JWT의 비밀키를 secretOrPrivateKey라는 문자열로 할당
    { expiresIn: '1h' }, // JWT의 인증 만료시간을 1시간으로 설정
  );

  // userJWT 변수를 sparta 라는 이름을 가진 쿠키에 Bearer 토큰 형식으로 할당
  res.cookie('sparta', `Bearer ${userJWT}`);
  return res.status(200).end();
});

app.listen(5002, () => {
  console.log(5002, '번호로 서버가 켜졌어요!');
});
  • 사용자의 정보를 Payload에 저장한 JWT를 sparta 이름을 가진 쿠키에 할당된다.
  • JWT를 생성할 때 위변조 여부를 확인할 수 있는 비밀키를 사용했다.
  • 쿠키의 만료시간과 별개로 JWT의 만료시간을 설정했다.

 

암호화 된 데이터의 사용 방법

보통 암호화 된 데이터는 클라이언트 ( 브라우저 ) 가 전달받아 다양한 수단 ( 쿠키, 로컬스토리지 등 )을 통해 저장해 API 서버에 요청을 할 때 서버가 요구하는 HTTP 인증 양식에 맞게 보내주어 인증을 시도한다.

 

+ Recent posts