From 438bfc3bc51e11632660307bbda417faf2cf7562 Mon Sep 17 00:00:00 2001 From: sohot8653 Date: Wed, 27 Aug 2025 15:11:29 +0900 Subject: [PATCH] =?UTF-8?q?[API=20=EC=9D=91=EB=8B=B5=20=EA=B0=9C=EC=84=A0]?= =?UTF-8?q?=20ApiResponseCode=EC=97=90=20COMMON=5FTARGET=5FNOT=5FFOUND=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20CommonCodeServiceImpl=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95.=20JwtTokenIssuanceFilt?= =?UTF-8?q?er=EC=99=80=20JwtTokenValidationFilter=EC=97=90=EC=84=9C=20Refr?= =?UTF-8?q?esh=20Token=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20IP=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=ED=8F=AC=ED=95=A8.=20JwtUtils=EC=97=90=EC=84=9C=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=83=9D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CommonCodeServiceImpl.java | 2 +- .../global/constants/ApiResponseCode.java | 1 + .../global/filter/JwtTokenIssuanceFilter.java | 2 +- .../filter/JwtTokenValidationFilter.java | 89 ++++++++++--------- .../global/security/WebSecurity.java | 2 +- .../bio_backend/global/utils/JwtUtils.java | 89 ++++++------------- 6 files changed, 79 insertions(+), 106 deletions(-) diff --git a/src/main/java/com/bio/bio_backend/domain/admin/common_code/service/CommonCodeServiceImpl.java b/src/main/java/com/bio/bio_backend/domain/admin/common_code/service/CommonCodeServiceImpl.java index 199b43c..b9fb8b5 100644 --- a/src/main/java/com/bio/bio_backend/domain/admin/common_code/service/CommonCodeServiceImpl.java +++ b/src/main/java/com/bio/bio_backend/domain/admin/common_code/service/CommonCodeServiceImpl.java @@ -46,7 +46,7 @@ public class CommonCodeServiceImpl implements CommonCodeService { @Transactional public void updateGroupCode(String code, CommonGroupCodeDto groupCodeDto) { CommonGroupCode existingGroupCode = commonGroupCodeRepository.findByCode(code) - .orElseThrow(() -> new ApiException(ApiResponseCode.COMMON_NOT_FOUND, "그룹 코드를 찾을 수 없습니다: " + code)); + .orElseThrow(() -> new ApiException(ApiResponseCode.COMMON_TARGET_NOT_FOUND, "그룹 코드를 찾을 수 없습니다: " + code)); commonGroupCodeMapper.updateCommonGroupCodeFromDto(groupCodeDto, existingGroupCode); commonGroupCodeRepository.save(existingGroupCode); diff --git a/src/main/java/com/bio/bio_backend/global/constants/ApiResponseCode.java b/src/main/java/com/bio/bio_backend/global/constants/ApiResponseCode.java index fc5bb88..b10f764 100644 --- a/src/main/java/com/bio/bio_backend/global/constants/ApiResponseCode.java +++ b/src/main/java/com/bio/bio_backend/global/constants/ApiResponseCode.java @@ -33,6 +33,7 @@ public enum ApiResponseCode { // 404 Not Found COMMON_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "리소스를 찾을 수 없습니다"), + COMMON_TARGET_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "대상을 찾을 수 없습니다"), // 405 Method Not Allowed COMMON_METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED.value(), "허용되지 않는 메소드입니다"), 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 8de7e9b..87acfd6 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 @@ -65,7 +65,7 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter // 토큰 생성 String accessToken = jwtUtils.createAccessToken(member.getUserId(), member.getRole().getValue()); - String refreshToken = jwtUtils.createRefreshToken(member.getUserId(), member.getRole().getValue()); + String refreshToken = jwtUtils.createRefreshToken(member.getUserId(), member.getRole().getValue(), httpUtils.getClientIp()); member.setRefreshToken(refreshToken); member.setLoginIp(httpUtils.getClientIp()); 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 001e2b9..74f209b 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 @@ -3,6 +3,7 @@ package com.bio.bio_backend.global.filter; import java.io.IOException; import java.util.Objects; +import com.bio.bio_backend.global.utils.HttpUtils; import org.springframework.core.env.Environment; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -30,6 +31,7 @@ import lombok.extern.slf4j.Slf4j; public class JwtTokenValidationFilter extends OncePerRequestFilter { private final JwtUtils jwtUtils; + private final HttpUtils httpUtils; private final MemberService memberService; private final Environment env; private final SecurityPathConfig securityPathConfig; @@ -66,52 +68,53 @@ public class JwtTokenValidationFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); return; } - } + } else { + // Access Token이 없거나 만료된 경우, Refresh Token으로 갱신 시도 + if (refreshToken != null) { + // 1. Refresh Token 유효성 검증 + if (!jwtUtils.isValidRefreshToken(refreshToken)) { + log.warn("Refresh Token이 유효하지 않습니다. URI: {}", request.getRequestURI()); + sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.JWT_TOKEN_EXPIRED)); + return; + } - // Access Token이 없거나 만료된 경우, Refresh Token으로 갱신 시도 - if (refreshToken != null) { - // 1. Refresh Token 유효성 검증 - if (!jwtUtils.isValidRefreshToken(refreshToken)) { - log.warn("Refresh Token이 유효하지 않습니다. URI: {}", request.getRequestURI()); - sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.JWT_TOKEN_EXPIRED)); + // 2. IP 주소 검증 + if (!jwtUtils.isValidClientIp(refreshToken, request.getRemoteAddr())) { + log.warn("클라이언트 IP 주소가 일치하지 않습니다. URI: {}, IP: {}", + request.getRequestURI(), request.getRemoteAddr()); + sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.INVALID_CLIENT_IP)); + return; + } + + // 모든 검증을 통과한 경우 토큰 갱신 진행 + String username = jwtUtils.extractUsername(refreshToken); + String role = jwtUtils.extractRole(refreshToken); + + // 새로운 Access Token 생성 + String newAccessToken = jwtUtils.generateToken(username, role, + Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access")))); + + // 새로운 Access Token을 응답 헤더에 설정 + response.setHeader("Authorization", "Bearer " + newAccessToken); + + // Refresh Token 갱신 + String newRefreshToken = jwtUtils.createRefreshToken(username, role, httpUtils.getClientIp()); + jwtUtils.setRefreshTokenCookie(response, newRefreshToken); + + // 인증 정보 설정 + UserDetails userDetails = memberService.loadUserByUsername(username); + if (userDetails != null) { + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + log.info("토큰 자동 갱신 성공: {}", username); + filterChain.doFilter(request, response); return; } - - // 2. IP 주소 검증 - if (!jwtUtils.isValidClientIp(refreshToken, request.getRemoteAddr())) { - log.warn("클라이언트 IP 주소가 일치하지 않습니다. URI: {}, IP: {}", - request.getRequestURI(), request.getRemoteAddr()); - sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.INVALID_CLIENT_IP)); - return; - } - - // 모든 검증을 통과한 경우 토큰 갱신 진행 - String username = jwtUtils.extractUsername(refreshToken); - String role = jwtUtils.extractRole(refreshToken); - - // 새로운 Access Token 생성 - String newAccessToken = jwtUtils.generateToken(username, role, - Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access")))); - - // 새로운 Access Token을 응답 헤더에 설정 - response.setHeader("Authorization", "Bearer " + newAccessToken); - - // Refresh Token 갱신 - String newRefreshToken = jwtUtils.refreshTokens(username, role); - jwtUtils.setRefreshTokenCookie(response, newRefreshToken); - - // 인증 정보 설정 - UserDetails userDetails = memberService.loadUserByUsername(username); - if (userDetails != null) { - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authentication); - } - - log.info("토큰 자동 갱신 성공: {}", username); - filterChain.doFilter(request, response); - return; + } // 토큰이 없거나 모두 유효하지 않은 경우 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 126e52e..88094bc 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 @@ -46,7 +46,7 @@ public class WebSecurity { } private JwtTokenValidationFilter getJwtTokenValidationFilter() { - return new JwtTokenValidationFilter(jwtUtils, memberService, env, securityPathConfig); + return new JwtTokenValidationFilter(jwtUtils, httpUtils, memberService, env, securityPathConfig); } 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 00cbd6a..7c9347b 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 @@ -37,7 +37,6 @@ public class JwtUtils { // Token 생성 public String generateToken(String username, String role, long expirationTime) { - return Jwts.builder() .subject(username) .claim("role", role) @@ -47,10 +46,34 @@ public class JwtUtils { .compact(); } + // Token 생성(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(); + } + + // Access Token 생성 + public String createAccessToken(String username, String role) { + long expirationTime = Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access"))); + return generateToken(username, role, expirationTime); + } + + // Refresh Token 생성 시 IP 정보 포함 + public String createRefreshToken(String username, String role, String clientIp) { + long expirationTime = Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_refresh"))); + return generateToken(username, role, clientIp, expirationTime); + } + // Token 검증 public Boolean validateAccessToken(String token) { try { - return isTokenExpired(token); + return isValidTokenExpired(token); } catch (io.jsonwebtoken.ExpiredJwtException e) { log.debug("Access Token 만료: {}", e.getMessage()); return false; @@ -60,50 +83,17 @@ public class JwtUtils { } } - // 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) { - // 1. 토큰 유효성 검증 - if (!isValidRefreshToken(token)) { - return false; - } - - // 2. IP 주소 검증 - if (!isValidClientIp(token, clientIp)) { - return false; - } - - return true; - } // Refresh Token 유효성 검증 (토큰 일치, 만료 여부) public Boolean isValidRefreshToken(String token) { try { String savedToken = memberService.getRefreshToken(extractUsername(token)); - return savedToken.equals(token) && !isTokenExpired(token); + return savedToken.equals(token) && isValidTokenExpired(token); } catch (Exception e) { log.debug("Refresh Token 검증 실패: {}", e.getMessage()); return false; @@ -127,8 +117,9 @@ public class JwtUtils { } } - private boolean isTokenExpired(String token) { - return !extractAllClaims(token).getExpiration().before(new Date()); + private boolean isValidTokenExpired(String token) { + Date expiration = extractAllClaims(token).getExpiration(); + return !expiration.before(new Date()); } public String extractUsername(String token) { @@ -142,7 +133,6 @@ public class JwtUtils { } public Claims extractAllClaims(String token) { - return Jwts.parser() .verifyWith(getSigningKey()) .build() @@ -167,27 +157,6 @@ public class JwtUtils { } return null; } - - // Access Token 생성 - public String createAccessToken(String username, String role) { - return generateToken(username, role, - Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access")))); - } - - // Refresh Token 생성 - public String createRefreshToken(String username, String role) { - return generateToken(username, role, - Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_refresh")))); - } - - // Refresh Token 갱신 (Access Token 갱신 시 함께) - public String refreshTokens(String username, String role) { - // 새로운 Refresh Token 생성 및 DB 저장 - String newRefreshToken = createRefreshToken(username, role); - - log.info("Refresh Token 갱신 완료: {}", username); - return newRefreshToken; - } // Refresh Token 쿠키 설정 public void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) {