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

관리 편하게 하려고 토큰 유형별로 쪼갬

님들 쓰라고 만든거 아닌데 태클걸면 님들 말이 맞음

Subscribe to X세대 신입사원

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe
774-86-01972 cinnabar.3d@gmail.com