Web/스프링부트(SpringBoot Framework)

(1) 스프링부트, JPA로 구현한 로그인을 시도한 사용자의 IP 저장

심플블루 2024. 7. 11. 20:17
반응형
추후 Redis로 이 기능을 변경하기 전 단계로 관계형 DB인 MariaDB에 로그인을 시도한 사용자의 IP를 저장하는 기능을 스프링부트, JPA로 구현하여 보자.

 

사이트를 유지보수 하다보면 로그인을 시도한 사용자의 ip정보나 로그를 수집할 필요가 있기도 합니다.

사용자가 웹브라우저를 통해 접속을 하거나 혹은 다른 HTTP 클라이언트를 통해 웹서버에 요청을 할 경우, Spring MVC에서 HttpServletRequest 객체에는 다양한 정보가 포함됩니다. 이 정보는 클라이언트의 HTTP 요청과 관련된 다양한 속성 및 헤더를 포함합니다.

몇 가지 주요한 정보는 다음과 같습니다.

  1. 요청 URL 및 메소드 정보:
    • request.getRequestURL(): 요청 URL을 가져옵니다.
    • request.getMethod(): HTTP 메소드 (GET, POST, PUT, DELETE 등)를 가져옵니다.
  2. 파라미터 및 쿼리 스트링 정보:
    • request.getParameter("parameterName"): 요청 파라미터 값을 가져옵니다.
    • request.getQueryString(): 전체 쿼리 스트링을 가져옵니다.
  3. 헤더 정보:
    • request.getHeader("headerName"): 특정 헤더 값을 가져옵니다. 예: request.getHeader("User-Agent").
  4. 세션 정보:
    • request.getSession(): 현재 세션을 가져옵니다.
    • request.getSession(false): 세션이 없으면 null을 반환합니다.
  5. 쿠키 정보:
    • request.getCookies(): 모든 쿠키를 가져옵니다.
  6. 클라이언트 IP 주소:
    • request.getRemoteAddr(): 클라이언트의 IP 주소를 가져옵니다.
  7. 기타 정보:
    • request.getLocale(): 클라이언트의 로케일 정보를 가져옵니다.
    • request.getCharacterEncoding(): 요청의 문자 인코딩을 가져옵니다.

이 외에도 HttpServletRequest에는 다양한 메소드와 속성이 있으며, 필요한 정보를 가져올 수 있습니다.

 

사용자의 식별정보는 HTTP 헤더에 담겨 웹서버에 전달되게 됩니다.

 

HTTP 헤더는 사용자가 어떤 브라우저를 사용하는지, 어떤 버전인지, 어떤 운영체계를 사용하는지, 어떤 디바이스를 사용하는지 등의 정보를 웹서버에게 알려주는 역할을 하게 됩니다.

이는 웹페이지가 사용자 환경에 맞게 최적화 하여 사용자에게 서비스할 수 있게 도와주는 역할을 하게 됩니다.

 

이 HTTP 헤더에 담겨 있는 사용자의 ip 정보를 가져와 데이터베이스에 저장하는 방법을 스프링부트, JPA로 구현해 보도록 하겠습니다.

 

1. 클라이언트 IP를 저장할 엔티티 클래스 작성

package org.zerock.mallapi.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;


@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Entity
@ToString
@Table(name = "tbl_ip")
public class UserIP {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String ipAddress;
}

JPA는 엔티티(entity)  객체를 사용해서 데이터베이스와 애플리케이션 사이에 데이터를 동기화하고 관리합니다.

엔티티 객체를 생성하기 위해서는 엔티티 클래스를 생성하는데 이를 위해서 entity 패키지를 추가하고 UserIP 클래스를 추가합니다. 

테이블 이름은 tbl_ip가 되도록 @Table 어노테이션으로 테이블 이름을 지정하고,  @Entity 어노테이션을 추가하여 엔티티 클래스가 되도록하며 해당 테이블에는 데이터베이스의 PK가 존재해야 하므로 @Id 어노테이션을 추가합니다.

고유한 PK를 가지기 위한 방법으로 @GeneratedValue를 이용해서 사용자가 지정하지 않고 자동으로 생성되는 방식을 이용했습니다. (GenerationType.IDENTITY는 PK의 생성 방식을 데이터베이스 쪽에서 알아서 처리한다는 의미로 MariaDB는 자동으로 생성되는 auto_increment 방식으로 처리됩니다.)

 

