Architecture/MSA

Gateway 필터

잔망루피 2023. 3. 4. 22:08
반응형
@Slf4j
@Component
public class CustomAuthFilter extends AbstractGatewayFilterFactory<CustomAuthFilter.Config>{
    @Value("${spring.security.jwt.secret}")
    private String SECRET_KEY;

    public static class Config {

    }

    public CustomAuthFilter(){
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)){
                return onError(exchange, "no authorization header", HttpStatus.UNAUTHORIZED);
            }

            String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
            String jwt = authorizationHeader.replace("Bearer", "");

            if (!isJwtValid(jwt)) {
                log.info("fail");
                return onError(exchange,"JWT token is not valid",HttpStatus.UNAUTHORIZED);
            }

            return chain.filter(exchange);
        };
    }
    private boolean isJwtValid(String jwt) {
        boolean returnValue = true;
        String subject = null;

        try {
            subject = Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)))
                    .build()
                    .parseClaimsJws(jwt)
                    .getBody()
                    .getSubject();
        } catch (Exception ex) {
            returnValue = false;
        }

        if (subject == null || subject.isEmpty()) {
            returnValue = false;
        }

        return returnValue;
    }

    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);

        log.error(err);
        return response.setComplete();
    }

}

Header에서 access token을 가져와서 검증

아래는 나의 시행착오들ㅎ


리팩토링 계기

cookieHeaders

 

쿠키에서 accessToken, refreshToken을 추출하는 코드

코드를 리팩토링해야겠다...

아래는 리팩토링한 것


리팩토링 후

@Slf4j
@Component
public class CustomAuthFilter extends AbstractGatewayFilterFactory<CustomAuthFilter.Config>{
    @Value("${spring.security.jwt.secret}")
    private String SECRET_KEY;

    private final CookieUtil cookieUtil;
    private static final String ACCESS_TOKEN = "accessToken";
    private static final String REFRESH_TOKEN = "refreshToken";

    public static class Config {

    }

    public CustomAuthFilter(){
        super(Config.class);
        cookieUtil = new CookieUtil();
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            Optional<String> access_token = cookieUtil.getCookie(request, ACCESS_TOKEN);
            Optional<String> refresh_token = cookieUtil.getCookie(request, REFRESH_TOKEN);

            if (access_token.isEmpty()){
                return onError(exchange, "no access token", HttpStatus.UNAUTHORIZED);
            }

            String jwt = access_token.get();

            if (!isJwtValid(jwt)) {
                log.info("fail");
                return onError(exchange,"JWT token is not valid",HttpStatus.UNAUTHORIZED);
            }

            return chain.filter(exchange);
        };
    }

    private boolean isJwtValid(String jwt) {
        try{
            Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)))
                    .build()
                    .parseClaimsJws(jwt)
                    .getBody()
                    .getSubject();
            return true;
        } catch(JwtException | IllegalArgumentException ex){
            log.error("JWT token is not valid: {}", ex.getMessage());
            return false;
        }
    }

    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);

        log.error(err);
        return response.setComplete();
    }

}

CookieUtil 서비스에서 쿠키를 추출하도록 했다.

 

@Service
public class CookieUtil {

    public Optional<String> getCookie(ServerHttpRequest req, String cookieName){
        final MultiValueMap<String, HttpCookie> cookies = req.getCookies();
        if(cookies.isEmpty()) return Optional.empty();
        return Optional.of(Objects.requireNonNull(cookies.getFirst(cookieName)).getValue());
    }
}

 

@ExtendWith(MockitoExtension.class)
public class CookieUtilTest {

    @InjectMocks
    private CookieUtil cookieUtil;

    private static MockServerHttpRequest request;

    @BeforeAll
    public static void beforeEach(){

        HttpCookie access_token = new HttpCookie("accessToken", "testValue");
        HttpCookie refresh_token = new HttpCookie("refreshToken", "testValue");

        request = MockServerHttpRequest
                .get("/membership-server/auth/profile")
                .cookie(access_token)
                .cookie(refresh_token)
                .build();
    }

    @Test
    @DisplayName("Cookie에서 원하는 값 추출")
    public void getCookie(){

        assertThat(cookieUtil.getCookie(request, "accessToken")).isNotNull();
        assertThat(cookieUtil.getCookie(request, "refreshToken")).isNotNull();
    }

    @Test
    @DisplayName("Cookie에서 원하는 값 추출 실패")
    public void FailToGetCookie(){

        assertThrows(NullPointerException.class, () -> cookieUtil.getCookie(request, "testToken"));
    }
}

이 서비스의 테스트 코드까지 작성한 후 커밋하기 전에 궁금한 점이 생겼다.

🤔 access token이 유효하지 않으면, refresh token을 검증한 후 다시 발급해줘야하는데?

Optional<String> refresh_token = cookieUtil.getCookie(request, REFRESH_TOKEN);

내가 작성한 필터에는 위처럼 refresh token을 추출하긴해도 검증하는 로직은 없다.

refresh token을 검증하는 로직까지 넣으면 좀 복잡해지는데???

찾아보니 access token은 FE에서 memory에 저장, refresh token은 Cookie에 저장한다.

선택지가 localStorage, Cookie만 있는 줄 알았다. 🤣🤣

localStorage에 저장 안할려고 Cookie에 access token을 저장했다. ➡️ 결국 Gateway의 filter까지 수정하게 됨

이 이유로 처음에 작성했던 header에서 token을 꺼내는 로직으로 돌아가게 되었다.

반응형