쌩로그

AOP는 필요없었다...(feat. Spring Security는 진짜 좋은 프레임워크다.) 본문

TroubleShooting & 고민/BE

AOP는 필요없었다...(feat. Spring Security는 진짜 좋은 프레임워크다.)

.쌩수. 2023. 7. 9. 17:57
반응형

목차

  1. 포스팅 개요
  2. 본론
      2-1. 문제가 무엇인가?
      2-2. 문제 해결
      2-3. 해결 결과
  3. 요약

1. 포스팅 개요

AOP로 AccessToken 검증(빡쳐서 만듬)이라는 제목으로 포스팅을 했다.

AccessToken을 검증하는데 코드상으론 400대 에러를 던지도록 해야하는데,
왠지 모르게 500번대 에러를 던졌다.

이걸 AOP로 해결했었는데,
물론 구현한 부분에서는 나름 괜찮았다고 생각하지만, 사실 AOP를 사용할 필요가 없었고,

또 한 분은 감사하게도 댓글로 알려주시길..

라고 남겨주셨다.
말씀하신 것처럼 AOP의 목적에 맞게 쓰이지 않았다.

이를 수정하고, 무엇이 문제였는지에 대한 포스팅이다.

2. 본론

2-1. 문제가 무엇인가?

먼저 당시 왜 500번대 에러가 발생했는지를 살펴보자.
먼저 SecurityConfiguration 클래스를 살펴보면, 다음과 같다.

해당 코드 중 이 부분이 문제였다.

 .authorizeRequests(authorize -> authorize   // url authorization 전체추가.
                        .anyRequest().permitAll()
)

어떤 요청이든 허용되도록 코드를 작성했었다.

그래서 만료된 토큰이나 유효하지 않은 토큰을 Header에 담아서 요청을 보내면, 다음과 같은 로직으로 인해서 NullPointException이 발생한 것이었다.

// Header에서 Authorization에 이상한 토큰이나 만료된 토큰을 보낸다.
// `anyRequest().permitAll()` 코드에 의해서 이상하거나, 만료된 토큰임에도 불구하고, 요청을 수락한다.
// 그런데 이상하거나, 만료된 토큰은 당연히 인증이 되지 않으니 SpringSecurityContext에 담기지않는다.
// Controller 메서드에서 인증된 사용자 객체인 Principal 객체를 SpringSecurityContext에서 가져와야 되는데, 안 담겨있으니 가져올 수 없다.
// 당연히 없는데, `getName()`메서드를 호출하고 앉았으니, 당연히 NullPointException이 발생한다.

그리고 위에 SecurityConfiguration코드를 보면, 인증 예외 발생시 예외를 처리하는 핸들러 설정 코드를 주석 처리했었다.

이전까지는 예외가 발생하더라도 해당 코드들이 수행되지 않길레 그래서 그냥 지웠었는데,
왜 예외 처리 핸들러까지 도달하지 않았는지 이번에 알게 되었다.

2-2. 문제 해결

이전 코드를 주석처리하고,
수정된 코드를 보자.

