[JWT 개선] MemberController에서 로그아웃 시 모든 토큰 쿠키 삭제 기능 추가 및 JwtUtils에 Access/Refresh Token 쿠키 설정 및 삭제 메서드 추가. JwtTokenIssuanceFilter와 JwtTokenValidationFilter에서 Access Token을 헤더 대신 쿠키에 저장하도록 변경.

This commit is contained in:
2025-08-28 16:46:53 +09:00
parent afef6dfa80
commit e8a785a20d
5 changed files with 56 additions and 22 deletions

View File

@@ -22,6 +22,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import com.bio.bio_backend.global.constants.ApiResponseCode; import com.bio.bio_backend.global.constants.ApiResponseCode;
import com.bio.bio_backend.global.annotation.LogExecution; import com.bio.bio_backend.global.annotation.LogExecution;
import com.bio.bio_backend.global.utils.SecurityUtils; import com.bio.bio_backend.global.utils.SecurityUtils;
import com.bio.bio_backend.global.utils.JwtUtils;
import jakarta.servlet.http.HttpServletResponse;
@Tag(name = "Member", description = "회원 관련 API") @Tag(name = "Member", description = "회원 관련 API")
@@ -33,6 +35,7 @@ public class MemberController {
private final MemberService memberService; private final MemberService memberService;
private final MemberMapper memberMapper; private final MemberMapper memberMapper;
private final JwtUtils jwtUtils;
@LogExecution("회원 등록") @LogExecution("회원 등록")
@Operation(summary = "회원 등록", description = "새로운 회원을 등록합니다.") @Operation(summary = "회원 등록", description = "새로운 회원을 등록합니다.")
@@ -57,10 +60,14 @@ public class MemberController {
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content(schema = @Schema(implementation = ApiResponseDto.class))) @ApiResponse(responseCode = "401", description = "인증 실패", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
}) })
@PostMapping("/logout") @PostMapping("/logout")
public ResponseEntity<ApiResponseDto<Void>> logout() { public ResponseEntity<ApiResponseDto<Void>> logout(HttpServletResponse response) {
try { try {
String userId = SecurityUtils.getCurrentUserId(); String userId = SecurityUtils.getCurrentUserId();
memberService.deleteRefreshToken(userId); memberService.deleteRefreshToken(userId);
// 모든 토큰 쿠키 삭제
jwtUtils.deleteAllTokenCookies(response);
log.info("사용자 로그아웃 완료: {}", userId); log.info("사용자 로그아웃 완료: {}", userId);
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS)); return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS));

View File

@@ -16,8 +16,6 @@ public class CorsConfig {
config.addAllowedHeader("*"); config.addAllowedHeader("*");
config.addAllowedMethod("*"); config.addAllowedMethod("*");
config.setAllowCredentials(true); config.setAllowCredentials(true);
config.addExposedHeader("Authorization");
source.registerCorsConfiguration("/**", config); source.registerCorsConfiguration("/**", config);
return new CorsFilter(source); return new CorsFilter(source);

View File

@@ -77,8 +77,8 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter
// Refresh 토큰 쿠키 저장 // Refresh 토큰 쿠키 저장
jwtUtils.setRefreshTokenCookie(response, refreshToken); jwtUtils.setRefreshTokenCookie(response, refreshToken);
// Access 토큰 전달 // Access 토큰 쿠키 저장
response.setHeader("Authorization", "Bearer " + accessToken); jwtUtils.setAccessTokenCookie(response, accessToken);
SecurityContextHolderStrategy contextHolder = SecurityContextHolder.getContextHolderStrategy(); SecurityContextHolderStrategy contextHolder = SecurityContextHolder.getContextHolderStrategy();
SecurityContext context = contextHolder.createEmptyContext(); SecurityContext context = contextHolder.createEmptyContext();

View File

@@ -92,8 +92,8 @@ public class JwtTokenValidationFilter extends OncePerRequestFilter {
// 새로운 Access Token 생성 // 새로운 Access Token 생성
String newAccessToken = jwtUtils.generateToken(username, Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access")))); String newAccessToken = jwtUtils.generateToken(username, Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access"))));
// 새로운 Access Token을 응답 헤더에 설정 // 새로운 Access Token을 쿠키에 설정
response.setHeader("Authorization", "Bearer " + newAccessToken); jwtUtils.setAccessTokenCookie(response, newAccessToken);
// Refresh Token 갱신 // Refresh Token 갱신
String newRefreshToken = jwtUtils.createRefreshToken(username, httpUtils.getClientIp()); String newRefreshToken = jwtUtils.createRefreshToken(username, httpUtils.getClientIp());

