From d8fe8399f7aecd6ea82990a39068b8e30223edc6 Mon Sep 17 00:00:00 2001 From: sohot8653 Date: Mon, 25 Aug 2025 11:25:45 +0900 Subject: [PATCH] =?UTF-8?q?[=ED=9A=8C=EC=9B=90=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0]=20Member=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EB=B0=8F=20DTO=EC=97=90=20login=5Fip=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20JWT=20=EA=B4=80=EB=A0=A8=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EC=97=90=EC=84=9C=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20IP=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B0=9C=EC=84=A0.=20Refresh=20Token=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20=EC=8B=9C=20IP=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=8F=AC=ED=95=A8=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=B3=B4=EC=95=88=20=EA=B0=95=ED=99=94.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ddl/schema.sql | 1 + .../domain/base/member/dto/MemberDto.java | 1 + .../domain/base/member/entity/Member.java | 3 ++ .../base/member/mapper/MemberMapper.java | 1 + .../base/member/service/MemberService.java | 1 - .../global/filter/JwtTokenIssuanceFilter.java | 3 ++ .../filter/JwtTokenValidationFilter.java | 2 +- .../global/security/WebSecurity.java | 4 +- .../bio_backend/global/utils/JwtUtils.java | 37 +++++++++++++++++-- src/main/resources/application.properties | 2 +- 10 files changed, 47 insertions(+), 8 deletions(-) diff --git a/ddl/schema.sql b/ddl/schema.sql index c8ddb0f..7483da0 100644 --- a/ddl/schema.sql +++ b/ddl/schema.sql @@ -8,6 +8,7 @@ updated_at timestamp(6) not null, updated_oid bigint, role varchar(40) not null check (role in ('MEMBER','ADMIN','SYSTEM_ADMIN')), + login_ip varchar(45), name varchar(100) not null, password varchar(100) not null, user_id varchar(100) not null, diff --git a/src/main/java/com/bio/bio_backend/domain/base/member/dto/MemberDto.java b/src/main/java/com/bio/bio_backend/domain/base/member/dto/MemberDto.java index 431d02a..53ab425 100644 --- a/src/main/java/com/bio/bio_backend/domain/base/member/dto/MemberDto.java +++ b/src/main/java/com/bio/bio_backend/domain/base/member/dto/MemberDto.java @@ -27,6 +27,7 @@ public class MemberDto implements UserDetails { private MemberRole role; private Boolean useFlag; private String refreshToken; + private String loginIp; private LocalDateTime lastLoginAt; private LocalDateTime createdAt; private LocalDateTime updatedAt; diff --git a/src/main/java/com/bio/bio_backend/domain/base/member/entity/Member.java b/src/main/java/com/bio/bio_backend/domain/base/member/entity/Member.java index 86d0f8b..86b8b9e 100644 --- a/src/main/java/com/bio/bio_backend/domain/base/member/entity/Member.java +++ b/src/main/java/com/bio/bio_backend/domain/base/member/entity/Member.java @@ -48,6 +48,9 @@ public class Member extends BaseEntity { @Column(name = "refresh_token", length = 1024) private String refreshToken; + @Column(name = "login_ip", length = 45) // IPv6 지원을 위해 45자 + private String loginIp; + @Column(name = "last_login_at") private LocalDateTime lastLoginAt; diff --git a/src/main/java/com/bio/bio_backend/domain/base/member/mapper/MemberMapper.java b/src/main/java/com/bio/bio_backend/domain/base/member/mapper/MemberMapper.java index e9ebedf..aabf064 100644 --- a/src/main/java/com/bio/bio_backend/domain/base/member/mapper/MemberMapper.java +++ b/src/main/java/com/bio/bio_backend/domain/base/member/mapper/MemberMapper.java @@ -22,6 +22,7 @@ public interface MemberMapper { @Mapping(target = "role", expression = "java(com.bio.bio_backend.domain.base.member.enums.MemberRole.getDefault())") @Mapping(target = "useFlag", constant = "true") @Mapping(target = "refreshToken", ignore = true) + @Mapping(target = "loginIp", ignore = true) @Mapping(target = "lastLoginAt", ignore = true) @Mapping(target = "createdAt", ignore = true) @Mapping(target = "updatedAt", ignore = true) diff --git a/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberService.java b/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberService.java index 134daa4..6d876c7 100644 --- a/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberService.java +++ b/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberService.java @@ -1,6 +1,5 @@ package com.bio.bio_backend.domain.base.member.service; -import java.time.LocalDateTime; import java.util.List; import java.util.Map; diff --git a/src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java b/src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java index 641e6a3..8de7e9b 100644 --- a/src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java +++ b/src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java @@ -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.global.constants.ApiResponseCode; import com.bio.bio_backend.global.utils.JwtUtils; +import com.bio.bio_backend.global.utils.HttpUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -38,6 +39,7 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter private final JwtUtils jwtUtils; private final ObjectMapper objectMapper; private final MemberService memberService; + private final HttpUtils httpUtils; // 사용자 login 인증 처리 @Override @@ -66,6 +68,7 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter String refreshToken = jwtUtils.createRefreshToken(member.getUserId(), member.getRole().getValue()); member.setRefreshToken(refreshToken); + member.setLoginIp(httpUtils.getClientIp()); member.setLastLoginAt(LocalDateTime.now()); memberService.updateMember(member); diff --git a/src/main/java/com/bio/bio_backend/global/filter/JwtTokenValidationFilter.java b/src/main/java/com/bio/bio_backend/global/filter/JwtTokenValidationFilter.java index 986fc18..4d5b2df 100644 --- a/src/main/java/com/bio/bio_backend/global/filter/JwtTokenValidationFilter.java +++ b/src/main/java/com/bio/bio_backend/global/filter/JwtTokenValidationFilter.java @@ -69,7 +69,7 @@ public class JwtTokenValidationFilter extends OncePerRequestFilter { } // Access Token이 없거나 만료된 경우, Refresh Token으로 갱신 시도 - if (refreshToken != null && jwtUtils.validateRefreshToken(refreshToken)) { + if (refreshToken != null && jwtUtils.validateRefreshToken(refreshToken, request.getRemoteAddr())) { String username = jwtUtils.extractUsername(refreshToken); String role = (String) jwtUtils.extractAllClaims(refreshToken).get("role"); diff --git a/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java b/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java index 1cfdbf7..126e52e 100644 --- a/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java +++ b/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java @@ -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.global.exception.CustomAuthenticationFailureHandler; 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 lombok.RequiredArgsConstructor; @@ -35,9 +36,10 @@ public class WebSecurity { private final ObjectMapper objectMapper; private final Environment env; private final SecurityPathConfig securityPathConfig; + private final HttpUtils httpUtils; 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.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler(objectMapper)); return filter; diff --git a/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java b/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java index b74d520..695a0ce 100644 --- a/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java +++ b/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java @@ -8,7 +8,6 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; 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.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -53,9 +52,39 @@ public class JwtUtils { return isTokenExpired(token); } - public Boolean validateRefreshToken(String token) { - String saveToken = memberService.getRefreshToken(extractUsername(token)); - return (saveToken.equals(token) && isTokenExpired(token)); + // Refresh Token 생성 시 IP 정보 포함 + public String createRefreshToken(String username, String role, String clientIp) { + 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) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a88ddc4..fde4a16 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -92,7 +92,7 @@ decorator.datasource.p6spy.log-format=%(sqlSingleLine) # JWT 설정 # ======================================== token.expiration_time_access=900000 -token.expiration_time_refresh=604800000 +token.expiration_time_refresh=86400000 token.secret_key=c3RhbV9qd3Rfc2VjcmV0X3Rva2Vuc3RhbV9qd3Rfc2VjcmV0X3RhbV9qd3Rfc2VjcmV0X3RhbV9qd3Rfc2VjcmV0X3Rva2Vu # 운영 환경 변수 설정 필요 # token.secret_key=${JWT_SECRET_KEY:}