본문 바로가기
spring boot/인증,인가

JWT는 어떻게 인증 과정을 처리할 수 있을까?

by junjunjun 2024. 3. 24.
반응형

로그인 기능을 구현하기 위해 대게 JWT 방식을 많이 사용합니다. (특히 spring boot와 react인 경우)

 

저 또한 JWT를 이용하며 로그인 구현을 하던 도중에 어떻게 안에 내용물을 디코딩(jwt.io)할 수 있는 토큰이 인증 과정을 처리할 수 있는지 궁금증을 가지게 되었습니다.

 

따라서 이러한 JWT 인증 과정에 대해 알아보고자 합니다.

 

목차

  1. JWT 란
  2. JWT 동작 과정
  3. JWT는 어떻게 인증 과정을 처리하는가?
  4. JWT 문제점

 


1. JWT 란

JWT는 JSON Web Token의 약자로, 인터넷상에서 정보를 안전하게 전달하기 위한 표준 방법 중 하나입니다. JWT는 JSON으로 인코딩 된 데이터로 구성되어 있으며, 두 개체 사이에서 컴팩트하게 전달할 수 있는 자가 수신된 정보를 검증할 수 있습니다. 주로 사용자의 인증 및 권한 부여에 활용됩니다.

 

쉽게 말해 웹 상에서 데이터를 안정적으로 전송할 수 있게 해주는 Json형태의 토큰입니다.

 

JWT의 구조는 헤더, 페이로드, 서명으로 되어있습니다.

디코딩된 형태 인코딩된 형태
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

 

헤더(header)
{
  "alg": "HS256",
  "typ": "JWT"
}
--------------------------------
페이로드(payload)
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
--------------------------------
서명(signature)
HMACSHA256(
    base64UrlEncode(header) +
    "." +
    base64UrlEncode(payload),
    secrey-key
)

 

중간 중간 .을 기준으로 "헤더.페이로드.서명" 으로 되어있습니다.

 

헤더(Header)

  • 토큰의 타입("typ")과 사용된 해싱 알고리즘("alg") 정보를 가집니다.

페이로드(Payload)

  • 전달하고자 하는 정보를 가지며 이를 클레임(claim)이라고 부릅니다.
  • 주로 유저를 구별해 주기 위한 값을 넣어줍니다.
  • 정보가 암호화되지 않기 때문에 민감한 정보를 넣어주면 안 됩니다.

서명(Signature)

  • 헤더와 페이로드를 지정된 해싱 알고리즘과 비밀 키로 생성됩니다.
  • 서명을 통해 토큰이 변조되지 않음을 확인할 수 있습니다.

 


2. JWT 동작 과정

다음은 JWT를 이용한 로그인 동작 과정입니다.

  1. 유저가 로그인을 요청합니다.
  2. 서버는 해당 유저가 DB에 존재하는지 확인합니다.
  3. 유저에게 전달해 줄 토큰을 비밀키를 이용하여 발급해 줍니다.
  4. 토큰을 클라이언트에 응답해 줍니다.
  5. 토큰은 클라이언트(브라우저)의 쿠키에 보관해 줍니다.
  6. 이후 게시글 작성을 위해 쿠키에 있는 JWT와 함께 서버에 요청을 보냅니다.
  7. 서버는 해당 토큰이 유효한지 비밀키를 이용하여 검증해 줍니다.
  8. 검증이 성공적으로 끝나면 게시글을 생성한 뒤 클라이언트에 성공 응답을 보냅니다.

 

토큰 내부에 유저 id값이 있기 때문에 유저 정보를 얻기 위해 DB를 조회하지 않아도 되는 이점이 생깁니다.

 


3. JWT는 어떻게 인증 과정을 처리하는가?

이 전에 먼저 JWT가 어떤 방식으로 생성되는지 코드로 알아보겠습니다.

 

1. 헤더와 페이로드를 Base64URL로 인코딩한 뒤 결합해 줍니다.

String header = '{"alg":"none"}'
String payload = '{"sub":"1"}'