스프링부트 프로젝트를 재시작하면 해당 엔티티 클래스에 구현한대로 쿼리가 자동생성됩니다.

 

ide의 로그를 확인해 보면 아래의 create 쿼리가 실행된 것을 확인할 수 있습니다.

 

 

데이터베이스에 접속하여 실제로 tbl_ip 테이블이 생성되었는지 확인해 봅니다.

 

2. UserIP 엔티티 처리를 위한 UserIpRepository 작성하기 

JpaRepository를 상속해서 만드는 UserIpRepository 는 별도의 메서드 등을 작성하지 않아도 기본적인 CRUD와 페이징 처리 등의 기능이 제공됩니다.

 

로그인 웹페이지에 접속한 클라이언트 IP를 단순히 데이터베이스에 저장하는 용도이기 때문에 복잡한 쿼리가 필요하지 않습니다.

따라서 JpaRepository에서 제공하는 기본 메서드의 사용만으로 충분하기 때문에 JpaRepository를 상속받은 UserIpRepository 인터페이스로 UserIP 엔티티를 다루도록 작성합니다.

package org.zerock.mallapi.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.zerock.mallapi.entity.UserIP;

public interface UserIpRepository extends JpaRepository<UserIP, Long> {
    // 추가적인 쿼리 메서드가 필요하다면 작성 가능
}

 

3. 서비스 계층 구현과 UserIPDTO 구현

  1.  UserIPDTO 구현

엔티티 객체는 단순한 자바의 인스턴스가 아니라 JPA를 통해서 관리되고 있는 객체(영속 객체) 입니다. 따라서 실제 데이터를 서비스할 때는 엔티티 객체의 내용물을 복사해서 사용하는 DTO를 이용합니다.

DTO (Data Transfer Object) 클래스는 주로 데이터 전송을 위해 사용되며, 데이터의 전송을 목적으로 데이터 필드들을 포함하고 있습니다. 주로 Controller와 Service 사이에서 데이터를 주고 받는 데 사용됩니다.

 

일반적으로 DTO 클래스는 다음과 같은 특징을 가집니다

  1. 데이터 전송 용도: 데이터를 전송하기 위한 목적으로 사용됩니다. 예를 들어 HTTP요청/응답의 바디에 담기거나, 메시지 큐를 통해 전송될 수 있습니다.
  2. 비지니스 로직 없음: 주로 데이터의 구조를 정의하며, 비지니스 로직을 포함하지 않습니다.
  3. 직렬화 가능: JSON/XML 등의 형식으로 변환될 수 있어야 합니다.

 

프로젝트 내 dto 패키지를 생성하고 UserIPDTO 클래스를 생성합니다. 

package org.zerock.mallapi.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserIPDTO {
    
    private Long id;

    private String ipAddress;
}

 

    2. UserIPDTO 타입으로 데이터를 주고 받도록 UserIPService 인터페이스, UserIPServiceImpl 클래스 구현하기

 

서비스 계층은 DTO 타입으로 데이터를 주고받도록 구성합니다.

 

UserIPService 인터페이스에는 IP 를 데이터베이스에 저장할 수 있는 등록 기능을 선언합니다.

등록 기능은 반환값으로 새로 등록된 IP의 번호를 반환하도록 합니다.

package org.zerock.mallapi.service;

import org.zerock.mallapi.dto.UserIPDTO;

public interface UserIPService {
    
    Long ipRegister(UserIPDTO userIPDTO);
}

 

UserIPServiceImpl은 UserIPService 인터페이스의 구현체로 'ModelMapper'를 사용하여 DTO를 엔티티로 변환하고, 

'UserIpRepository'를 사용하여 엔티티를 데이터베이스에 저장합니다.

package org.zerock.mallapi.service;

import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import org.zerock.mallapi.dto.UserIPDTO;
import org.zerock.mallapi.entity.UserIP;
import org.zerock.mallapi.repository.UserIpRepository;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Service
@Transactional
@Log4j2
@RequiredArgsConstructor
public class UserIPServiceImpl implements UserIPService {

