[Spring Security] @AuthenticationPrincipal, HandlerMethodArgumentResolver

 스프링 시큐리티와 jwt를 이용하여 api를 개발할때, 사용자의 정보를 제대로 전송하기 위해서는 크게 @AuthenticationPrincipal 어노테이션을 이용하여 인증이 필요한 요청마다 SecurityContextHolder에 저장한 인증값을 가져오는 방법도 있고, HandlerMethodArgumentResolver를 이용하여 토큰에서 인증값을 가져오는 클래스를 직접 만드는 방법이 있다.

 

 

@AuthenticationPrincipal 사용하기

 

 @AuthenticationPrincipal 어노테이션을 사용하면 UserDetailsService의 loadUserByUsername을 통해서 return한 객체(UserDetails)를 파라미터로 직접 받아서 사용할 수 있다.

@PostMapping("/create")
fun createArticle(@Valid @RequestBody dto:CreateArticleRequestDTO, @AuthenticationPrincipal user:UserDetails):CreateArticleResponseDTO {
//        println("test:"+user.toString())
    return articleService.createArticle(user.username, dto.title, dto.content)
}
@Transactional
fun createArticle(email:String, title:String, content:String):CreateArticleResponseDTO {
    var user = userRepository.findByEmail(email)
    if(user==null)
        throw UserNotFoundException("유저 정보를 찾을 수 없습니다.")

    val article = Article(title, content, user)
    articleRepository.save(article)

    return CreateArticleResponseDTO(article.article_id, user.email!!, article.title!!, article.content!!)
}

이렇게 SecurityContextHolder에서 직접 UserDetails를 가져오고, UserDetails의 principal(email)을 가져와서 userRepository에서 찾아서 사용할 수 있다.

 

추가)

엔티티 객체를 직접 받아오는 것보다는, 엔티티 객체를 필드로 갖는 User 어댑터 클래스(dto)를 만들어 반환하는 어댑터 패턴을 이용하는 것이 좋다. (나중에 실습 - User(https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/userdetails/User.html)객체에 대해서 알아보기)

-> 2023.09 현장실습중 회사 시큐리티 로직이 User에 어댑터 패턴이 적용되어 있는것을 확인하였다. UserDetails를 직접 커스텀하는것과의 차이는 초기 개발환경에는 커스텀할 필요가 없다고 생각했고, 다시 말해 조건에 따라서 선택해서 사용하면 될 것 같다.

 

 

궁금)

+Q. 나의 경우 UserDetails가 아니라 그것을 커스텀한 MyUserDetails를 사용했는데, 이렇게 하면 어노테이션이 동작하지 않는것같음. UserDetailsService의 loadUserByUsername에서는 일단 MyUserDetails를 반환하게 해놨는데, 그냥 인자만 UserDetails타입으로 설정해주어야 하는것인지, 아니면 아예 MyUserDetails가 사용되지 않는 것인지 모르겠다.

 

 

 

HandlerMethodArgumentResolver로 커스텀하기

 

 HandlerMethodArgumentResolver는 특정 조건을 만족하는 파라미터가 있을 때 그것을 바인딩해주는 인터페이스이다. 이것을 사용하면 토큰으로부터 email의 값을 직접 얻어서 바인딩 해주는 어노테이션 클래스를 만들 수 있다. 

 

 

spring api doc 사이트에서 찾아보면 supportsParameter와 resolveArgument라는 두가지 메소드를 가지는 것을 볼 수 있다. supportsParameter는 어떤 파라미터에 작업을 수행할 것인지를 정의하는 메소드이고, resolveArgument에 해당 파라미터를 사용했을때 어떤 로직을 수행할지를 작성해주면 된다.

 

class AuthArgumentResolver:HandlerMethodArgumentResolver {

    //대상이 되는 파라미터를 정의
    override fun supportsParameter(parameter: MethodParameter): Boolean {
        return parameter.hasParameterAnnotation(Auth::class.java)
                && parameter.parameterType == AuthInfo::class.java
    }

    //Auth 어노테이션을 사용하면 jwt토큰의 email값이 AuthInfo의 email로 바인딩되도록 해야 한다.
    override fun resolveArgument(
        parameter: MethodParameter,
        mavContainer: ModelAndViewContainer?,
        webRequest: NativeWebRequest,
        binderFactory: WebDataBinderFactory?
    ): Any {
        //SecurityContextHolder에서 인증객체의 username(email) 반환되게끔
        val authentication = SecurityContextHolder.getContext().authentication.principal as UserDetails
        return AuthInfo(authentication.username)
    }
}

 

supportsParameter에서 Auth라는 어노테이션을 가지고, 파라미터 타입이 AuthInfo인 파라미터를 대상으로 동작하도록 설정하고, resolveArgument에서 해당 파라미터값으로 전달해줄 값을 설정해주면 된다. 여기서는 SecurityContextHolder에서 인증객체를 가져온다음, 인증객체의 email을 가져와서 해당 파라미터에 주입시켜주고 있다.

 

이제 이 ArgumentResolver를 스프링 빈으로 등록시켜주고, WebMvcConfigurer를 상속받은 addArgumentResolvers에서 resolver 리스트에 추가해주면 동작하게 된다.

 

class AuthInfo (
    var email:String
)

 

@PostMapping("/withdraw")
fun withdraw(@Auth authInfo: AuthInfo) {
    return userService.withdraw(authInfo.email)
}

그러면 위와 같이 authInfo라는 일종의 DTO를 통해 값을 넘겨받아서 서비스 로직을 수행할 수 있다.