String encodedHeader = base64URLEncode( header.getBytes("UTF-8") )
String encodedPayload = base64URLEncode( payload.getBytes("UTF-8") )

String concatenated = encodedHeader + '.' + encodedPayload + '.'
// concatenated = eyJhbGciOiJub25lIn0.VGhlIHRydWUgc2lnbiBvZiBpbnRlbGxpZ2VuY2UgaXMgbm90IGtub3dsZWRnZSBidXQgaW1hZ2luYXRpb24u.

 

참고로 인코딩은 데이터를 다른 형식으로 변환하는 과정이며 디코딩을 할 경우 원래 데이터로 복구할 수 있습니다.

 

2. 해당 문자열(concatenated)에 비밀 키와 HMAC(해시 알고리즘)으로 서명을 만들어 줍니다.

SecretKey key = getMySecretKey() // 비밀 키는 본인이 직접 지정해 주는 값으로 잘 보관해야 됩니다.
byte[] signature = hmacSha256( concatenated, key )

 

HMAC는 해시 알고리즘입니다.

해시 알고리즘은 해시 함수를 사용하여 메시지와 비밀 키를 결합하여 고유한 서명을 생성합니다. 또한 해싱된 결과(서명)를 원본으로 변환하는 것은 불가능합니다.

 

3. 만들어진 서명을 Base64URL로 인코딩한 뒤 기존 문자열에 결합해 주면 JWT가 생성됩니다.

String jwt = concatenated + '.' + base64URLEncode( signature )

 

 

여기서 주목해야 할 부분은 서명을 만들어 주는 과정입니다.

 

서명은 해시 알고리즘을 이용하여 헤더와 페이로드 그리고 비밀 키로 고유한 값을 생성합니다.

만약 헤더나 페이로드가 변조되거나 또는 비밀 키가 다를 경우 서명은 완전히 다른 값으로 생성됩니다.

 

따라서 우리는 악성 유저가 토큰값을 변조한 경우(=헤더나 페이로드가 바뀐 경우)와 우리 서비스에서 제공한 토큰이 아닌 경우(=비밀키가 다른 경우)를 검증할 수 있게 됩니다.

 

 

아래는 JWT의 검증 과정을 수행하는 코드입니다.

 

1. JWT에서 헤더와 페이로드 서명을 분리해 줍니다..

String header = jwt.split("\\.")[0];
String payload = jwt.split("\\.")[1];
String signature = jwt.split("\\.")[2];

 

2. 해당 헤더와 페이로드를 가지고 새로운 서명을 만들어 줍니다.

String concatenated = header + '.' + payload + '.'
byte[] newSignature = hmacSha256( concatenated, key )

 

 

3. 기존 서명과 새로 만들어준 서명을 비교하여 검증을 해줍니다.

if (signature.equals(new String(newSignature)) {
	"검증 성공"
} else {
	"검증 실패"
}

 


4. JWT 문제점

위에서 언급한 대로 JWT는 변조에 대한 검증만 가능하므로 JWT 자체가 탈취당할 경우 해당 토큰을 사용하여 인증된 사용자로 위장하는 공격이 가능합니다.

이러한 문제를 해결하기 위해 가장 간단한 방법은 토큰의 유효 기간을 설정하는 것입니다. 토큰의 유효 기간이 지나면 해당 토큰을 더 이상 유효한 토큰이 아니게 됩니다. 

 

하지만 이 방법도 문제점을 가지고 있습니다. 유효 기간을 길게 설정하면 토큰이 탈취된 후 해당 기간 동안 아무런 보안 조치를 취할 수 없습니다. 그렇다고 유효 기간을 짧게 설정하면 유저가 자주 로그인해야 하므로 사용자 편의성이 떨어집니다.

이러한 JWT 관련된 보안 문제에 대해서는 다양한 해결 방안이 존재합니다.

그중 가장 많이 쓰이는 방식이 바로 Refresh 토큰입니다.

 

이에 대한 자세한 설명은 추후 다루도록 하겠습니다.

반응형

댓글