    // 자동주입 대상은 final로
    private final ModelMapper modelMapper; // ModelMapper를 주입받음
    
    private final UserIpRepository userIpRepository;

    @Override
    public Long ipRegister(UserIPDTO userIPDTO) {
        
        log.info("ipRegister");

        UserIP userIP = modelMapper.map(userIPDTO, UserIP.class);
        UserIP savedUserIP = userIpRepository.save(userIP);

        return savedUserIP.getId();

    } 
}

 

   

4. 스프링 웹서비스에 접속한 유저 IP를 추출하는 GetIpFromHeader 유틸 클래스 작성하기

 

RequestContextHolder는 Spring Framework에서 현재 HTTP 요청에 관련된 모든 정보와 상태를 보유하고 관리합니다. 

주요 기능

RequestContextHolder는 기본적으로 현재 스레드의 ServletRequestAttributes를 관리합니다. 이를 통해 개발자는 현재 요청의 HttpServletRequest와 HttpServletResponse 객체를 쉽게 접근할 수 있습니다.

여기에는 다음과 같은 요소들이 포함됩니다

 

  • HttpServletRequest 객체: 클라이언트가 서버에 보낸 HTTP 요청에 대한 세부 정보를 담고 있는 객체입니다. 이 객체는 요청 메서드(GET, POST 등), 요청 URI, 헤더, 파라미터, 세션 정보 등을 포함합니다.
  • HttpServletResponse 객체: 서버가 클라이언트에 보낼 HTTP 응답을 구성하는 객체입니다. 이 객체는 응답 상태 코드, 헤더, 응답 본문 등을 설정할 수 있게 합니다.
  • Attributes: 현재 요청과 관련된 추가적인 데이터입니다. 예를 들어, RequestAttributes 인터페이스는 요청 범위(scope)에서 데이터를 저장하고 가져오는 메서드를 제공합니다.
  • Session: 사용자의 세션 데이터를 포함합니다. 세션은 여러 요청에 걸쳐 지속되는 데이터를 저장하는 데 사용됩니다.
  • Locale: 요청과 관련된 로케일 정보입니다. 로케일 정보는 다국어 지원을 위해 사용됩니다.
  • Other Context Data: 필터, 인터셉터, 컨트롤러 등에서 설정한 기타 데이터들이 포함될 수 있습니다.

 

예시로 이해하기

예를 들어, 클라이언트가 다음과 같은 HTTP 요청을 서버에 보냈다고 가정해 봅시다.

GET /myapp/resource?id=123 HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0

 

이 요청에 대한 "컨텍스트"는 다음과 같은 정보를 포함할 수 있습니다:

  • HttpServletRequest 객체:
    • 요청 URI: /myapp/resource
    • 요청 메서드: GET
    • 요청 파라미터: id=123
    • 헤더: Host, User-Agent 등
  • HttpServletResponse 객체: 아직 응답이 작성되지 않았으므로 초기 상태.
  • Attributes: 필터나 인터셉터에서 설정한 추가 데이터.
  • Session: 사용자 세션 데이터 (예: 로그인 상태, 사용자 정보).
  • Locale: 요청의 로케일 정보 (예: en-US).

이 모든 정보들이 "HTTP 요청의 컨텍스트"에 포함되며, Spring의 RequestContextHolder는 이러한 정보를 어디서나 쉽게 접근할 수 있게 해줍니다.

주요 메서드

  1. currentRequestAttributes(): 현재 요청의 RequestAttributes를 반환합니다. 이 메서드는 현재 요청과 관련된 속성들을 담고 있는 객체를 제공합니다. 
  2. getRequestAttributes(): 현재 요청의 RequestAttributes를 반환합니다. 만약 현재 요청이 없다면, null을 반환합니다.
  3. setRequestAttributes(RequestAttributes attributes): 현재 스레드에 대한 요청 속성을 설정합니다.
  4. resetRequestAttributes(): 현재 스레드에 설정된 요청 속성을 제거합니다.
package org.zerock.mallapi.util;

import java.util.List;
import java.util.Collections;
import java.util.Enumeration;

import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Log4j2
@Component
@RequiredArgsConstructor
public class GetIpFromHeader {

