본문 바로가기
개발/Spring

[Spring Security] @AuthenticationPrincipal 로그인한 사용자 정보 받아오기

by solchan98 2022. 2. 10.

Spring Security에서는 Session에서 현재 사용자의 정보를 다음과 같이 Principal로 조회할 수 있다.

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User)authentication.getPrincipal();

Principal 객체는 Java 표준 객체이고, 받을 수 있는 정보는 name뿐이다. 하지만 우리는 name뿐이 아닌 Account의 많은 정보를 얻고싶다. 그리고 Account의 정보를 Controller에서 맵핑 메서드의 파라미터로 받는 것을 효율적으로 받기 위해 @AuthenticationPrincipal과 어댑터 패턴을 적용하여 사용할 수 있다.

즉,

  • 로그인한 사용자 정보를 어노테이션을 통해 간편하게 받고싶다.
  • 정보는 name뿐이 아닌 Account의 많은 정보를 받고싶다.
  • Controller의 맵핑 메서드의 길어지는 파라미터의 코드를 효율적으로 줄이고 싶다.

Adapter (Account)

Account 객체로 직접 받을 수 있지만 다음과 같은 이유로 어댑터 패턴을 사용한다.

  • 정보 객체로 사용되는 객체는 UserDetails를 구현하는 User를 상속받아야 한다.
    왜? loadUserByUsername메서드의 반환 타입이 UserDetails이기 때문이다.
  • account에 UserDetails를 직접 구현하면 도메인 객체는 특정 기술에 종속되지 않도록 개발하는 Best Practice(모범 사례)에 위반하게 된다.
@Getter 
public class UserAccount extends User {
  private Account account;
  public UserAccount(Account account) {
    	super(account.getEmail(), account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_USER"))); 
        this.account = account; 
  } 
}
  • 따라서 UserAccount 객체를 생성하여 사용한다.

UserDetailsService (AccountService)

@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    Account account = accountRepository.findByEmail(email);
    if (account == null) {
        throw new UsernameNotFoundException(email);
    }
    return new UserAccount(account);
}

User를 상속받는 UserAccount를 통해 커스텀한 Principal를 사용할 수 있게 되었다.

그래서 인증을 하는 loadUserByUsername에서 Principal(UserDetails) 대신 위에서 만든 UserAccount를 리턴을 한다.

즉, loadUserByUsername메서드의 반환되는 타입을 변경하기 위해 Principal(UserDetails)을 커스텀한 것이다.
(스프링 세션을 사용하면 첫 로그인 시에만 loadUserByUsername메서드가 호출된다.
JWT로 구현하였다면 매 요청마다 loadUserByUsername메서드가 호출된다.)

Controller에서 실제 사용(3가지 방법)

1. UserAccount 객체로 받아와 사용하기

@GetMapping("/")
public String home(@AuthenticationPrincipal UserAccount userAccount, Model model){
    if(userAccount != null) {
        model.addAttribute(userAccount.getAccount());
    }
    return "index";
}

다음과 같이 UserAccount 객체로 직접 받아와 사용할 수 있다.

하지만 getAccount() 메서드를 통해 account 객체를 가져와서 사용해야 하는데 이는 getAccount()의 코드가 중복될 가능성이 있다.

메서드에서 한 번 getAccount()로 가져오면 되지만, 다른 메서드에서는 또 다시 getAccount()로 꺼내와야 하기 때문이다.

2. SpEL 사용하기

@AuthenticationPrincipal은 SpEL을 지원한다.
SpEL을 통해 Adapter 객체가 아닌 Account를 직접 가져올 수 있다.
SpEL에 대한 자세한 내용 - 참고자료 3번

@GetMapping("/")
public String home(@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account") Account account, Model model){
    if(account != null) {
        model.addAttribute(account);
    }
    return "index";
}

다음과 같이

  • AnonymousAuthenticationFilter에 의해 생성된 Authentication의 경우 null을 반환.
  • UserAccount 객체가 확인되면 UserAccount의 account를 반환한다.

하지만 위 방식은 파라미터 공간의 코드가 너무 길어 가독성이 매우 떨어진다.
모든 컨트롤러 맵핑 메서드의 파라미터 공간에 매우 긴 코드를 계속 넣을수는 없다.

3. 커스텀 어노테이션 사용

커스텀 어노테이션을 만들어 사용한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
public @interface AuthUser {
}
  • @Retention(RetentionPolicy.RUNTIME) : 설정한 부분까지 정책이 유지되어야 한다. / RunTime
  • @Target(ElementType.PARAMETER) : 커스텀한 어노테이션이 사용되는 위치를 설정한다. / 파라미터
  • @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")2번 방법과 비교해보면 맵핑 메서드의 파라미터 공간의 가독성이 매우 좋아졌음을 알 수 있다.
@GetMapping("/") 
public String home(@AuthUser Account account, Model model){ 
  if(account != null) { 
    model.addAttribute(account); 
  } 
  return "index"; 
}

통합 정리

  • @AuthenticationPrincipal을 통해 로그인한 사용자 정보를 받아 사용할 수 있다.
  • Account의 어댑터 클래스를 정의하여 시큐리티가 제공하는 로그인한 사용자 정보 조회 타입을 Principal(UserDetails)에서 UserAccount로 커스텀할 수 있다.
    • UserAccount는 UserDetails를 상속받은 Account 객체를 담고있는 어댑터이다.
    • UserAccount로 커스텀하기 위해 loadUserByUserName의 반환 객체를 UserAccount로 커스텀해야한다.
  • 어노테이션을 커스텀하여 코드의 가독성을 높일 수 있다.

해당 게시글은 공부를 하며 기록해 나가는 글입니다.
혹여나 정리 내용에서 잘못된 부분이 있다면 언제든 피드백은 환영입니다!

참고자료

https://ncucu.me/137
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=lsc401&logNo=221619361975
https://blog.outsider.ne.kr/835