서버에서 인증된 사용자가 인증을 유지해주는 방법은 보통 세션을 사용한다. 서버 세션을 사용하면 인증된 사용자는 매우 편리하게 서비스를 이용할 수 있고, 대부분 웹 애플리케이션 서버가 세션을 지원하기 때문에 편리하다.
하지만, 서버를 여러대 둘 경우(scale out),
같은 사용자가 서로 다른 도메인의 데이터를 요청할 경우, (SSO)에는 세션을 유지하기 위한 비용이 매우 커진다.
MSA는 각 서비스별로 각각의 서버를 가지고 있다. 한쪽에서 로그인을 하면 다른 쪽에서도 연속성 있게 로그인 정보를 유지해야 한다. 세션을 사용하는 경우 서버는 request 사용자에 대해 서버에 저장된 세션을 구분해서 인식해야 한다. 여러대의 서버라면 세션을 공유하거나 복사하거나 하는 전략이 필요하다.
따라서,. 서버에 사용자 정보를 저장하는 대신 클라이언트에 사용자 정보를 내려주고,서버는 토큰의 사용자 정보를 모든 요청에서 확인하고 서비스를 해주는 방식(SESSIONLESS)일 때, JWT 방식이 유용하다.
JWT 자바 라이브러리는 보통 auth0.com에서 만든 java-jwt 라이브러리 혹은 okta에서 만든 jjwt 라이브러리를 사용한다.
remember-me 쿠키 탈취를 방지하기 위해 별도의 서버에서 관리하던것처럼, Refresh Token또한 Client에만 맡기지 않고 서버에 관리하도록 해야 한다.
* JWT 스펙에서 지정한 claim
- iss : Issuer 토큰을 발행한 사람(단체,사이트)이 누구인지
- sub : Subject 무엇에 관한 토큰인지
- aud : Audience 누구를 대상으로 한 토큰인지
- exp : Expiration 토큰의 만료 시간은 언제인지
- nbf : Not Before 토큰이 언제부터 유효한지
- iat : Issued At 토큰 발행 시간
- jti : JWT ID : 토큰 자체의 아이디(일련번호?)
- 그 밖에 인증에 필요하거나 대상서버에서 필요로 하는 데이터
* 토큰에는 어떤 내용을 넣어야 할까?
- 일반적으로는 인증에 필요한 최소한의 데이터를 넣습니다.
- 비밀번호나 전화번호등을 넣는 것은 안전하지 않습니다.
- 이 토큰은 언제든 공개할 수 있는 정보를 넣는 것이 좋습니다.
- 왜냐하면 서버에서 인증된 키가 아니라도 언제든 서버는 이 토큰을 열어서 그 안에 어떤 Claim 이 있는지를 볼 수 있습니다.
* 토큰을 어떻게 관리할 것인가?
- 이론적으로는 토큰을 클라이언트가 관리하게 합니다.
- 하지만, 실제로 서버는 사용자 정보 캐싱이나 토큰의 유효성 평가, 혹은 refresh 토큰 정책을 위해 서버에 토큰을 관리하기도 합니다.
- 이 경우, 토큰과 사용자 정보를 관리하는 방법으로 다음과 같은 방법들을 사용하기도 합니다.
- redis, hazelcast
- db 저장
* 간단한 SSO 사이트 구현할 때
인증 서버를 두고, 해당 인증서버로부터 토큰의 유효성을 검증하다록 하는 방식이 이상적이지만, 토큰을 다루는 방식이 동일하다면, 한 서버에서 발행한 토큰으로 SSO와 같은 효과를 볼 수도 있습니다.
- 인증 서버와 리소스 서버를 분리한다.
- 인증토큰을 여러 리소스 서버에 재사용한다.
@DisplayName("1. jjwt 를 이용한 토큰 테스트")
@Test
void test_1(){
String okta_token = Jwts.builder().addClaims(
Map.of("name", "jongwon", "price", 3000)
).signWith(SignatureAlgorithm.HS256, "jongwon")
.compact();
System.out.println(okta_token);
printToken(okta_token);
Jws<Claims> tokenInfo = Jwts.parser().setSigningKey("jongwon").parseClaimsJws(okta_token);
System.out.println(tokenInfo);
}
@DisplayName("2. java-jwt 를 이용한 토큰 테스트")
@Test
void test_2() {
byte[] SEC_KEY = DatatypeConverter.parseBase64Binary("jongwon");
String oauth0_token = JWT.create().withClaim("name", "jongwon").withClaim("price", 3000)
.sign(Algorithm.HMAC256(SEC_KEY));
System.out.println(oauth0_token);
printToken(oauth0_token);
DecodedJWT verified = JWT.require(Algorithm.HMAC256(SEC_KEY)).build().verify(oauth0_token);
System.out.println(verified.getClaims());
Jws<Claims> tokenInfo = Jwts.parser().setSigningKey(SEC_KEY).parseClaimsJws(oauth0_token);
System.out.println(tokenInfo);
}
java-jwt의 jjwt와 다르게, sign하는 key값이 내부적으로 인코딩 된다. security를 관리하는 방법이 차이가 있기 때문에, 차이점을 이해하고 있어야 한다. 두 라이브러리를 공통적으로 처리하는 방법은? DatatypeConverter.parseBase64Binary("jongwon");로 인코딩을 해주면, 다른 JWT 라이브러리라도 동일한 방법으로 decode가 가능하다.
client가 어디있든지간에, 알고리즘과 key값이 동일하면 누구든지 인증이 가능하다.
@DisplayName("2. 만료시간 테스트")
@Test
void test_3() throws InterruptedException {
final Algorithm AL = Algorithm.HMAC256("jongwon");
String token = JWT.create()
.withSubject("a1234")
.withNotBefore(new Date(System.currentTimeMillis() + 1000))
.withExpiresAt(new Date(System.currentTimeMillis() + 3000))
.sign(Algorithm.HMAC256("jongwon"));
//Thread.sleep(2000);
try {
DecodedJWT verify = JWT.require(AL).build().verify(token);
System.out.println(verify.getClaims());
} catch(Exception ex) {
System.out.println("유효하지 않은 토큰입니다.");
DecodedJWT decode = JWT.decode(token);
System.out.println(decode.getClaims());
}
}
withNoBefore로 언제부터 토큰이 유효한지 설정할 수 있다. withExpireAt로 언제 토큰 유효가 마감되는지 설정할 수 있다. 현재 테스트에는 1초후부터 유효하며 3초후에 끝난다.
java-jwt와는 다르게, jjwt 토큰에서는 유효하지 않은 토큰의 decode 정보를 제공하지 않는다.
'Spring > Spring Security' 카테고리의 다른 글
Security AccessDecisionManager 구현 학습하기 (0) | 2024.05.15 |
---|---|
AuthToken 사용하기 (0) | 2021.10.26 |
임시권한 부여하기 (0) | 2021.10.23 |
@Secured 기반 권한체크 (0) | 2021.10.20 |
메서드 후처리 (0) | 2021.10.19 |