Spring Security
                
              @AuthenticationPrincipal
                채마스
                 2022. 2. 28. 20:51
              
                          
            개요
- 아래와 같이 웹 MVC 핸들러 아규먼트로 Principal 객체를 받을 수 있다.
@GetMapping("/")
public String index(Model model, Principal principal){
    if (principal == null){
        model.addAttribute("message", "null");
    }else {
        model.addAttribute("message", "hello" + principal.getName());
    }
}- 여기서 Principal 대신 Account 를 받을 수 없을까? -> @AuthenticationPrincipal 를 사용하면 된다.
@AuthenticationPrincipal
- SecurityContextHolder.getContext().getAuthentication().getPrincipal() 반환된 Principal 은 UserDetailService 의 loadUserbyUsername 메소드에서 (UserDatils) 타입으로 리턴된 값인 User객체이다. (위 컨트롤러의 파라미터는 principal 과는 다르다.)
@Service
public class AccountService implements UserDetailsService {
    @Autowired AccountRepository accountRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findByUsername(username);
        if (account == null) {
            throw new UsernameNotFoundException(username);
        }
        return User.builder()
                .username(account.getUsername())
                .password(account.getPassword())
                .roles(account.getRole())
                .build();
    }
}- Spring Security 에서 제공하는 User 를 상속받아 UserAccount 를 구현한다. -> UserDetails 와 Account를 연결해주는 어댑터 역할을 한다.
public class UserAccount extends User {
    private Account account;
    public UserAccount(Account account) {
        super(account.getUsername(), account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_" + account.getRole())));
    }
    public Account getAccount() {
        return account;
    }
}- 서비스는 아래와 같이 바뀔 수 있다.
@Service
public class AccountService implements UserDetailsService {
    @Autowired AccountRepository accountRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findByUsername(username);
        if (account == null) {
            throw new UsernameNotFoundException(username);
        }
        return new UserAccount(account);
    }
}- 위와 같이 반환값이 User 가 아닌 UserAccount가 반환되어 Principal 에 저장된다.
- 이제 Controller 에서 @AuthenticationPrincipal 를 이용해서 UserAccount 를 인자로 받을 수 있다.
@GetMapping("/")
public String index(Model model, @AuthenticationPrincipal UserAccount userAccount) {
    if (userAccount == null) {
        model.addAttribute("message", "null");
    } else {
        model.addAttribute("message", "hello" + userAccount.getUsername());
    }
    return "index";
}- 여기서 UserAccount가 아닌 Account 를 파라미터로 받고 싶다면? -> spring expression 을 사용해서 해결할 수 있다. ->@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")로 애노테이션을 설정해주면 된다.
커스텀 애노테이션 적용
- 파라미터로 Account를 받기위해서 파라미터 마다 @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account") 를 붙이기는 너무 번거롭다. -> 커스텀 애노테이션으로 이를 해결할 수 있다.
- CurrentUser 라는 커스텀 애노테이션을 구현한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
public @interface CurrentUser {
}@GetMapping("/")
public String index(Model model, @CurrentUser Account account) {
    if (account == null) {
        model.addAttribute("message", "Hello Spring Security");
    } else {
        model.addAttribute("message", "Hello" + account.getUsername());
    }
    return "index";
}- 위와 같이 Account 를 Principal 대신 받을 수 있다.
스프링 데이터 연동
- implementation 'org.springframework.boot:spring-boot-starter-security'로 dependency 를 추가해 준다.
- @Query 애노테이션에서 SpEL로 principal 참조할 수 있는 기능 제공한다.
- 아래와 같이 @Query에서 principal 을 사용할 수 있다.
- 여기서 principal 은 당연히 UserDetailService 의 loadUserbyUsername 메소드에서 (UserDatils) 타입으로 리턴된 값인 UserAccount 객체이다.
@Query("select b from Book b where b.author.id = ?#{principal.account.id}")
List<Book> findCurrentUserBooks();REFERENCES
- 백기선님의 스프링 시큐리티