이전 방식
@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 애노테이션과 어댑터 패턴을 적용해 사용자 세션 정보를 사용할 수 있다.
@AuthenticationPrincipal
세션 정보 UserDetails에 접근할 수 있는 어노테이션
@AuthenticationPrincipal 은 UserDetails 타입을 가지고 있음 -> UserDetails 타입을 구현한 PrincipalDetails 클래스를 받아 User object를 얻는다
- userDetails(PrincipalDetails 타입).getUser()
따라서 로그인 세션 정보가 필요한 컨트롤러에서 매번 @AuthenticationPrincipal로 세션 정보를 받아서 사용한다.
- @AuthenticationPrincipal UserAdapter 타입
- 로그인 세션 정보를 애노테이션으로 간편하게 받을 수 있다
- UserDetailsService에서 Return한 객체를 파라미터로 직접 받아 사용할 수 있다.
- name 뿐만 아니라 다양한 정보를 받을 수 있다
- 중복 코드를 효율적으로 줄일 수 있다
CustomUserDetails
- UserDetails, OAuth2User, 즉 기존 폼 로그인과 OAuth 2.0 로그인을 따로 처리하지 않고 함께 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;
}
}
- 처음에 getNickname 하려고 했지만 직렬화 관련 에러가 났다. local class incompatible 그래서 오버라이딩할 수 있는 메소드만 오버라이딩했다.
- UserDetails 타입 오버라이딩
- OAuth2User 타입 오버라이딩 (getAttributes(), getName())
- 마찬가지로 OAuth2User 생성자도 만들었다.
public CustomUserDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attribute = attributes;
}
UserAdapter
- User 정보를 직접 받는 게 아니라 어댑터 패턴을 이용해 받는다.
- 정보 객체로 사용되는 객체는 UserDetails을 상속받아야 한다.
(loadUserByUsername 메서드의 반환 타입이 UserDetails이기 때문) - 도메인 객체는 특정 기술에 종속되지 않도록 개발해야 한다는 것이 Best Practice
- 정보 객체로 사용되는 객체는 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;
}
}
CustomUserDetailsService
@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메서드가 호출된다.)
'Spring' 카테고리의 다른 글
Spring. JPA ORM 및 연관관계 (0) | 2022.10.13 |
---|---|
Spring. Form input값 DTO 자동 주입 (1) | 2022.10.08 |
Spring. Model객체 / model.addAttribute (0) | 2022.10.07 |
Spring Security. UserDetails / UserDetailsService란? (0) | 2022.10.07 |
Spring. DI / IOC / Bean 이란? (0) | 2022.10.06 |
댓글