간단한 소셜로그인 로직을 완성했으니 이제 로그인 성공 시 JWT 토큰을 생성 후 반환하는 로직을 만들어보겠다

Untitled

login 성공시 → successhandler로 이동하고 response header에 access token을 같이 반환해준다

이후 프론트 서버에서는 이 토큰을 http only 쿠키로 관리하며, 요청마다 토큰을 같이 전달한다

Untitled

이후 Jwt Filter를 수정해주었다.

유효한 토큰의 경우(만료된 토큰 포함) → refresh token이 redis에 있는지 검증 → 있을 시 만료됐으면 새로운 access token 발급 만료안됐으면 filter 통과

구현하면서 하나 착각한 것이 있었다. WebSecurityConfig에서 filterchain을 구성할 때 permitall을 해준 URL에 대해서는 filter를 거치지 않는 줄 알았는데 인증을 거치지 않는 것이었다. 따라서 JwtFilter에 화이트리스트를 넣어주고, Jwt 토큰 인증이 필요없을 경우 다음 필터로 가게 구현해주었다

package com.hong.blog.config.filter;

import com.hong.blog.common.constants.JwtConstants;
import com.hong.blog.common.constants.WhiteList;
import com.hong.blog.config.JwtTokenProvider;
import com.hong.blog.model.member.TokenInfo;
import io.jsonwebtoken.*;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.NonNull;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;
    private final RedisTemplate<String,Object> redisTemplate;

    private final String[] WHITE_LIST = WhiteList.WHITE_LIST;
    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {

        String accessToken = resolveToken((HttpServletRequest) request);

        AntPathMatcher pathMatcher = new AntPathMatcher();
        for(String whiteList:WHITE_LIST){

            if(pathMatcher.match(whiteList,request.getRequestURI())){
                filterChain.doFilter(request,response);
                return;
            }
        }

        if(accessToken!=null) {

            try{
                jwtTokenProvider.isValidateToken(accessToken);
                //로그아웃 되어있지 않다면 filter 통과
                String logout = (String) redisTemplate.opsForValue().get(accessToken);
                if (ObjectUtils.isEmpty(logout)) {
                    Authentication authentication = jwtTokenProvider.getAuthentication(accessToken);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
                //로그아웃 되어있다면 filter 통과 못함 => 다시 로그인
                else {
                    unauthorized(response, "로그아웃 되었습니다");
                    return;
                }
            } catch (ExpiredJwtException e) {

                Authentication authentication = jwtTokenProvider.getAuthentication(accessToken);
                TokenInfo tokenInfo = jwtTokenProvider.refreshAccessToken(authentication);

                //토큰이 단순히 만료된거라면 새로운 access token 발급
                if (tokenInfo != null) {
                    response.addHeader(JwtConstants.HEADER, tokenInfo.getAccessToken());
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
                //refresh token도 만료됐다면 filter 통과 못함 => 다시 로그인
                else {
                    unauthorized(response, "로그인한지 너무 오래됐습니다");
                    return;
                }
            }catch (SecurityException | MalformedJwtException e){
                unauthorized(response,"토큰이 변조되었습니다");
                return;
            }catch(UnsupportedJwtException e){
                unauthorized(response,"지원하지 않는 타입의 토큰입니다");
                return;
            }catch (IllegalArgumentException e){
                unauthorized(response,"토큰의 claim이 비었습니다");
                return;
            }catch(Exception e){
                unauthorized(response,"유효하지 않은 토큰입니다");
                return;
            }
        }else{
            unauthorized(response,"토큰이 존재하지 않습니다");
            return;
        }

        filterChain.doFilter(request,response);

    }

    private void unauthorized(HttpServletResponse httpServletResponse, String message) throws IOException {

        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.getWriter().write(message);
        httpServletResponse.getWriter().flush();
    }

    //Request Header 에서 Token 정보를 추출합니다
    private String resolveToken(HttpServletRequest httpServletRequest){
        String authHeader = httpServletRequest.getHeader(JwtConstants.HEADER);
        logger.warn("header : " + authHeader);
        if(authHeader!=null && authHeader.startsWith(JwtConstants.TYPE)) {
            //Bearer을 제외한 Token 값을 추출합니다
            return authHeader.substring(7);
        }
        return null;
    }
}

로그인 시 Access Token을 Response Header로 반환해주고