From b730538b43af12d46e532473104db2e6e7bc2290 Mon Sep 17 00:00:00 2001 From: dainnida Date: Mon, 10 Feb 2025 19:55:44 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[#71]=20Chore:=20=EC=95=A0=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/gradle.yml | 8 +++++++- build.gradle | 5 +++++ src/main/resources/application.yml | 24 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 60e6273..7ef2f4b 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -41,7 +41,13 @@ jobs: jwt.refresh-token-validity: ${{ secrets.JWT_REFRESH_VALIDITY }} spring.security.oauth2.client.registration.google.client-id: ${{ secrets.OAUTH_GOOGLE_ID }} spring.security.oauth2.client.registration.google.client-secret: ${{ secrets.OAUTH_GOOGLE_SECRET }} - spring.security.oauth2.client.registration.google.redirect-uri: ${{ secrets.SERVER_HTTPS_URL }}/login/oauth2/code/google + spring.security.oauth2.client.registration.apple.client-id: ${{ secrets.OAUTH_APPLE_CID }} + spring.security.oauth2.client.registration.apple.client-secret: ${{ secrets.OAUTH_APPLE_P8 }} + spring.security.oauth2.client.registration.apple.redirect-uri: ${{ secrets.SERVER_HTTPS_URL }}/login/oauth2/code/apple + apple.key-content: ${{ secrets.OAUTH_APPLE_P8_KEY_CONTENT }} + apple.cid: ${{ secrets.OAUTH_APPLE_CID }} + apple.tid: ${{ secrets.OAUTH_APPLE_TID }} + apple.kid: ${{ secrets.OAUTH_APPLE_KID }} # 실행 속도 향상을 위한 Gradle 종속성 캐싱 diff --git a/build.gradle b/build.gradle index 3e2e0c7..771faa4 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,11 @@ dependencies { // 소셜로그인 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'com.auth0:java-jwt:4.4.0' + implementation 'org.bouncycastle:bcpkix-jdk18on:1.77' + implementation 'org.bouncycastle:bcprov-jdk18on:1.77' + // implementation 'commons-io:commons-io:2.14.0' // IOUtils 사용을 위해 + // implementation 'com.nimbusds:nimbus-jose-jwt:9.31' + implementation 'com.auth0:jwks-rsa:0.20.2' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b92e8fa..c5308b6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,6 +26,28 @@ spring: scope: - email - profile + apple: + client-id: ${OAUTH_APPLE_CID} # Service ID의 identifier + client-secret: ${OAUTH_APPLE_P8} # private key 파일 이름 + redirect-uri: ${OAUTH_APPLE_REDIRECT} + client-authentication-method: POST + authorization-grant-type: authorization_code + client-name: Apple + scope: + - name + - email + provider: + apple: + authorization-uri: https://appleid.apple.com/auth/authorize?response_mode=form_post + token-uri: https://appleid.apple.com/auth/token + +apple: + url: https://appleid.apple.com + # key-path: ${OAUTH_APPLE_P8} + key-content: ${OAUTH_APPLE_P8_KEY_CONTENT} + cid: ${OAUTH_APPLE_CID} + tid: ${OAUTH_APPLE_TID} + kid: ${OAUTH_APPLE_KID} jwt: secret: ${JWT_SECRET} @@ -36,6 +58,8 @@ logging.level: org.hibernate: SQL: DEBUG type: trace + org.springframework.security: DEBUG + com.cmc.mercury: DEBUG # swagger 관련 설정 springdoc: From 33a85c4d624033a4c6948cfd435b364754b05a1e Mon Sep 17 00:00:00 2001 From: dainnida Date: Mon, 10 Feb 2025 19:58:15 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[#71]=20Chore:=20=EC=9E=98=EB=AA=BB=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=90=9C=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/gradle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 7ef2f4b..a2715e2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -41,6 +41,7 @@ jobs: jwt.refresh-token-validity: ${{ secrets.JWT_REFRESH_VALIDITY }} spring.security.oauth2.client.registration.google.client-id: ${{ secrets.OAUTH_GOOGLE_ID }} spring.security.oauth2.client.registration.google.client-secret: ${{ secrets.OAUTH_GOOGLE_SECRET }} + spring.security.oauth2.client.registration.google.redirect-uri: ${{ secrets.SERVER_HTTPS_URL }}/login/oauth2/code/google spring.security.oauth2.client.registration.apple.client-id: ${{ secrets.OAUTH_APPLE_CID }} spring.security.oauth2.client.registration.apple.client-secret: ${{ secrets.OAUTH_APPLE_P8 }} spring.security.oauth2.client.registration.apple.redirect-uri: ${{ secrets.SERVER_HTTPS_URL }}/login/oauth2/code/apple From 922494fc99a3846511cecaa2f19647caa154baf3 Mon Sep 17 00:00:00 2001 From: dainnida Date: Mon, 10 Feb 2025 19:59:42 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[#71]=20Feat:=20=EC=95=A0=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20error=20code=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 --- .../java/com/cmc/mercury/global/exception/ErrorCode.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cmc/mercury/global/exception/ErrorCode.java b/src/main/java/com/cmc/mercury/global/exception/ErrorCode.java index c8ba569..de6ffa8 100644 --- a/src/main/java/com/cmc/mercury/global/exception/ErrorCode.java +++ b/src/main/java/com/cmc/mercury/global/exception/ErrorCode.java @@ -53,6 +53,11 @@ public enum ErrorCode { OAUTH2_PROCESSING_ERROR(HttpStatus.UNAUTHORIZED, "OAuth401", "소셜 로그인 처리 중 오류가 발생했습니다."), OAUTH2_LOGIN_FAIL(HttpStatus.UNAUTHORIZED, "OAuth401", "소셜 로그인에 실패했습니다."), + // Apple + APPLE_CLIENT_SECRET_ERROR(HttpStatus.BAD_REQUEST, "Apple400", "Apple client secret 생성 실패"), + APPLE_PRIVATE_KEY_ERROR(HttpStatus.BAD_REQUEST, "Apple400", "Apple private key 생성 실패"), + APPLE_TOKEN_VALIDATION_ERROR(HttpStatus.UNAUTHORIZED, "Apple401", "Apple ID 토큰 검증 실패"), + // JWT INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Jwt401", "유효하지 않은 토큰입니다."), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "Jwt401", "만료된 토큰입니다."), @@ -63,4 +68,4 @@ public enum ErrorCode { private final HttpStatus httpStatus; private final String code; private final String message; -} \ No newline at end of file +} From b1226149af76a418204c7bd04e02d80cc3465343 Mon Sep 17 00:00:00 2001 From: dainnida Date: Mon, 10 Feb 2025 20:05:06 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[#71]=20Feat:=20=EC=95=A0=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=A0=84=EC=B2=B4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth/apple/AppleClientSecretService.java | 114 ++++++++++++++++++ .../oauth/apple/AppleIdTokenVerifier.java | 74 ++++++++++++ .../apple/CustomRequestEntityConverter.java | 52 ++++++++ 3 files changed, 240 insertions(+) create mode 100644 src/main/java/com/cmc/mercury/global/oauth/apple/AppleClientSecretService.java create mode 100644 src/main/java/com/cmc/mercury/global/oauth/apple/AppleIdTokenVerifier.java create mode 100644 src/main/java/com/cmc/mercury/global/oauth/apple/CustomRequestEntityConverter.java diff --git a/src/main/java/com/cmc/mercury/global/oauth/apple/AppleClientSecretService.java b/src/main/java/com/cmc/mercury/global/oauth/apple/AppleClientSecretService.java new file mode 100644 index 0000000..9fd6984 --- /dev/null +++ b/src/main/java/com/cmc/mercury/global/oauth/apple/AppleClientSecretService.java @@ -0,0 +1,114 @@ +package com.cmc.mercury.global.oauth.apple; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.cmc.mercury.global.exception.CustomException; +import com.cmc.mercury.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +//import org.apache.commons.io.IOUtils; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.springframework.beans.factory.annotation.Value; +//import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +//import java.io.IOException; +//import java.io.InputStream; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.interfaces.ECPrivateKey; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Base64; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Slf4j +public class AppleClientSecretService { + +// @Value("${apple.key-path}") +// private String keyPath; + @Value("${apple.key-content}") + private String appleKeyContent; + + @Value("${apple.kid}") + private String keyId; + + @Value("${apple.tid}") + private String teamId; + + @Value("${apple.cid}") + private String clientId; + + public String createClientSecret() { + try { +// 직접 파일 업로드 시 사용 +// log.info("Reading private key from path: {}", keyPath); +// PrivateKey privateKey = getPrivateKey(); + + // Base64 디코딩 + byte[] decoded = Base64.getDecoder().decode(appleKeyContent); + // PEM 파싱 → PrivateKey + PrivateKey privateKey = getPrivateKey(new String(decoded, StandardCharsets.UTF_8)); + log.info("Private key successfully created"); + + Map headerClaims = new HashMap<>(); + headerClaims.put("kid", keyId); + headerClaims.put("alg", "ES256"); + + Date expirationDate = Date.from(LocalDateTime.now().plusDays(30) + .atZone(ZoneId.systemDefault()).toInstant()); + // code/token 교환 직전에 항상 client secret 새로 생성 + String clientSecret = JWT.create() + .withHeader(headerClaims) + .withKeyId(keyId) + .withIssuer(teamId) + .withAudience("https://appleid.apple.com") + .withSubject(clientId) + .withExpiresAt(expirationDate) + .withIssuedAt(new Date(System.currentTimeMillis())) + .sign(Algorithm.ECDSA256(null, (ECPrivateKey) privateKey)); + + log.info("Generated client secret: {}", clientSecret); + + // JWT 디코딩해서 내용 확인 + DecodedJWT jwt = JWT.decode(clientSecret); + log.info("Decoded JWT - Header: {}", jwt.getHeader()); + log.info("Decoded JWT - Payload: {}", jwt.getPayload()); + + return clientSecret; + + } catch (Exception e) { + throw new CustomException(ErrorCode.APPLE_CLIENT_SECRET_ERROR); + } + } + + public PrivateKey getPrivateKey(String keyContent) throws IOException { + + try { +// 직접 파일 업로드 시 사용 +// ClassPathResource resource = new ClassPathResource(keyPath); +// InputStream in = resource.getInputStream(); + +// PEMParser pemParser = new PEMParser(new StringReader(IOUtils.toString(in, StandardCharsets.UTF_8))); + PEMParser pemParser = new PEMParser(new StringReader(keyContent)); + PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject(); + log.info("PEMParser created"); + log.info("PEM object read, type: {}", object != null ? object.getClass().getName() : "null"); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + + return converter.getPrivateKey(object); + + } catch (IOException e) { + throw new CustomException(ErrorCode.APPLE_PRIVATE_KEY_ERROR); + } + } +} diff --git a/src/main/java/com/cmc/mercury/global/oauth/apple/AppleIdTokenVerifier.java b/src/main/java/com/cmc/mercury/global/oauth/apple/AppleIdTokenVerifier.java new file mode 100644 index 0000000..f6debc6 --- /dev/null +++ b/src/main/java/com/cmc/mercury/global/oauth/apple/AppleIdTokenVerifier.java @@ -0,0 +1,74 @@ +package com.cmc.mercury.global.oauth.apple; + + +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.UrlJwkProvider; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.cmc.mercury.global.exception.CustomException; +import com.cmc.mercury.global.exception.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.net.URL; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; + +@Service +@Slf4j +public class AppleIdTokenVerifier { + + @Value("${apple.cid}") + private String clientId; + + public DecodedJWT verify(String idToken) { + try { + + // id_token 헤더 파싱 (kid, alg 등 확인) + DecodedJWT unverifiedJWT = JWT.decode(idToken); + String keyId = unverifiedJWT.getKeyId(); + String algFromToken = unverifiedJWT.getAlgorithm(); + log.info("Apple id_token의 kid = {}, alg = {}", keyId, algFromToken); + + // 애플의 공개키 엔드포인트 URL + URL appleJwkUrl = new URL("https://appleid.apple.com/auth/keys"); + JwkProvider jwkProvider = new UrlJwkProvider(appleJwkUrl); + Jwk jwk = jwkProvider.get(keyId); + PublicKey publicKey = jwk.getPublicKey(); + log.info("애플 공개키 성공적으로 가져옴, key type: {}", publicKey.getAlgorithm()); + + // 공개키가 RSA인지 EC인지 구분하여 Algorithm 인스턴스 생성 + Algorithm algorithm; + if ("EC".equalsIgnoreCase(publicKey.getAlgorithm())) { + // EC 키인 경우 + algorithm = Algorithm.ECDSA256((ECPublicKey) publicKey, null); + } else if ("RSA".equalsIgnoreCase(publicKey.getAlgorithm())) { + // RSA 키인 경우 + algorithm = Algorithm.RSA256((RSAPublicKey) publicKey, null); + } else { + throw new IllegalArgumentException("지원하지 않는 공개키 타입: " + publicKey.getAlgorithm()); + } + + // JWTVerifier를 생성하여 서명, issuer, audience 등의 클레임 검증 + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer("https://appleid.apple.com") + .withAudience(clientId) + .build(); + + // 서명 및 필수 클레임 검증 + DecodedJWT verifiedJwt = verifier.verify(idToken); + log.info("id_token 검증 성공: subject={}, issuer={}", verifiedJwt.getSubject(), verifiedJwt.getIssuer()); + + return verifiedJwt; + + } catch (Exception e) { + log.error("id_token 검증 실패", e); + throw new CustomException(ErrorCode.APPLE_TOKEN_VALIDATION_ERROR); + } + } +} diff --git a/src/main/java/com/cmc/mercury/global/oauth/apple/CustomRequestEntityConverter.java b/src/main/java/com/cmc/mercury/global/oauth/apple/CustomRequestEntityConverter.java new file mode 100644 index 0000000..bcaf7a9 --- /dev/null +++ b/src/main/java/com/cmc/mercury/global/oauth/apple/CustomRequestEntityConverter.java @@ -0,0 +1,52 @@ +package com.cmc.mercury.global.oauth.apple; + +import com.cmc.mercury.global.exception.CustomException; +import com.cmc.mercury.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CustomRequestEntityConverter implements Converter> { + + private final AppleClientSecretService appleClientSecretService; + private final OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter = + new OAuth2AuthorizationCodeGrantRequestEntityConverter(); + + @Override + public RequestEntity convert(OAuth2AuthorizationCodeGrantRequest request) { + + log.info("Converting OAuth2 request for provider: {}", request.getClientRegistration().getRegistrationId()); + RequestEntity entity = defaultConverter.convert(request); + String registrationId = request.getClientRegistration().getRegistrationId(); + + MultiValueMap params = (MultiValueMap) entity.getBody(); + + if ("apple".equals(registrationId)) { + try { + // client_secret JWT 생성 및 설정 + String clientSecret = appleClientSecretService.createClientSecret(); + params.set("client_secret", clientSecret); + + } catch (Exception e) { + throw new CustomException(ErrorCode.APPLE_CLIENT_SECRET_ERROR); + } + } + + log.info("Converter의 param: {}", params); + + return new RequestEntity<>( + params, + entity.getHeaders(), + entity.getMethod(), + entity.getUrl() + ); + } +} From e88eca51cda9f1f6a4934551e9234c59e2573d73 Mon Sep 17 00:00:00 2001 From: dainnida Date: Mon, 10 Feb 2025 20:22:09 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[#71]=20Feat:=20=EC=95=A0=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20loadUser=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CustomOAuth2UserService.java | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/cmc/mercury/global/oauth/service/CustomOAuth2UserService.java b/src/main/java/com/cmc/mercury/global/oauth/service/CustomOAuth2UserService.java index 81077a4..d7fe351 100644 --- a/src/main/java/com/cmc/mercury/global/oauth/service/CustomOAuth2UserService.java +++ b/src/main/java/com/cmc/mercury/global/oauth/service/CustomOAuth2UserService.java @@ -1,11 +1,13 @@ package com.cmc.mercury.global.oauth.service; +import com.auth0.jwt.interfaces.DecodedJWT; import com.cmc.mercury.domain.user.entity.OAuthType; import com.cmc.mercury.domain.user.entity.User; import com.cmc.mercury.domain.user.entity.UserStatus; import com.cmc.mercury.domain.user.repository.UserRepository; import com.cmc.mercury.global.exception.CustomException; import com.cmc.mercury.global.exception.ErrorCode; +import com.cmc.mercury.global.oauth.apple.AppleIdTokenVerifier; import com.cmc.mercury.global.oauth.userinfo.AppleOAuthUserInfo; import com.cmc.mercury.global.oauth.userinfo.GoogleOAuthUserInfo; import com.cmc.mercury.global.oauth.userinfo.KakaoOAuthUserInfo; @@ -21,6 +23,8 @@ import org.springframework.stereotype.Service; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; @Service @RequiredArgsConstructor @@ -28,16 +32,57 @@ public class CustomOAuth2UserService extends DefaultOAuth2UserService { private final UserRepository userRepository; + private final AppleIdTokenVerifier appleIdTokenVerifier; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - log.info("CustomOAuth2UserService.loadUser() 실행 - OAuth2 로그인 요청 진입"); + log.info("CustomOAuth2UserService.loadUser() 실행 - OAuth2 로그인 요청 진입"); - OAuth2User oAuth2User = super.loadUser(userRequest); - log.info("OAuth2User attributes: {}", oAuth2User.getAttributes()); + OAuth2User oAuth2User; + String userNameAttributeName; try { + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + log.info("Provider: {}", registrationId); + + if ("apple".equalsIgnoreCase(registrationId)) { + // Apple 로그인의 경우 + + // Apple의 응답에서 id_token 가져오기 + String idToken = userRequest.getAdditionalParameters().get("id_token").toString(); + log.info("Successfully get id_token: {}", idToken); + + // id_token 서명 및 클레임 검증 + DecodedJWT verifiedJwt = appleIdTokenVerifier.verify(idToken); + + // id_token에서 필요한 claim을 추출 + String sub = verifiedJwt.getSubject(); + String email = verifiedJwt.getClaim("email").asString(); + log.info("Apple's verified sub={}, email={}", sub, email); + + // OAuth2User의 attributes 구성 + Map attributes = new HashMap<>(); + attributes.put("sub", sub); + attributes.put("email", email); + + // SecurityContext에 저장될 DefaultOAuth2User 객체 생성 + oAuth2User = new DefaultOAuth2User( + Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), + attributes, + "sub" + ); + userNameAttributeName = "sub"; + + } else { + // Apple 외 다른 OAuth2 로그인 + oAuth2User = super.loadUser(userRequest); + userNameAttributeName = userRequest.getClientRegistration().getProviderDetails() + .getUserInfoEndpoint().getUserNameAttributeName(); + } + + log.info("OAuth2User attributes: {}", oAuth2User.getAttributes()); + OAuth2UserInfo oAuth2UserInfo = getOAuthUserInfo(userRequest.getClientRegistration().getRegistrationId(), oAuth2User); log.info("OAuth2UserInfo 생성 완료: oauthId={}, email={}, type={}", @@ -61,12 +106,11 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic return new DefaultOAuth2User( Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), oAuth2User.getAttributes(), - userRequest.getClientRegistration().getProviderDetails() - .getUserInfoEndpoint().getUserNameAttributeName() - ); + userNameAttributeName); + } catch (Exception e) { - log.error("OAuth2 로그인 처리 중 오류 발생: ", e); - throw new CustomException(ErrorCode.OAUTH2_PROCESSING_ERROR); + log.error("OAuth2 로그인 처리 중 오류 발생: ", e); + throw new CustomException(ErrorCode.OAUTH2_PROCESSING_ERROR); } } From 0f495f2b97ee47b423eea9eb4e263fa989658ac2 Mon Sep 17 00:00:00 2001 From: dainnida Date: Mon, 10 Feb 2025 20:24:11 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[#71]=20Feat:=20=EC=95=A0=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=9A=A9=20JWT=20client=20secret=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20converter=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mercury/global/config/SecurityConfig.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cmc/mercury/global/config/SecurityConfig.java b/src/main/java/com/cmc/mercury/global/config/SecurityConfig.java index cfdc392..eb26286 100644 --- a/src/main/java/com/cmc/mercury/global/config/SecurityConfig.java +++ b/src/main/java/com/cmc/mercury/global/config/SecurityConfig.java @@ -3,6 +3,8 @@ import com.cmc.mercury.domain.user.repository.UserRepository; import com.cmc.mercury.global.jwt.JwtAuthenticationFilter; import com.cmc.mercury.global.jwt.JwtProvider; +import com.cmc.mercury.global.oauth.apple.AppleClientSecretService; +import com.cmc.mercury.global.oauth.apple.CustomRequestEntityConverter; import com.cmc.mercury.global.oauth.handler.OAuth2FailureHandler; import com.cmc.mercury.global.oauth.handler.OAuth2SuccessHandler; import com.cmc.mercury.global.oauth.service.CustomOAuth2UserService; @@ -12,6 +14,9 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -31,6 +36,16 @@ public class SecurityConfig { private final OAuth2SuccessHandler oAuth2SuccessHandler; private final OAuth2FailureHandler oAuth2FailureHandler; private final CustomOAuth2UserService customOAuth2UserService; + private final AppleClientSecretService appleClientSecretService; + + @Bean + public OAuth2AccessTokenResponseClient accessTokenResponseClient() { + + DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient(); + client.setRequestEntityConverter(new CustomRequestEntityConverter(appleClientSecretService)); + + return client; + } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -40,17 +55,21 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .formLogin(formLogin -> formLogin.disable()) .httpBasic(httpBasic -> httpBasic.disable()) .sessionManagement(session -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) + // Spring Security는 OAuth2 로그인 플로우에서 인증 요청 시 생성된 OAuth2AuthorizationRequest 객체를 HTTP 세션에 저장해두는데, + // STATELESS 설정 시 OAuth2AuthorizationRequest가 저장되지 않으므로 콜백 요청 시 이를 찾지 못해 authorization_request_not_found 오류가 발생 .authorizeHttpRequests(authorize -> authorize // Swagger UI 관련 경로 허용 .requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/api-docs/**", "/swagger-resources/**").permitAll() // OAuth2 관련 경로 허용 .requestMatchers("/login/**", "/oauth2/**").permitAll() - // 도서 검색 + // 도서 검색, 사용자 api .requestMatchers("/books/search", "/users/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 + .tokenEndpoint(token -> token + .accessTokenResponseClient(accessTokenResponseClient())) .userInfoEndpoint(userInfo -> userInfo .userService(customOAuth2UserService)) .successHandler(oAuth2SuccessHandler) From 945c4cd23d1166b5bb4ff3c40528c81492df6f49 Mon Sep 17 00:00:00 2001 From: dainnida Date: Mon, 10 Feb 2025 20:33:21 +0900 Subject: [PATCH 7/7] =?UTF-8?q?Refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=8B=9C=20redirect=20uri=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=BF=A0=ED=82=A4=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/oauth/handler/OAuth2FailureHandler.java | 3 +-- .../global/oauth/handler/OAuth2SuccessHandler.java | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/cmc/mercury/global/oauth/handler/OAuth2FailureHandler.java b/src/main/java/com/cmc/mercury/global/oauth/handler/OAuth2FailureHandler.java index 627e011..bfedffb 100644 --- a/src/main/java/com/cmc/mercury/global/oauth/handler/OAuth2FailureHandler.java +++ b/src/main/java/com/cmc/mercury/global/oauth/handler/OAuth2FailureHandler.java @@ -20,8 +20,7 @@ public void onAuthenticationFailure(HttpServletRequest request, HttpServletRespo log.error("Social Login Failed: {}", exception.getMessage()); - String targetUrl = UriComponentsBuilder.fromUriString("/login/fail") - .queryParam("redirect_url", "https://www.mercuryplanet.co.kr/") + String targetUrl = UriComponentsBuilder.fromUriString("https://www.mercuryplanet.co.kr/login/fail") .build(true).toUriString(); getRedirectStrategy().sendRedirect(request, response, targetUrl); diff --git a/src/main/java/com/cmc/mercury/global/oauth/handler/OAuth2SuccessHandler.java b/src/main/java/com/cmc/mercury/global/oauth/handler/OAuth2SuccessHandler.java index d7b61ac..b4c50fd 100644 --- a/src/main/java/com/cmc/mercury/global/oauth/handler/OAuth2SuccessHandler.java +++ b/src/main/java/com/cmc/mercury/global/oauth/handler/OAuth2SuccessHandler.java @@ -44,23 +44,24 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo // JWT 토큰 생성 String accessToken = jwtProvider.createAccessToken(user.getId(), user.getEmail()); String refreshToken = jwtProvider.createRefreshToken(user.getId(), user.getEmail()); + log.info("accessToken, refreshToken: {}, {}", accessToken, refreshToken); // Access Token은 Authorization 헤더에 추가 response.setHeader("Authorization", "Bearer " + accessToken); + log.info("Header에 설정은 성공"); // Refresh Token은 보안을 위해 HttpOnly 쿠키로 설정 Cookie refreshTokenCookie = new Cookie("refresh_token", refreshToken); refreshTokenCookie.setHttpOnly(true); // JavaScript에서 접근 방지 - refreshTokenCookie.setSecure(true); // HTTPS만 허용 + // refreshTokenCookie.setSecure(true); // HTTPS만 허용 refreshTokenCookie.setPath("/"); // 모든 경로에서 접근 가능 - refreshTokenCookie.setDomain("mercuryplanet.co.kr"); // 도메인 간 쿠키 공유 + // refreshTokenCookie.setDomain("mercuryplanet.co.kr"); // 도메인 간 쿠키 공유 refreshTokenCookie.setMaxAge((int) refreshTokenValidity / 1000); // ms를 초 단위로 변환 response.addCookie(refreshTokenCookie); // 리다이렉트 URL에 토큰 포함하여 이동 - String targetUrl = UriComponentsBuilder.fromUriString("/login/success") + String targetUrl = UriComponentsBuilder.fromUriString("https://www.mercuryplanet.co.kr/login/success") .queryParam("access_token", accessToken) - .queryParam("redirect_url", "https://www.mercuryplanet.co.kr/home") .build(true).toUriString(); getRedirectStrategy().sendRedirect(request, response, targetUrl);