토큰으로 인증
세션의 장단점
- 장점
- JSESSIONID는 유의미한 값이 아니라 서버에서 세션(사용자) 정보를 찾는 Key로만 활용
- 탈취되었다고 해서 개인정보가 탈취된건 아니다.
- 세션 하이재킹 공겨을 당할 수도 있기 때문에 절대적으로 안전 한 것은 아니다.
- JSESSIONID는 유의미한 값이 아니라 서버에서 세션(사용자) 정보를 찾는 Key로만 활용
- 단점
- 서버에 세션(사용자) 정보를 저장할 공간이 필요하다.
- 서비스를 이용하는 사용자가 많다면 저장할 공간도 더 많이 필요하다.
- 분산 서버에서는 세션을 공유하는데 어려움이 있다.
- 세션 서버를 따로 두거나 공유하는 방법으로 해결
- 서버에 세션(사용자) 정보를 저장할 공간이 필요하다.
- 세션의 단점을 해결하기 위해서 토큰으로 인증하는 방법을 사용
- 유저가 로그인을 하면 서버에서는 토큰을 생성한 뒤에 저장하지 않고 (stateless) 토큰값을 내려준다.
- 토큰 값을 유저가 게시글 조회 요청을 할 때 함께 보내고 서버2(서버1이여도 상관없음)에서 이 토큰을 의미있
는 값으로 해석 그리고 그 값을 토대로 유저를 인증 - 세션방식에서의 JSESSIONID은 key로만 활용될 수 있는 의미없는 값
- 토큰은 유저를 설명할 수 있는 데이터를(ex username)포함
토큰 인증의 장단점
- 장점
- 세션관리를 할필요가 없어 별도의 저장소가 필요하지 않다.
- 서버 분산&클러스터 환경과 같은 확장성에 좋다.
- 단점
- 한 번 제공된 토큰은 회수가 어렵다.
- 세션의 경우에는 서버에서 세션을 삭제해버리면 브라우저의 JSESSIONID는 무용지물이 된다.
- 토큰은 세션을 저장하지 않기 때문에 한번 제공된 토큰을 회수할수없다.
- 그래서 보통 토큰의 유효기간을 짧게 한다.
- 토큰에는 유저의 정보가 있기 때문에 상대적으로 안정성이 우려된다.
- 민감정보를 토큰에 포함시키면 안된다. (ex 패스워드, 개인정보)
- 한 번 제공된 토큰은 회수가 어렵다.
JWT의 구조
- HEADER.PAYLOAD.SIGNATURE로 이루어져 있다.
- HEADER
- JWT를 검증하는데 필요한 정보를 가진 객체
- Signature에 사용한 암호화 알고리즘이 무엇인지, Key의 ID가 무엇인지 정보를 담고 있다.
- 암호화 되어 있지 않아서 정보를 Json으로 변환해서 UTF-8로 인코딩한 뒤 Base64 URL-Safe로 인코딩한 값이 들어가있다.
{
"alg": "HS512",
"kid": "key1"
}
// 결과
eyJhbGciOiJIUzUxMiIsImtpZCI6ImtleTEifQ
- PAYLOAD
- 실질적으로 인증에 필요한 데이터를 저장
- Claim : 데이터의 각각 필드들
- 인증할 때 payload에 있는 username을 가져와서 유저 정보를 조회할 때 사용해야하기 때문에 대부분의 경우에 Claim에 username을 포함
- 토큰 발행시간(iat)와 토큰 만료시간(exp)를 포함
- 원하는 Claim을 얼마든지 추가할수 있지만 민감정보는 포함시켜서는 안된다.
- Payload 역시 Header와 마찬가지로 암호화되지 않아서 Json으로 바꾼뒤 UTF8로 인코딩하고 Base64로 변경
{
"sub": "user",
"iat": 1629626974,
"exp": 1629627574
}
// 결과
eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjI5NjI2OTc0LCJleHAiOjE2Mjk2Mjc1NzR9
- SIGNATURE
- Header와 Payload는 암호화하지 않았고 json -> utf8 -> base64로 변환한 데이터
- 두개의 데이터만 있다면 토큰에 대한 진위여부 판단은 전혀 이루어질수 없다.
- JWT의 구조에서 가장 마지막에 있는 Signature는 토큰 자체의 진위여부를 판단하는 용도로 사용
- Header와 Payload를 합친뒤 비밀키로 Hash를 생성하여 암호화
// Header와 Payload를 합친뒤 비밀키로 Hash를 생성하여 암호화
eyJraWQiOiJrZXkxIiwiYWxnIjoiSFM1MTIifQ.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjI5NjI2OTc0LCJleHAiOjE2Mjk2Mjc1NzR9
// HS512 + Secret Key
// 결과
TrK2IFel3Rv5ftoPXtLTEMFwHq8o-tTLMVUTR51zwP48VfOvklJ4GLHzBipfxrCLYz0QBZosVHkR4xjpn2bllA
- Key Rolling
- JWT의 토큰 생성 메커니즘을 보다보면 Secret Key가 노출되면 사실상 모든 데이터가 유출될 수 있다는 걸 알수 있다.
- 이런 문제를 방지하기 위해서 Secret Key를 여러개 두고 Key 노출에 대비
- Secret Key를 여러개를 사용하고 수시로 추가하고 삭제해줘서 변경한다면 SecretKey 중에 1개가 노출되어도 다른 Secret Key와 데이터는 안전한 상태
- Key Rolling에서는 여러개의 Secret Key가 존재
- Secret Key 1개에 Unique한 ID (kid 혹은 key id라고 부름) 를 연결
- JWT 토큰을 만들 때 헤더에 kid를 포함하여 제공하고 서버에서 토큰을 해석할 때 kid로 Secret Key를 찾아서 Signature를 검증
JWT 구현
- 의존성 추가
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
- JwtKey
- JWT Secret Key 를 관리하고 제공
- Key Rolling을 지원
- JwtUtils
- JWT 토큰을 생성하거나 Parsing하는 메소드를 제공
- JwtKey.getKey()
- kid로 Secret Key를 찾아오는 기능
/**
* kid로 Key찾기
*
* @param kid kid
* @return Key
*/
public static Key getKey(String kid) {
String key = SECRET_KEY_SET.getOrDefault(kid, null);
if (key == null)
return null;
return Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));
}
- JwtKey.getRandomKey()
- 여러개의 Secret Key 중에 랜덤으로 선택하여 kid와 SecretKey를 제공
/**
* SECRET_KEY_SET 에서 랜덤한 KEY 가져오기
*
* @return kid와 key Pair
*/
public static Pair<String, String> getRandomKey() {
String kid = KID_SET[randomIndex.nextInt(KID_SET.length)];
return Pair.of(kid, SECRET_KEY_SET.get(kid));
- JwtUtils.createToken()
- User로 JWT Token을 생성
- HEADER : alg(알고리즘종류), kid
public static String createToken(User user) {
Claims claims = Jwts.claims().setSubject(user.getUsername());// subject
Date now = new Date(); // 현재 시간
Pair<String, String> key = JwtKey.getRandomKey();
// JWT Token 생성
return Jwts.builder()
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setHeaderParam(JwsHeader.KEY_ID, key.getFirst()) // kid
.setExpiration(new Date(now.getTime() + JwtProperties.EXPIRATION_TIME)) // 토큰 만료 시간 설정 (now + 10분)
.signWith(Keys.hmacShaKeyFor(key.getSecond().getBytes(StandardCharsets.UTF_8))) // 알고리즘과 SecretKey
.compact();
}
- JwtUtils.getUsername()
- JWT Token에서 username을 구한다.
- SigningKeyResolver로 Signature를 검증
- 이 과정중에 토큰이 만료가 되었거나 적합하지 않으면 Exception이 발생
/**
* 토큰에서 username 찾기
* @param token 토큰
* @return username
*/
public static String getUsername(String token) {
return Jwts.parserBuilder() // parser용 builder
.setSigningKeyResolver(SigningKeyResolver.instance)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
- SigningKeyResolver
- JWT의 헤더에서 kid를 찾아서 Key(SecretKey+알고리즘)에 해당하는 비밀키를 찾아온다.
- Signature를 검증할 때 사용
- PAYLOAD : sub(username), iat(토큰발행시간), exp(토큰만료시간)
- SIGNATURE : JwtKey.getRandomKey로 구한 Secret Key로 HS512 해시
/**
* JwsHeader를 통해 Signature 복호화에 필요한 Key를 조회해옵니다.
*/
public class SigningKeyResolver extends SigningKeyResolverAdapter {
public static SigningKeyResolver instance = new SigningKeyResolver();
@Override
public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
String kid = jwsHeader.getKeyId();
if (kid == null)
return null;
return JwtKey.getKey(kid);
}
}
JWT Filter
- JwtAuthenticationFilter
- 로그인을 하면 JWT 토큰을 응답 쿠키에 넣어준다.
- UsernamePasswordAuthenticationFilter를 상속했기 때문에 기본동작은 거의 비슷
- 로그인에 성공하면 User 정보로 JWT Token을 생성하고 응답 쿠키에 값을 넣어준다.
@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult
) throws IOException {
User user = (User) authResult.getPrincipal();
String token = JwtUtils.createToken(user);
// 쿠키 생성
Cookie cookie = new Cookie(JwtProperties.COOKIE_NAME, token);
cookie.setMaxAge(JwtProperties.EXPIRATION_TIME); // 쿠키의 만료시간 설정
cookie.setPath("/");
response.addCookie(cookie);
response.sendRedirect("/");
- JwtAuthorizationFilter
- Cookie에서 JWT Token을 구한다.
- JWT Token을 파싱하여 username을 구한다.
- username으로 User를 구하고 Authentication을 생성
- 생성된 Authentication을 SecurityContext에 넣는다.
- Exception이 발생하면 응답의 쿠키를 null로 변경
결과(EditCookie)
- 로그인시 JWT-AUTHENTICATION 쿠키가 생성된걸 확인
- 로그아웃시 쿠키가 없어지는것을 확인
'개발 > Spring' 카테고리의 다른 글
Spring MVC 구현 (0) | 2024.03.02 |
---|---|
Spring Security Config (1) | 2024.03.01 |
Spring security Architecture, Filter (0) | 2024.02.29 |
Spring security Test (0) | 2024.02.28 |
Spring Security 구현 (0) | 2024.02.27 |