프로젝트를 시작할 때 작성 시간과, 수정 시간만 entity에 포함시켰다.
이제 로그인 기능을 구현했기 때문에 작성자와, 수정자를 추가해줄 것이다.
앞서 구현한 JwtAuthenticationFilter 부분에서 사용자의 권한 정보를 주지 않아 AnonymousAuthenticationToken으로 처리되는 문제가 있다.
그렇기 때문에 먼저 코드를 수정해준다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsernameFromToken(token);
// 🔹 UserDetailsService를 통해 사용자 정보 가져오기
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 🔹 권한 정보를 포함한 Authentication 객체 생성
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// 인증 정보 설정
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
거기에 맞게 WebSecurityConfig도 수정해준다.
private final UserDetailsService userDetailsService;
.addFilterBefore(new JwtAuthenticationFilter(tokenProvider, userDetailsService), UsernamePasswordAuthenticationFilter.class)
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private Long createdBy;
@LastModifiedBy
private Long lastModifiedBy;
}
public class Post extends BaseEntity{
}
@Builder
public record PostResponse(
Long id,
String title,
String content,
LocalDateTime createdAt,
LocalDateTime lastModifiedAt,
Long createdBy,
Long lastModifiedBy
) {
public static PostResponse toDto(Post post) {
return PostResponse.builder()
.id(post.getId())
.title(post.getTitle())
.content(post.getContent())
.createdAt(post.getCreatedAt())
.lastModifiedAt(post.getLastModifiedAt())
.createdBy(post.getCreatedBy())
.lastModifiedBy(post.getLastModifiedBy())
.build();
}
}
@SpringBootApplication
@EnableJpaAuditing
public class SpringbootDeveloperApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDeveloperApplication.class, args);
}
@Bean
public AuditorAware<Long> auditorProvider() {
return new AuditorAwareImpl();
}
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
@Slf4j
public class AuditorAwareImpl implements AuditorAware<Long> {
@Override
public Optional<Long> getCurrentAuditor() {
// SecurityContext에서 현재 사용자의 Autehntication 객체를 가져옴
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.info("Authentication-> {}", authentication);
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.empty();
}
// JWT에서 사용자 ID 추출
Object principal = authentication.getPrincipal();
log.info("principal -> {}", principal);
if (principal instanceof CustomUserDetails) {
return Optional.of(((CustomUserDetails) principal).getUserId());
}
return Optional.empty();
}
}
Spring Security의 Auditing 기능과 연동하여, 현재 인증된 사용자의 ID를 가져오는 역할을 한다.
Spring Data JPA의 @CreatedBy 및 @LastModifiedBy 어노테이션과 함께 사용되며, 자동으로 엔티티에 작성자 정보를 저장할 수 있다.
SecurityContextHolder.getContext().getAuthentication()를 통해 현재 로그인한 사용자의 인증 객체를 가져온다.
AnonymousAuthenticationToken이 반환될 수도 있다.인증 객체가 null이거나, 사용자가 인증되지 않았다면 작성자를 저장할 필요가 없으므로 Optional.empty()를 반환한다.
authentication.getPrincipal()을 호출하여 현재 로그인한 사용자의 정보(Principal 객체)를 가져온다.
principal이 CustomUserDetails의 인스턴스인지 확인한다.
CustomUserDetails는 사용자의 ID, 이메일, 권한 등의 정보를 가지고 있는 객체다.getUserId()를 호출하여 현재 로그인한 사용자의 ID를 가져와 반환한다.
principal이 CustomUserDetails가 아니라면, 익명 사용자 또는 예상치 못한 타입일 가능성이 있다
Optional.empty()를 반환하여 작성자 정보를 저장하지 않도록 설정한다.@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
private final Long userId;
private final String username;
private final String password;
private final Collection<? extends GrantedAuthority> authorities;
public Long getUserId() {
return userId;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Spring Security의 UserDetails 인터페이스를 구현한 CustomUserDetails 클래스다.
UserDetails 인터페이스는 Spring Security에서 사용자 인증 및 권한 관리를 위한 핵심 인터페이스로, 사용자 정보를 제공하는 데 사용된다.
userId: 사용자의 고유 ID (예: 데이터베이스에서 사용자를 구분하는 ID)username: 사용자 이름 (로그인에 사용되는 값)password: 사용자의 암호 (로그인 인증에 사용)authorities: 사용자의 권한을 담은 컬렉션 (권한을 GrantedAuthority 객체로 표현)@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) {
User user = userRepository.findByEmail(email).orElseThrow(NotFoundUserException::new);
return new CustomUserDetails(
user.getId(),
user.getEmail(),
user.getPassword(),
user.getAuthorities()
);
}
}
이 클래스는 Spring Security에서 인증을 처리하는 UserDetailsService를 구현하여, 이메일을 기반으로 사용자를 조회하고, 조회된 사용자 정보를 CustomUserDetails 객체로 반환한다.
loadUserByUsername() 메서드는 Spring Security의 인증 과정에서 호출되며, 사용자의 ID, 이메일, 비밀번호, 권한 등을 포함한 사용자 정보를 반환한다.
사용자 인증을 위해 CustomUserDetails 객체를 생성하고, 이를 통해 Spring Security에서 인증과 권한 관리를 처리한다.