    private static final List<String> IP_HEADERS = List.of(
        "X-Forwarded-For",          // 클라이언트의 원 IP 주소를 나타내기 위한 일반적인 헤더, 여러 IP 주소가 쉼표로 구분되어 있을 수 있음
        "HTTP_FORWARDED",           // RFC 7239에 정의된 표준화된 포워드 헤더, 클라이언트 및 프록시 서버 정보를 포함
        "Proxy-Client-IP",          // 일부 프록시 서버에서 사용하는 헤더, 클라이언트 IP 주소를 포함
        "WL-Proxy-Client-IP",       // WebLogic 서버에서 사용하는 헤더, 클라이언트 IP 주소를 포함
        "HTTP_CLIENT_IP",           // HTTP 요청의 클라이언트 IP를 나타내는 헤더, 일부 프록시 서버에서 사용
        "HTTP_X_FORWARDED_FOR",     // 클라이언트의 원 IP 주소를 나타내는 또 다른 헤더, X-Forwarded-For와 유사
        "X-RealIP",                 // Nginx와 같은 일부 웹 서버에서 사용하는 헤더, 클라이언트의 원 IP 주소를 포함
        "X-Real-IP",                // Nginx와 같은 일부 웹 서버에서 사용하는 헤더, 클라이언트의 원 IP 주소를 포함 (대시 포함 버전)
        "REMOTE_ADDR"               // Java의 ServletRequest에서 제공하는 메서드로, 직접 연결된 클라이언트의 IP 주소를 반환
    );

    /**
     * HTTP 요청의 헤더에서 실제 클라이언트 IP 주소를 추출합니다.
     * 다수의 프록시를 거친 경우 마지막 클라이언트 IP를 반환합니다.
     *
     * @return 클라이언트의 실제 IP 주소
     */
    public String getIpFromHeader() {
        log.info("클라이언트 IP 수집");

        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        HttpServletRequest request = sra.getRequest();
        log.info("리퀘스트: " + request);

		// IP_HEADERS 목록에서 클라이언트 IP 확인
        for (String ipHeader : IP_HEADERS) {
            String clientIp = request.getHeader(ipHeader);
            log.info("헤더 {}: {}", ipHeader, clientIp);
            if (StringUtils.hasLength(clientIp) && !"unknown".equalsIgnoreCase(clientIp)) {
                log.info("헤더 {}: {}", ipHeader, clientIp);
                return clientIp;
            }
        }
        
        // 모든 헤더에서 IP를 찾지 못한 경우, 기본적으로 제공되는 원격 주소 반환
        String remoteAddr = request.getRemoteAddr();
        log.info("리모트 주소: " + remoteAddr);
        return remoteAddr;
    }
}

로직처리 방식

  1. getClientIp(HttpServletRequest request) 메소드: 이 메소드는 주어진 HttpServletRequest 객체에서 클라이언트의 실제 IP 주소를 추출합니다. 다수의 프록시를 거친 경우, X-Forwarded-For 헤더를 통해 마지막 클라이언트 IP를 식별합니다.
  2. 헤더 검색 순서: 다양한 헤더들(X-Forwarded-For, Proxy-Client-IP, WL-Proxy-Client-IP, HTTP_CLIENT_IP, HTTP_X_FORWARDED_FOR)을 순서대로 검사하여 실제 IP 주소를 찾습니다. 각 헤더가 비어 있거나 알 수 없는 값을 포함할 경우, 다음 헤더를 검사합니다.
  3. getRemoteAddr() 사용: 만약 모든 헤더에서 유효한 IP 주소를 찾지 못하면, request.getRemoteAddr()을 통해 기본적으로 제공되는 IP 주소를 반환합니다.
  4. 다수의 프록시 처리: 클라이언트 IP 주소는 다수의 프록시를 거칠 수 있으므로, X-Forwarded-For 헤더에서 추출된 IP 주소 목록 중 마지막 주소를 반환합니다.

