| User IP와 User Agent 정보란?
- 클라이언트가 HTTP를 통해 어떤 요청을 보내면 HTTP header에 사용자 IP주소와 기기정보(Agent)가 담기게 된다.
- User IP 주소는, 다양한 종류의 proxy를 고려하여 각 header를 전부 확인하는 것이 필요하다.
- 만약 IPv4 형식으로만 IP주소를 얻길 원한다면 [Run]-[Configuration] Arguments VM에 설정을 걸어줄 수 있다.
-Djava.net.preferIPv4Stack=true
- 사이트 정책 : 로그인 시 히스토리 내역 저장한 후 메인화면으로 이동한다.
- 위 정책에 따라
1) 히스토리를 저장할 Entity를 만들고,
2) 스프링 시큐러티를 이용해 로그인 성공시 사용자 IP주소와 Agent정보를 DB에 저장한다.
- 스프링 시큐러티 레퍼런스 : https://atin.tistory.com/585
1. Entity
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LoginHistory {
@Id
@GeneratedValue
private Long id;
private String userId;
private String userIp;
private String userAgent;
private LocalDateTime lastLoginDate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
2. WebUtils에 사용자 IP와 Agent 정보를 가져오는 메소드를 만들었다.
package com.example.fastlms.util;
import javax.servlet.http.HttpServletRequest;
public class WebUtils {
public static String getUserAgent(HttpServletRequest request) {
return request.getHeader("User-Agent");
}
public static String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("x-real-ip");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("x-original-forwarded-for");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("HTTP_X_FORWARDED");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("HTTP_X_CLUSTER_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("HTTP_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("HTTP_FORWARDED");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("HTTP_VIA");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getHeader("REMOTE_ADDR");
}
if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
ip = request.getRemoteAddr();
}
return ip;
}
}
3. 스프링 시큐러티를 통해 userId, userIP, userAgent를 LoginHistory(DB)에 저장
(1) SecurityConfig에서 Config(HttpSecurity http)의 defaultSuccessUrl() 사용
- 로그인 Success Handler를 만드는 방법도 있다고 하는데, 메소드 사용이 너무 복잡해서,
대신 httpSecurity API 메소드의 defaultSuccessUrl()을 사용했다.
- defaultSuccessUrl()을 통해 로그인 성공 시 특정 경로로 이동하여 로그인 히스토리를 저장할 것이다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final MemberService memberService;
@Bean
UserAuthenticationFailureHandler getFailureHandler() {
return new UserAuthenticationFailureHandler();
}
@Bean
PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Http Security
* 모두 접근 가능 : 홈, 회원 가입, 메일 인증
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
http.authorizeRequests()
.antMatchers(
"/",
"/member/register",
"/member/email-auth",
"/member/find/password",
"/member/reset/password"
)
.permitAll();
http.authorizeRequests()
.antMatchers("/admin/**")
.hasAuthority("ROLE_ADMIN");
http.formLogin()
.loginPage("/member/login")
//.successForwardUrl("/member/login-success-handler") // 로그인이 성공한 후 보내는 URL
//.successHandler(getSuccessHandler()) // 로그인 핸들링 URL
.defaultSuccessUrl("/member/login-success") // 로그인이 성공한 후 보내는 디폴트 URL
.failureHandler(getFailureHandler())
.permitAll();
http.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true);
http.exceptionHandling()
.accessDeniedPage("/error/denied");
super.configure(http);
}
}
(2) 컨트롤러에서 사용자 정보를 가져온다.
- Principal로 userId를 가져오고,
- WebUtils를 통해 Request Header의 IP와 Agent 정보를 가져온다.
@Controller
@RequiredArgsConstructor
@Slf4j
public class MainController {
private final LoginHistoryService loginHistoryService;
@RequestMapping(value = "/")
public String index() {
return "index";
}
@RequestMapping(value = "/member/login-success")
public String saveLoginHistory(HttpServletRequest request, Principal principal){
String userId = principal.getName();
String userIp = WebUtils.getClientIp(request);
String userAgent = WebUtils.getUserAgent(request);
loginHistoryService.saveLoginHistory(userId, userIp, userAgent);
return "index";
}
}
(3) 로그인 서비스를 통해 히스토리를 DB에 저장한다.
@Service
@RequiredArgsConstructor
public class LoginHistoryImpl implements LoginHistoryService {
private final LoginHistoryRepository loginHistoryRepository;
@Override
public void saveLoginHistory(String userId, String userIp, String userAgent) {
loginHistoryRepository.save(
LoginHistory.builder()
.userId(userId)
.userIp(userIp)
.userAgent(userAgent)
.lastLoginDate(LocalDateTime.now())
.build()
);
}
}
[ 참고 및 출처 ]
https://recordsoflife.tistory.com/248
https://galid1.tistory.com/698
'Framework > Spring' 카테고리의 다른 글
[스프링] @AutoWired 동작 원리 및 DI injection 관련 설명 모음 (0) | 2022.10.20 |
---|---|
[HTTP] @RequestParam vs @RequestBody (0) | 2022.10.19 |
[Validation] 데이터 검증, 비즈니스 로직 검증 (1) | 2022.09.21 |
[스프링] Entity 객체를 생성 : 영속성의 개념 + 자동 Auditing (0) | 2022.09.15 |
[스프링] 개발을 시작하기 전에 - 요구 사항 분석, 기본 구조 잡기 (1) | 2022.09.15 |