Skip to content

Commit

Permalink
Feature/#408-JwtTokenFilter와_logout개선 (#409)
Browse files Browse the repository at this point in the history
* refactor: 다형성을 이용해 토큰을 발행하도록 수정

* refactor: 코드 리뷰 내용을 반영해 List에 의존적이지 않게 수정

* chore: 우테코 -> 구글 포매터로 수정

* chore: 가독성을 위해서 _ 추가

* refactor: interface default method를 사용해 Token의 상태를 확인하도록 수정

* refactor: interface default method를 사용해 SecurityContextHolder에 인증을 넣도록 수정

* fix: sign-out이 401 떨어지도록 수정

* fix: 미사용 클래스 삭제

* fix: accessToken만 valid 해도 시큐리티 컨텍스트에 저장하도록 수정
  • Loading branch information
02ggang9 authored Feb 10, 2024
1 parent 3b0f783 commit de3e567
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 104 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.keeper.homepage.global.config.security;

import com.keeper.homepage.global.config.security.filter.JwtAuthenticationFilter;
import com.keeper.homepage.global.config.security.filter.RefreshTokenFilter;
import com.keeper.homepage.global.config.security.handler.CustomAccessDeniedHandler;
import com.keeper.homepage.global.config.security.handler.CustomAuthenticationEntryPoint;
Expand All @@ -23,10 +22,8 @@
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfiguration {

private final JwtTokenProvider jwtTokenProvider;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final RefreshTokenFilter refreshTokenFilter;

@Bean
Expand All @@ -48,8 +45,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.and()
.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(refreshTokenFilter, JwtAuthenticationFilter.class);
.addFilterBefore(refreshTokenFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,80 +1,50 @@
package com.keeper.homepage.global.config.security.filter;

import static com.keeper.homepage.global.config.security.data.JwtType.ACCESS_TOKEN;
import static com.keeper.homepage.global.config.security.data.JwtType.REFRESH_TOKEN;
import static com.keeper.homepage.global.config.security.data.JwtValidationType.EXPIRED;
import static org.springframework.http.HttpHeaders.USER_AGENT;

import com.keeper.homepage.domain.auth.application.AuthCookieService;
import com.keeper.homepage.global.config.security.JwtTokenProvider;
import com.keeper.homepage.global.config.security.data.JwtValidationType;
import com.keeper.homepage.global.config.security.data.TokenValidationResultDto;
import com.keeper.homepage.global.util.redis.RedisUtil;
import com.keeper.homepage.global.config.security.filter.token_condition.JwtTokenCondition;
import com.keeper.homepage.global.config.security.filter.token_condition.JwtTokenConditionFactory;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import java.io.IOException;
import java.util.List;

import static com.keeper.homepage.global.config.security.data.JwtType.ACCESS_TOKEN;
import static com.keeper.homepage.global.config.security.data.JwtType.REFRESH_TOKEN;

@Slf4j
@RequiredArgsConstructor
@Component
public class RefreshTokenFilter extends GenericFilterBean {

private final JwtTokenProvider jwtTokenProvider;
private final RedisUtil redisUtil;
private final AuthCookieService authCookieService;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
final var accessTokenDto = jwtTokenProvider.tryCheckTokenValid(httpRequest, ACCESS_TOKEN);
final var refreshTokenDto = jwtTokenProvider.tryCheckTokenValid(httpRequest, REFRESH_TOKEN);
boolean isAccessExpiredAndRefreshValid = isAccessTokenExpired(accessTokenDto.getResultType()) &&
isRefreshTokenValid(refreshTokenDto) &&
isTokenInRedis(refreshTokenDto, httpRequest.getHeader(USER_AGENT));

if (isAccessExpiredAndRefreshValid) {
Authentication auth = jwtTokenProvider.getAuthentication(refreshTokenDto.getToken());
SecurityContextHolder.getContext().setAuthentication(auth);

String authId = String.valueOf(jwtTokenProvider.getAuthId(refreshTokenDto.getToken()));
String[] roles = jwtTokenProvider.getRoles(refreshTokenDto.getToken());
HttpServletResponse httpResponse = (HttpServletResponse) response;

authCookieService.setNewCookieInResponse(authId, roles, httpRequest.getHeader(USER_AGENT), httpResponse);
} else if (accessTokenDto.getResultType() == JwtValidationType.UNKNOWN && refreshTokenDto.getResultType() == JwtValidationType.UNKNOWN) {
filterChain.doFilter(request, response);
return;
} else if (accessTokenDto.getResultType() == EXPIRED && refreshTokenDto.getResultType() == EXPIRED) {
authCookieService.setCookieExpired((HttpServletResponse) response);
private final JwtTokenProvider jwtTokenProvider;
private final JwtTokenConditionFactory jwtTokenConditionFactory;
private final AuthCookieService authCookieService;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
final var accessTokenDto = jwtTokenProvider.tryCheckTokenValid(httpRequest, ACCESS_TOKEN);
final var refreshTokenDto = jwtTokenProvider.tryCheckTokenValid(httpRequest, REFRESH_TOKEN);
List<JwtTokenCondition> jwtTokenConditions = jwtTokenConditionFactory.createJwtTokenConditions();

jwtTokenConditions.stream()
.filter(jwtTokenCondition -> jwtTokenCondition.isSatisfiedBy(accessTokenDto, refreshTokenDto, httpRequest))
.findFirst()
.ifPresentOrElse(jwtTokenCondition -> jwtTokenCondition.setJwtToken(accessTokenDto, refreshTokenDto, httpRequest, httpResponse),
() -> authCookieService.setCookieExpired(httpResponse));

filterChain.doFilter(request, response);
}

filterChain.doFilter(request, response);
}

private boolean isTokenInRedis(TokenValidationResultDto refreshTokenDto, String userAgent) {
long authId = jwtTokenProvider.getAuthId(refreshTokenDto.getToken());
String refreshTokenKey = JwtTokenProvider.getRefreshTokenKeyForRedis(String.valueOf(authId), userAgent);
Optional<String> tokenInRedis = redisUtil.getData(refreshTokenKey, String.class);
return tokenInRedis.isPresent() && tokenInRedis.get().equals(refreshTokenDto.getToken());
}

private static boolean isAccessTokenExpired(JwtValidationType resultType) {
return EXPIRED.equals(resultType);
}

private boolean isRefreshTokenValid(TokenValidationResultDto refreshTokenDto) {
return refreshTokenDto.isValid();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.keeper.homepage.global.config.security.filter.token_condition;

import com.keeper.homepage.domain.auth.application.AuthCookieService;
import com.keeper.homepage.global.config.security.JwtTokenProvider;
import com.keeper.homepage.global.config.security.data.TokenValidationResultDto;
import com.keeper.homepage.global.util.redis.RedisUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Optional;

import static org.springframework.http.HttpHeaders.USER_AGENT;

@Component
@RequiredArgsConstructor
public class AccessTokenReissueCondition implements JwtTokenCondition {

private final AuthCookieService authCookieService;
private final JwtTokenProvider jwtTokenProvider;
private final RedisUtil redisUtil;

@Override
public boolean isSatisfiedBy(TokenValidationResultDto accessTokenDto,
TokenValidationResultDto refreshTokenDto,
HttpServletRequest httpRequest) {
return isTokenExpired(accessTokenDto) &&
isTokenValid(refreshTokenDto) &&
isTokenInRedis(refreshTokenDto, httpRequest.getHeader(USER_AGENT));
}

@Override
public void setJwtToken(TokenValidationResultDto accessTokenDto, TokenValidationResultDto refreshTokenDto,
HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
setAuthentication(jwtTokenProvider, refreshTokenDto);

String authId = String.valueOf(jwtTokenProvider.getAuthId(refreshTokenDto.getToken()));
String[] roles = jwtTokenProvider.getRoles(refreshTokenDto.getToken());
authCookieService.setNewCookieInResponse(authId, roles, httpRequest.getHeader(USER_AGENT), httpResponse);
}

private boolean isTokenInRedis(TokenValidationResultDto refreshTokenDto, String userAgent) {
long authId = jwtTokenProvider.getAuthId(refreshTokenDto.getToken());
String refreshTokenKey = JwtTokenProvider.getRefreshTokenKeyForRedis(String.valueOf(authId), userAgent);
Optional<String> tokenInRedis = redisUtil.getData(refreshTokenKey, String.class);
return tokenInRedis.isPresent() && tokenInRedis.get().equals(refreshTokenDto.getToken());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.keeper.homepage.global.config.security.filter.token_condition;

import com.keeper.homepage.global.config.security.JwtTokenProvider;
import com.keeper.homepage.global.config.security.data.TokenValidationResultDto;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;

import static com.keeper.homepage.global.config.security.data.JwtValidationType.EXPIRED;
import static com.keeper.homepage.global.config.security.data.JwtValidationType.VALID;

public interface JwtTokenCondition {

boolean isSatisfiedBy(TokenValidationResultDto accessTokenDto,
TokenValidationResultDto refreshTokenDto,
HttpServletRequest httpRequest);

void setJwtToken(TokenValidationResultDto accessTokenDto, TokenValidationResultDto refreshTokenDto,
HttpServletRequest request, HttpServletResponse response);

default void setAuthentication(JwtTokenProvider jwtTokenProvider,
TokenValidationResultDto jwtTokenDto) {
SecurityContextHolder.getContext()
.setAuthentication(jwtTokenProvider.getAuthentication(jwtTokenDto.getToken()));
}

default boolean isTokenValid(TokenValidationResultDto jwtTokenDto) {
return jwtTokenDto.getResultType() == VALID;
}

default boolean isTokenExpired(TokenValidationResultDto jwtTokenDto) {
return jwtTokenDto.getResultType() == EXPIRED;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.keeper.homepage.global.config.security.filter.token_condition;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@RequiredArgsConstructor
public class JwtTokenConditionFactory {

private final JwtTokenValidCondition jwtTokenValidCondition;
private final AccessTokenReissueCondition accessTokenReissueCondition;

public List<JwtTokenCondition> createJwtTokenConditions() {
return List.of(jwtTokenValidCondition, accessTokenReissueCondition);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.keeper.homepage.global.config.security.filter.token_condition;

import com.keeper.homepage.global.config.security.JwtTokenProvider;
import com.keeper.homepage.global.config.security.data.TokenValidationResultDto;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JwtTokenValidCondition implements JwtTokenCondition {

private final JwtTokenProvider jwtTokenProvider;

@Override
public boolean isSatisfiedBy(TokenValidationResultDto accessTokenDto, TokenValidationResultDto refreshTokenDto,
HttpServletRequest httpRequest) {
return accessTokenDto.isValid();
}

@Override
public void setJwtToken(TokenValidationResultDto accessTokenDto, TokenValidationResultDto refreshTokenDto,
HttpServletRequest request, HttpServletResponse response) {
setAuthentication(jwtTokenProvider, accessTokenDto);
}
}

0 comments on commit de3e567

Please sign in to comment.