헤더 설명

  1. X-Forwarded-For: 일반적으로 클라이언트의 원 IP 주소를 나타내며, 프록시 서버나 로드 밸런서가 요청을 전달할 때 클라이언트의 원래 IP 주소를 포함하기 위해 사용됩니다. 이를 위해 서버 설정 또는 클라우드 서비스 설정에서 프록시나 로드 밸런서가 이러한 헤더를 추가하도록 구성해야 합니다.
  2. HTTP_FORWARDED: RFC 7239에 따라 표준화된 포워드 헤더로, 클라이언트 및 프록시 서버 정보를 포함합니다.
  3. Proxy-Client-IP: 일부 프록시 서버에서 사용하는 헤더로, 클라이언트의 IP 주소를 포함하고 있습니다.
  4. WL-Proxy-Client-IP: Oracle WebLogic 서버에서 사용하는 헤더로, 클라이언트의 IP 주소를 제공합니다.
  5. HTTP_CLIENT_IP: HTTP 요청에서 클라이언트 IP를 나타내는 헤더로, 일부 프록시 서버에서 사용될 수 있습니다.
  6. HTTP_X_FORWARDED_FOR: X-Forwarded-For와 유사한 다른 헤더로, 클라이언트의 원 IP 주소를 포함합니다.
  7. X-RealIPX-Real-IP: 일부 웹 서버(Nginx 등)에서 사용되는 헤더로, 클라이언트의 원 IP 주소를 포함합니다. 대시(-)가 있는 버전과 없는 버전이 있을 수 있습니다.
  8. REMOTE_ADDR: Java ServletRequest에서 직접 연결된 클라이언트의 IP 주소를 제공하는 메서드입니다.

왜 다양한 헤더를 추가했는지

다양한 HTTP 헤더를 추가한 이유는 다음과 같습니다:

  • 다중 프록시 지원: 클라이언트와 서버 사이에 다수의 프록시가 있을 수 있습니다. 각 프록시는 클라이언트의 IP를 다양한 헤더에 추가하여 전달할 수 있습니다. 이 클래스는 가능한 모든 헤더를 검사하여 클라이언트의 실제 IP를 식별하려고 합니다.
  • 헤더 표준화: 표준화된 헤더(RFC 7239) 외에도 일반적으로 사용되는 헤더(X-Forwarded-For 등)를 포함하여 다양한 환경에서 호환성을 유지하려고 합니다.

IP를 찾지 못하는 경우

헤더에서 IP를 찾지 못하는 경우, 일반적으로 "unknown"이나 빈 문자열로 설정됩니다. 이 경우에는 기본적으로 제공되는 원격 주소를 사용합니다

 

로컬에서 테스트할 때 로그가 모든 헤더 목록에 대해 'null' 값을 출력하는 이유 

 

  • 로컬 환경에서의 HTTP 요청 특성
    • 일반적으로 로컬에서 개발 시 HTTP 요청은 직접적으로 클라이언트에서 서버로 전송되는 것이 아니라, 로컬 루프백(localhost)을 통해 처리됩니다. 로컬 개발 환경에서 localhost를 통해 접속하면, 대부분의 경우 클라이언트 IP 주소는 0:0:0:0:0:0:0:1 (IPv6의 로컬 루프백 주소)로 나타납니다.
    • 이는 HTTP 요청에 대해 프록시 서버가 사용되지 않고, 클라이언트의 원격 주소가 REMOTE_ADDR 헤더로부터 전달되는 것을 의미합니다.
  • 헤더 값의 존재 여부
    • 실제 운영 환경에서는 클라이언트 IP가 여러 개의 헤더를 통해 전달될 수 있습니다. 예를 들어, X-Forwarded-For 헤더는 클라이언트와 프록시 서버 간의 IP 체인을 포함할 수 있습니다.
    • 하지만 로컬 환경에서는 이러한 프록시가 없기 때문에, 대부분의 HTTP 헤더에는 실제 IP 주소가 포함되어 있지 않습니다. 따라서 null 값이 출력됩니다.
    • 배포된 환경에서는 클라이언트의 실제 IP 주소를 얻기 위해 서버 앞단에 프록시나 로드 밸런서를 설정하여, 이러한 헤더를 통해 클라이언트 IP를 전달해야 합니다. 하지만 로컬 환경에서는 이러한 프록시가 없기 때문에, 대부분의 HTTP 헤더에는 실제 IP 주소가 포함되어 있지 않습니다. 따라서 null 값이 출력됩니다.
    • 로컬 개발 환경에서는 request.getRemoteAddr()를 통해서만 IP 주소를 얻을 수 있습니다. 

 

