From f3f0d507805319c20b9612b07cf3084be40d3294 Mon Sep 17 00:00:00 2001 From: Sohee Date: Fri, 14 Nov 2025 16:49:27 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[BE]=20SISC1-209=20[FIX]=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 221 +----------------- .../backend/auth/dto/SignupRequest.java | 4 +- .../auth/config/CustomOAuth2UserService.java | 120 ++++++++++ .../auth/config/OAuth2SuccessHandler.java | 91 ++++++++ .../common/auth/config/SecurityConfig.java | 33 ++- .../user/dto/PasswordResetCommitRequest.java | 18 ++ .../user/dto/PasswordResetSendRequest.java | 6 + .../user/dto/PasswordResetVerifyRequest.java | 10 + .../backend/user/dto/UserIdFindRequest.java | 20 +- .../backend/user/dto/UserInfoResponse.java | 2 +- .../backend/user/dto/UserUpdateRequest.java | 2 +- .../backend/user/service/UserService.java | 3 + .../backend/user/service/UserServiceImpl.java | 40 ++++ backend/src/main/resources/application.yml | 3 +- 14 files changed, 339 insertions(+), 234 deletions(-) create mode 100644 backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java create mode 100644 backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java diff --git a/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java b/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java index d892f641..ac7deebb 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java @@ -47,31 +47,11 @@ ) public class AuthController { - private final Map> oauth2Services; private final LoginService loginService; private final UserService userService; private final JwtProvider jwtProvider; - private final OauthStateService oauthStateService; private final RefreshTokenService refreshTokenService; - @Value("${google.client.id}") - private String googleClientId; - - @Value("${google.redirect.uri}") - private String googleRedirectUri; - - @Value("${kakao.client.id}") - private String kakaoClientId; - - @Value("${kakao.redirect.uri}") - private String kakaoRedirectUri; - - @Value("${github.client.id}") - private String githubClientId; - - @Value("${github.redirect.uri}") - private String githubRedirectUri; - @Operation( summary = "회원가입 API", @@ -136,7 +116,7 @@ public ResponseEntity signup(@Valid @RequestBody SignupRequest r "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", "userId": "1c54b9f3-8234-4e8f-b001-11cc4d9012ab", "name": "홍길동", - "role": "USER", + "role": "TEAM_MEMBER", "phoneNumber": "01012345678" } """)) @@ -164,205 +144,6 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest requ .body(response); } - // OAuth 로그인 시작 (state 생성 + 각 provider별 인증 URL 반환) - @Operation( - summary = "OAuth 로그인 시작 (INIT)", - description = "소셜 로그인 시작 시 각 Provider(GOOGLE, KAKAO, GITHUB)의 인증 URL을 반환합니다.", - responses = { - @ApiResponse( - responseCode = "200", - description = "OAuth 인증 URL 반환 성공", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = "\"https://accounts.google.com/o/oauth2/v2/auth?...\"")) - ), - @ApiResponse(responseCode = "400", description = "지원하지 않는 Provider 요청") - } - ) - @GetMapping("/oauth/{provider}/init") - public ResponseEntity startOauthLogin( - @Parameter(description = "소셜 로그인 제공자 (GOOGLE, KAKAO, GITHUB)", example = "GOOGLE") - @PathVariable String provider, - HttpSession session) { - String state = oauthStateService.generateAndSaveState(session); - String authUrl; - - switch (provider.toUpperCase()) { - case "GOOGLE" -> authUrl = "https://accounts.google.com/o/oauth2/v2/auth" + - "?client_id=" + googleClientId + - "&redirect_uri=" + googleRedirectUri + - "&response_type=code" + - "&scope=email%20profile" + - "&state=" + state; - case "KAKAO" -> authUrl = "https://kauth.kakao.com/oauth/authorize" + - "?client_id=" + kakaoClientId + - "&redirect_uri=" + kakaoRedirectUri + - "&response_type=code" + - "&state=" + state; - case "GITHUB" -> authUrl = "https://github.com/login/oauth/authorize" + - "?client_id=" + githubClientId + - "&redirect_uri=" + githubRedirectUri + - "&scope=user:email" + - "&state=" + state; - default -> throw new IllegalArgumentException("Unknown provider " + provider); - } - - log.debug("Generated OAuth URL for {}: {}", provider, authUrl); - return ResponseEntity.ok(authUrl); - } - - //redirection api - @Operation( - summary = "OAuth 로그인 리다이렉트 (GET)", - description = "소셜 로그인 후 리다이렉션 시 호출되는 엔드포인트입니다. " - + "code와 state 값을 받아 실제 로그인 과정을 처리하며 일반적으로 프론트엔드에서 이 요청을 자동으로 POST로 전달합니다." - - ) - @GetMapping("/login/{provider}") - public void handleOauthRedirect( - @Parameter(description = "소셜 로그인 제공자", example = "GOOGLE") - @PathVariable("provider") String provider, - - @Parameter(description = "OAuth 인증 코드", example = "4/0AbCdEfG...") - @RequestParam("code") String code, - - @Parameter(description = "CSRF 방지용 state 값", example = "a1b2c3d4") - @RequestParam("state") String state, - - HttpSession session, - HttpServletResponse response - ) throws IOException { - - log.info("[{}] OAuth GET redirect received: code={}, state={}", provider, code, state); - - // 기존 POST OauthLogin() 재활용 (로그인 처리 + 토큰 발급) - ResponseEntity result = OauthLogin(provider, code, state, session); - LoginResponse body = result.getBody(); - - if (body == null) { - log.error("OAuth 로그인 실패: 응답 본문이 null입니다."); - response.sendRedirect("http://localhost:5173/oauth/fail"); - return; - } - - // 프론트로 리다이렉트 (accessToken, userId, name 전달) - String redirectUrl = "http://localhost:5173/oauth/success" - + "?accessToken=" + URLEncoder.encode(body.getAccessToken(), StandardCharsets.UTF_8) - + "&userId=" + URLEncoder.encode(body.getUserId().toString(), StandardCharsets.UTF_8) - + "&name=" + URLEncoder.encode(body.getName(), StandardCharsets.UTF_8); - - log.info("[{}] OAuth 로그인 완료 → 프론트로 리다이렉트: {}", provider, redirectUrl); - response.sendRedirect(redirectUrl); - } - - - // OAuth 인증 완료 후 Code + State 처리 - @Operation( - summary = "OAuth 로그인 완료 (POST)", - description = "OAuth 인증 후 전달된 code와 state를 이용해 토큰을 발급받고 사용자 로그인 처리합니다.", - responses = { - @ApiResponse( - responseCode = "200", - description = "OAuth 로그인 성공", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "accessToken": "eyJhbGciOiJIUzI1NiJ9...", - "userId": "3a93f8c2-412b-4d9c-84a2-52bdfec91d11", - "name": "카카오홍길동", - "role": "USER", - "phoneNumber": "01099998888" - } - """)) - ), - @ApiResponse( - responseCode = "401", - description = "잘못된 state 값 또는 만료된 인증 코드", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = """ - { - "message": "Invalid OAuth state or expired authorization code" - } - """)) - ) - } - ) - @PostMapping("/login/{provider}") - public ResponseEntity OauthLogin( - @Parameter(description = "소셜 로그인 제공자", example = "KAKAO") @PathVariable("provider") String provider, - @Parameter(description = "OAuth 인증 코드", example = "4/0AbCdEfG...") @RequestParam("code") String code, - @Parameter(description = "CSRF 방지용 state 값", example = "a1b2c3d4") @RequestParam("state") String state, - HttpSession session) { - - // 서버에 저장된 state와 요청으로 받은 state 비교 - String savedState = oauthStateService.getStateFromSession(session); - - if(savedState == null || !savedState.equals(state)) { - log.warn("[{}] Invalid OAuth state detected. Expected={}, Received={}", provider, savedState, state); - return ResponseEntity.status(401).build(); - } - - oauthStateService.clearState(session); - - Oauth2Service service = oauth2Services.get(provider.toUpperCase()); - if (service == null) { - throw new IllegalArgumentException("Unknown provider " + provider); - } - - User user = switch (provider.toUpperCase()) { - case "GOOGLE" -> { - var googleService = (Oauth2Service) service; - var token = googleService.getAccessToken(code); - var info = googleService.getUserInfo(token.getAccessToken()); - yield userService.findOrCreateUser(new GoogleUserInfoAdapter(info, token.getAccessToken())); - } - case "KAKAO" -> { - var kakaoService = (Oauth2Service) service; - var token = kakaoService.getAccessToken(code); - var info = kakaoService.getUserInfo(token.getAccessToken()); - yield userService.findOrCreateUser(new KakaoUserInfoAdapter(info, token.getAccessToken())); - } - case "GITHUB" -> { - var githubService = (Oauth2Service) service; - var token = githubService.getAccessToken(code); - var info = githubService.getUserInfo(token.getAccessToken()); - yield userService.findOrCreateUser(new GithubUserInfoAdapter(info, token.getAccessToken())); - } - default -> throw new IllegalArgumentException("Unknown provider " + provider); - }; - - // Access 토큰 발급 - String accessToken = jwtProvider.createToken(user.getUserId(), user.getRole(), user.getEmail()); - - String refreshToken = jwtProvider.createRefreshToken(user.getUserId()); - - refreshTokenService.saveOrUpdateToken(user.getUserId(), refreshToken); - - // HttpOnly 쿠키에 담기 - ResponseCookie cookie = ResponseCookie.from("refresh", refreshToken) - .httpOnly(true) - .secure(true) - .sameSite("None") - .path("/") - .maxAge(60L * 60 * 24 * 14) // 2주 - .build(); - - // LoginResponse 생성 - LoginResponse response = LoginResponse.builder() - .accessToken(accessToken) - .userId(user.getUserId()) - .name(user.getName()) - .role(user.getRole()) - .phoneNumber(user.getPhoneNumber()) - .build(); - - log.info("{} 로그인 성공: userId={}, provider={}", provider.toUpperCase(), user.getUserId(), provider.toUpperCase()); - - return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, cookie.toString()) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) - .body(response); - } - @Operation( summary = "Access Token 재발급 API", description = "만료된 Access Token을 Refresh Token으로 재발급받습니다.", diff --git a/backend/src/main/java/org/sejongisc/backend/auth/dto/SignupRequest.java b/backend/src/main/java/org/sejongisc/backend/auth/dto/SignupRequest.java index 11340635..6b443ab7 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/dto/SignupRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/dto/SignupRequest.java @@ -40,7 +40,7 @@ public class SignupRequest { @Schema( description = "사용자 비밀번호 (8자 이상, 숫자/영문/특수문자 조합 권장)", - example = "abcd1234!", + example = "Abcd1234!", requiredMode = Schema.RequiredMode.REQUIRED ) @NotBlank(message = "비밀번호는 필수입니다.") @@ -48,7 +48,7 @@ public class SignupRequest { @Schema( description = "사용자 역할 (USER 또는 ADMIN 등)", - example = "USER", + example = "TEAM_MEMBER", requiredMode = Schema.RequiredMode.REQUIRED ) @NotNull(message = "역할은 필수입니다.") diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java new file mode 100644 index 00000000..6676971f --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java @@ -0,0 +1,120 @@ +package org.sejongisc.backend.common.auth.config; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sejongisc.backend.auth.dao.UserOauthAccountRepository; +import org.sejongisc.backend.auth.entity.AuthProvider; +import org.sejongisc.backend.auth.entity.UserOauthAccount; +import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.entity.Role; +import org.sejongisc.backend.user.entity.User; +import org.sejongisc.backend.user.service.UserServiceImpl; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class CustomOAuth2UserService implements OAuth2UserService { + private final UserRepository userRepository; + private final UserOauthAccountRepository oauthAccountRepository; + + + + @Override + public OAuth2User loadUser(OAuth2UserRequest req) throws OAuth2AuthenticationException { + + + DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oauthUser = delegate.loadUser(req); + + String provider = req.getClientRegistration().getRegistrationId(); + Map attrs = oauthUser.getAttributes(); + + log.info("[OAuth2] Provider = {}", provider); + log.info("[OAuth2] Attributes = {}", attrs); + + String providerUid; + String email = null; + String name = null; + + if (provider.equals("google")) { + providerUid = (String) attrs.get("sub"); + email = (String) attrs.get("email"); + name = (String) attrs.get("name"); + + } else if (provider.equals("kakao")) { + providerUid = String.valueOf(attrs.get("id")); + Map account = (Map) attrs.get("kakao_account"); + Map profile = (Map) account.get("profile"); + + email = (String) account.get("email"); + if (email == null) email = "no-email@kakao.com"; + + name = (String) profile.get("nickname"); + + } else if (provider.equals("github")) { + providerUid = String.valueOf(attrs.get("id")); + email = (String) attrs.get("email"); + if (email == null) email = "no-email@github.com"; + + name = (String) attrs.get("name"); + if (name == null) name = email; + + } else { + throw new OAuth2AuthenticationException("Unsupported provider: " + provider); + } + + // 순환참조 해결: 여기서 userService 호출하지 않음 + + final String fProviderUid = providerUid; + final String fEmail = email; + final String fName = name; + final AuthProvider fAuthProvider = AuthProvider.valueOf(provider.toUpperCase()); + + User user = oauthAccountRepository + .findByProviderAndProviderUid(fAuthProvider, providerUid) + .map(UserOauthAccount::getUser) + .orElseGet(() -> { + + User newUser = User.builder() + .email(fEmail) + .name(fName) + .role(Role.TEAM_MEMBER) + .build(); + + User saved = userRepository.save(newUser); + + UserOauthAccount oauth = UserOauthAccount.builder() + .user(saved) + .provider(fAuthProvider) + .providerUid(fProviderUid) + .build(); + + oauthAccountRepository.save(oauth); + + return saved; + }); + + return new DefaultOAuth2User( + List.of(new SimpleGrantedAuthority("ROLE_TEAM_MEMBER")), + Map.of( + "id", user.getUserId(), + "email", user.getEmail(), + "name", user.getName() + ), + "email" + ); + } +} diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java new file mode 100644 index 00000000..be26a773 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java @@ -0,0 +1,91 @@ +package org.sejongisc.backend.common.auth.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sejongisc.backend.common.auth.jwt.JwtProvider; +import org.sejongisc.backend.auth.service.RefreshTokenService; +import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.entity.User; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtProvider jwtProvider; + private final RefreshTokenService refreshTokenService; + private final UserRepository userRepository; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException{ + + // 1. CustomOAuth2UserService에서 넣어준 attributes 가져오기 + DefaultOAuth2User oauthUser = (DefaultOAuth2User) authentication.getPrincipal(); + + String userIdStr = oauthUser.getAttributes().get("id").toString(); + UUID userId = UUID.fromString(userIdStr); + String email = (String) oauthUser.getAttributes().get("email"); + String name = (String) oauthUser.getAttributes().get("name"); + + log.info("[OAuth2 Success] userId = {}, email = {}", userId, email); + + // 2. User 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found during OAuth login")); + + // 3. AccessToken 발급 + String accessToken = jwtProvider.createToken( + user.getUserId(), + user.getRole(), + user.getEmail() + ); + + // 4. RefreshToken 생성 + String refreshToken = jwtProvider.createRefreshToken(user.getUserId()); + + // 5. RefreshToken 저장(DB or Redis) + refreshTokenService.saveOrUpdateToken(user.getUserId(), refreshToken); + + // 6. HttpOnly 쿠키로 refreshToken 저장 + ResponseCookie cookie = ResponseCookie.from("refresh", refreshToken) + .httpOnly(true) + .secure(false) // 로컬 개발 환경 + .sameSite("None") + .path("/") + .maxAge(60L * 60 * 24 * 14) + .build(); + + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + + // 7. 프론트로 redirect + String redirectUrl = "http://localhost:5173/oauth/success" + + "?accessToken=" + accessToken + + "&name=" + URLEncoder.encode(name, StandardCharsets.UTF_8) + + "&userId=" + userId; + + log.info("[OAuth2 Redirect] {}", redirectUrl); + + getRedirectStrategy().sendRedirect(request, response, redirectUrl); + } + +} diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java index 391998cf..baba71ab 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java @@ -14,6 +14,9 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -31,6 +34,13 @@ public class SecurityConfig { private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAccessDeniedHandler jwtAccessDeniedHandler; + private final CustomOAuth2UserService customOAuth2UserService; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + + @Bean + public AuthorizationRequestRepository authorizationRequestRepository() { + return new HttpSessionOAuth2AuthorizationRequestRepository(); + } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http @@ -42,6 +52,19 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authenticationEntryPoint(jwtAuthenticationEntryPoint) // 인증 실패 시 JSON 응답 .accessDeniedHandler(jwtAccessDeniedHandler) // 인가 실패 시 JSON 응답 ) + .oauth2Login(oauth -> oauth + .authorizationEndpoint(a -> + a.authorizationRequestRepository(authorizationRequestRepository()) + ) + .userInfoEndpoint(u -> + u.userService(customOAuth2UserService) + ) + .successHandler(oAuth2SuccessHandler) + .failureHandler((req, res, ex) -> + res.sendRedirect("http://localhost:5173/oauth/fail") + ) + ) + .authorizeHttpRequests(auth -> { auth .requestMatchers( @@ -68,7 +91,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti // .anyRequest().authenticated(); .anyRequest().permitAll(); }) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + //.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)); if(jwtAuthenticationFilter != null) { http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); @@ -79,9 +104,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); -// config.setAllowedOrigins(List.of( -// "http://localhost:5173" // 허용할 프론트 주소 -// )); + config.setAllowedOrigins(List.of( + "http://localhost:5173" // 허용할 프론트 주소 + )); config.setAllowedOriginPatterns(List.of("*")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.setAllowedHeaders(List.of("*")); diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetCommitRequest.java b/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetCommitRequest.java index 1755fae9..3e135c14 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetCommitRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetCommitRequest.java @@ -1,12 +1,30 @@ package org.sejongisc.backend.user.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; public record PasswordResetCommitRequest( + + @Schema( + example = "b21c9f41-6f8b-4af7-bd42-93f2716c3142", + description = "비밀번호 재설정을 위한 임시 토큰" + ) @NotBlank(message = "resetToken은 필수입니다.") String resetToken, + + @Schema( + example = "Newpass123!", + description = """ + 새로운 비밀번호 입력 + - 8~20자 + - 대문자 최소 1개 + - 소문자 최소 1개 + - 숫자 최소 1개 + - 특수문자 최소 1개 (!@#$%^&*()_+=-{};:'",.<>/?) + """ + ) @NotBlank(message = "새 비밀번호는 필수입니다.") @Pattern( regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+=\\-{}\\[\\];:'\",.<>/?]).{8,20}$", diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetSendRequest.java b/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetSendRequest.java index ef0fee16..79fe5861 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetSendRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetSendRequest.java @@ -1,9 +1,15 @@ package org.sejongisc.backend.user.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; public record PasswordResetSendRequest( + + @Schema( + example = "testuser@example.com", + description = "사용자의 이메일 주소" + ) @NotBlank(message = "이메일은 필수입니다.") @Email(message = "올바른 이메일 형식이 아닙니다.") String email diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetVerifyRequest.java b/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetVerifyRequest.java index aaf5005e..d8407e18 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetVerifyRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetVerifyRequest.java @@ -1,14 +1,24 @@ package org.sejongisc.backend.user.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; public record PasswordResetVerifyRequest( + + @Schema( + example = "testuser@example.com", + description = "사용자의 이메일 주소" + ) @NotBlank(message = "이메일은 필수입니다.") @Email(message = "올바른 이메일 형식이 아닙니다.") String email, + @Schema( + example = "482915", + description = "이메일로 발송된 6자리 인증 코드" + ) @NotBlank(message = "인증코드는 필수입니다.") @Size(min = 6, max = 6, message = "인증코드는 6자리여야 합니다.") String code diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/UserIdFindRequest.java b/backend/src/main/java/org/sejongisc/backend/user/dto/UserIdFindRequest.java index 69a19219..0f679e7b 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/UserIdFindRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/UserIdFindRequest.java @@ -1,12 +1,22 @@ package org.sejongisc.backend.user.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; -public record UserIdFindRequest(@NotBlank(message = "이름은 필수입니다.") - String name, +public record UserIdFindRequest( + @Schema( + example = "홍길동", + description = "수정할 사용자 이름" + ) + @NotBlank(message = "이름은 필수입니다.") + String name, - @NotBlank(message = "전화번호는 필수입니다.") - @Pattern(regexp = "^010\\d{8}$", message = "전화번호 형식이 올바르지 않습니다.") - String phoneNumber) { + @Schema( + example = "01098765432", + description = "수정할 사용자 전화번호 (숫자만 입력)" + ) + @NotBlank(message = "전화번호는 필수입니다.") + @Pattern(regexp = "^010\\d{8}$", message = "전화번호 형식이 올바르지 않습니다.") + String phoneNumber) { } diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java b/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java index 5caa117f..1c89f847 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/UserInfoResponse.java @@ -47,7 +47,7 @@ public class UserInfoResponse { @Schema( description = "사용자 역할 (예: USER, ADMIN)", - example = "USER" + example = "TEAM_MEMBER" ) private String role; // enum Role을 String으로 변환 diff --git a/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java b/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java index e71c33f5..964067d2 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/user/dto/UserUpdateRequest.java @@ -33,7 +33,7 @@ public class UserUpdateRequest { @Schema( description = "변경할 비밀번호 (선택 입력, 변경 시에만 포함)", - example = "newpassword123!" + example = "Newpassword123!" ) @Size(min = 8, message = "비밀번호는 최소 8자 이상 입력해야 합니다.") private String password; diff --git a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java index eede6c02..baefcb30 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java +++ b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java @@ -27,4 +27,7 @@ public interface UserService { String verifyResetCodeAndIssueToken(String email, String code); void resetPasswordByToken(String resetToken, String newPassword); + + User upsertOAuthUser(String provider, String providerId, String email, String name); + } diff --git a/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java index 625519a5..bcb962f4 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java @@ -1,12 +1,19 @@ package org.sejongisc.backend.user.service; +import org.sejongisc.backend.auth.entity.AuthProvider; import org.sejongisc.backend.auth.service.EmailService; import org.sejongisc.backend.auth.service.OauthUnlinkService; import org.sejongisc.backend.auth.service.RefreshTokenService; import org.sejongisc.backend.common.auth.jwt.TokenEncryptor; import org.sejongisc.backend.user.util.PasswordPolicyValidator; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,6 +33,8 @@ import org.springframework.stereotype.Service; import java.time.Duration; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -314,4 +323,35 @@ public void resetPasswordByToken(String resetToken, String newPassword) { refreshTokenService.deleteByUserId(user.getUserId()); } + + + @Override + @Transactional + public User upsertOAuthUser(String provider, String providerUid, String email, String name) { + + AuthProvider authProvider = AuthProvider.valueOf(provider.toUpperCase()); + + return oauthAccountRepository + .findByProviderAndProviderUid(authProvider, providerUid) + .map(UserOauthAccount::getUser) + .orElseGet(() -> { + User newUser = User.builder() + .email(email) + .name(name) + .role(Role.TEAM_MEMBER) + .build(); + + User savedUser = userRepository.save(newUser); + + UserOauthAccount oauthAccount = UserOauthAccount.builder() + .user(savedUser) + .provider(authProvider) + .providerUid(providerUid) + .build(); + + oauthAccountRepository.save(oauthAccount); + + return savedUser; + }); + } } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 738afc18..05e368cc 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -12,4 +12,5 @@ spring: show_sql: true format_sql: true open-in-view: false - use_sql_comments: true \ No newline at end of file + use_sql_comments: true + From 39ac3ffb9eb930e2b2f7230932561a98551ad3ef Mon Sep 17 00:00:00 2001 From: Sohee Date: Fri, 14 Nov 2025 17:41:41 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[BE]=20SISC1-209=20[FIX]=20=EB=B3=B4?= =?UTF-8?q?=EC=95=88=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/config/CustomOAuth2UserService.java | 4 ++- .../auth/config/OAuth2SuccessHandler.java | 29 +++++++++++++------ .../common/auth/config/SecurityConfig.java | 8 +++-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java index 6676971f..e66b81a9 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java @@ -43,7 +43,9 @@ public OAuth2User loadUser(OAuth2UserRequest req) throws OAuth2AuthenticationExc Map attrs = oauthUser.getAttributes(); log.info("[OAuth2] Provider = {}", provider); - log.info("[OAuth2] Attributes = {}", attrs); + if (log.isDebugEnabled()) { + log.debug("[OAuth2] Attributes = {}", attrs); + } String providerUid; String email = null; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java index be26a773..95f3189f 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java @@ -67,23 +67,34 @@ public void onAuthenticationSuccess( refreshTokenService.saveOrUpdateToken(user.getUserId(), refreshToken); // 6. HttpOnly 쿠키로 refreshToken 저장 - ResponseCookie cookie = ResponseCookie.from("refresh", refreshToken) + ResponseCookie accessCookie = ResponseCookie.from("refresh", refreshToken) .httpOnly(true) - .secure(false) // 로컬 개발 환경 + .secure(true) .sameSite("None") .path("/") - .maxAge(60L * 60 * 24 * 14) + .maxAge(60L * 60) // 1 hour .build(); - response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + ResponseCookie refreshCookie = ResponseCookie.from("refresh", refreshToken) + .httpOnly(true) + .secure(true) + .sameSite("None") + .path("/") + .maxAge(60L * 60 * 24 * 14) // 2 weeks + .build(); + + + response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString()); + response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); + // 7. 프론트로 redirect - String redirectUrl = "http://localhost:5173/oauth/success" - + "?accessToken=" + accessToken - + "&name=" + URLEncoder.encode(name, StandardCharsets.UTF_8) - + "&userId=" + userId; + String redirectUrl = "http://localhost:5173/oauth/success"; +// + "?accessToken=" + accessToken +// + "&name=" + URLEncoder.encode(name, StandardCharsets.UTF_8) +// + "&userId=" + userId; - log.info("[OAuth2 Redirect] {}", redirectUrl); + // log.info("[OAuth2 Redirect] {}", redirectUrl); getRedirectStrategy().sendRedirect(request, response, redirectUrl); } diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java index baba71ab..e88ee49e 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java @@ -71,7 +71,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/auth/signup", "/api/auth/login", "/api/auth/login/**", - "/api/auth/oauth", "/api/auth/oauth/**", "/actuator", "/actuator/**", @@ -86,8 +85,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/email/**", "/swagger-resources/**", "/webjars/**" - ).permitAll() - .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + ).permitAll(); + + auth.requestMatchers("/api/user/**").authenticated(); + + auth.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // .anyRequest().authenticated(); .anyRequest().permitAll(); }) From b21edaa40fcb2423df45557879bb43bd360bc60d Mon Sep 17 00:00:00 2001 From: Sohee Date: Fri, 14 Nov 2025 17:59:57 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[BE]=20SISC1-209=20[FIX]=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/common/auth/config/OAuth2SuccessHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java index 95f3189f..66fb704b 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java @@ -67,7 +67,7 @@ public void onAuthenticationSuccess( refreshTokenService.saveOrUpdateToken(user.getUserId(), refreshToken); // 6. HttpOnly 쿠키로 refreshToken 저장 - ResponseCookie accessCookie = ResponseCookie.from("refresh", refreshToken) + ResponseCookie accessCookie = ResponseCookie.from("access", refreshToken) .httpOnly(true) .secure(true) .sameSite("None") From 7c5549687163593db0899be7514b8bb39d31b931 Mon Sep 17 00:00:00 2001 From: Sohee Date: Fri, 14 Nov 2025 18:05:26 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[BE]=20SISC1-209=20[FIX]=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/common/auth/config/OAuth2SuccessHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java index 66fb704b..673664c5 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java @@ -67,7 +67,7 @@ public void onAuthenticationSuccess( refreshTokenService.saveOrUpdateToken(user.getUserId(), refreshToken); // 6. HttpOnly 쿠키로 refreshToken 저장 - ResponseCookie accessCookie = ResponseCookie.from("access", refreshToken) + ResponseCookie accessCookie = ResponseCookie.from("access", accessToken) .httpOnly(true) .secure(true) .sameSite("None") From e88792b170ea7a812fce8be92ce15eb72d879d0c Mon Sep 17 00:00:00 2001 From: Sohee Date: Sat, 15 Nov 2025 17:25:02 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[BE]=20SISC1-209=20[FIX]=20OAuth2SuccessHan?= =?UTF-8?q?der=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20CustomOidUserService=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/entity/AuthProvider.java | 11 ++- .../auth/config/CustomOAuth2UserService.java | 97 +++++++++---------- .../auth/config/CustomOidcUserService.java | 56 +++++++++++ .../auth/config/OAuth2SuccessHandler.java | 58 ++++++++--- .../common/auth/config/SecurityConfig.java | 12 ++- 5 files changed, 165 insertions(+), 69 deletions(-) create mode 100644 backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java diff --git a/backend/src/main/java/org/sejongisc/backend/auth/entity/AuthProvider.java b/backend/src/main/java/org/sejongisc/backend/auth/entity/AuthProvider.java index 177a50cc..6f488d8d 100644 --- a/backend/src/main/java/org/sejongisc/backend/auth/entity/AuthProvider.java +++ b/backend/src/main/java/org/sejongisc/backend/auth/entity/AuthProvider.java @@ -3,5 +3,14 @@ public enum AuthProvider { GOOGLE, // 구글 GITHUB, // 깃허브 - KAKAO // 카카오 + KAKAO; // 카카오 + + public static AuthProvider from(String providerName) { + return switch (providerName.toLowerCase()) { + case "google" -> GOOGLE; + case "kakao" -> KAKAO; + case "github" -> GITHUB; + default -> throw new IllegalArgumentException("Unsupported provider: " + providerName); + }; + } } diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java index e66b81a9..20ed4c85 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java @@ -35,50 +35,46 @@ public class CustomOAuth2UserService implements OAuth2UserService delegate = + new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(req); - String provider = req.getClientRegistration().getRegistrationId(); - Map attrs = oauthUser.getAttributes(); + String provider = req.getClientRegistration().getRegistrationId(); // google, kakao, github + Map attrs = oAuth2User.getAttributes(); - log.info("[OAuth2] Provider = {}", provider); + String providerUid; + String email; + String name; + + // log.info("[OAuth2] Provider = {}", provider); if (log.isDebugEnabled()) { log.debug("[OAuth2] Attributes = {}", attrs); } - String providerUid; - String email = null; - String name = null; - - if (provider.equals("google")) { - providerUid = (String) attrs.get("sub"); - email = (String) attrs.get("email"); - name = (String) attrs.get("name"); - - } else if (provider.equals("kakao")) { - providerUid = String.valueOf(attrs.get("id")); - Map account = (Map) attrs.get("kakao_account"); - Map profile = (Map) account.get("profile"); - - email = (String) account.get("email"); - if (email == null) email = "no-email@kakao.com"; - - name = (String) profile.get("nickname"); - - } else if (provider.equals("github")) { - providerUid = String.valueOf(attrs.get("id")); - email = (String) attrs.get("email"); - if (email == null) email = "no-email@github.com"; - - name = (String) attrs.get("name"); - if (name == null) name = email; - - } else { - throw new OAuth2AuthenticationException("Unsupported provider: " + provider); + switch (provider) { + case "google" -> { + providerUid = (String) attrs.get("sub"); + email = (String) attrs.get("email"); + name = (String) attrs.get("name"); + } + case "kakao" -> { + providerUid = attrs.get("id").toString(); + Map kakaoAccount = (Map) attrs.get("kakao_account"); + email = (String) kakaoAccount.get("email"); // null 가능 + Map profile = (Map) kakaoAccount.get("profile"); + name = (String) profile.get("nickname"); + } + case "github" -> { + providerUid = attrs.get("id").toString(); + email = (String) attrs.get("email"); + name = (String) attrs.get("login"); // GitHub은 login이 닉네임 + } + default -> throw new RuntimeException("지원하지 않는 provider: " + provider); } - // 순환참조 해결: 여기서 userService 호출하지 않음 + //log.info("provider={}, providerUid={}, email={}, name={}", provider, providerUid, email, name); final String fProviderUid = providerUid; final String fEmail = email; @@ -86,37 +82,40 @@ public OAuth2User loadUser(OAuth2UserRequest req) throws OAuth2AuthenticationExc final AuthProvider fAuthProvider = AuthProvider.valueOf(provider.toUpperCase()); User user = oauthAccountRepository - .findByProviderAndProviderUid(fAuthProvider, providerUid) + .findByProviderAndProviderUid(AuthProvider.from(provider), providerUid) .map(UserOauthAccount::getUser) .orElseGet(() -> { - User newUser = User.builder() - .email(fEmail) - .name(fName) + .email(email) + .name(name) .role(Role.TEAM_MEMBER) .build(); - User saved = userRepository.save(newUser); UserOauthAccount oauth = UserOauthAccount.builder() .user(saved) - .provider(fAuthProvider) - .providerUid(fProviderUid) + .provider(AuthProvider.from(provider)) + .providerUid(providerUid) .build(); - oauthAccountRepository.save(oauth); return saved; }); + // log.info("[CustomOAuth2UserService] User resolved → returning OAuth2User"); + + Map attributes = new java.util.HashMap<>(); + attributes.put("provider", provider); // google / kakao / github + attributes.put("providerUid", providerUid); // 소셜 계정 UID + attributes.put("email", user.getEmail()); // DB email + attributes.put("name", user.getName()); + attributes.put("userId", user.getUserId()); // DB user uuid + return new DefaultOAuth2User( List.of(new SimpleGrantedAuthority("ROLE_TEAM_MEMBER")), - Map.of( - "id", user.getUserId(), - "email", user.getEmail(), - "name", user.getName() - ), - "email" + attributes, + "userId" // 또는 "email" -> email null 이면 id가 더 안전 ); + } } diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java new file mode 100644 index 00000000..5745c109 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java @@ -0,0 +1,56 @@ +package org.sejongisc.backend.common.auth.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sejongisc.backend.auth.dao.UserOauthAccountRepository; +import org.sejongisc.backend.user.dao.UserRepository; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomOidcUserService extends OidcUserService { + + private final UserRepository userRepository; + private final UserOauthAccountRepository oauthAccountRepository; + + @Override + public OidcUser loadUser(OidcUserRequest req) throws OAuth2AuthenticationException { + // log.info("[CustomOidcUserService] Google OIDC loadUser START"); + + OidcUser oidcUser = super.loadUser(req); + + Map original = oidcUser.getAttributes(); + // log.info("OIDC claims: {}", original); + + // SuccessHandler가 필요로 하는 attributes 넣기 + Map attrs = new HashMap<>(original); + attrs.put("provider", "google"); // provider + attrs.put("providerUid", original.get("sub")); // 구글 고유 ID + attrs.put("email", original.get("email")); + attrs.put("name", original.get("name")); + + // 여기서 attrs를 defaultOidcUser에 직접 넣음 + return new DefaultOidcUser( + oidcUser.getAuthorities(), + oidcUser.getIdToken(), + oidcUser.getUserInfo(), + "sub" + ) { + @Override + public Map getAttributes() { + return attrs; // 커스텀 attributes 적용 + } + }; + } +} diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java index 673664c5..f63b374a 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java @@ -3,6 +3,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.sejongisc.backend.auth.dao.UserOauthAccountRepository; +import org.sejongisc.backend.auth.entity.AuthProvider; +import org.sejongisc.backend.auth.entity.UserOauthAccount; import org.sejongisc.backend.common.auth.jwt.JwtProvider; import org.sejongisc.backend.auth.service.RefreshTokenService; import org.sejongisc.backend.user.dao.UserRepository; @@ -11,6 +14,7 @@ import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; @@ -22,6 +26,7 @@ import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.Map; import java.util.UUID; @Slf4j @@ -32,6 +37,7 @@ public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler private final JwtProvider jwtProvider; private final RefreshTokenService refreshTokenService; private final UserRepository userRepository; + private final UserOauthAccountRepository userOauthAccountRepository; @Override public void onAuthenticationSuccess( @@ -39,27 +45,49 @@ public void onAuthenticationSuccess( HttpServletResponse response, Authentication authentication) throws IOException{ + // log.info("[OAuth2SuccessHandler] SUCCESS HANDLER CALLED!"); + + if (!(authentication.getPrincipal() instanceof DefaultOAuth2User oauthUser)) { + throw new IllegalStateException("Unknown principal type: " + authentication.getPrincipal().getClass()); + } + // 1. CustomOAuth2UserService에서 넣어준 attributes 가져오기 - DefaultOAuth2User oauthUser = (DefaultOAuth2User) authentication.getPrincipal(); + Map attrs = oauthUser.getAttributes(); + + String providerStr = (String) attrs.get("provider"); + String providerUid = (String) attrs.get("providerUid"); + + if (providerStr == null) { + throw new IllegalStateException("OAuth provider attribute missing from attributes"); + } - String userIdStr = oauthUser.getAttributes().get("id").toString(); - UUID userId = UUID.fromString(userIdStr); - String email = (String) oauthUser.getAttributes().get("email"); - String name = (String) oauthUser.getAttributes().get("name"); + AuthProvider provider = + switch (providerStr) { + case "kakao" -> AuthProvider.KAKAO; + case "github" -> AuthProvider.GITHUB; + case "google" -> AuthProvider.GOOGLE; + default -> throw new IllegalStateException("Unknown OAuth provider: " + providerStr); + }; - log.info("[OAuth2 Success] userId = {}, email = {}", userId, email); - // 2. User 조회 - User user = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found during OAuth login")); + // log.info("[OAuth2SuccessHandler] provider={}, providerUid={}", provider, providerUid); - // 3. AccessToken 발급 + // DB 조회 + UserOauthAccount account = userOauthAccountRepository + .findByProviderAndProviderUid(provider, providerUid) + .orElseThrow(() -> new RuntimeException("소셜 계정이 DB에 없습니다. (회원가입 필요)")); + + User user = userRepository.findById(account.getUser().getUserId()) + .orElseThrow(() -> new RuntimeException("User not found")); + + // JWT 생성 String accessToken = jwtProvider.createToken( user.getUserId(), user.getRole(), user.getEmail() ); + // 4. RefreshToken 생성 String refreshToken = jwtProvider.createRefreshToken(user.getUserId()); @@ -69,16 +97,16 @@ public void onAuthenticationSuccess( // 6. HttpOnly 쿠키로 refreshToken 저장 ResponseCookie accessCookie = ResponseCookie.from("access", accessToken) .httpOnly(true) - .secure(true) - .sameSite("None") + .secure(false) // 로컬 개발 + .sameSite("Lax") // 로컬에서는 None 비추천 .path("/") .maxAge(60L * 60) // 1 hour .build(); ResponseCookie refreshCookie = ResponseCookie.from("refresh", refreshToken) .httpOnly(true) - .secure(true) - .sameSite("None") + .secure(false) + .sameSite("Lax") .path("/") .maxAge(60L * 60 * 24 * 14) // 2 weeks .build(); @@ -94,7 +122,7 @@ public void onAuthenticationSuccess( // + "&name=" + URLEncoder.encode(name, StandardCharsets.UTF_8) // + "&userId=" + userId; - // log.info("[OAuth2 Redirect] {}", redirectUrl); + // log.info("[OAuth2 Redirect] {}", redirectUrl); getRedirectStrategy().sendRedirect(request, response, redirectUrl); } diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java index e88ee49e..8875728f 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java @@ -35,6 +35,7 @@ public class SecurityConfig { private final JwtAccessDeniedHandler jwtAccessDeniedHandler; private final CustomOAuth2UserService customOAuth2UserService; + private final CustomOidcUserService customOidcUserService; private final OAuth2SuccessHandler oAuth2SuccessHandler; @Bean @@ -56,8 +57,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizationEndpoint(a -> a.authorizationRequestRepository(authorizationRequestRepository()) ) - .userInfoEndpoint(u -> - u.userService(customOAuth2UserService) + .userInfoEndpoint(u -> { + u.userService(customOAuth2UserService); // kakao, github + u.oidcUserService(customOidcUserService); //google + } ) .successHandler(oAuth2SuccessHandler) .failureHandler((req, res, ex) -> @@ -71,7 +74,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/auth/signup", "/api/auth/login", "/api/auth/login/**", - "/api/auth/oauth/**", "/actuator", "/actuator/**", "/api/auth/logout", @@ -84,7 +86,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/email/**", "/swagger-resources/**", - "/webjars/**" + "/webjars/**", + "/login/**", + "/oauth2/**" ).permitAll(); auth.requestMatchers("/api/user/**").authenticated(); From bb7bf441f3909a759b4d449190c5ca4a179f3a56 Mon Sep 17 00:00:00 2001 From: Sohee Date: Sun, 16 Nov 2025 16:39:38 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[BE]=20SISC1-209=20[FIX]=20=EA=B5=AC?= =?UTF-8?q?=EA=B8=80=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=97=86=EC=9C=BC=EB=A9=B4=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/config/CustomOAuth2UserService.java | 6 +-- .../auth/config/CustomOidcUserService.java | 48 +++++++++++++++++-- .../auth/config/OAuth2SuccessHandler.java | 2 +- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java index 20ed4c85..165e2cec 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java @@ -35,7 +35,7 @@ public class CustomOAuth2UserService implements OAuth2UserService delegate = new DefaultOAuth2UserService(); @@ -48,7 +48,7 @@ public OAuth2User loadUser(OAuth2UserRequest req) throws OAuth2AuthenticationExc String email; String name; - // log.info("[OAuth2] Provider = {}", provider); + // log.info("[OAuth2] Provider = {}", provider); if (log.isDebugEnabled()) { log.debug("[OAuth2] Attributes = {}", attrs); } @@ -74,7 +74,7 @@ public OAuth2User loadUser(OAuth2UserRequest req) throws OAuth2AuthenticationExc default -> throw new RuntimeException("지원하지 않는 provider: " + provider); } - //log.info("provider={}, providerUid={}, email={}, name={}", provider, providerUid, email, name); + // log.info("provider={}, providerUid={}, email={}, name={}", provider, providerUid, email, name); final String fProviderUid = providerUid; final String fEmail = email; diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java index 5745c109..e8a2c060 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java @@ -1,9 +1,14 @@ package org.sejongisc.backend.common.auth.config; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.auth.dao.UserOauthAccountRepository; +import org.sejongisc.backend.auth.entity.AuthProvider; +import org.sejongisc.backend.auth.entity.UserOauthAccount; import org.sejongisc.backend.user.dao.UserRepository; +import org.sejongisc.backend.user.entity.Role; +import org.sejongisc.backend.user.entity.User; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; @@ -19,6 +24,7 @@ @Slf4j @Service @RequiredArgsConstructor +@Transactional public class CustomOidcUserService extends OidcUserService { private final UserRepository userRepository; @@ -31,14 +37,46 @@ public OidcUser loadUser(OidcUserRequest req) throws OAuth2AuthenticationExcepti OidcUser oidcUser = super.loadUser(req); Map original = oidcUser.getAttributes(); - // log.info("OIDC claims: {}", original); + // log.info("OIDC claims: {}", original); // SuccessHandler가 필요로 하는 attributes 넣기 + String provider = "google"; // provider + String providerUid = (String) original.get("sub"); // 구글 고유 ID + String email = (String) original.get("email"); + String name = (String) original.get("name"); + + // 신규 가입 or 기존 유저 조회 + User user = oauthAccountRepository + .findByProviderAndProviderUid(AuthProvider.GOOGLE, providerUid) + .map(UserOauthAccount::getUser) + .orElseGet(() -> { + // (1) User 생성 + User newUser = User.builder() + .email(email) + .name(name) + .role(Role.TEAM_MEMBER) + .build(); + User savedUser = userRepository.save(newUser); + + // (2) UserOauthAccount 생성 + UserOauthAccount oauth = UserOauthAccount.builder() + .user(savedUser) + .provider(AuthProvider.GOOGLE) + .providerUid(providerUid) + .build(); + oauthAccountRepository.save(oauth); + + // log.info("[CustomOidcUserService] 신규 User 및 UserOauthAccount 생성됨"); + return savedUser; + }); + Map attrs = new HashMap<>(original); - attrs.put("provider", "google"); // provider - attrs.put("providerUid", original.get("sub")); // 구글 고유 ID - attrs.put("email", original.get("email")); - attrs.put("name", original.get("name")); + attrs.put("provider", provider); + attrs.put("providerUid", providerUid); + attrs.put("email", user.getEmail()); + attrs.put("name", user.getName()); + attrs.put("userId", user.getUserId()); // SuccessHandler가 필요로 함 + // 여기서 attrs를 defaultOidcUser에 직접 넣음 return new DefaultOidcUser( diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java index f63b374a..d20ea22a 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java @@ -122,7 +122,7 @@ public void onAuthenticationSuccess( // + "&name=" + URLEncoder.encode(name, StandardCharsets.UTF_8) // + "&userId=" + userId; - // log.info("[OAuth2 Redirect] {}", redirectUrl); + // log.info("[OAuth2 Redirect] {}", redirectUrl); getRedirectStrategy().sendRedirect(request, response, redirectUrl); } From d5c3f226b84d9fd25e5ea71c542843dbac9f64ac Mon Sep 17 00:00:00 2001 From: Sohee Date: Mon, 17 Nov 2025 16:07:03 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[BE]=20SISC1-209=20[FIX]=20redirect=20URL?= =?UTF-8?q?=EC=9D=84=20=ED=99=98=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/config/CustomOidcUserService.java | 2 +- .../auth/config/OAuth2SuccessHandler.java | 28 +++++++++++++++---- .../common/auth/config/SecurityConfig.java | 2 +- .../JwtAuthenticationFilter.java | 24 ++++++++++++++-- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java index e8a2c060..11e020d9 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOidcUserService.java @@ -66,7 +66,7 @@ public OidcUser loadUser(OidcUserRequest req) throws OAuth2AuthenticationExcepti .build(); oauthAccountRepository.save(oauth); - // log.info("[CustomOidcUserService] 신규 User 및 UserOauthAccount 생성됨"); + // log.info("[CustomOidcUserService] 신규 User 및 UserOauthAccount 생성됨"); return savedUser; }); diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java index d20ea22a..52d2c10a 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.core.env.Environment; import org.sejongisc.backend.auth.dao.UserOauthAccountRepository; import org.sejongisc.backend.auth.entity.AuthProvider; import org.sejongisc.backend.auth.entity.UserOauthAccount; @@ -10,6 +11,7 @@ import org.sejongisc.backend.auth.service.RefreshTokenService; import org.sejongisc.backend.user.dao.UserRepository; import org.sejongisc.backend.user.entity.User; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; @@ -26,6 +28,7 @@ import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Map; import java.util.UUID; @@ -38,6 +41,10 @@ public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler private final RefreshTokenService refreshTokenService; private final UserRepository userRepository; private final UserOauthAccountRepository userOauthAccountRepository; + private final Environment env; + + @Value("${app.oauth2.redirect-success}") + private String redirectSuccessBase; @Override public void onAuthenticationSuccess( @@ -94,19 +101,26 @@ public void onAuthenticationSuccess( // 5. RefreshToken 저장(DB or Redis) refreshTokenService.saveOrUpdateToken(user.getUserId(), refreshToken); + boolean isProd = Arrays.asList(env.getActiveProfiles()).contains("prod"); + + String sameSite = isProd ? "None" : "Lax"; + boolean secure = isProd; + String domain = isProd ? "sisc-web.duckdns.org" : "localhost"; + + // 6. HttpOnly 쿠키로 refreshToken 저장 ResponseCookie accessCookie = ResponseCookie.from("access", accessToken) .httpOnly(true) - .secure(false) // 로컬 개발 - .sameSite("Lax") // 로컬에서는 None 비추천 + .secure(secure) // 로컬=false, 배포=true + .sameSite(sameSite) // 로컬= "Lax", 배포="None" .path("/") .maxAge(60L * 60) // 1 hour .build(); ResponseCookie refreshCookie = ResponseCookie.from("refresh", refreshToken) .httpOnly(true) - .secure(false) - .sameSite("Lax") + .secure(secure) + .sameSite(sameSite) .path("/") .maxAge(60L * 60 * 24 * 14) // 2 weeks .build(); @@ -117,14 +131,16 @@ public void onAuthenticationSuccess( // 7. 프론트로 redirect - String redirectUrl = "http://localhost:5173/oauth/success"; + // application-local.yml → http://localhost:5173/oauth/success + // application-prod.yml → https://sisc-web.duckdns.org/oauth/success + //String redirectUrl = redirectSuccessBase; // + "?accessToken=" + accessToken // + "&name=" + URLEncoder.encode(name, StandardCharsets.UTF_8) // + "&userId=" + userId; // log.info("[OAuth2 Redirect] {}", redirectUrl); - getRedirectStrategy().sendRedirect(request, response, redirectUrl); + getRedirectStrategy().sendRedirect(request, response, redirectSuccessBase); } } diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java index 8875728f..5fae8783 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java @@ -113,10 +113,10 @@ public CorsConfigurationSource corsConfigurationSource() { config.setAllowedOrigins(List.of( "http://localhost:5173" // 허용할 프론트 주소 )); - config.setAllowedOriginPatterns(List.of("*")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); + config.addExposedHeader("Authorization"); config.setMaxAge(3600L); // 캐시 시간(초) UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java b/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java index 25bbebea..34d20242 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java @@ -6,6 +6,7 @@ import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotNull; @@ -40,15 +41,15 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { "/api/auth/signup", "/api/auth/login", "/api/auth/login/**", - "/api/auth/oauth", - "/api/auth/oauth/**", "/api/auth/logout", "/api/auth/reissue", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui/index.html", "/swagger-resources/**", - "/webjars/**" + "/webjars/**", + "/login/**", + "/oauth2/**" ); @Override @@ -73,6 +74,10 @@ protected void doFilterInternal(@NotNull HttpServletRequest request, try { String token = resolveToken(request); + if (token == null) { + token = resolveTokenFromCookie(request); + } + if (token != null && jwtParser.validationToken(token) ) { UsernamePasswordAuthenticationToken authentication = jwtParser.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); @@ -122,6 +127,19 @@ private String resolveToken(HttpServletRequest request) { return null; } + private String resolveTokenFromCookie(HttpServletRequest request) { + if (request.getCookies() == null) return null; + + for (Cookie cookie : request.getCookies()) { + if ("access".equals(cookie.getName())) { + log.info("쿠키에서 access token 추출됨"); + return cookie.getValue(); + } + } + + return null; + } + private String toJson(ErrorResponse errorResponse) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); From 5b4c1aeac7e05fa044735747527f1103ffac2052 Mon Sep 17 00:00:00 2001 From: Sohee Date: Mon, 17 Nov 2025 16:49:12 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[BE]=20SISC1-209=20[FIX]=20=ED=97=88?= =?UTF-8?q?=EC=9A=A9=ED=95=A0=20=ED=94=84=EB=A1=A0=ED=8A=B8=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EC=84=9C=EB=B2=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sejongisc/backend/common/auth/config/SecurityConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java b/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java index 5fae8783..6cbc15e4 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/config/SecurityConfig.java @@ -111,7 +111,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of( - "http://localhost:5173" // 허용할 프론트 주소 + "http://localhost:5173", // 허용할 프론트 주소 + "https://sisc-web.duckdns.org" )); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.setAllowedHeaders(List.of("*"));