Spring Boot

[Spring] @AuthenticationPrincipal 커스텀 어노테이션 만들기

HEY__ 2024. 8. 26. 12:23
728x90

이 글은 공부를 하면서 알게 된 내용들을 기록하는 글 입니다. 오류나 고쳐야 할 사항들이 있다면 지적 부탁드립니다!

✅ 커스텀 어노테이션이 필요한 이유

🔥 기존에 사용했던 방법

GitGet 프로젝트에서는 소셜로그인 & JWT 발급 이후에 서비스 이용이 가능합니다.

Spring security의 filterChain을 거치면서 SecurityContext에 인증된 객체 (`UserPrincipal`)에 있는 `User` 객체를 받아와 사용합니다.

 

`@AuthenticationPrincipal` 어노테이션 덕분에 SecurityContext의 메서드를 직접 호출하지 않아도 되지만, `userPrincipal.getUser()`와 같이 중복된 코드가 발생했습니다.

코드 리뷰를 받았을 때 커스텀 어노테이션을 작성하여 이를 적용하면 좋겠다는 피드백을 받았습니다.

@GetMapping("/items")
public ResponseEntity<ListResponse<ItemResponse>> getItemList(
    @AuthenticationPrincipal UserPrincipal userPrincipal,
    @RequestParam String category
) {
    ItemCategory itemCategory = ItemCategory.findCategory(category);
    User user = userPrincipal.getUser();
    List<ItemResponse> itemResponses = storeFacade.getItemsByCategory(user, itemCategory);

    return ResponseEntity.ok().body(
        new ListResponse<>(SUCCESS.getStatus(), SUCCESS.getMessage(), itemResponses)
    );
}

 

 

🔥 목표

커스텀 어노테이션을 적용하여 밑과 같이 중복 코드를 없애고, 바로 `user` 객체를 받아와 사용할 수 있도록 합시다!

@GetMapping("/items")
public ResponseEntity<ListResponse<ItemResponse>> getItemList(
    @GitGetUser User user,
    @RequestParam String category
) {
    ItemCategory itemCategory = ItemCategory.findCategory(category);
    List<ItemResponse> itemResponses = storeFacade.getItemsByCategory(user, itemCategory);

    return ResponseEntity.ok().body(
        new ListResponse<>(SUCCESS.getStatus(), SUCCESS.getMessage(), itemResponses)
    );
}

 @AuthenticationPrincipal 동작 방법

우리는 `@AuthenticationPrincipal` 어노테이션을 확장한 커스텀 어노테이션을 통해 User 객체를 받고자 했습니다.

`@AuthenticationPrincipal`은 어떻게 동작하는 것일까요?

 

공식문서를 통해 전체적인 흐름을 확인해봅시다!

 

https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/annotation/AuthenticationPrincipal.html

 

AuthenticationPrincipal (spring-security-docs 6.3.3 API)

If specified will use the provided SpEL expression to resolve the principal. This is convenient if users need to transform the result. For example, perhaps the user wants to resolve a CustomUser object that is final and is leveraging a UserDetailsService.

docs.spring.io

 

 

https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.html

 

AuthenticationPrincipalArgumentResolver (spring-security-docs 6.3.3 API)

Allows resolving the Authentication.getPrincipal() using the AuthenticationPrincipal annotation. For example, the following Controller: @Controller public class MyController { @MessageMapping("/im") public void im(@AuthenticationPrincipal CustomUser custom

docs.spring.io

 

 

 

`@AuthenticationPrincipal`은 `Authentication.getPrincipal()`의 값을 받기 위해 메서드의 파라미터로 사용할 수 있으며,

해당 어노테이션은 Spring security의 `AuthenticationPrincipalArgumentResolver`를 통해 SecurityContext에 있는 Authentication을 받을 수 있습니다.

공식 문서에서는 밑과 같이 설명하고 있습니다.

Allows resolving the Authentication.getPrincipal() using the AuthenticationPrincipal annotation. For example, the following Controller:
... 
Will resolve the CustomUser argument using `Authentication.getPrincipal()` from the SecurityContextHolder. If the Authentication or `Authentication.getPrincipal()` is null, it will return null. If the types do not match, null will be returned unless `AuthenticationPrincipal.errorOnInvalidType()` is true in which case a ClassCastException will be thrown.

`AuthenticationPrincipal` 어노테이션을 사용함으로서 `Authentication.getPrincipal()`의 값을 받을 수 있습니다.
...
`Authentication.getPrincipal()`을 통해 SecurityContextHolder에 있는 CustomUser 를 받아올 수 있습니다. `Authentication.getPrincipal()`가 null이라면 null을 반환합니다.
만일 type이 맞지 않은 경우, `AuthenticationPrincipal.errorOnInvalidType()`가 true가 아니라면 null을 반환합니다. (원래는 ClassCastException이 발생)

 

 

그 하단에는 커스텀 어노테이션을 만드는 방법도 설명하고 있습니다! 밑의 코드가 공식 문서에서 소개한 방법입니다.

 @Target({ ElementType.PARAMETER })
 @Retention(RetentionPolicy.RUNTIME)
 @AuthenticationPrincipal
 public @interface CurrentUser {
 }

 

 


✅ @GitGetUser 커스텀 어노테이션 만들기

만들고 싶은 커스텀 어노테이션 `@GitGetUser`는 여러 Controller에서 사용되었던 `@AuthenticationPrincipal`을 대체할 어노테이션입니다.

 

@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : user")
public @interface GitGetUser {
}

 

어노테이션을 생성할 때에는 `@interface`로 선언하여 생성할 수 있고, @Target, @Retention 어노테이션 등을 통해 어노테이션에 대한 설정을 해주어야 합니다.

 

🔥 Retention policy (@Retention)

@Retention 어노테이션을 통해 Retention policy를 설정할 수 있습니다. Retention을 영어 사전에 검색해보면 보유, 유지라는 결과가 나옵니다.

@Retention은 어노테이션을 얼마나 유지할지에 대해 정의하며, 옵션에는 `SOURCE`, `CLASS`, `RUNTIME`이 있습니다.

Retention 어노테이션에 

 

1) RetentionPolicy.SOURCE

소스 코드에서만 유지되며, 컴파일 타임이 되면 컴파일러에 의해 제거됩니다. 컴파일하기 이전까지만 영향을 미칩니다.

`@Override`, `@Deprecated`, `@Functional`와 같은 어노테이션에서 이 정책을 사용합니다.

 

2) RetentionPolicy.CLASS

