By zhuyeel — Oct 21, 2024 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.javaimport 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.javapublic class JwtTokenAccess extends JwtToken{ public JwtTokenAccess() { super(); key = JwtManager.KEY_ACCESS; lifetime = JwtManager.LIFETIME_ACCESS; } }JwtTokenAccess.javapublic class JwtTokenRefresh extends JwtToken { public JwtTokenRefresh() { super(); key = JwtManager.KEY_REFRESH; lifetime = JwtManager.LIFETIME_REFRESH; } }JwtTokenRefresh.javaimport 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.javaimport 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.javaimport 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관리 편하게 하려고 토큰 유형별로 쪼갬님들 쓰라고 만든거 아닌데 태클걸면 님들 말이 맞음