jwt 인증 프로세스에서 공통 로직을 분리하자

 

문제

토큰 처리에 필요한 util 구현체 TokenProvider의 로직에서 Bearer prefix를 제거하고 validate가 반복된다.

 

AS-IS

@Slf4j
@RequiredArgsConstructor
@Component
public class JwtProvider {

    private final JwtService jwtService;

    public final static String ISSUER = "side-match";
    public final static String HEADER_AUTHORIZATION = "Authorization";
    public final static String TOKEN_PRIFIX = "Bearer";
    public static final Duration REFRESH_TOKEN_DURATION = Duration.ofDays(14);
    public static final Duration ACCESS_TOKEN_DURATION = Duration.ofHours(2);

    public String generateToken(User user, String role, Duration expiredAt) {
        Date now = new Date();

        Jwt jwt = jwtService.createJwt(user);

        return Jwts.builder()
                .issuer(ISSUER)
                .subject(String.valueOf(user.getId()))
                .id(String.valueOf(jwt.getId()))
                .claim("role", role)
                .issuedAt(now)
                .expiration(new Date(now.getTime() + expiredAt.toMillis()))
                .signWith(getKey(jwt.getJwtKey()))
                .compact();
    }

    public Authentication getAuthentication(String authorizationHeader, User user) {
        String token = getToken(authorizationHeader);
        Long userId = getUserId(authorizationHeader);
        String jwtKey = jwtService.loadJwtKeyByUserId(userId);
        Claims payload = Jwts.parser()
                .verifyWith(getKey(jwtKey))
                .build()
                .parseSignedClaims(token)
                .getPayload();

        return new UsernamePasswordAuthenticationToken(
                user,
                null,
                List.of(new SimpleGrantedAuthority((String) payload.get("role")))
        );
    }

    public Long getUserId(String authorizationHeader) throws JwtException {
        String token = getToken(authorizationHeader);
        String jwtKey = getTokenKey(token);
        Claims payload = Jwts.parser()
                .verifyWith(getKey(jwtKey))
                .build()
                .parseSignedClaims(token)
                .getPayload();

        return Long.parseLong(payload.getSubject());
    }

    private String getToken(String authorizationHeader) {
        if (!authorizationHeader.startsWith(TOKEN_PRIFIX)) {
            throw new IllegalArgumentException("잘못된 인증 헤더 요청입니다.");
        }

        return authorizationHeader.substring(TOKEN_PRIFIX.length());
    }

    private String getTokenKey(String token) {
        return Jwts.parser().build().parseSignedClaims(token).getPayload().getId();
    }

    private SecretKey getKey(String jwtKey) {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtKey));
    }
}

header에서 토큰을 분리하는 메서드를 매번 호출해야 하고 verifyWith로 매번 검증해야 하는 문제를 해결해보자.

 

TO-BE

웹 요청의 처리는 필터에서 제공하는 HttpServletRequest과 HttpServletResponse를 활용하면 간단하게 해결할 수 있다. 

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;
    private final UserService userService;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {
        try {
            String authorizationHeader = getAuthorizationHeader(request);
            String token = getToken(authorizationHeader);
            jwtProvider.validateToken(token);
            request.setAttribute("token", token);

            Long userId = jwtProvider.getUserId(token);
            User user = userService.loadUserById(userId);
            Authentication authentication = getAuthentication(token, user);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            filterChain.doFilter(request, response);
        } catch (JwtException jwtException) {
            log.info("잘못된 토큰으로 접근했습니다.");
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않은 토큰입니다.");
        }
    }
}

request의 header에서 Authorization을 가져와 Bearer을 제거한다.

 

TokenProvider의 모든 메서드에서 진행한 validateToken을 한 번만 실행할 수 있도록 필터에서 구현한다.

public void validateToken(String token) throws JwtException{
        SecretKey key = getJwtKeyFromToken(token);
        Jwts.parser()
                .verifyWith(key)
                .build()
                .parseSignedClaims(token);
    }

 

결과

컨트롤러에서 Authorization 헤더의 토큰 값을 꺼내와 TokenProvider를 통해 사용자의 식별자를 꺼내는 로직이 반복되던 것을

필터에서 일괄적으로 처리할 수 있게 구현했다. 이제는 컨트롤러에서 간편하게 Authentication 인터페이스를 통해

SecurityContextHolder에 저장된 인증 객체를 사용하여 사용자를 식별할 수 있다.