컴파일러에 의해 코드를 컴파일 할 때까지 유지하고, 런타임에는 제거됩니다.

 

3) RetentionPolicy.RUNTIME

런타임동안 JVM을 통해 어노테이션이 유지됩니다. 

 

🔥 Target (@Target)

어노테이션을 적용할 위치를 정의하는 옵션입니다.

`ElementType` enum의 값을 매개변수(argument)로 받으며, 어노테이션을 적용할 수 있는 위치를 지정할 수 있습니다.

 

METHOD, FIELD, CONSTUCTOR, PARAMETER, ANNOTATION_TYPE와 같이 어노테이션을 적용하고자하는 곳에 맞는 값을 넣어주면 됩니다.

 

여러 곳에 적용하고자 한다면 중괄호(`{}`) 안에 값을 여러 개 넣어 적용할 수 있습니다.

 

 

🔥 @AuthenticationPrincipal

Spring 공식 문서에서 소개한 방법에 의하면 `@AuthenticationPrincipal` 어노테이션에 별도의 옵션을 넣어주지 않고 있습니다. 

하지만 테스트를 돌려본 결과 분명 SecurityContextHolder에 인증 값이 있음에도 불구하고, User의 값이 null이 뜨는 상황이 발생했습니다.

 

구글링을 해본 결과, `@AuthenticationPrincipal` 어노테이션에 SpEL(Spring Expression Language) 표현식을 통해 어떤 필드의 값을 가져올 것인지 설정을 해줘야 한다고 합니다.

 

@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : user")

 

만일 현재의 객체(`#this`)가 `anonymousUser` 즉, Spring security에서 인증되지 않은 사용자라면 `null`을 반환하고, 

그렇지 않은 경우 즉, Spring security에서 인증된 사용자라면 Authentication.getPrincipal을 통해 `user` 객체를 반환받습니다.

 

 

💡SpEL에서 반환 값으로 user을 설정한 이유는, 최종적으로 받고자하는 값이 UserPrincipal의 user 객체이기 때문입니다.

만일 반환받고자하는 값이 User가 아니라 다른 값이라면 해당 값을 넣어주시면 됩니다!

@Getter
public class UserPrincipal implements UserDetails, OAuth2User {

    private User user;
    private String nameAttributeKey;
    private Map<String, Object> attributes;
    private Collection<? extends GrantedAuthority> authorities;

    ...
}

 

 

🔥 동작 확인

그럼 우리가 만든 커스텀 어노테이션이 제대로 작동하는지 확인해볼까요?

 

사용법은 아주 간단합니다.

이전에 @AuthenticationPrincipal을 사용했던 것처럼, 컨트롤러의 메서드에 어노테이션을 작성하면 됩니다.

@GetMapping("/test")
public String test(@GitGetUser User user) {
    return "";
}

 

break point를 걸고 User 객체를 확인해보면 우리가 원하던 객체를 정상적으로 받아옴을 확인할 수 있습니다!

 


✅  참고 자료 & 링크

- Spring Boot에서 @AuthUser 커스텀 어노테이션 생성

- @AuthenticationPrincipal 로그인한 사용자 정보 받아오기

- @AuthenticationPrincipal 동작 원리와 사용 예시

- Create your own custom annotation in Spring Boot

 

 

728x90