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
'개발 > Spring' 카테고리의 다른 글
[SpringBoot] FCM을 통해 Push알림 보내보기 (15) | 2022.02.10 |
---|---|
[Spring] DispatcherServlet이란 무엇인가? (0) | 2022.02.10 |
[SpringBoot] Redis를 SpringBoot 프로젝트에서 사용해보자 (0) | 2022.02.10 |
[SpringBoot] Controller의 여러가지 요청과 응답 처리 (0) | 2022.02.10 |
[Spring] 스프링 컨테이너, Bean 등록 및 사용하기 (0) | 2022.02.10 |