solyrion
[Spring Security] CustomUserDetails 본문
스프링 시큐리티를 사용해서 로그인을 구현했다면,
실제 Controller나 Service 계층에서 로그인 한 Member의 정보를 활용해야 합니다.
사용자 로그인
- Spring Security가 UserDetailsService의 loadUserByUsername(username)을 호출하여 UserDetails 객체를 가져옴.
- 해당 UserDetails 객체는 SecurityContext에 저장됨.
- 이후 @AuthenticationPrincipal을 사용하면, 현재 로그인한 사용자의 UserDetails 객체를 가져올 수 있음.
따라서 저는 처음에 아래와 같이 코드를 작성했습니다.
@PatchMapping("/{postId}")
public void updatePost(@PathVariable Long postId,
@RequestBody PostDto postDto,
@AuthenticationPrincipal UserDetails user){
String title = postDto.getTitle();
String content = postDto.getContent();
String fileUrl = postDto.getFile();
String email = user.getUsername();
MemberEntity member = memberRepository.findByEmail(email)
.orElseThrow(() -> new EntityNotFoundException(email));
postService.updatePost(postId, title, content, fileUrl, member.getId());
}
@AuthenticationPrincipal 통해 UserDetails 정보를 가져오고, UserDetails에서 설정한 username을 가져와서 Member 객체를 찾습니다.
여기서 불편한 점은 Member 엔티티에서 username에 설정한 정보가 아닌 다른 정보가 필요하다면 굳이 리포지토리에서 Member를 찾아서 서비스 계층에 넘겨줘야 합니다.
@Service
@Transactional
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
MemberEntity member = memberRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email));
return User.builder()
.username(member.getEmail())
.password(member.getPassword())
.roles("USER")
.build();
}
}
이미 UserDetails 객체를 반환하는 loadUserByUsername에서 Member를 찾아서 반환하는 과정이 있는데,
불필요하게 Controller에서 다시 한번 Member를 찾고 있습니다.
만약 loadUserByUsername에서 이왕 Member를 찾는 김에 Controller에서 필요한 정보를 같이 반환 해준다면
Controller에서 불 필요하게 한번 더 Member를 찾을 필요는 없다고 생각했습니다.
하지만 그냥 UserDetails에서는 추가 정보를 넣을 수 없기 때문에 CustomUserDetails를 구현했습니다.
@Getter
public class CustomUserDetails extends User {
private final Long id;
private final String nickname;
public CustomUserDetails(Long id, String email, String password, String nickname,
Collection<? extends GrantedAuthority> authorities){
super(email, password, authorities);
this.id = id;
this.nickname = nickname;
}
}
UserDetails에 필요한 정보인 email, password, authorities는 super로 넘겨주고
Controller에서 주로 쓰일 수 있는 id와 nickname 필드를 추가해줬습니다.
이 CustomUserDetails를 이제 CustomUserDetailService에 적용시키면 됩니다.
@Service
@Transactional
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public CustomUserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
MemberEntity member = memberRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email));
List<SimpleGrantedAuthority> authorities = List.of(
new SimpleGrantedAuthority("ROLE_" + member.getLoginRole().name())
);
return new CustomUserDetails(
member.getId(),
member.getEmail(),
member.getPassword(),
member.getNickname(),
authorities
);
}
}
이제는 loadUserByUsername매서드가 반환하는 값이 CustomUserDetail로 변경되었고 필요한 정보가 담겨서 반환됩니다.
@PatchMapping("/{postId}")
public void updatePost(@PathVariable Long postId,
@RequestBody PostDto postDto,
@AuthenticationPrincipal CustomUserDetails user){
String title = postDto.getTitle();
String content = postDto.getContent();
String fileUrl = postDto.getFile();
postService.updatePost(postId, title, content, fileUrl, user.getId());
}
이제는 Controller에서 리포지토리를 통해 별도로 Member를 찾을 필요 없이 CustomUserDetails에서 바로 필요한 정보를 가져오면 됩니다.