본문 바로가기

기록/Web

[Spring Security] @AuthenticationPrincipal과 ArgumentResolver

SMALL

스프링 시큐리티를 구현하며 가장 어려웠던 점은 커스텀한 객체를 사용하여 메소드도 그에 맞게 변환하는 과정이었다.

기존 컨트롤러에서 인증된 객체를 가져오는 메소드로 다음과 같은 방식을 사용했다.

매번 객체를 꺼내줘야하고, 컨트롤러가 지저분해보이기도 하다. 그래서 아예 핸들러 파라미터에서 @AuthenticationPrincipal 어노테이션을 사용하여 로그인 객체를 받아오도록 구현했다.

 


문제 1. 리턴된 객체의 null

@AuthenticationPrincipal 어노테이션은 UserDetailService에서 return하는 객체를 받아와 사용한다.

return된 객체를 로그로 출력했을 때 객체 타입도 나오지 않고 null이 반환되는 결과를 얻었다.

찾아보니 Spring boot 2.4.5 버전 또는 그 이상 버전의 일부에서 어노테이션을 사용했을 경우 null이 발생한다고 한다.

그래서 이 어노테이션을 제거했더니 객체는 나오지만 들어있어야 할 내부 정보들이 하나도 없었다.

 

문제 2. 비어있는 객체 내부

일반적인 (스프링에서 제공해주는) UserDetails와 user를 사용했다면 위의 문제로 해결되었을 것이다.

사실 문제 1을 해결하는 방법에는 위의 어노테이션 제거를 하는 방법도 있지만, PrincipalMethodArgumentResolver를 사용하는 방법도 있다. 어노테이션 제거 방법으로 해결을 못했으니 resolver를 사용하는 방식으로 자료를 찾아보았다.

 

https://sas-study.tistory.com/410

 

[Spring Security] @AuthenticationPrincipal 어노테이션은 어떻게 동작할까??

안녕하세요. 오늘은 스프링 시큐리티를 활용하면서 궁금했던 부분을 공부해보았습니다. 스프링 시큐리티는 SecurityContext에 인증된 Authentication 객체를 넣어두고 현재 스레드 내에서 공유

sas-study.tistory.com

이 블로그의 내용을 보면서 처음 인증 부분을 구현하며 custom 객체라서 추가로 커스텀 설정해줘야 했던 부분이 떠올랐다. 이번 문제도 로그인 객체를 구현할 때 UserDetails 인터페이스를 받아 구현해서 내가 만든 객체가 들어오지 않고 UserDetails의 디폴트 객체가 나오는 것 같다는 생각을 했다. 그리고 인터셉터, 핸들러를 구현한다면 객체를 받아오는 과정에서 내 객체를 넣어 리턴할 수 있을 것 같다는 생각을 했다.

 


해결

결론적으로, HandlerMethodArgumentResolver와 이 핸들러를 WebConfig에 등록해서 문제를 해결했다.

 

 

- CustomArgumentResolver

이 클래스는 필수 메소드로 두가지의 메소드를 구현해야 한다. 

 

public class CustomArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        Class<?> parameterType = methodParameter.getParameterType();
        return LoginDTO.class.equals(parameterType);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter,
                                  ModelAndViewContainer modelAndViewContainer,
                                  NativeWebRequest nativeWebRequest,
                                  WebDataBinderFactory webDataBinderFactory) throws Exception {
        Object principal = null;
        MyAuthentication authentication =
                (MyAuthentication) SecurityContextHolder.getContext().getAuthentication();
        if(authentication != null ) {
            principal = authentication.principal;
        }
        if(principal == null || principal.getClass() == String.class) {
            return null;
        }

        return principal;
    }

}

 

- supportsParameter

받아온 파라미터 타입이 설정한 타입과 같을 경우 true를 리턴한다. 

예제의 경우 로그인 객체를 LoginDTO라고 설정했기 때문에 받아온 타입이 LoginDTO라면 true를 리턴한다.

 

- resolveArgument

실제 바인딩할 객체를 만들어 리턴한다.

원래 컨트롤러에서 사용하던 authentication을 구하는 객체를 여기서 사용하여 리턴하도록 구현했다.

이부분은 정말로 "커스텀"이기 때문에 리턴할 객체에 맞춰 작성하면 되는 곳이다.

 

- WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public CustomArgumentResolver customArgumentResolver() {
        return new CustomArgumentResolver();
    }


    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(customArgumentResolver());
    }
}

- CustomArgumentResolver

위에서 작성한 resolver 파일을 Bean으로 만들어서 등록한다.

 

- addArgumentResolvers(List)

리스트 형태로 resolver를 넣어 관리한다. 이 리스트에 등록되면 호출하여 사용이 가능하다.

 


이번 오류를 통해 스프링 시큐리티의 인증 권한의 객체를 리턴하는 메소드를 깊이 생각해볼 수 있었다.

원래 하던 방식으로 매번 메소드를 호출하는 방법을 계속 사용할 수도 있었지만, 팀 프로젝트를 하고 있고, 더 나은 방식을 고민하면서 해결했다. 시큐리티를 하면서 정말 힘들었다고 생각했는데 커스텀 객체들은 꼬리를 물고 몰랐던 것을 공부하게 만든다.. 

 

 

SMALL