JWT란 JSON 포맷을 이용하여 사용자의 정보를 저장하는 Claim기반의 Web Token이다. JWT를 사용하는 가장 큰 특징은 사용자의 정보를 Server에 저장하지 않기 때문에 서버에 부하가 낮아지게된다. 하지만 쿠키/세션에 비해 구현을 하는 과정이 복잡해지고 JWT에 담겨있는 내용이 많아질수록 네트워크 비용이 증가한다.
JWT는 Header
{
"alg": "HS256",
"typ": "JWT"
}
alg:토큰의 타입 지정
typ:알고리즘 방식 지정, 서명 및 토큰 검증에 사용
Payload
{
"sub": "1234567890",
"username": "카즈하",
"admin": true
}
Payload에는 실제 유저에 대한 정보가 담겨져 있다.
iss: 토근 발급자
sub: 토큰 제목
aud: 토큰 대상자
exp: 토큰 만료 시간
nbf: 토큰 활성 날짜, 이 날이 지나기전에 활성화되지 않음
iat: 토큰 발급 시간, 토큰 발급 이후 경과 시간을 알 수 있음
jti: JWT 토큰 식별자, 중복 방지 위해 사용, 일회용 토큰 등에 사용
Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
Signature는 토큰을 인코딩하거나 암호화할때 사용하는 유효한 코드이다.Header와 Payload의 값을 BASE64Url을 이용해 인코딩하고 인코딩한 값을 비밀키를 이용하여 해싱을하고 다시 BASE64Url로 인코딩한다.
@Component
public class JwtUtil {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String AUTHORIZATION_KEY = "auth";
public static final String BEARER_PREFIX = "Bearer ";
private final long TOKEN_TIME = 360 * 60 * 1000L;
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// 토큰 생성
public String createToken(String email, AdminRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(email)
.claim(AUTHORIZATION_KEY, role)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
// JWT Cookie 에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
token = URLEncoder.encode(token, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token);
cookie.setPath("/");
res.addCookie(cookie);
}
// JWT 토큰 substring
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
logger.error("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) {
logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
// 토큰에서 사용자 정보 가져오기
public Claims getAdminInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
// HttpServletRequest 에서 Cookie Value : JWT 가져오기
public String getTokenFromRequest(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if(cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
try {
return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
}
return null;
}
}
Enumerated Type의 줄임말로 열거형이라고도 불리며 요소,멤버라 불리는 명명된 집합을 이루는 자료형이다. 상수 역할을 하는 식별자라고 생각하면 편하다.
public enum AdminRoleEnum {
MANAGER(Authority.MANAGER), // 매니저 권한
STAFF(Authority.STAFF); // 스태프 권한
private final String authority;
AdminRoleEnum(String authority) {
this.authority = authority;
}
public static class Authority {
public static final String MANAGER = "MANAGER";
public static final String STAFF = "STAFF";
}
}
이 표현 방식을
public enum AdminRoleEnum {
MANAGER(Authority.MANAGER), // 매니저 권한
STAFF(Authority.STAFF); // 스태프 권한
}
이렇게 줄여도 똑같은 의미이다.