[회원 정보 개선] Member 엔티티 및 DTO에 login_ip 필드 추가, JWT 관련 필터에서 클라이언트 IP 처리 로직 개선. Refresh Token 생성 및 검증 시 IP 정보 포함하여 보안 강화.
This commit is contained in:
		@@ -8,6 +8,7 @@
 | 
				
			|||||||
        updated_at timestamp(6) not null,
 | 
					        updated_at timestamp(6) not null,
 | 
				
			||||||
        updated_oid bigint,
 | 
					        updated_oid bigint,
 | 
				
			||||||
        role varchar(40) not null check (role in ('MEMBER','ADMIN','SYSTEM_ADMIN')),
 | 
					        role varchar(40) not null check (role in ('MEMBER','ADMIN','SYSTEM_ADMIN')),
 | 
				
			||||||
 | 
					        login_ip varchar(45),
 | 
				
			||||||
        name varchar(100) not null,
 | 
					        name varchar(100) not null,
 | 
				
			||||||
        password varchar(100) not null,
 | 
					        password varchar(100) not null,
 | 
				
			||||||
        user_id varchar(100) not null,
 | 
					        user_id varchar(100) not null,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,6 +27,7 @@ public class MemberDto implements UserDetails {
 | 
				
			|||||||
    private MemberRole role;
 | 
					    private MemberRole role;
 | 
				
			||||||
    private Boolean useFlag;
 | 
					    private Boolean useFlag;
 | 
				
			||||||
    private String refreshToken;
 | 
					    private String refreshToken;
 | 
				
			||||||
 | 
					    private String loginIp;
 | 
				
			||||||
    private LocalDateTime lastLoginAt;
 | 
					    private LocalDateTime lastLoginAt;
 | 
				
			||||||
    private LocalDateTime createdAt;
 | 
					    private LocalDateTime createdAt;
 | 
				
			||||||
    private LocalDateTime updatedAt;
 | 
					    private LocalDateTime updatedAt;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,6 +48,9 @@ public class Member extends BaseEntity {
 | 
				
			|||||||
    @Column(name = "refresh_token", length = 1024)
 | 
					    @Column(name = "refresh_token", length = 1024)
 | 
				
			||||||
    private String refreshToken;
 | 
					    private String refreshToken;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Column(name = "login_ip", length = 45)  // IPv6 지원을 위해 45자
 | 
				
			||||||
 | 
					    private String loginIp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Column(name = "last_login_at")
 | 
					    @Column(name = "last_login_at")
 | 
				
			||||||
    private LocalDateTime lastLoginAt;
 | 
					    private LocalDateTime lastLoginAt;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@ public interface MemberMapper {
 | 
				
			|||||||
    @Mapping(target = "role", expression = "java(com.bio.bio_backend.domain.base.member.enums.MemberRole.getDefault())")
 | 
					    @Mapping(target = "role", expression = "java(com.bio.bio_backend.domain.base.member.enums.MemberRole.getDefault())")
 | 
				
			||||||
    @Mapping(target = "useFlag", constant = "true")
 | 
					    @Mapping(target = "useFlag", constant = "true")
 | 
				
			||||||
    @Mapping(target = "refreshToken", ignore = true)
 | 
					    @Mapping(target = "refreshToken", ignore = true)
 | 
				
			||||||
 | 
					    @Mapping(target = "loginIp", ignore = true)
 | 
				
			||||||
    @Mapping(target = "lastLoginAt", ignore = true)
 | 
					    @Mapping(target = "lastLoginAt", ignore = true)
 | 
				
			||||||
    @Mapping(target = "createdAt", ignore = true)
 | 
					    @Mapping(target = "createdAt", ignore = true)
 | 
				
			||||||
    @Mapping(target = "updatedAt", ignore = true)
 | 
					    @Mapping(target = "updatedAt", ignore = true)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
package com.bio.bio_backend.domain.base.member.service;
 | 
					package com.bio.bio_backend.domain.base.member.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.time.LocalDateTime;
 | 
					 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ import com.bio.bio_backend.domain.base.member.dto.MemberDto;
 | 
				
			|||||||
import com.bio.bio_backend.domain.base.member.service.MemberService;
 | 
					import com.bio.bio_backend.domain.base.member.service.MemberService;
 | 
				
			||||||
import com.bio.bio_backend.global.constants.ApiResponseCode;
 | 
					import com.bio.bio_backend.global.constants.ApiResponseCode;
 | 
				
			||||||
import com.bio.bio_backend.global.utils.JwtUtils;
 | 
					import com.bio.bio_backend.global.utils.JwtUtils;
 | 
				
			||||||
 | 
					import com.bio.bio_backend.global.utils.HttpUtils;
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
import lombok.extern.slf4j.Slf4j;
 | 
					import lombok.extern.slf4j.Slf4j;
 | 
				
			||||||
import org.springframework.http.HttpStatus;
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
@@ -38,6 +39,7 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter
 | 
				
			|||||||
    private final JwtUtils jwtUtils;
 | 
					    private final JwtUtils jwtUtils;
 | 
				
			||||||
    private final ObjectMapper objectMapper;
 | 
					    private final ObjectMapper objectMapper;
 | 
				
			||||||
    private final MemberService memberService;
 | 
					    private final MemberService memberService;
 | 
				
			||||||
 | 
					    private final HttpUtils httpUtils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 사용자 login 인증 처리
 | 
					    // 사용자 login 인증 처리
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
@@ -66,6 +68,7 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter
 | 
				
			|||||||
        String refreshToken = jwtUtils.createRefreshToken(member.getUserId(), member.getRole().getValue());
 | 
					        String refreshToken = jwtUtils.createRefreshToken(member.getUserId(), member.getRole().getValue());
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        member.setRefreshToken(refreshToken);
 | 
					        member.setRefreshToken(refreshToken);
 | 
				
			||||||
 | 
					        member.setLoginIp(httpUtils.getClientIp());
 | 
				
			||||||
        member.setLastLoginAt(LocalDateTime.now());
 | 
					        member.setLastLoginAt(LocalDateTime.now());
 | 
				
			||||||
        memberService.updateMember(member);
 | 
					        memberService.updateMember(member);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -69,7 +69,7 @@ public class JwtTokenValidationFilter extends OncePerRequestFilter {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Access Token이 없거나 만료된 경우, Refresh Token으로 갱신 시도
 | 
					        // Access Token이 없거나 만료된 경우, Refresh Token으로 갱신 시도
 | 
				
			||||||
        if (refreshToken != null && jwtUtils.validateRefreshToken(refreshToken)) {
 | 
					        if (refreshToken != null && jwtUtils.validateRefreshToken(refreshToken, request.getRemoteAddr())) {
 | 
				
			||||||
            String username = jwtUtils.extractUsername(refreshToken);
 | 
					            String username = jwtUtils.extractUsername(refreshToken);
 | 
				
			||||||
            String role = (String) jwtUtils.extractAllClaims(refreshToken).get("role");
 | 
					            String role = (String) jwtUtils.extractAllClaims(refreshToken).get("role");
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 | 
				
			|||||||
import com.bio.bio_backend.domain.base.member.service.MemberService;
 | 
					import com.bio.bio_backend.domain.base.member.service.MemberService;
 | 
				
			||||||
import com.bio.bio_backend.global.exception.CustomAuthenticationFailureHandler;
 | 
					import com.bio.bio_backend.global.exception.CustomAuthenticationFailureHandler;
 | 
				
			||||||
import com.bio.bio_backend.global.utils.JwtUtils;
 | 
					import com.bio.bio_backend.global.utils.JwtUtils;
 | 
				
			||||||
 | 
					import com.bio.bio_backend.global.utils.HttpUtils;
 | 
				
			||||||
import com.bio.bio_backend.global.config.SecurityPathConfig;
 | 
					import com.bio.bio_backend.global.config.SecurityPathConfig;
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -35,9 +36,10 @@ public class WebSecurity {
 | 
				
			|||||||
    private final ObjectMapper objectMapper;
 | 
					    private final ObjectMapper objectMapper;
 | 
				
			||||||
    private final Environment env;
 | 
					    private final Environment env;
 | 
				
			||||||
    private final SecurityPathConfig securityPathConfig;
 | 
					    private final SecurityPathConfig securityPathConfig;
 | 
				
			||||||
 | 
					    private final HttpUtils httpUtils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private JwtTokenIssuanceFilter getJwtTokenIssuanceFilter(AuthenticationManager authenticationManager) throws Exception {
 | 
					    private JwtTokenIssuanceFilter getJwtTokenIssuanceFilter(AuthenticationManager authenticationManager) throws Exception {
 | 
				
			||||||
        JwtTokenIssuanceFilter filter = new JwtTokenIssuanceFilter(authenticationManager, jwtUtils, objectMapper, memberService);
 | 
					        JwtTokenIssuanceFilter filter = new JwtTokenIssuanceFilter(authenticationManager, jwtUtils, objectMapper, memberService, httpUtils);
 | 
				
			||||||
        filter.setFilterProcessesUrl("/login");
 | 
					        filter.setFilterProcessesUrl("/login");
 | 
				
			||||||
        filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler(objectMapper));
 | 
					        filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler(objectMapper));
 | 
				
			||||||
        return filter;
 | 
					        return filter;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,6 @@ import jakarta.servlet.http.Cookie;
 | 
				
			|||||||
import jakarta.servlet.http.HttpServletRequest;
 | 
					import jakarta.servlet.http.HttpServletRequest;
 | 
				
			||||||
import jakarta.servlet.http.HttpServletResponse;
 | 
					import jakarta.servlet.http.HttpServletResponse;
 | 
				
			||||||
import com.bio.bio_backend.domain.base.member.service.MemberService;
 | 
					import com.bio.bio_backend.domain.base.member.service.MemberService;
 | 
				
			||||||
import com.bio.bio_backend.domain.base.member.dto.MemberDto;
 | 
					 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
import lombok.extern.slf4j.Slf4j;
 | 
					import lombok.extern.slf4j.Slf4j;
 | 
				
			||||||
import org.springframework.beans.factory.annotation.Value;
 | 
					import org.springframework.beans.factory.annotation.Value;
 | 
				
			||||||
@@ -53,9 +52,39 @@ public class JwtUtils {
 | 
				
			|||||||
        return isTokenExpired(token);
 | 
					        return isTokenExpired(token);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Boolean validateRefreshToken(String token) {
 | 
					    // Refresh Token 생성 시 IP 정보 포함
 | 
				
			||||||
        String saveToken = memberService.getRefreshToken(extractUsername(token));
 | 
					    public String createRefreshToken(String username, String role, String clientIp) {
 | 
				
			||||||
        return (saveToken.equals(token) && isTokenExpired(token));
 | 
					        return generateToken(username, role, clientIp,
 | 
				
			||||||
 | 
					                Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_refresh"))));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // IP 정보를 포함한 토큰 생성
 | 
				
			||||||
 | 
					    public String generateToken(String username, String role, String clientIp, long expirationTime) {
 | 
				
			||||||
 | 
					        return Jwts.builder()
 | 
				
			||||||
 | 
					                .subject(username)
 | 
				
			||||||
 | 
					                .claim("role", role)
 | 
				
			||||||
 | 
					                .claim("ip", clientIp)  // IP 정보 추가
 | 
				
			||||||
 | 
					                .issuedAt(new Date(System.currentTimeMillis()))
 | 
				
			||||||
 | 
					                .expiration(new Date(System.currentTimeMillis() + expirationTime))
 | 
				
			||||||
 | 
					                .signWith(getSigningKey())
 | 
				
			||||||
 | 
					                .compact();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // IP 정보 추출
 | 
				
			||||||
 | 
					    public String extractClientIp(String token) {
 | 
				
			||||||
 | 
					        Claims claims = extractAllClaims(token);
 | 
				
			||||||
 | 
					        return claims.get("ip", String.class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Refresh Token 검증 시 IP도 함께 검증
 | 
				
			||||||
 | 
					    public Boolean validateRefreshToken(String token, String clientIp) {
 | 
				
			||||||
 | 
					        String savedToken = memberService.getRefreshToken(extractUsername(token));
 | 
				
			||||||
 | 
					        String tokenIp = extractClientIp(token);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 토큰 일치, 만료되지 않음, IP 일치 확인
 | 
				
			||||||
 | 
					        return (savedToken.equals(token) && 
 | 
				
			||||||
 | 
					                isTokenExpired(token) && 
 | 
				
			||||||
 | 
					                Objects.equals(tokenIp, clientIp));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private boolean isTokenExpired(String token) {
 | 
					    private boolean isTokenExpired(String token) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -92,7 +92,7 @@ decorator.datasource.p6spy.log-format=%(sqlSingleLine)
 | 
				
			|||||||
# JWT 설정
 | 
					# JWT 설정
 | 
				
			||||||
# ========================================
 | 
					# ========================================
 | 
				
			||||||
token.expiration_time_access=900000
 | 
					token.expiration_time_access=900000
 | 
				
			||||||
token.expiration_time_refresh=604800000
 | 
					token.expiration_time_refresh=86400000
 | 
				
			||||||
token.secret_key=c3RhbV9qd3Rfc2VjcmV0X3Rva2Vuc3RhbV9qd3Rfc2VjcmV0X3RhbV9qd3Rfc2VjcmV0X3RhbV9qd3Rfc2VjcmV0X3Rva2Vu
 | 
					token.secret_key=c3RhbV9qd3Rfc2VjcmV0X3Rva2Vuc3RhbV9qd3Rfc2VjcmV0X3RhbV9qd3Rfc2VjcmV0X3RhbV9qd3Rfc2VjcmV0X3Rva2Vu
 | 
				
			||||||
# 운영 환경 변수 설정 필요
 | 
					# 운영 환경 변수 설정 필요
 | 
				
			||||||
# token.secret_key=${JWT_SECRET_KEY:}
 | 
					# token.secret_key=${JWT_SECRET_KEY:}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user