Token

2023. 2. 8. 21:57프로그래밍/JavaScript

    목차

HTTP 는 상태 비저장 프로포콜

서버가 여러 요청 기간동안 각 사용자에 대한 정보/상태를 유지할 필요가 없음

각 요청에 대한 연결을 재설정하는 데 소요되는 시간을 최소화하기 위해서임

 

Token

1. 클라이언트에서 서버로 첫 요청을 보냄

2. 서버에서 사용자 정보를 포함하는 token 을 생성함

3. 서버에서 클라이언트로 token 을 보냄

4. 클라이언트에서 token 을 받아 저장함

5. 클라이언트에서 token 과 함께 서버로 요청을 보냄

6. 서버는 token 을 복호화하여 사용자 정보를 알 수 있음


JWT (JSON Web Token)

당사자간에 정보를 JSON 객체로 안전하게 전송하기 위한

컴팩트하고 독립적인 방식의 표준

디지털 서명이 되어있기 때문에 신뢰 가능

 

Header

토큰에 대한 메타 데이터를 포함함

ex. 타입, 해싱 알고리즘 SHA256, RSA ...

 

Payload

유저 정보, 만료 기간, 주제 ...

 

Verify Signature

토큰이 보낸 사람에 의해 서명되었으며

어떤 식으로든 변경되지 않았는지 확인하는 데 사용되는 서명

헤더 및 페이로드 세그먼트, 서명 알고리즘, 비밀/공개 키를 사용해 생성됨


인증 시스템 구현

package.json 파일 생성

npm init -y

 

패키지 설치

  • dotenv: 환경 변수 생성
  • jsonwebtoken: 토큰 생성
npm install dotenv express jsonwebtoken nodemon

 

.env 파일 생성

ACCESS_TOKEN_SECRET=superSecret

 

.env 파일 앱에 연결

require("dotenv").config();

 

express 코드 작성

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

app.use(express.json());

app.listen(4000, () => {
  console.log('listening on port 4000');
});

 

로그인 api 생성

const jwt = require('jsonwebtoken');

app.post('/login', (req, res) => {
  const username = req.body.username;
  const user = { name: username };

  const accessToken = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET);
  res.json({ accessToken });
});

 

Test

토큰 생성 확인

유효한 토큰으로 요청할 경우에만 데이터 응답하기 

verify 메서드: sign 메서드를 이용해 token을 생성할 때 넣어준 user 정보를 가져옴

next() 를 이용해 다음으로 이동 가능

const posts = [
  {
    username: 'John',
    title: 'Post 1',
  },
  {
    username: 'Han',
    title: 'Post 2',
  },
];

app.get('/posts', authMiddleware, (req, res) => {
  res.json(posts);
});

function authMiddleware(req, res, next) {
  const authHeader = req.headers('authorization');
  const token = authHeader && authHeader.split(' ')[1];
  if (token == null) return res.sendStatus(401);

  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    console.log(err);
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

 

Test

Authoriaztion 에서 Bearer Token 선택 후 토큰 값 넣어주기

토큰 값 없이 요청할 경우 Unauthorized 가 출력됨


RefreshToken

accessToken 처럼 jwt를 이용해 발급 가능

주로 accessToken 유효시간은 짧게, refreshToken 유효시간은 길게 설정함

accessToken 유효시간이 끝나면 refreshToken 을 이용해 새로운 accessToken 을 발급해 줌

 

accessToekn

리소스에 접근하기 위해 사용

 

refreshToken

기존에 클라이언트가 가지고 있던 accessToken 이 만료되었을 때 새로 발급받기 위해 사용

 

  • 클라이언트에서 로그인을 요청함
  • 서버에서 로그인 완료 시 accessToken, refreshToken 를 생성하여 클라이언트에 보냄
    회원 DB 에는 일반적으로 refreshToken 을 저장함

 

  • 클라이언트에서 refreshToken 을 안전하게 보관한 후,
    서버에 요청을 보낼 때 헤더에 accessToken 을 넣어 보냄
  • 서버에서 accessToken 을 검증함, 올바른 값일 경우 요청한 데이터를 보내줌

 

  • 클라이언트에서 accessToken 유효기간이 만료되었을 때 요청을 보냄
  • 서버에서는 accessToken 유효기간이 만료되었기 때문에 권한 없음 에러를 보냄

 

  • 클라이언트에서 refreshToken 을 서버로 보내며 accessToken 발급을 요청함
  • 서버에서 사용자가 보낸 refreshToken 과 DB에 저장된 refreshToken 을 비교함
    두 값이 동일하고 유효기간이 만료되지 않았을 경우 accessToken 을 새로 발급함

refreshToken 생성

app.post('/login', (req, res) => {
  const username = req.body.username;
  const user = { name: username };

  // accessToken 유효시간은 짧게
  const accessToken = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '30s' });
  
  // refreshToken 유효시간은 그보다 길게
  const refreshToken = jwt.sign(user, 'process.env.ACCESS_TOKEN_SECRET, { expiresIn: '1d' });

  refreshTokens.push(refreshToken);

  res.cookie('jwt', refreshToken, {
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000,
  });
  res.json({ accessToken });
});

refreshToken 은 쿠키에 저장하지만 주로 httpOnly 옵션을 설정해서

javascript 을 이용해 탈취하거나 조작할 수 없도록 만듦 (XSS 공격 방어)

 

accessToken 은 쿠키나 로컬스토리지보다 메모리에 저장해서 (변수)

서버가 다운되면 바로 없어지도록 하는게 좋음

 

응답 body 에 accessToken 이 들어있음

쿠키에 refreshToken 이 들어있음

 

accessToken 유효시간이 만료된 후 데이터 요청

Forbidden 이 출력됨


refreshToken 으로 accessToken 새로 생성

패키지 설치

npm install cookie-parser
app.use(cookieParser);

app.get('/refresh', (req, res) => {
  const cookies = req.cookies;
  if (!cookies?.jwt) return res.sendStatus(401);

  const refreshToken = cookies.jwt;
  if (!refreshTokens.includes(refreshToken)) {
    return res.sendStatus(403);
  }

  jwt.verify(refreshToken, 'superSecret', (err, user) => {
    if (err) return res.sendStatus(403);
    const accessToken = jwt.sign({ name: user.name }, 'superSecret', { expiresIn: '30s' });
    res.json({ accessToken });
  });
});

refreshToken 은 쿠키에 담겨있기 때문에 쿠키에서 가져옴

원래는 DB에서 refreshToken을 찾아야 하지만

현재는 메모리에 넣어놓은 refreshToken 을 찾음

refreshToken 을 verify 하여 유효할 경우 다시 accessToken 을 생성해서 json 으로 보내줌