스프링 시큐리티는 인증된 정보를 Authentication으로 감싸서 Security Context Holder에 보관해 놓는다.
아래와 같이 메서드 파라미터로 Authentication을 받거나, @AuthenticationPrincipal 을 부착한
UserDetails 구현체를 받으면(PrincipalDetails implements UserDetails) 로그인한 유저의 세션 정보를 얻을 수 있다.
참고로 Authentication 객체의 getPrincipal()의 리턴 타입은 Object다.
@Controller
public class IndexController {
@GetMapping("/test/login")
public @ResponseBody
String testLogin(Authentication authentication, @AuthenticationPrincipal PrincipalDetails userDetails) {
System.out.println("/test/login======");
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
System.out.println("authentication : " + principalDetails.getUser());
System.out.println("userDetails:" + userDetails.getUser());
return "세션 정보 확인하기";
}
}
로그를 보면, 결과가 똑같다.
authentication : User(id=1, username=ssar, password=$2a$10$2qissCjGbv8EBCpbp.bfyusrCakaSgeOLnzDTDuHTTpexp5FUJIxq, email=ssar@naver.com, role=ROLE_USER, provider=null, providerId=null, createDate=2021-12-30 11:11:59.0)
userDetails:User(id=1, username=ssar, password=$2a$10$2qissCjGbv8EBCpbp.bfyusrCakaSgeOLnzDTDuHTTpexp5FUJIxq, email=ssar@naver.com, role=ROLE_USER, provider=null, providerId=null, createDate=2021-12-30 11:11:59.0)
OAuth 로그인 유저의 세션 정보를 얻는 방법은 약간 다르다.
OAuth 로그인 유저의 경우 UserDetails 구현체로 다운 캐스팅하면 에러가 난다.
대신, OAuth2User로 다운 캐스팅해야 에러가 발생하지 않고 세션 정보를 얻을 수 있다.
이 역시 로그를 보면 결과가 같다. (보안상 이유로 생략)
@Controller
public class IndexController {
@GetMapping("/test/oauth/login")
public @ResponseBody
String testOauthLogin(Authentication authentication, @AuthenticationPrincipal OAuth2User oauth) {
System.out.println("/test/login======");
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); // Oauth 의 경우엔 OAuth2User 로 다운 캐스팅해야 에러 안난다.
System.out.println("authentication : " + oAuth2User.getAttributes());
System.out.println("oauth : "+oauth.getAttributes());
return "oauth 세션 정보 확인하기";
}
}
인증 정보를 담는 Authentication은 UserDetails 구현체 또는 OAuth2User 구현체를 가질 수 있다.
전자의 경우 일반 로그인 유저의 인증 정보를 담고, 후자의 경우는 OAuth 로그인 유저의 인증정보를 담는다.
하지만 이로 발생할 수 있는 문제가 있다.
로그인 방법마다 인증 정보를 담는 객체의 타입이 다르다면, 컨트롤러의 메서드마다 두 구현체를 파라미터로 받아야 할까?
다음과 같이 로직은 똑같은데, 로그인 방법에 따라 파라미터를 달리 해야한다면 유지보수 상에 엄청난 비용이 따른다.
public doSomething(@AuthenticationPrincipal UserDetials userDetails){}
public doSomething(@AuthenticationPrincipal OAuth2User oauth){}
해결책
UserDetials 와 OAuth2User를 모두 implements 한 클래스를 만들고, 이를 사용하면 된다.
public class PrincipalDetails implements UserDetails, OAuth2User {
private final User user;//합성을 이용
private Map<String,Object>attributes;
//일반 로그인 시 사용되는 생성자
public PrincipalDetails(User user) {
this.user = user;
}
//Oauth 로그인 시 사용되는 생성자.
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
//해당 User 의 권한을 리턴하는 메서드
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
//비밀 번호 만기 여부
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//계정 활성화 여부.
//휴면 계정 전환할 때 쓰임.
@Override
public boolean isEnabled() {
return true;
}
//아래부터 Oauth 관련 메서드. OAuth2User 오버라이드 메서드임.
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
//Oauth 의 기본키 getter 메서드. 잘 안씀.
@Override
public String getName() {
return null;
}
}
이렇게 하면, 로그인 방식에 따라 메소드를 여러개 만들 필요가 없어진다. 예를 들면 다음과 같다.
@GetMapping("/user")
public @ResponseBody
String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
return "user";
}