5. 스프링 시큐리티에서 필터체인 구성

 

RequestContextHolder는 Servlet 기반의 Web 애플리케이션에서 동작합니다.

스프링 시큐리티가 설정된 이후에는 보통 이러한 필터 체인 구성을 순차적으로 거치며 Web 애플리케이션이 동작하게 됩니다.

이 필터들을 통 내부에서 이미 RequestContextHolder를 사용하여 현재 요청에 대한 HttpServletRequest 객체를 얻습니다. 이후에는 HttpServletRequest 객체를 사용하여 클라이언트의 IP 주소를 추출하거나 다른 요청 관련 정보를 처리할 수 있습니다.

따라서 JWTCheckFilter 내부에서 클라이언트의 IP 주소를 추출할 수도 있습니다.

(하지만 나의 경우는 컨트롤러에서 IP 주소를 추출하는 방향으로 진행하였음)

 

package org.zerock.mallapi.config;


import java.util.Arrays;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.RequestContextFilter;
import org.zerock.mallapi.security.filter.JWTCheckFilter;
import org.zerock.mallapi.security.handler.APILoginFailHandler;
import org.zerock.mallapi.security.handler.APILoginSuccessHandler;
import org.zerock.mallapi.security.handler.CustomAccessDeniedHandler;
import org.zerock.mallapi.security.handler.CustomAuthenticationEntryPoint;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Configuration
@Log4j2
@RequiredArgsConstructor
@EnableMethodSecurity
public class CustomSecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        log.info("------------security config--------------");

        // CORS 설정
        http.cors(httpSecurityCorsConfigurer -> {
            httpSecurityCorsConfigurer.configurationSource(corsConfigurationSource());
        });
        
        // 세션 관리 설정
        http.sessionManagement(sessionConfig -> sessionConfig.
            sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        // CSRF 설정 비활성화    
        http.csrf(config -> config.disable());

        // JWT 필터 추가
        http.addFilterBefore(new JWTCheckFilter(),
        UsernamePasswordAuthenticationFilter.class); // JWT체크

        // 인증되지 않은 사용자가 리소스에 접근했을 때 수행되는 핸들러를 등록
        http.exceptionHandling(exceptionHandling -> {
            exceptionHandling.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
            exceptionHandling.accessDeniedHandler(new CustomAccessDeniedHandler());
        });

        // formLogin() 인증이 필요한 요청은 스프링 시큐리티에서 사용하는 기본 Form Login Page 사용
        http.formLogin(config -> {
            config.loginPage("/member/login");
            config.loginProcessingUrl("/api/member/login");
            config.successHandler(new APILoginSuccessHandler());
            config.failureHandler(new APILoginFailHandler());
        });

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() { // CORS 설정에 필요한 메서드
        
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

 

6. 컨트롤러에서 GetIpFromHeader 유틸을 사용하여 클라이언트의 IP 주소를 얻고 DB에 저장하는 기능을 구현

 

클라이언트의 IP 주소를 가져와서 DB에 저장하는 로직을 컨트롤러에서 처리하기

package org.zerock.mallapi.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.zerock.mallapi.dto.UserIPDTO;
import org.zerock.mallapi.service.UserIPService;
import org.zerock.mallapi.util.GetIpFromHeader;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Log4j2
@RequiredArgsConstructor
@Controller
public class MemberController {
    
    private final UserIPService userIPService;

    private final GetIpFromHeader getIpUtil;


    @GetMapping("/member/login") // 로그인 페이지 URL
    public String getLoginForm() {
        
        log.info("로그인페이지");

        // 로그인 페이지에 접근한 사용자의 IP주소를 가져오기
        String clientIp = getIpUtil.getIpFromHeader();
        log.info("아이피: " + clientIp);

        // 로그인 페이지에 접근한 사용자의 IP주소를 DB에 저장
        UserIPDTO userIPDTO = new UserIPDTO();
        userIPDTO.setIpAddress(clientIp);

        userIPService.ipRegister(userIPDTO); 

        return "loginPage"; // 로그인 페이지 뷰 이름 반환
    }
}

 

 

7. 데이터베이스에 INSERT 결과 확인하기

 

 

반응형