.and()
//                .authorizeRequests(authorize -> authorize   // url authorization 전체추가.
//                        .anyRequest().permitAll()
//                )
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/v1/members/**").hasAnyRole("USER", "ADMIN")
                .and()

모든 요청에 대해서 다 허용해주던 코드를

.and()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/v1/members/**").hasAnyRole("USER", "ADMIN")

antMatchers를 이용해서, HTTP메서드와 권한 별로 허용할 URL을 설정했다.

의미는 다음과 같다.

// HTTP메서드와 URL이 다음과 같은 요청이 왔을 때, 
// "USER"권한 혹은 "ADMIN" 권한이 있는 사용자만 해당 요청에 대해서 허용됩니다.

라는 의미로 코드를 사용했다.

이 때 인증이 되지 않은 사용자는
SpringSecurityFilterChain에 설정된 예외 Handler에 의해 처리되도록 했다.

NullPointerException이 발생한 이유
권한 상관없이 모든 요청을 허용했기 때문에 인증이 되지 않은 사용자라도, SpringSecurityFilterChain을 통과했고,
그로 인해서 Controller단에서 예외가 발생했다.

SpringSecurityFilter에서 처리해야 될 예외를 스프링 카탈리나에서 예외를 발생 시킨 것이다.

SpringSecurityFilterChain 자체에서 사용자 인증처리를 할 수 있도록 한다면, 컨트롤러에서 Exception을 발생시키지 않고, SpringSecurityFilter에서 설정한 exceptionHandler를 통해서 예외 처리를 할 수 있다.

그래서 이를 수정한 현재 'SpringSecurityConfiguration'코드를 보면, 다음과 같다.

antMatchers를 통해 도메인별로 Controller의 URL을 HTTP메서드와 맞게 매핑해주었다.

✅참고로 antMatchers는 범위를 좁은 것부터 설정해줘야한다..!!
✅그리고 점진적으로 범위가 넓은 URL을 매핑시켜줘야 한다.
✅범위가 넓은 것부터 처리되게 되면, 그보다 범위가 좁은 것은 넓은 것에 의해서 허용하지 말아야됨에도 불구하고, 허용하게 된다.

그리고 JwtVerificationFilter를 지나가기 전 JwtExceptionfilter를 생성해서,
JwtExceptionfilter를 거친 후, JwtVerificationFilter를 지나가게 했다.

왜냐하면,
자바의 예외 처리는 예외를 던질 때,
자신을 호출한 곳으로 예외를 던지도록 되어있기 때문이다.

만약 JwtVerificationFilter에서 예외가 발생하면 예외를 던질 것인데,
발생한 예외를 JwtExceptionfilter가 잡아서 응답을 처리하도록 하기위해 추가해주었다.

다음은 JwtExceptionFilter 클래스이다.

그리고 JwtVerification 클래스에서 JWT를 검증하면서 claims를 추출하는데,
예외가 발생했을 시 유형 별로 예외가 발생하도록 해주었다.

다음은 해당 코드다.

public Jws<Claims> getClaims(String jws, String base64EncodedSecretKey) {  
    Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);  

    Jws<Claims> claims;  
    try {  
        claims = Jwts.parserBuilder()  
                .setSigningKey(key)  
                .build()  
                .parseClaimsJws(jws);   // JWT를 검증하고 예외를 발생시키는 메서드  
    } catch (SignatureException se) {   // 토큰이 유효하지 않을 때,
        throw new CustomLogicException(ExceptionCode.TOKEN_INVALID);  
    } catch (ExpiredJwtException ee) {  // 토큰의 기간이 만료되었을 때,
        throw new CustomLogicException(ExceptionCode.TOKEN_EXPIRED);  
    }  
    return claims;  
}

이처럼 try-catch문을 통해서 JWT가 유효하지 않거나, 혹은 만료된 JWT에 대해서 예외를 던지게 했다.

여기서 발생한 예외는
JwtVerificationFilter가 호출했으므로, 결국은 JwtExceptionFiilter가 잡아서 처리한다.

만약 JWT가 있어야 하는데, 없는 경우는
SecurityContextAuthentication 객체가 저장되지 않으므로,
AuthenticationEntryPoint를 상속하거나, 구현한 객체에서 해당 예외를 처리하게 된다.

토큰이 없는 경우는 MemberAuthenticationEntryPoint 클래스를 통해서 처리하도록 했다.
코드는 다음과 같다.

만약 요청 Header에서 Authorization부분을 추출한 값(JWT)이 null이면,
AccessToken이 없는 것이므로, "Token이 없다"는 메세지와 함께 예외 처리 하도록 했다.

그리고 이제 Token관리를 하던 AOP클래스는 없다.;;
ㅋㅋㅋㅋ

2-3. 해결 결과

다음은 결과들이다.

  • 유효하지 않을 때,
  • 토큰이 없을 때,

  • 토큰의 기간이 만료되었을 때,

3. 요약

이전의 포스팅에서 AccessToken 검증을 AOP를 통해서 구현했지만,
이번에는 어떤 문제 때문에 그런 문제가 발생할 수 밖에 없었는지,
원인을 정확하게 이해해서 문제를 해결했다.

SpringSecurityFilterChain의 설정이 모든 권한에 대한 요청을 허용해주었는데,

antMatcher를 이용해서
HTTP메서드와 매핑된 URL별로 권한을 설정해주었다.
그리고 이 Filter내에서 예외가 발생하면 FilterChain에 설정한 예외 처리 핸들러를 통해서 예외가 처리되도록 했다.

그리고 각 JWT예외의 유형 별로 적절하게 예외처리 되도록 했다.


그리고 이번 문제로 인해서 SpringSecurity 프레임워크에 대해서 너무 큰 매력을 느꼈다.
SpringSecurity의 매커니즘과 각각 필터들의 역할에 대해서 감만 익히면, 정말 내 맘대로 인증 처리를 달거나, 짭짤하게 커스터마이징 가능하겠구나라는 생각을 가지게 되었다.


끝.

728x90
Comments