@LoginUser 커스텀 어노테이션을 이용해 로그인 세션 정보를 받아왔으나 null 일 때에도 세션에 정보가 담기는 에러가 있었다. 따라서 @AuthenticationPrincipal
애노테이션을 이용해 로그인 세션 정보를 받아오는 방법으로 변경했다.
아직 미해결한 점이 있다. user 엔티티가 아니라 userDto로 감싸려고 하는데 로그인 정보가 없을 때, 즉 비로그인 사용자일 때에는 null인데 이 null처리가 까다로웠다.
CustomUserDetails
을 상속받은 UserAdapter
에 따로 getUserDto 메서드를 만들어 Controller에서 UserDetails가 기본적으로 제공하는 정보 외 로그인 유저 정보 (닉네임 등)를 getUserDto.getXXX 형식으로 받기로 했다.로그인 사용자의 정보가 필요할 때 매번 서버에 요청을 보내 DB에 접근해서 데이터를 가져오는 것은 비효율적이다.
따라서 한번 인증된 사용자 정보를 세션에 담아놓고 세션이 유지되는 동안 사용자 객체를 DB로 접근하는 방법 없이 바로 사용할 수 있도록 한다.
Spring Security
에서는 해당 정보를 SecurityContextHolder
내부의 SecurityContext
에 Authentication
객체로 저장해두고 있으며 이를 참조하는 방법은 크게 3가지가 있다.
- 컨트롤러에서 Principal 객체를 주입받아 사용
- 가장 간단한 방법
Spring Security가 제공하는SecurityContextHolder
의 Principal 객체가 아닌 자바에 정의돼있는Principal
객체를 바인딩해주는 것이므로 사용할 수 있는 메소드가getName()
뿐이다.- 컨트롤러에서 @AuthenticationPrincipal 선언하여 엔티티 객체 받아오기
- 엔티티에 있는 모든 필드 참조 가능
- 컨트롤러에서 @AuthenticationPrincipal 선언하여 엔티티의 어댑터 객체 받아오기(가장 권장)
- 엔티티 객체를 필드로 갖고 있는 어댑터 클래스(DTO) 생성하여 회원 객체(User) 상속
- 해당 어댑터 클래스의 엔티티 객체는 DB의 회원 객체의 정보를 담고 있어야 한다
- UserDetailsService의
loadByUsername()
에서 어댑터 클래스를 반환하도록 수정
스프링 시큐리티에서는 세션에 현재 사용자 정보를 다음과 같이 조회할 수 있다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User)authentication.getPrincipal();
principal
객체는 자바 표준 객체이며 우리가 받을 있는 정보는 name 뿐이다.
name 말고도 다양한 정보를 얻기 위해 @AuthenticationPrincipal
애노테이션과 어댑터 패턴을 적용해 사용자 세션 정보를 사용할 수 있다.
세션 정보 UserDetails에 접근할 수 있는 어노테이션
@AuthenticationPrincipal
은 UserDetails
타입을 가지고 있음 -> UserDetails
타입을 구현한 PrincipalDetails
클래스를 받아 User object를 얻는다
따라서 로그인 세션 정보가 필요한 컨트롤러에서 매번 @AuthenticationPrincipal로 세션 정보를 받아서 사용한다.
UserAdapter
타입UserDetails
로 묶어 처리할 수 있게 코드를 리팩토링했다.CustomUserDetails implements UserDetails, OAuth2User
package com.jy.config.auth;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import com.jy.domain.user.User;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
public class CustomUserDetails implements UserDetails, OAuth2User {
private User user;
private Map<String, Object> attribute;
/* 일반 로그인 생성자 */
public CustomUserDetails(User user) {
this.user = user;
}
/* OAuth2 로그인 사용자 */
public CustomUserDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attribute = attributes;
}
/* 유저의 권한 목록, 권한 반환*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole().getValue();
}
});
return collect;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// public String getNickname() {
// return user.getNickname();
// }
/* 계정 만료 여부
* true : 만료 안됨
* false : 만료
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/* 계정 잠김 여부
* true : 잠기지 않음
* false : 잠김
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/* 비밀번호 만료 여부
* true : 만료 안 됨
* false : 만료
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/* 사용자 활성화 여부
* true : 활성화 됨
* false : 활성화 안 됨
*/
@Override
public boolean isEnabled() {
return true;
}
/* OAuth2User 타입 오버라이딩 */
@Override
public Map<String, Object> getAttributes() {
return null;
}
@Override
public String getName() {
return null;
}
}
public CustomUserDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attribute = attributes;
}
UserDetails
을 상속받아야 한다.loadUserByUsername
메서드의 반환 타입이 UserDetails
이기 때문)@Getter
public class UserAdapter extends CustomUserDetails{
private Member member;
private Map<String, Object> attributes;
public UserAdapter(Member member){
super(member);
this.member = member;
}
public UserAdapter(Member member, Map<String, Object> attributes){
super(member, attributes);
this.member = member;
this.attributes = attributes;
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
private final HttpSession session;
/** username이 DB에 존재하는지 확인 **/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findByUsername(username).orElseThrow(() ->
new UsernameNotFoundException("사용자가 존재하지 않습니다."));
/** 시큐리티 세션에 유저 정보 저장**/
return new UserAdapter(member);
}
}
UserDetails
를 상속 받은 UserAdapter
을 통해 커스텀한 Principal를 사용할 수 있다.
인증을 담당하는 loadUserByUsername
에서 Principal(UserDetails)
대신 위에서 만든 UserAdapter
을 반환한다.
loadUserByUsername메서드의 반환되는 타입을 변경하기 위해 Principal(UserDetails)을 커스텀한 것이다.
(스프링 세션을 사용하면 첫 로그인 시에만 loadUserByUsername메서드가 호출된다.
JWT로 구현하였다면 매 요청마다 loadUserByUsername메서드가 호출된다.)
참고
https://sol-devlog.tistory.com/3
https://dev-gyus.github.io/spring/2021/03/09/Spring-ConfigurationProperties0.html
https://moonsbeen.tistory.com/312
https://velog.io/@yoho98/Spring-Security-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%9B%84-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A0%95%EB%B3%B4%EC%96%BB%EA%B8%B0
인터셉터 관련
https://semtax.tistory.com/74?category=804335
기타
https://azurealstn.tistory.com/91
https://tmdrl5779.tistory.com/72