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
@@ -1,6 +1,7 @@
package hackerthon.likelion13th.canfly.global.config;

import hackerthon.likelion13th.canfly.login.auth.jwt.AuthCreationFilter;
import hackerthon.likelion13th.canfly.login.auth.jwt.FrontRedirectCaptureFilter;
import hackerthon.likelion13th.canfly.login.auth.jwt.JwtValidationFilter;
import hackerthon.likelion13th.canfly.login.auth.utils.OAuth2SuccessHandler;
import hackerthon.likelion13th.canfly.login.auth.utils.OAuth2UserServiceImpl;
Expand All @@ -11,9 +12,9 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
Expand All @@ -31,6 +32,7 @@ public class SecurityConfig {
private final JwtValidationFilter jwtValidationFilter;
private final OAuth2UserServiceImpl oAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final FrontRedirectCaptureFilter frontRedirectCaptureFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
Expand All @@ -45,11 +47,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/health",
"/swagger-ui/**",
"/v3/api-docs/**",
"/oauth2/authorization/kakao",
"/login/oauth2/code/**",
"/oauth2/**", // 🟡 카카오 OAuth 리디렉션
"/login/oauth2/**", // 🟡 카카오 OAuth 콜백
"/token/**",
"/oauth/**",
"/token/return",
"/index.html",
"/users/me",
"/users/logout",
Expand All @@ -68,6 +68,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(frontRedirectCaptureFilter, OAuth2AuthorizationRequestRedirectFilter.class)
.addFilterBefore(authCreationFilter, AuthorizationFilter.class)
.addFilterBefore(jwtValidationFilter, AuthCreationFilter.class);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package hackerthon.likelion13th.canfly.login.auth.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j
@Component
public class FrontRedirectCaptureFilter extends OncePerRequestFilter {

public static final String ATTR = "FRONT_REDIRECT_URI";
public static final String PARAM = "front_redirect";

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws ServletException, IOException {

// 인가 시작점으로 들어오는 요청에서만 캡처
String path = request.getServletPath(); // ex) /oauth2/authorization/kakao
if (path != null && path.startsWith("/oauth2/authorization/")) {
String redirect = request.getParameter(PARAM);
if (redirect == null) {
// 혹시 프론트가 아직 redirect_uri를 쓰고 있다면 백업으로도 받자
redirect = request.getParameter("redirect_uri");
}
if (redirect == null) {
// 헤더로 보냈다면 이것도 백업
redirect = request.getHeader("X-Front-Redirect");
}
if (redirect != null && !redirect.isBlank()) {
HttpSession session = request.getSession(true);
session.setAttribute(ATTR, redirect);
log.info("[FRONT_REDIRECT] saved into session: {}", redirect);
}
}

chain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import hackerthon.likelion13th.canfly.login.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -18,6 +19,9 @@
import java.io.IOException;
import java.util.List;

import static hackerthon.likelion13th.canfly.login.auth.jwt.FrontRedirectCaptureFilter.ATTR;
import static hackerthon.likelion13th.canfly.login.auth.jwt.FrontRedirectCaptureFilter.PARAM;

@Slf4j
@Component
@RequiredArgsConstructor
Expand All @@ -29,15 +33,6 @@ public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
@Value("${frontend.base-url}")
private String defaultRedirect;

/** 허용 URI 화이트리스트 */
private List<String> getAuthorizedUris() {
return List.of(
defaultRedirect,
"http://localhost:3000",
"http://localhost:3000/"
);
}

@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
Expand All @@ -47,10 +42,10 @@ public void onAuthenticationSuccess(

/* 1. OAuth2User 추출 */
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
String provider = oAuth2User.getAttribute("provider"); // kakao
String providerId = oAuth2User.getAttribute("id"); // 4351993247
String nickname = oAuth2User.getAttribute("name"); // 강민준
String email = oAuth2User.getAttribute("email");
String provider = oAuth2User.getAttribute("provider"); // kakao
String providerId = oAuth2User.getAttribute("id"); // 4351993247
String nickname = oAuth2User.getAttribute("name"); // 강민준
String email = oAuth2User.getAttribute("email");

/* 2. username 형식: {kakao}강민준 */
String username = String.format("{%s}%s", provider, nickname);
Expand All @@ -74,17 +69,39 @@ public void onAuthenticationSuccess(
/* 4. JWT 즉시 발급 (providerId 기준) */
JwtDto jwt = userService.jwtMakeSave(providerId);
log.info("🔑 JWT 발급 완료 | providerId={}", providerId);

/* 5. 프로필 미완료 여부(needsProfile) 계산 */
boolean needsProfile = userService.needsProfile(providerId);

/* 6. redirect_uri 검증 */
String frontRedirect = request.getParameter("redirect_uri");
if (frontRedirect == null || !getAuthorizedUris().contains(frontRedirect)) {
HttpSession session = request.getSession(false);
String frontRedirect = null;
if (session != null) {
frontRedirect = (String) session.getAttribute(ATTR);
session.removeAttribute(ATTR); // 일회성
log.info("[FRONT_REDIRECT] loaded from session: {}", frontRedirect);
}

// 혹시 세션이 비어있다면 파라미터/헤더 백업
if (frontRedirect == null) {
frontRedirect = request.getParameter(PARAM);
if (frontRedirect == null) {
frontRedirect = request.getParameter("redirect_uri");
}
if (frontRedirect == null) {
frontRedirect = request.getHeader("X-Front-Redirect");
}
}

// 화이트리스트(정확 매칭) — 필요 시 정규화 로직 추가
List<String> allow = List.of(
defaultRedirect,
"http://localhost:3000",
"http://localhost:3000/"
);
if (frontRedirect == null || !allow.contains(frontRedirect)) {
log.warn("[FRONT_REDIRECT] not allowed or missing → fallback: {}", defaultRedirect);
frontRedirect = defaultRedirect;
}

/* 7. accessToken + needsProfile로 리다이렉트 */
String redirectUrl = UriComponentsBuilder.fromUriString(frontRedirect)
.queryParam("accessToken", jwt.getAccessToken())
.queryParam("needsProfile", needsProfile)
Expand Down