[Spring Security] 회원 로그인시 로그인 히스토리(로그인 이력) 남기기

John·2023년 5월 31일
6

개발 메모🌷

목록 보기
9/13
post-thumbnail

회원이 정상적으로 로그인 되었을 때 로그인 히스토리 저장 기능을 구현 후 정리했습니다.
Spring JPASpring Security 공부하고 내용을 정리한 글입니다.


요구사항

  1. 회원이 정상적으로 로그인 되었을 때, 로그인 히스토리 테이블에 로그인 아이디와 로그인 날짜, 접속 IP, 접속 UserAgent를 저장하는 로직 구현

  1. 회원 관리 페이지에서 로그인 이력 출력

  1. 회원 목록에 회원 별 마지막 로그인 일자 추가

그 중 요구사항 1번에 대해 글을 작성하겠습니다.


구현


요구사항에서 히스토리 테이블에 저장되어야 하는 데이터 로그인 아이디, 로그인 날짜, 접속 IP, 접속 UserAgent 와 회원 관리 페이지에서 보여주는 로그인 일자NO(id)를 칼럼으로 가지는 테이블을 생성해야합니다.


JPA Auditing

로그인 일자 생성을 자동으로 기록하기 위해 JpaAuditingConfiguration.java을 먼저 구현하겠습니다.

JpaAuditingConfiguration.java

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {
}

참고자료
Spring Data JPA Auditing으로 엔티티의 생성/수정 시각 자동으로 기록


Entity 생성

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 생성

엔티티를 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();
    }

}

Repository 구현

JPA Repository를 사용하면 Repository의 구현체를 직접 구현하지 않아도 되며, 코드가 간결해집니다. 그리고 네이밍 규칙을 맞추어 Query문을 생성하여 사용할 수 있어 JpaRepository를 상속받아 사용했습니다.

HistoryRepository.java

public interface HistoryRepository extends JpaRepository<LoginHistory, Integer> {

	// 회원 목록에 회원 별 마지막 로그인 일자 추가를 위한 메소드
	List<LoginHistory> findLoginHistoriesByUserIdOrderByLoginDtDesc(String userId);
}

Service 구현

회원이 정상적으로 로그인 되었을 때, 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를 상속 받아 구현하겠습니다.

SimpleUrlAuthenticationSuccessHandlerAuthenticationSuccessHandler를 상속받은 구현체이며, 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에 핸들러를 추가해주면 됩니다.

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를 공부하는데 도움이 많이 되었던 것 같습니다.

글을 읽고 도움이 되셨기를..👨‍🎓

profile
기록을 습관으로

0개의 댓글