JWT, 웹 토큰 인증 방식

홍정민·2024년 5월 9일
0


홈페이지를 사용할 때, 대부분의 서비스를 이용하기 위해서는 우리는 로그인이라는 인증 과정을 거치게 된다.

같은 도메인에서 페이지를 이동하는데 어떻게 나의 정보가 계속 따라다닐 수 있을까? 해당 섹션에서 웹 토큰 기반 인증 방식을 이해하고 직접 구현해 보도록 하자

JWT(Json Web Token)란?

  • 사용자 인증을 위한 암호화된 JSON 토큰이다.
  • 정보를 JSON 개체로 안전하게 전송하기 위해 만들어진 표준이다.
  • JWT토큰은 공개 키 알고리즘을 사용하여 서명한다.

JWT 구조

JWT는 Header, Payload, Signature로 구성되고, .으로 구분한다.

토큰의 구조는 다음과 같다.
xxxxx.yyyyy.zzzzz

실제 예시:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.kK9JnTXZwzNo3BYNXJT57PGLnQk-Xyu7IBhRWFmc4C0

Header

헤더에는 다음과 같은 정보가 있다.

  • 토큰의 종류 (Jwt)
  • 알고리즘의 종류 (RSA, HMAC, SHA-256 등)

Encoded:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Decoded:


{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

페이로드에는 우리가 사용할 데이터를 담는 공간이며, Key-Value 형식의 JSON타입으로 정의되어 있다.

Encoded:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

Decoded:


{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

페이로드에는 Claim(토큰에 저장되는 메타데이터)이 존재한다.

Registered Claim Names:
미리 지정된 클레임이다.

  • iss (Issuer, 발급자)
  • sub (Subject, 제목)
  • aud (Audience, 수신자)
  • exp (Expiration Time, 만료시간)
  • iat (Issued At, 발급일)
  • nbf (Not Before, 활성 시간)
  • jti (JWT ID, 고유 식별자)

Public Claim Names:
공개 사용자 클레임으로, 직접 페이로드에 등록한 데이터이다.
예를 들어, email, name, age 등이 있을 수 있다.

Private Claim Names:
비공개 사용자 클레임이다.

Signature

서명(Signature)은 인코딩된 헤더페이로드, 비밀 키, 위에서 지정한 알고리즘로 암호화 하여 만든다.

Encoded:
kK9JnTXZwzNo3BYNXJT57PGLnQk-Xyu7IBhRWFmc4C0

Decoded:


HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret-key
)

JWT 작동 방식

토큰 인증 방식으로 Access TokenRefresh Token이 존재한다. JWT 를 통해 생성한 두 개의 토큰으로 인증 절차를 거치게 된다.

Access Token

암호화된 토큰이다. 해당 엑세스 토큰으로 사용자를 인증하는데 사용한다. 보안을 위해 짧은 유효기간(1시간, 1일, 7일 등)을 가진다.

Refresh Token

엑세스 토큰을 재생성 할때 사용되는 토큰이다. 액세스 토큰보다 만료시간이 더 길다. (7일, 한달 또는 그이상)

보안 향상

  • 토큰은 Authorization 헤더에 Bearer 스키마를 사용하여 서버에 전송한다.
    Authorization: Bearer <token>
    Bearer이라는 키워드는 토큰의 타입을 나타내는 것이다. 간단하게 인증에 사용되는 토큰을 표시한 것이라고 생각하자.

  • 토큰은 오랜 기간 보관해서는 안된다. 보안을 위해 짧은 시간 뒤, 파기하고 자주 재발급 받는 방식이다.


JavaScript JWT 사용법

패키지 설치
npm install jsonwebtoken

JWT 암호화

함수:
jwt.sign(payload, secretOrPrivateKey, [options, callback])

jwt.sign함수를 사용하여 웹 토큰을 발급시킨다. 함수의 인자에는 다음과 같은 데이터가 들어간다.

  • 1 args: 암호화시킬 데이터
  • 2 args: 본인의 비밀 키 (문자열 형태)
  • 3 args: option 또는 콜백함수 (option에서 클레임을 지정 가능)

사용법:

const ACCESS_SECRET_KEY = 'abmkcpwenf'

const accessToken = jwt.sign({
            name: 'jm'
        }, ACCESS_SECRET_KEY, {
            expiresIn: '1m',
            issuer: 'access issuer'
        })

JWT Decoded
함수:
jwt.verify(token, secretOrPublicKey, [options, callback])

jwt.verify 함수로 토큰을 복호화 시킨다. 함수의 인자에는 다음과 같은 데이터가 들어간다.

  • 1 args: 암호화된 토큰
  • 2 args: 암호화 시킬 때 사용했던 비밀 키
  • 3 args: 옵션 또는 콜백 함수

사용법:

const ACCESS_SECRET_KEY = 'abmkcpwenf'

const decoded = jwt.verify(token, ACCESS_SECRET_KEY, (err,decoded) => {
  console.log(decoded); // { name: 'jm' }
});

npm 문서에서 세 번째 인자 옵션과 콜백 함수에 대해 자세히 알아볼 수 있다.

JWT 실사용

전체코드는 nodejs의 express 환경에서 jwt를 구현한 것이다. 오로지 이해를 위해서 단순하게 작성되었기 때문에 Authorization 헤더, 미들 웨어 사용이 없는 코드이다. 이것에 대해서는 뒤에서 설명할 것이다.

JWT 사용 흐름

로그인 단계

  1. 사용자는 홈페이지에서 아이디와 패스워드를 입력한 후 서버에 전송
  2. 서버는 계정 DB에 계정이 있는지 확인
  3. 계정이 있다면 Access Token과 Refresh Token을 생성한다. ( jwt.sign )
  4. 생성한 토큰을 클라이언트의 쿠키에 전달한다.

인증 및 인가 단계

  1. 클라이언트는 토큰을 가지고 Access Token을 서버로 보낸다.
  2. 서버는 전달 받은 Access Token을 복호화 하여 서명이 올바른지, 권한이 유효한지 등을 확인하고 올바르다면 Payload를, 올바르지 않다면 error를 반환한다. ( jwt.verify )
  3. 동시에 서명이 올바르지만 Access Token이 만료된 상태라면 Refresh Token을 서버에 보내 Access Token을 재발급 받게 된다.
  4. Refresh Token도 만료된 상태라면 로그인을 다시 하는 등의 절차를 밟게 한다.

※ 참고
!) 인증은 빈번하게 발생하며 홈페이지 이동, 버튼과의 상호작용 등에 주로 일어난다.
!) 토큰을 전송할 때는 Authorization 헤더에 붙여 Bearer 키워드와 함께 전송한다.

Authorization Header

보안을 위해 토큰은 Authorization 헤더에 담아 전송한다. 해당 섹션에서 인증 헤더에 토큰을 담아 전송 및 수신 하는 코드를 알아 보자.

Usage in Client

import axios from 'axios'

await axios.post(serverUrl, body, {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

Usage in Server
Express 미들웨어 방식으로 동작한다면 next() 함수가 사용된다.

const auth = (req, res, next) => {
  const token = req.headers.authorization.split("Bearer ")[1];

  jwt.verify(token, "secret_key", (error) => {
    if (error) {
      res.status(401).json({ error: "Auth Error" });
    } else {
      next();
    }
  });
};

//route.js
app.get('/userInfo', auth, fetchUserInfo) //example

잘못된 정보나 새로운 정보는 댓글에 알려주시면 감사합니다. 😐

0개의 댓글