간단한 소셜로그인 로직을 완성했으니 이제 로그인 성공 시 JWT 토큰을 생성 후 반환하는 로직을 만들어보겠다
login 성공시 → successhandler로 이동하고 response header에 access token을 같이 반환해준다
이후 프론트 서버에서는 이 토큰을 http only 쿠키로 관리하며, 요청마다 토큰을 같이 전달한다
이후 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로 반환해주고