View File

@@ -13,7 +13,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.util.Date; import java.util.Date;
@@ -49,7 +48,7 @@ public class JwtUtils {
public String generateToken(String username, String clientIp, long expirationTime) { public String generateToken(String username, String clientIp, long expirationTime) {
return Jwts.builder() return Jwts.builder()
.subject(username) .subject(username)
.claim("ip", clientIp) // IP 정보 추가 .claim("ip", clientIp)
.issuedAt(new Date(System.currentTimeMillis())) .issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expirationTime)) .expiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey()) .signWith(getSigningKey())
@@ -123,8 +122,6 @@ public class JwtUtils {
public String extractUsername(String token) { public String extractUsername(String token) {
return extractAllClaims(token).getSubject(); return extractAllClaims(token).getSubject();
} }
public Claims extractAllClaims(String token) { public Claims extractAllClaims(String token) {
return Jwts.parser() return Jwts.parser()
@@ -133,14 +130,19 @@ public class JwtUtils {
.parseSignedClaims(token).getPayload(); .parseSignedClaims(token).getPayload();
} }
// Access Token을 쿠키에서 추출
public String extractAccessJwtFromRequest(HttpServletRequest request) { public String extractAccessJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization"); if (request.getCookies() != null) {
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { for (Cookie cookie : request.getCookies()) {
return bearerToken.substring(7); // "Bearer " 제거 if ("AccessToken".equals(cookie.getName())) {
return cookie.getValue();
}
}
} }
return bearerToken; return null;
} }
// Refresh Token을 쿠키에서 추출
public String extractRefreshJwtFromCookie(HttpServletRequest request) { public String extractRefreshJwtFromCookie(HttpServletRequest request) {
if (request.getCookies() != null) { if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) { for (Cookie cookie : request.getCookies()) {
@@ -154,12 +156,39 @@ public class JwtUtils {
// Refresh Token 쿠키 설정 // Refresh Token 쿠키 설정
public void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) { public void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) {
Cookie refreshTokenCookie = new Cookie("RefreshToken", refreshToken); setCookie(response, "RefreshToken", refreshToken,
refreshTokenCookie.setHttpOnly(true); Integer.parseInt(env.getProperty("token.expiration_time_refresh")) / 1000);
refreshTokenCookie.setSecure(false); }
refreshTokenCookie.setPath("/");
refreshTokenCookie.setMaxAge(Integer.parseInt(env.getProperty("token.expiration_time_refresh"))); // Access Token 쿠키 설정
public void setAccessTokenCookie(HttpServletResponse response, String accessToken) {
response.addCookie(refreshTokenCookie); setCookie(response, "AccessToken", accessToken,
Integer.parseInt(env.getProperty("token.expiration_time_access")) / 1000);
}
// Access Token 쿠키 삭제
public void deleteAccessTokenCookie(HttpServletResponse response) {
setCookie(response, "AccessToken", "", 0);
}
// Refresh Token 쿠키 삭제
public void deleteRefreshTokenCookie(HttpServletResponse response) {
setCookie(response, "RefreshToken", "", 0);
}
// 모든 토큰 쿠키 삭제
public void deleteAllTokenCookies(HttpServletResponse response) {
deleteAccessTokenCookie(response);
deleteRefreshTokenCookie(response);
}
// 쿠키 설정 헬퍼 메서드
private void setCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
} }
} }