내일배움캠프 25일차 TIL : Spring - JWT 다루기
슬슬 JWT 구현해봐야 하니까...
// JWT
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
먼저 build.gradle에 의존성을 먼저 추가해줘야 한다.
의존성을 추가해줬다면 다음으로는 application.properties에 jwt.secret.key 값을 추가한다.
jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==
클래스에서 필요한 멤버들을 먼저 생성할 예정이다.
// Header KEY 값, Cookie의 name 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간 60분
private final long TOKEN_TIME = 60 * 60 * 1000L;
@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey, application.properties에 선언
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct // 최초 한 번만 받아오면 되는 값을 여러번 호출하는 실수를 방지하기 위한 어노테이션
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
public String generateToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX + Jwts.builder()
.setSubject(username)
.setIssuedAt(date)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.signWith(key, signatureAlgorithm)
.claim(AUTHORIZATION_KEY, role)
.compact();
}
Jwts.builder()를 통해 JWT 토큰을 생성한다.
- .setSubject(username)
: 사용자를 식별하는 값으로 PK, Id, username 다 상관 없다.
- .setIssuedAt(date)
: 이슈를 설정하는 부분이다. 여기서는 발급일을 저장하며, 선택사항이다.
- .setExpiration(new Date(date.getTime() + TOKEN_TIME))
: 위에서 설정한 만료기간을 저장하는 부분이다.
- .signWith(key, signatureAlgorithm)
: 암호화 알고리즘과 SecretKey를 디코딩한 키 값으로 서명하여 JWT를 생성한다.
- .claim(AUTHORIZATION_KEY, role)
: 보통 Payload에 담겨지는 Key-value 값을 담당한다. 주로 사용자 ID, 권한, 만료 시간 등을 포함하며, 여기서는 권한을 담당하게 됬다.
해당 빌더에 들어간 것 외에 옵션을 더 추가하거나 프로젝트 설정에 맞게 뺄 수 있다.
JWT 토큰을 나누는 과정이다.
이 과정이 필요한 이유는 우리가 JWT를 빌드할 때 Bearer 뒤에 붙여놨기 때문이다.
물론 토큰을 직접 사용할 수 있나..?
Bearer토큰은 OAuth 2.0 및 JWT 표준에서 정의된 방식으로 단순히 헤더에 토큰 값을 추가하는 방식으로 구현할 수 있다.
여튼 전달하기에는 편리한 방식이긴 하지만 실제로 발급받은 JWT 토큰은 Bearer 의 뒷 부분이기 때문에 일단 자르도록 하겠다.
public String substringToken(String tokenValue) {
// 토큰의 값 검사
if(StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7); // BAERER_PREFIX 숫자의 길이가 7이므로 그 이후의 값을 짤라서 가져옴
}
System.out.println("Not Found Token");
throw new NullPointerException("Not Found Token");
}
토큰을 발급받고 사용하기 전에 해당 토큰이 유효한지 검사를 해야한다.
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
System.out.println("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
System.out.println("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
System.out.println("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
System.out.println("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
- Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token):
이 부분은 주어진 토큰을 파싱하고, 서명을 확인하여 유효한 JWT인지 확인한다.
key는 토큰 서명에 사용되는 키이며, parseClaimsJws(token)는 토큰의 클레임(claim)을 추출한다.
그 다음 try catch를 통해 각 예외를 잡아 적절한 상황의 메시지를 출력하며
예외가 없이 정상적으로 작동하는 경우 true를 반환해준다.
검증 메서드는 따로 커스텀 하여 더 좋게 만들 수 있다.
// JWT Cookie 에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
try {
token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
cookie.setPath("/");
// Response 객체에 Cookie 추가
res.addCookie(cookie);
catch (UnsupportedEncodingException e) {
System.out.println(e.getMessage());
}
}
서버가 토큰을 발급받아서 클라이언트(브라우저 즉 사용자) 에게 전달을 해준다 하여도 저장해놓지 않으면 사용할 수 없기 때문에 쿠키에 저장하여 사용자에게 던져준다.
public Claims getUserInfoFromToken(String token) {
// 담겨 있는 사용자의 정보를 사용 Key - value
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
위에서 Claim은 Payload에 들어가는 값을 담당한다고 했고 해당 메서드를 통해 payload의 값을 가져와 사용할 수 있다.
@GetMapping("/create-token")
public ResponseEntity<CustomResponse<?>> createJwt(HttpServletResponse res, @RequestBody UserLoginRequestDto loginRequestDto) {
String token = jwtUtil.generateToken(loginRequestDto.getUsername(), UserRoleEnum.valueOf(loginRequestDto.getRole()));
jwtUtil.addJwtToCookie(token, res);
return ResponseEntity.ok().body(CustomResponse.makeResponse(token, HttpStatus.OK));
}
따로 컨트롤러에서 토큰을 생성하고 쿠키에 저장해준다.

@GetMapping("/get-token")
public ResponseEntity<CustomResponse<?>> getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
// JWT 토큰 substring
String token = jwtUtil.substringToken(tokenValue);
// 토큰 검증
if(!jwtUtil.validateToken(token)){
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 사용자 정보 가져오기
Claims info = jwtUtil.getUserInfoFromToken(token);
// 사용자 username
String username = info.getSubject();
System.out.println("username = " + username);
// 사용자 권한
String authority = (String) info.get(JwtUtil.AUTHORIZATION_KEY);
System.out.println("authority = " + authority);
String result = "getJwt : " + username + ", " + authority;
return ResponseEntity.ok().body(CustomResponse.makeResponse(result, HttpStatus.OK));
}
