들어가며
개인 블로그 프로젝트를 구현하기 위한 코드들을 뒤돌아보며 겸사겸사 리팩토링 과정을 담기위해 글을 작성하게 되었습니다.
이번 글은 로그인/로그아웃이라고 하였지만, 사실상 Token Filter 를 리팩토링 하는 글입니다!
관련 포스트 : https://kongdevlog.tistory.com/9
JWT 로그인 방식 구현과 생각 정리
들어가며인턴을 하던 시절에 세션을 적용해보았던 것을 빼면, 한번도 세션 방식의 로그인을 구현해보지 않았습니다. 그저 관성적으로 (대부분의 로그인 방식으로 JWT를 사용하니..) 토큰 방식의
kongdevlog.tistory.com
구현 코드 (JwtFilter.class)
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!isNeededAuthentication(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
String accessToken = getTokenFromHeader(request);
String refreshToken = getTokenFromCookie(request);
if (accessToken == null) {
// 예외 처리.
handleUnAuthorized(request, response);
return;
}
Long id = userJwtProvider.extractIdFromAccessToken(accessToken);
if (id == null) {
if (refreshToken == null) {
handleUnAuthorized(request, response);
return;
} else if (refreshToken != null) {
Long idFromRedis = tokenRepository.getIdFromToken(refreshToken);
if (idFromRedis == null) {
handleUnAuthorized(request, response);
return;
}
// 로그인 갱신.
tokenRepository.deleteToken(refreshToken);
handleLoginExtension(response, idFromRedis);
return;
}
}
// 토큰 정보 저장.
userContext.setId(id);
filterChain.doFilter(request, response);
}
코드를 구현할 당시, 최대한 함수별 역할을 분리하여 코드를 작성하고자 하였으나 지금 보니, JwtFilter.class 자체의 책임이 아닌 함수들까지 내부에 구현한 것 같다는 생각이 듭니다.. 이를 중점적으로 리팩토링하고자 합니다. 이를 위해서 먼저 JwtFilter 의 역할을 분명히 해야할 것 같습니다.
'Token 의 유효성을 검증하는 것' 이것이 JwtFilter 의 핵심 역할이라고 정의하였습니다. 이를 토대로 이전 구현에서 클래스 및 메서드를 분리하고자 합니다!
클래스와 메소드 쪼개기
현재 JwtFilter 에서 수행하고 있는 작업을 나열하면
1. 토큰 추출 (헤더, 쿠키)
2. 예외 처리
3. 유효성 검증.
4. 토큰 정보 추출.
5. TokenRepository 조회 / 삭제
6. 토큰 정보 저장.
여기서 JwtFilter의 책임과 직접적으로 연관된 것은 3번 뿐입니다. (+ 6번 : Conext 저장까지...) 따라서, 다음과 같은 클래스를 추가로 작성하려고 합니다!
토큰을 추출하는 책임을 가진 클래스. (1번)
인증 및 인가 예외 처리를 하기 위한 클래스. (2번)
4번과 5번은 이미 클래스가 정의되어 있습니다. (TokenProvider, TokenRepository)
-> TokenRepository 같은 경우, 이를 의존하고 있는 새로운 클래스를 구현해야하는가 대한 고민중 . . . .. (나중에 filter 내부에 private 메서드로 따로 분리하는 방식으로!)
#### 리팩토링
1. TokenExtractor
@Component
public class TokenExtractor {
private static final String HEADER_PREFIX = "Bearer ";
private static final String COOKIE_NAME = "refresh_token";
public String extractFromHeader(String header) {
if (header != null && header.startsWith(HEADER_PREFIX)) {
return header.substring(HEADER_PREFIX.length());
}
return null;
}
public String extractFromCookies(Cookie[] cookies) {
return cookies == null ? null :
Arrays.stream(cookies).filter(cookie -> COOKIE_NAME.equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
}
2. AuthExceptionHandler
@Component
public class AuthExceptionHandler {
private static final ObjectMapper mapper = new ObjectMapper();
public void handleUnAuthorize(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
Map<String, String> body = new HashMap<>();
body.put("message", message);
response.getWriter().write(mapper.writeValueAsString(body));
}
public void handleLoginExtension(HttpServletResponse response, UserLoginResponse userLoginResponse) throws IOException {
response.setStatus(HttpServletResponse.SC_ACCEPTED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
Cookie cookie = new Cookie("refresh_token", userLoginResponse.refreshToken());
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(60 * 60 * 24);
response.addCookie(cookie);
response.getWriter().write(mapper.writeValueAsString(userLoginResponse));
}
}
3. JwtFilter 최종
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
// 인증 작업이 필요한 URL
private static final String[] AUTH_NEEDED_URI = {"/post", "/user/logout", "/user"};
private final UserJwtProvider userJwtProvider;
private final UserContext userContext;
private final TokenRepository tokenRepository;
private final TokenExtractor tokenExtractor;
private final AuthExceptionHandler authExceptionHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!isNeededAuthentication(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
String accessToken = tokenExtractor.extractFromHeader(request.getHeader("Authorization"));
String refreshToken = tokenExtractor.extractFromCookies(request.getCookies());
if (accessToken == null) { // AccessToken 이 없는 경우.
authExceptionHandler.handleUnAuthorize(response, "토큰 발급이 필요합니다.");
return;
}
Long id = userJwtProvider.extractIdFromAccessToken(accessToken);
Long idFromRefreshToken = tokenRepository.getIdFromToken(refreshToken);
if (id == null && idFromRefreshToken == null) { // Access Token ,Refresh Token 유효하지 않은 경우.
authExceptionHandler.handleUnAuthorize(response, "Token Invalid.");
return;
}
if (id == null && idFromRefreshToken != null) {
authExceptionHandler.handleLoginExtension(response, extendLogin(idFromRefreshToken, refreshToken, response));
return;
}
// 토큰 정보 저장.
userContext.setId(id);
filterChain.doFilter(request, response);
}
private String extendLogin(Long id, String refreshedToken, HttpServletResponse response) {
tokenRepository.deleteToken(refreshedToken);
String accessToken = userJwtProvider.createToken(id);
String refreshToken = userJwtProvider.createRefreshToken(id);
Cookie cookie = new Cookie("refresh_token", refreshToken);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(60 * 60 * 24);
response.addCookie(cookie);
tokenRepository.saveToken(refreshToken, id);
return accessToken;
}
private boolean isNeededAuthentication(String requestUri) {
if (Arrays.stream(AUTH_NEEDED_URI).anyMatch((url) -> url.equals(requestUri))) {
return true;
}
return false;
}
}
마치며
이렇게 구현하니, 기존 JwtFilter 의 볼륨도 크게 줄었고 쉽게 파악하기 편해진 것 같은 기분이 듭니다 . . . .
함수와 클래스의 책임과 역할이 잘 구분되었는지에 대해 신경쓰면서 리팩토링을 진행해봤지만, 좀 더 코드를 많이 작성해보고 고민을 많이 해봐야할 것 같습니다.... (나중에 또 블로그에 추가 기능을 넣고, 여러 작업을 하면서 추가적인 리팩토링을 해보겠습니다!)
'Back-End > Server' 카테고리의 다른 글
| [팀 프로젝트] queryDsl 도입과 테스트 코드 (0) | 2025.01.21 |
|---|---|
| [Kong's Blog] 프로젝트 회고와 리팩토링 (4) - 스케쥴링 적용 (0) | 2025.01.17 |
| [Kong's Blog] 프로젝트 회고와 리팩토링 (3) - 배치 사용해보기 (0) | 2025.01.15 |
| [Kong's Blog] 프로젝트 회고와 리팩토링 (2) - 티스토리 크롤링 서비스 (0) | 2025.01.14 |
| JWT 로그인 방식 구현과 생각 정리 (1) | 2025.01.11 |
댓글