회원이 정상적으로 로그인 되었을 때 로그인 히스토리 저장 기능을 구현 후 정리했습니다.
Spring JPA
와 Spring Security
공부하고 내용을 정리한 글입니다.
그 중 요구사항 1번에 대해 글을 작성하겠습니다.
요구사항에서 히스토리 테이블에 저장되어야 하는 데이터 로그인 아이디
, 로그인 날짜
, 접속 IP
, 접속 UserAgent
와 회원 관리 페이지에서 보여주는 로그인 일자
와 NO(id)
를 칼럼으로 가지는 테이블을 생성해야합니다.
로그인 일자 생성을 자동으로 기록하기 위해 JpaAuditingConfiguration.java
을 먼저 구현하겠습니다.
JpaAuditingConfiguration.java
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {
}
id
는 자동으로 생성하기 위해 @GeneratedValue
어노테이션을 사용했으며, loginDt
는 자동으로 기록하기 위해 엔티티에 @EntityListeners(AuditingEntityListener.class)
loginDt 필드에 @CreatedDate
어노테이션을 추가했습니다.
LoginHistory.java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@EntityListeners(AuditingEntityListener.class)
public class LoginHistory {
@Id
@GeneratedValue
private Integer id;
private String userId;
@CreatedDate
private LocalDateTime loginDt; // 로그인 날짜
private String clientIp;
private String userAgent;
}
엔티티를 DTO로 변환하기 위한 fromEntity() 메소드를 추가했습니다.
UserLoginHistoryDto.java
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserLoginHistoryDto {
private Integer id;
private String userId;
private LocalDateTime loginDt;
private String clientIp;
private String userAgent;
public static UserLoginHistoryDto fromEntity(LoginHistory loginHistory) {
return UserLoginHistoryDto.builder()
.id(loginHistory.getId())
.userId(loginHistory.getUserId())
.loginDt(loginHistory.getLoginDt())
.clientIp(loginHistory.getClientIp())
.userAgent(loginHistory.getUserAgent())
.build();
}
}
JPA Repository를 사용하면 Repository의 구현체를 직접 구현하지 않아도 되며, 코드가 간결해집니다. 그리고 네이밍 규칙을 맞추어 Query문을 생성하여 사용할 수 있어 JpaRepository를 상속받아 사용했습니다.
HistoryRepository.java
public interface HistoryRepository extends JpaRepository<LoginHistory, Integer> {
// 회원 목록에 회원 별 마지막 로그인 일자 추가를 위한 메소드
List<LoginHistory> findLoginHistoriesByUserIdOrderByLoginDtDesc(String userId);
}
회원이 정상적으로 로그인 되었을 때, LoginHistory 정보를 저장하기 위해 saveLogOnLogin()
메소드를 작성했습니다.
HistoryService.java
Service
@RequiredArgsConstructor
public class HistoryService {
private final HistoryRepository historyRepository;
public void saveLogOnLogin(LoginHistory loginHistory) {
historyRepository.save(loginHistory);
}
public List<UserLoginHistoryDto> getUserLoginHistoryDtos(String userId) {
List<LoginHistory> loginHistories = historyRepository.findLoginHistoriesByUserIdOrderByLoginDtDesc(userId);
return loginHistories.stream()
.map(UserLoginHistoryDto::fromEntity)
.collect(Collectors.toList());
}
}
Spring Security
에서 로그인 성공 후 특정 동작을 제어하기 위해 구현하는 인터페이스는 AuthenticationSuccessHandler
입니다.
로그인 성공 후 로그인 히스토리 저장만 해주면 되기 때문에 SimpleUrlAuthenticationSuccessHandler
를 상속 받아 구현하겠습니다.
SimpleUrlAuthenticationSuccessHandler
는 AuthenticationSuccessHandler
를 상속받은 구현체이며, onAuthenticationSuccess
메소드를 통해 로그인 성공 후 작업을 작성해주면 됩니다.
UserAuthenticationSuccessHandler.java
public class UserAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final HistoryService historyService;
private final MemberService memberService;
public UserAuthenticationSuccessHandler(HistoryService historyService, MemberService memberService) {
this.historyService = historyService;
this.memberService = memberService;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// SpringSecurity 인증 후 로그인 객체를 가져오기 위해 작성
Object principal = authentication.getPrincipal();
UserDetails userDetails = (UserDetails) principal;
String userId = userDetails.getUsername();
String userAgent = RequestUtils.getUserAgent(request);
String clientIp = RequestUtils.getClientIp(request);
// LoginDt는 JPA Auditing에서 관리
LoginHistory loginHistory = LoginHistory.builder()
.userId(userId)
.userAgent(userAgent)
.clientIp(clientIp)
.build();
// 히스토리 저장 작업
historyService.saveLogOnLogin(loginHistory);
// 회원 별 최종 로그인 날짜 업데이트 작업
memberService.updateLastLoginDt(userId, LocalDateTime.now());
super.onAuthenticationSuccess(request, response, authentication);
}
}
위 작업을 모두 끝냈다면 SecurityConfiguration
에 핸들러를 추가해주면 됩니다.
SecurityConfiguration.java
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final MemberService memberService;
private final HistoryService historyService;
...
@Bean
UserAuthenticationFailureHandler getFailureHandler() {
return new UserAuthenticationFailureHandler();
}
// 로그인 성공시 동작하는 UserAuthenticationSuccessHandler 핸들러 추가
@Bean
UserAuthenticationSuccessHandler getSuccessHandler() {
return new UserAuthenticationSuccessHandler(historyService, memberService);
}
...
@Override
protected void configure(HttpSecurity http) throws Exception {
...
http.formLogin()
.loginPage("/member/login")
.failureHandler(getFailureHandler())
.successHandler(getSuccessHandler()) // SuccessHandler 적용
.permitAll();
...
super.configure(http);
}
...
}
요즘 공부를 하면서 새로운 걸 배울때 뿌듯함을 느끼는데, 위 작업을 진행하면서 오랜만에 뿌듯함도 느끼고 Spring Security
를 공부하는데 도움이 많이 되었던 것 같습니다.
글을 읽고 도움이 되셨기를..👨🎓