Java 21 에서 JWT 구현 예제
jdk 21 에서 jwt 를 개발해야 하는데 첫 경험(?)이라 삽질하고 난 결과물을 올
import io.jsonwebtoken.Claims;
public interface iToken {
boolean verify();
iToken generate();
Claims claims();
String toString();
iToken subject(String key);
iToken token(String key);
}
iToken.java
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.Key;
import java.util.Date;
import java.util.Map;
@Data
public class JwtToken implements iToken {
protected String key;
protected String lifetime;
protected String _subject = "";
protected static final Logger LOG = LoggerFactory.getLogger(JwtToken.class);
private String _token;
private Key _key;
private Long _lifetime;
@Override
public iToken subject(String subject) {
_subject = subject;
return this;
}
@Override
public iToken token(String token) {
_token = token;
return this;
}
private void init() {
if(null != _key)
return;
_key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(key));
_lifetime = Long.parseLong(lifetime) * 1000;
}
@Override
public iToken generate() {
if (null != _token)
return this;
init();
long now = (new Date()).getTime();
Date expiryDate = new Date(now + _lifetime);
Header header = Jwts.header();
header.setType("JWT");
Claims claims = Jwts.claims().setSubject(_subject);
claims.setExpiration(expiryDate);
claims.setIssuedAt(new Date());
_token = Jwts.builder()
// .setSubject("jkss")
//.claim(AUTHORITIES_KEY, role) // 여가다 넣으면 아래 setClaims 가 덮어버려서 없어짐.
.setHeader((Map<String, Object>) header)
.signWith(_key, SignatureAlgorithm.HS256)
.setClaims(claims)
.compact();
return this;
}
@Override
public boolean verify() {
return null != claims();
}
@Override
public Claims claims() {
init();
try {
return Jwts.parserBuilder().setSigningKey(_key).build().parseClaimsJws(_token).getBody();
} catch (SecurityException e) {
LOG.debug("Invalid JWT signature.");
// TODO 여기 예와 다 처리해야됨.
throw e;
} catch (MalformedJwtException e) {
LOG.debug("Invalid JWT token.");
throw e;
} catch (ExpiredJwtException e) {
LOG.debug("JWT 토큰이 만료됨.");
throw e;
} catch (UnsupportedJwtException e) {
LOG.debug("Unsupported JWT token.");
throw e;
} catch (IllegalArgumentException e) {
LOG.debug("JWT token compact of handler are invalid.");
throw e;
}
}
@Override
public String toString() {
return _token;
}
}
JwtToken.java
public class JwtTokenAccess extends JwtToken{
public JwtTokenAccess() {
super();
key = JwtManager.KEY_ACCESS;
lifetime = JwtManager.LIFETIME_ACCESS;
}
}
JwtTokenAccess.java
public class JwtTokenRefresh extends JwtToken {
public JwtTokenRefresh() {
super();
key = JwtManager.KEY_REFRESH;
lifetime = JwtManager.LIFETIME_REFRESH;
}
}
JwtTokenRefresh.java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtManager {
private static final Logger LOG = LoggerFactory.getLogger(JwtManager.class);
@Value("${jwt.access.key}")
private String _KEY_ACCESS;
@Value("${jwt.access.lifetime}")
private String _LIFETIME_ACCESS;
@Value("${jwt.refresh.key}")
private String _KEY_REFRESH;
@Value("${jwt.refresh.lifetime}")
private String _LIFETIME_REFRESH;
public static String KEY_ACCESS;
public static String LIFETIME_ACCESS;
public static String KEY_REFRESH;
public static String LIFETIME_REFRESH;
public static final int TOKEN_TYPE_ACCESS = 1;
public static final int TOKEN_TYPE_REFRESH = 2;
@PostConstruct
public void init() {
KEY_ACCESS = _KEY_ACCESS;
LIFETIME_ACCESS = _LIFETIME_ACCESS;
KEY_REFRESH = _KEY_REFRESH;
LIFETIME_REFRESH = _LIFETIME_REFRESH;
}
// 토큰 팩토리 메서드
public iToken generateToken(String subject, int tokenType) {
iToken token = null;
switch (tokenType) {
case TOKEN_TYPE_ACCESS:
token = new JwtTokenAccess().subject(subject).generate();
break;
case TOKEN_TYPE_REFRESH:
token = new JwtTokenRefresh().subject(subject).generate();
break;
default:
}
return token;
}
//토큰 문자열로 토큰 객체 만들기
public iToken adapt(String tokenString, int tokenType) {
iToken token = null;
switch (tokenType) {
case TOKEN_TYPE_ACCESS:
token = new JwtTokenAccess().token(tokenString);
break;
case TOKEN_TYPE_REFRESH:
token = new JwtTokenRefresh().token(tokenString);
break;
default:
}
return token;
}
public Map<String, String> generateJwtTokens(HttpServletRequest request, HttpServletResponse response) {
String subdomain = getSubDomainString(request, 1);
iToken accessToken = generateToken(subdomain, TOKEN_TYPE_ACCESS);//액세스 토큰 생성
iToken refreshToken = generateToken(subdomain, TOKEN_TYPE_REFRESH);//리프레시 토큰 생성
JwtFilter.MutableHttpServletRequest mutableRequest = new JwtFilter.MutableHttpServletRequest(request);
mutableRequest.putHeader(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + accessToken.toString());
response.addHeader(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + accessToken.toString());
LOG.debug("newRefreshToken.getToken() -> {}", refreshToken.toString());
//TODO zhuyeel 쿠키 유효기간을 리프레시 토큰 유효기간이랑 같게 수정
CookieManager.addCookie(response, "refreshToken", refreshToken.toString(), "localhost", 999999);
Map<String, String> result = new HashMap<>();
result.put("accessToken", accessToken.toString());
result.put("refreshToken", refreshToken.toString());
return result;
}
public String getSubDomainString(HttpServletRequest request, int index) {
// 요청의 호스트 헤더에서 서브도메인 추출
String host = request.getServerName();
String[] hostParts = host.split("\\.");
// 서브도메인만 가져오기 (예: sub.example.com -> sub)
if (hostParts.length > 2) {
return hostParts[index];
}
return null;
}
}
JwtManager.java
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.*;
public class JwtFilter extends OncePerRequestFilter {
public static final String AUTHORIZATION_HEADER = "Authorization";
private static final Logger LOG = LoggerFactory.getLogger(ExternalController.class);
private final JwtManager jwtUtil;
public JwtFilter(JwtManager jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Optional<String> token = resolveToken(request);
if (token.isPresent()) {
String authToken = token.get();
String bearer = authToken.replaceAll("Bearer", "");
String tokenString = bearer.trim();
LOG.debug("엑세스 토큰 : {}", tokenString);
iToken accessToken = jwtUtil.adapt(tokenString, JwtManager.TOKEN_TYPE_ACCESS);
try {
if (!accessToken.verify()) {
// 토큰 검징이 실패하면? 일단 패스
LOG.warn("검증 실패 ");
}
} catch (ExpiredJwtException e) {
LOG.debug("엑세스 토큰 만료 : {}", tokenString);
if (CookieManager.hasCookie(request)) {
LOG.debug("쿠키가 존재함.");
String refreshTokenPrev = CookieManager.getCookieString(request, "refreshToken");
LOG.debug("쿠키에 리플래시 토큰 있음 : {}", refreshTokenPrev);
iToken refreshToken = jwtUtil.adapt(refreshTokenPrev, JwtManager.TOKEN_TYPE_REFRESH);
if (refreshToken.verify()) {
LOG.debug("리프레시 토큰 통과");
}
Map<String, String> tokenMap = jwtUtil.generateJwtTokens(request, response);
String newAccessToken = tokenMap.get("accessToken");
String newRefreshToken = tokenMap.get("refreshToken");
MutableHttpServletRequest mutableRequest = new MutableHttpServletRequest(request);
mutableRequest.putHeader(AUTHORIZATION_HEADER, "Bearer " + newAccessToken);
response.addHeader(AUTHORIZATION_HEADER, "Bearer " + newAccessToken);
LOG.debug("newRefreshToken.getToken() -> {}", newRefreshToken);
CookieManager.addCookie(response, "refreshToken", newRefreshToken, "localhost", 999999);
}
}
}
filterChain.doFilter(request, response);
}
private Optional<String> resolveToken(HttpServletRequest request) {
String authToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(authToken)) {
return Optional.of(authToken);
} else {
return Optional.empty();
}
}
static final class MutableHttpServletRequest extends HttpServletRequestWrapper {
// holds custom header and value mapping
private final Map<String, String> customHeaders;
public MutableHttpServletRequest(HttpServletRequest request) {
super(request);
this.customHeaders = new HashMap<String, String>();
}
public void putHeader(String name, String value) {
this.customHeaders.put(name, value);
}
public String getHeader(String name) {
// check the custom headers first
String headerValue = customHeaders.get(name);
if (headerValue != null) {
return headerValue;
}
// else return from into the original wrapped object
return ((HttpServletRequest) getRequest()).getHeader(name);
}
public Enumeration<String> getHeaderNames() {
// create a set of the custom header names
Set<String> set = new HashSet<String>(customHeaders.keySet());
// now add the headers from the wrapped request object
@SuppressWarnings("unchecked")
Enumeration<String> e = ((HttpServletRequest) getRequest()).getHeaderNames();
while (e.hasMoreElements()) {
// add the names of the request headers into the list
String n = e.nextElement();
set.add(n);
}
// create an enumeration from the set and return
return Collections.enumeration(set);
}
}
}
JwtFilter.java
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseCookie;
import java.util.Arrays;
public class CookieManager {
public static final boolean USE_RESPONSE_COOKIE = false;
public static Cookie getCookie(HttpServletRequest request, String key) {
Cookie[] cookies = request.getCookies();
if (hasCookie(request)) {
return Arrays.stream(cookies).filter(x -> key.equals(x.getName())).findFirst().orElse(null);
}
return null;
}
public static String getCookieString(HttpServletRequest request, String key) {
Cookie[] cookies = request.getCookies();
if (hasCookie(request)) {
return Arrays.stream(cookies).filter(x -> key.equals(x.getName())).findFirst().orElse(null).getValue();
}
return null;
}
public static boolean hasCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
return cookies != null && cookies.length > 0;
}
public static void addCookie(HttpServletResponse response, String key, String value, String domain, long maxAge) {
//쿠키에 바로 넣는게 안되어서 response 헤더에 담기도 한다는데 기능 죽이지 않고 남겨둠
if (USE_RESPONSE_COOKIE) {
ResponseCookie responseCookie = ResponseCookie
.from(key, value)
.maxAge(maxAge)
.domain(domain)
.httpOnly(true)
.secure(true)
.path("/")
.sameSite("None")
.build();
response.addHeader("Set-Cookie", responseCookie.toString());
} else {
Cookie cookie = new Cookie(key, value);
response.addCookie(cookie);
}
}
}
CookieManager.java
관리 편하게 하려고 토큰 유형별로 쪼갬
님들 쓰라고 만든거 아닌데 태클걸면 님들 말이 맞음