Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.back.web7_9_codecrete_be.domain.auth.dto.request.LoginRequest;
import com.back.web7_9_codecrete_be.domain.auth.dto.request.SignupRequest;
import com.back.web7_9_codecrete_be.domain.auth.dto.response.LoginResponse;
import com.back.web7_9_codecrete_be.domain.auth.dto.response.TokenResponse;
import com.back.web7_9_codecrete_be.domain.auth.service.AuthService;
import com.back.web7_9_codecrete_be.domain.auth.service.TokenService;
import com.back.web7_9_codecrete_be.domain.users.dto.response.UserResponse;
Expand Down Expand Up @@ -87,8 +88,8 @@ public RsData<?> getMyInfo() {
@Operation(summary = "액세스 토큰 재발급", description = "리프레시 토큰을 이용하여 새로운 액세스 토큰을 발급합니다.")
@PostMapping("/refresh")
public RsData<?> refresh() {
String newAccessToken = tokenService.reissueAccessToken();
return RsData.success("토큰 재발급 완료", newAccessToken);
TokenResponse response = tokenService.reissueAccessToken();
return RsData.success("토큰 재발급 완료", response);
}

@Operation(summary = "카카오 소셜 로그인", description = "카카오 OAuth 인가 코드를 이용해 로그인/회원가입을 진행합니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
package com.back.web7_9_codecrete_be.domain.auth.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class TokenResponse {
@Schema(description = "액세스 토큰")
private String accessToken;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.back.web7_9_codecrete_be.domain.auth.service;

import com.back.web7_9_codecrete_be.domain.auth.dto.response.TokenResponse;
import com.back.web7_9_codecrete_be.domain.auth.entity.RefreshToken;
import com.back.web7_9_codecrete_be.domain.auth.repository.RefreshTokenRedisRepository;
import com.back.web7_9_codecrete_be.domain.users.entity.User;
Expand Down Expand Up @@ -28,8 +29,8 @@ public void issueTokens(User user) {
String access = jwtTokenProvider.generateAccessToken(user.getEmail());
String refresh = jwtTokenProvider.generateRefreshToken(user.getEmail());

rq.setCookie("ACCESS_TOKEN", access, (int) jwtProperties.getAccessTokenExpiration());
rq.setCookie("REFRESH_TOKEN", refresh, (int) jwtProperties.getRefreshTokenExpiration());
rq.setCookie("ACCESS_TOKEN", access, jwtProperties.getAccessTokenExpiration());
rq.setCookie("REFRESH_TOKEN", refresh, jwtProperties.getRefreshTokenExpiration());

refreshTokenRedisRepository.save(
new RefreshToken(
Expand All @@ -47,18 +48,15 @@ public void removeTokens(User user) {
refreshTokenRedisRepository.deleteByUserId(user.getId());
}

public String reissueAccessToken() {
public TokenResponse reissueAccessToken() {

// Refresh Token 쿠키 찾기
String refresh = rq.getCookieValue("REFRESH_TOKEN");
if (refresh == null) {
throw new BusinessException(AuthErrorCode.TOKEN_MISSING);
}

// RefreshToken 검증
jwtTokenProvider.validateToken(refresh);

// RefreshToken에서 email(Subject) 추출
String email = jwtTokenProvider.getEmailFromToken(refresh);

User user = userRepository.findByEmail(email)
Expand All @@ -69,17 +67,14 @@ public String reissueAccessToken() {
}

String savedRefresh = refreshTokenRedisRepository.findByUserId(user.getId());

if (savedRefresh == null || !savedRefresh.equals(refresh)) {
throw new BusinessException(AuthErrorCode.INVALID_TOKEN);
}

// AccessToken 재발급
String newAccess = jwtTokenProvider.generateAccessToken(email);

// AccessToken 쿠키에 다시 저장
rq.setCookie("ACCESS_TOKEN", newAccess, (int) jwtProperties.getAccessTokenExpiration());
rq.setCookie("ACCESS_TOKEN", newAccess, jwtProperties.getAccessTokenExpiration());

return newAccess;
return new TokenResponse(newAccess);
}
}
55 changes: 41 additions & 14 deletions src/main/java/com/back/web7_9_codecrete_be/global/rq/Rq.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
Expand All @@ -16,10 +18,15 @@ public class Rq {

private final HttpServletRequest request;
private final HttpServletResponse response;
private final boolean isProd;

public Rq(HttpServletRequest request, HttpServletResponse response) {
public Rq(HttpServletRequest request,
HttpServletResponse response,
@Value("${spring.profiles.active:local}") String activeProfile)
{
this.request = request;
this.response = response;
this.isProd = activeProfile.equals("prod");
}

// 현재 인증된 사용자 정보 가져오기
Expand All @@ -39,27 +46,47 @@ public User getUser() {
}

// 쿠키 설정
public void setCookie(String name, String value, int maxAge) {
public void setCookie(String name, String value, long maxAge) {
String safeValue = value != null ? value : "";

Cookie cookie = new Cookie(name, safeValue);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(maxAge);
cookie.setSecure(true); // https 환경 권장 옵션
ResponseCookie.ResponseCookieBuilder builder = ResponseCookie.from(name, safeValue)
.path("/")
.httpOnly(true)
.maxAge(maxAge);

if (isProd) {
builder
.secure(true)
.sameSite("None")
.domain(".naeconcertbutakhae.shop");
} else {
builder
.secure(false)
.sameSite("Lax");
}

response.addCookie(cookie);
response.addHeader("Set-Cookie", builder.build().toString());
}

// 쿠키 제거
public void removeCookie(String name) {
Cookie cookie = new Cookie(name, null);
cookie.setPath("/");
cookie.setMaxAge(0);
cookie.setHttpOnly(true);
cookie.setSecure(true);
ResponseCookie.ResponseCookieBuilder builder = ResponseCookie.from(name, "")
.path("/")
.httpOnly(true)
.maxAge(0);

if (isProd) {
builder
.secure(true)
.sameSite("None")
.domain(".naeconcertbutakhae.shop");
} else {
builder
.secure(false)
.sameSite("Lax");
}

response.addCookie(cookie);
response.addHeader("Set-Cookie", builder.build().toString());
}

public String getCookieValue(String name) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.back.web7_9_codecrete_be.global.security;

import com.back.web7_9_codecrete_be.domain.auth.service.TokenService;
import com.back.web7_9_codecrete_be.global.error.code.AuthErrorCode;
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand Down Expand Up @@ -33,42 +32,33 @@ protected void doFilterInternal(HttpServletRequest request,
FilterChain filterChain)
throws ServletException, IOException {

String accessToken = resolveToken(request);
String accessToken = resolveToken(request);

log.info("[JwtFilter] URI = {}", request.getRequestURI());
log.info("[JwtFilter] ACCESS_TOKEN = {}", accessToken);

// Access Token이 있는 경우 우선 검증 시도
if (StringUtils.hasText(accessToken)) {
try {
log.info("[JwtFilter] validating token");

if (jwtTokenProvider.validateToken(accessToken)) {
log.info("[JwtFilter] token valid");

Authentication auth =
jwtTokenProvider.getAuthentication(accessToken);

log.info("[JwtFilter] auth = {}", auth);

SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
return;
}
} catch (BusinessException e) {
// Access Token 만료가 아닌 경우 재발급 안 함
if (e.getErrorCode() != AuthErrorCode.TOKEN_EXPIRED) {
log.debug("Invalid access token: {}", e.getErrorCode());
filterChain.doFilter(request, response);
return;
}
// TOKEN_EXPIRED 인 경우만 아래 재발급 로직으로 내려감
log.info("[JwtFilter] token invalid: {}", e.getErrorCode());
SecurityContextHolder.clearContext();
}
}

// Access Token이 없거나 / 만료된 경우 Refresh 기반 재발급 시도
try {
String newAccess = tokenService.reissueAccessToken();

Authentication auth =
jwtTokenProvider.getAuthentication(newAccess);
SecurityContextHolder.getContext().setAuthentication(auth);

} catch (BusinessException ex) {
// 재발급 실패 시 SecurityContext 비우기
SecurityContextHolder.clearContext();
log.debug("Access Token 재발급 실패: {}", ex.getErrorCode());
}
log.info("[JwtFilter] SecurityContext = {}",
SecurityContextHolder.getContext().getAuthentication());

filterChain.doFilter(request, response);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.back.web7_9_codecrete_be.global.security;

import java.util.List;

import com.back.web7_9_codecrete_be.domain.auth.service.TokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
Expand All @@ -14,9 +14,7 @@
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.back.web7_9_codecrete_be.domain.auth.service.TokenService;

import lombok.RequiredArgsConstructor;
import java.util.List;

@Configuration
@RequiredArgsConstructor
Expand Down Expand Up @@ -85,7 +83,7 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

configuration.setAllowedOrigins(List.of("http://localhost:3000", "https://web-6-7-codecrete-fe.vercel.app", "https://www.naeconcertbutakhae.shop"));
configuration.setAllowedOrigins(List.of("http://localhost:3000", "https://*.naeconcertbutakhae.shop"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));

configuration.setAllowedHeaders(List.of("*"));
Expand All @@ -94,7 +92,7 @@ public UrlBasedCorsConfigurationSource corsConfigurationSource() {
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
source.registerCorsConfiguration("/**", configuration);

return source;
}
Expand Down