JWT란 ?
서버에서 인증된 사용자가 인증을 유지해주는 방법으로 보통은 세션을 사용한다 . 서버 세션을 사용하면 인증된 사용자는 편리하게 서비스를 이용할 수 있고 , 대부분의 웹애플리케이션 서버가 세션을 지원하기 때문에 편리하다 , 하지만 scale out 같이 사용자가 서로 다른 도메인의 데이터를 요청할 경우 sso에는 세션을 유지하기 위한 비용이 매우 커지게 된다 .
세션으로 서버에 사용자 정보를 저장하는 대신 클라이언트에 사용자 정보를 내려주고 , 서버는 토큰의 사용자 정보를 모든 요청에서 확인하고 서비스를 해주는 토큰 인증방식(sessionless)일 때 , JWT 토큰이 유용하게 사용된다.
jwt 토큰은 세션방식과는 다르게 모든 요청에 대해서 클라이언트에 저장된 토큰을 통해 인증하는 방식으로 인증이 이뤄지며
세션의 timeout 역할을 하는 expiredTime을 제공하고 , 사용자가 일정 시간동안 인증없이 로그인 할 수 있게 해주는 Remeber-Me 기능을 하는 Refresh Token을 사용하여 세션과 같은 방식으로 동작할 수 있게 해준다 .
토큰을 클라이언트에만 저장해놓으면 탈취됬을 때 위험하기 때문에 persistanceBasedRemberMeToken 을 이용했던 것 처럼
Refresh Token도 해당 토큰에 대해서 서버에서도 추적하고 관리할 수 있는 방식으로 구현하는게 좋다 .
JWT 토큰은 Header , Payload , Signature 세 부분으로 나눠지며
Header : 토큰의 유형 , 서명 알고리즘의 유형 (HMAC SHA256, RSA ) 정보가 들어간다.
Payload: claim(토큰 사용자 및 인증에 필요한 데이터들)이 들어간다 .
Signature : 토큰에 대한 서명값이 들어간다.
(간단하게만 설명한 것이니 jwt.io에서 확인하길 추천드립니다.)
사용자가 보호된 경로 또는 리소스에 액세스하려고 할 때마다 브라우저에서는 일반적으로 Bearer 스키마를 사용하여 Authorization 헤더에서 JWT를 보내야 한다
JWT RFC 스펙에서 정의한 claim에 들어가는 정보들은 아래와 같다
RFC7519 : https://datatracker.ietf.org/doc/html/rfc7519 (JWT RFC 스펙)
iss: Issuer 토큰을 방행한 사람이 누구인지
sub: Subject 무엇에 관한 토큰인지
aud: Audience 누구를 대상으로 한 토큰인지
nbf : Not Before 토큰이 언제부터 유효한지
exp: 토큰 유효시간
iat: Issued At 토큰이 발행된 시간
jti: JWT ID: 토큰 자체의 일련번호(아이디)
그 밖에 인증에 필요하거나 대상서버에서 필요로 하는 데이터 (array, object 다 넣을 수 있다 . )
토큰에는 어떤 내용을 넣어야 하나 ?
- 일반적으로 인증에 필요한 최소한의 데이터만 넣는다.
- 비밀번호나 전화번호등을 넣는 것은 안전하지 않다 .
- 이 토큰은 언제든 공개할 수 있는 정보를 넣는 것이 좋다 .
- 서버에서 인증된 키가 아니라도 언제든 서버는 이ㅗ큰을 열어서 그 안에 어떤 Claim이 있는지 볼 수 있다 .
토큰을 어떻게 관리할 것인가 ?
- 이론적으로는 토큰을 클라이언트가 관리하게 한다 .
- 하지만, 실제로 서버는 사용자 정보 캐싱이나 토큰의 유효성 평가 , 혹은 refresh 토큰 정책을 위해 서버에서 토큰을 관리하기도 한다.
- 이 경우 토큰과 사용자 정보를 관리하는 방법으로 redis , hazelcast을 사용하거나 DB에 저장하는 방식을 사용한다 .
JWT 토큰 테스트
프로젝트를 만들고 라이브러리를 주입받았다 .
dependencies {
implementation("com.auth0:java-jwt:3.16.0")
implementation("io.jsonwebtoken:jjwt:0.9.1")
}
첫번째 라이브러리는 OAuth0 에서 만든 라이브러리이고
두번째 라이브러리는 okta 라는 곳에서 만든 라이브러리이다 .
https://developer.okta.com/docs/guides/build-self-signed-jwt/java/overview/
위의 두 라이브러리를 활용하여 JWT 토큰을 테스트 해보자
jjwt 라이브러리(Okta)
먼저 jjwt 라이브러리를 사용해서 토큰을 만들었다 jjwt는 Builder패턴으로 토큰을 생성하고
claim()으로 클레임을 추가 ,
signWith()의 첫번째 파라미터로 서명에 사용될 알고리즘, 서명키값으로 주고
compact()를 통해 서명한다 .
. 으로 구분되고 순서대로 헤더.바디.헤더와 바디를 통해 서명한 값이 아래와 같이 출력된다.
okta_token = eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidWdvIiwicHJpY2UiOjEwMDB9.ZNow3wMb2LqdZHLw134HBw-BLYDnpxTfsiN9LMRhkrE
토큰을 디코드하여 프린트하는 메서드를 만들어서 출력해봤다.
헤더에는 알고리즘 정보 , 바디에는 전달했던 claim들이 담겨있다 .
OAuth0 JWT 라이브러리
다음으로는 OAuth0 JWT 라이브러리를 사용해봤다
마찬가지로 builder 패턴으로 생성되며
create()를 통해 토큰을 생성
withClaim()으로 claim 추가
sign()으로 서명하도록 되어있다.
출력값
다른점은 OAuth0 라이브러리에서는 헤더에 토큰의 타입이 들어가 있다
만약 OAuth0 라이브러리를 사용하는 서버에서 를 jjwt 라이브러리 통해 생성된 토큰을 받으면 어떻게 될까 ?
OAuth0 라이브러리에서 토큰을 열어보는 코드이다 require()를 사용하면 토큰을 verify하고 decode()를 사용하면 디코딩만 해준다.
결과는 아래와 같이 에러가 뜬다 이유는 jjwt 라이브러리 의 경우 서명키값을 넘길때 해싱해서 저장하지만
OAuth0 라이브러리는 해싱 없이 키값 그대로를 저장하기 떄문에 서명키를 확인할때 에러가 일어난다.
이를 해결하기 위해 아래와 같이 키값을 맞춰 줘야한다. DatatypeConverter는 jjwt 라이브러리에서 키값을 해싱할때 사용하는 클래스이다 . 해당 클래스로 키값을 해싱해주고 같은 키값으로 맞춰주면 다른 라이브러리라도 verify를 할 수 있다.
expriedTime으로 토큰의 유효기간을 지정할 수 있고 ,
토큰발행 후 NotBefore에 지정한 시간 이전에는 토큰을 사용할 수 없다 .
그 밖에도 여러가지 클레임을 지정할수 있는 메서드들이 있다.
'Spring Security' 카테고리의 다른 글
스프링 시큐리티 공부 19 - JWT 을 이용한 로그인 (3) | 2021.07.29 |
---|---|
스프링 시큐리티 공부 15 - WebExpressionVoter 와 PreinvocationAuthorizationAdviceVoter (0) | 2021.07.14 |
스프링시큐리티 공부 15 - 권한이란? (0) | 2021.07.08 |
스프링시큐리티 공부 14 - FilterSecurityInterceptor 와 ExceptionTranslationFilter (0) | 2021.07.07 |
스프링시큐리티 공부 13 - 세션관리 (세션 모니터링) (0) | 2021.07.06 |