From 52ff86c8e22f86adca3dbef589165cfe3924de89 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Mon, 14 Jul 2025 15:08:23 +0900 Subject: [PATCH 01/14] =?UTF-8?q?:bug:=20fix:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=20enum(ProviderType)=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withtime/be/withtimebe/domain/member/entity/Member.java | 1 - .../withtimebe/domain/member/entity/enums/ProviderType.java | 5 ----- 2 files changed, 6 deletions(-) delete mode 100644 src/main/java/org/withtime/be/withtimebe/domain/member/entity/enums/ProviderType.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java index fb0aba4..c97e6a7 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java @@ -3,7 +3,6 @@ import jakarta.persistence.*; import lombok.*; import org.withtime.be.withtimebe.domain.member.entity.enums.Gender; -import org.withtime.be.withtimebe.domain.member.entity.enums.ProviderType; import org.withtime.be.withtimebe.domain.member.entity.enums.Role; import org.withtime.be.withtimebe.domain.member.entity.enums.UserRank; import org.withtime.be.withtimebe.global.common.BaseEntity; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/enums/ProviderType.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/enums/ProviderType.java deleted file mode 100644 index d2f7a66..0000000 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/enums/ProviderType.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.withtime.be.withtimebe.domain.member.entity.enums; - -public enum ProviderType { - KAKAO, GOOGLE, NAVER, LOCAL -} From 57a09e61b0f89ba1e565ef602885de76734e47c2 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Mon, 14 Jul 2025 15:54:06 +0900 Subject: [PATCH 02/14] =?UTF-8?q?:recycle:=20refactor:=20=EC=BF=A0?= =?UTF-8?q?=ED=82=A4=EC=97=90=20=ED=86=A0=ED=81=B0=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomAuthenticationSuccessHandler.java | 21 ++-------- .../security/manager/CookieTokenManager.java | 38 +++++++++++++++++++ .../global/security/manager/TokenManager.java | 9 +++++ 3 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/global/security/manager/CookieTokenManager.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/security/manager/TokenManager.java diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/handler/CustomAuthenticationSuccessHandler.java b/src/main/java/org/withtime/be/withtimebe/global/security/handler/CustomAuthenticationSuccessHandler.java index 5de3ab8..b0ca269 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/handler/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/handler/CustomAuthenticationSuccessHandler.java @@ -17,6 +17,7 @@ import org.withtime.be.withtimebe.domain.auth.service.command.TokenStorageCommandService; import org.withtime.be.withtimebe.global.security.constants.AuthenticationConstants; import org.withtime.be.withtimebe.global.security.domain.CustomUserDetails; +import org.withtime.be.withtimebe.global.security.manager.TokenManager; import org.withtime.be.withtimebe.global.util.CookieUtil; import java.io.IOException; @@ -25,15 +26,12 @@ @RequiredArgsConstructor public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { - private final TokenStorageCommandService tokenStorageCommandService; - private final TokenCommandService tokenCommandService; - private final TokenQueryService tokenQueryService; + private final TokenManager tokenManager; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { CustomUserDetails customUserDetails =(CustomUserDetails) authentication.getPrincipal(); - AuthResponseDTO.Login loginResponse = tokenCommandService.createLoginToken(customUserDetails); - addToken(request, response, loginResponse, customUserDetails); + tokenManager.addToken(request, response, customUserDetails); ObjectMapper objectMapper = new ObjectMapper(); response.setStatus(HttpStatus.OK.value()); @@ -42,16 +40,5 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo objectMapper.writeValue(response.getOutputStream(), DefaultResponse.noContent()); } - private void addToken(HttpServletRequest request, HttpServletResponse response, AuthResponseDTO.Login loginResponse, CustomUserDetails customUserDetails) { - // 쿠키에 이미 토큰이 있는 경우 블랙리스트 처리 - for (String token : new String[] {CookieUtil.getCookie(request, AuthenticationConstants.ACCESS_TOKEN_NAME), CookieUtil.getCookie(request, AuthenticationConstants.REFRESH_TOKEN_NAME)}) { - if (token != null) { - tokenStorageCommandService.addBlackList(token); - } - } - - CookieUtil.addCookie(request, response, AuthenticationConstants.ACCESS_TOKEN_NAME, loginResponse.accessToken(), (int) tokenQueryService.getAccessTokenExpiration().toSeconds()); - CookieUtil.addCookie(request, response, AuthenticationConstants.REFRESH_TOKEN_NAME, loginResponse.refreshToken(), (int) tokenQueryService.getRefreshTokenExpiration().toSeconds()); - tokenStorageCommandService.addRefreshToken(customUserDetails.getId(), loginResponse.refreshToken()); - } + } diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/manager/CookieTokenManager.java b/src/main/java/org/withtime/be/withtimebe/global/security/manager/CookieTokenManager.java new file mode 100644 index 0000000..913e753 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/security/manager/CookieTokenManager.java @@ -0,0 +1,38 @@ +package org.withtime.be.withtimebe.global.security.manager; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.withtime.be.withtimebe.domain.auth.dto.response.AuthResponseDTO; +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.auth.service.command.TokenCommandService; +import org.withtime.be.withtimebe.domain.auth.service.command.TokenStorageCommandService; +import org.withtime.be.withtimebe.domain.auth.service.query.TokenQueryService; +import org.withtime.be.withtimebe.global.security.constants.AuthenticationConstants; +import org.withtime.be.withtimebe.global.security.domain.CustomUserDetails; +import org.withtime.be.withtimebe.global.util.CookieUtil; + +@Component +@RequiredArgsConstructor +public class CookieTokenManager implements TokenManager { + + private final TokenStorageCommandService tokenStorageCommandService; + private final TokenCommandService tokenCommandService; + private final TokenQueryService tokenQueryService; + + @Override + public void addToken(HttpServletRequest request, HttpServletResponse response, CustomUserDetails customUserDetails) { + AuthResponseDTO.Login loginResponse = tokenCommandService.createLoginToken(customUserDetails); + for (String token : new String[] {CookieUtil.getCookie(request, AuthenticationConstants.ACCESS_TOKEN_NAME), CookieUtil.getCookie(request, AuthenticationConstants.REFRESH_TOKEN_NAME)}) { + if (token != null) { + tokenStorageCommandService.addBlackList(token); + } + } + + CookieUtil.addCookie(request, response, AuthenticationConstants.ACCESS_TOKEN_NAME, loginResponse.accessToken(), (int) tokenQueryService.getAccessTokenExpiration().toSeconds()); + CookieUtil.addCookie(request, response, AuthenticationConstants.REFRESH_TOKEN_NAME, loginResponse.refreshToken(), (int) tokenQueryService.getRefreshTokenExpiration().toSeconds()); + tokenStorageCommandService.addRefreshToken(customUserDetails.getId(), loginResponse.refreshToken()); + } + +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/manager/TokenManager.java b/src/main/java/org/withtime/be/withtimebe/global/security/manager/TokenManager.java new file mode 100644 index 0000000..38ad7b0 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/security/manager/TokenManager.java @@ -0,0 +1,9 @@ +package org.withtime.be.withtimebe.global.security.manager; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.withtime.be.withtimebe.global.security.domain.CustomUserDetails; + +public interface TokenManager { + void addToken(HttpServletRequest request, HttpServletResponse response,CustomUserDetails customUserDetails); +} From 65310d9cc5730b942aae350225532bc7aeba7973 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Mon, 14 Jul 2025 16:01:15 +0900 Subject: [PATCH 03/14] =?UTF-8?q?:sparkles:=20feat:=20=EC=86=8C=EC=85=9C?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B4=88=EA=B8=B0=20=ED=8B=80?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + .../auth/controller/OAuth2Controller.java | 22 +++++++ .../auth/converter/OAuth2Converter.java | 13 ++++ .../auth/dto/response/OAuth2ResponseDTO.java | 26 ++++++++ .../domain/auth/factory/OAuth2UserLoader.java | 8 +++ .../auth/factory/OAuth2UserLoaderFactory.java | 23 +++++++ .../support/AbstractOAuth2UserLoader.java | 25 ++++++++ .../auth/factory/support/KakaoUserLoader.java | 25 ++++++++ .../service/command/OAuth2CommandService.java | 9 +++ .../command/OAuth2CommandServiceImpl.java | 64 +++++++++++++++++++ .../member/converter/SocialConverter.java | 13 ++++ .../member/repository/SocialRepository.java | 11 ++++ .../global/error/code/OAuthErrorCode.java | 26 ++++++++ .../error/exception/OAuthException.java | 10 +++ 14 files changed, 277 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/dto/response/OAuth2ResponseDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoader.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoaderFactory.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/member/converter/SocialConverter.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/member/repository/SocialRepository.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/code/OAuthErrorCode.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/exception/OAuthException.java diff --git a/build.gradle b/build.gradle index d211311..c7b02f9 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + // OAuth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // test testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java new file mode 100644 index 0000000..141c8a7 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java @@ -0,0 +1,22 @@ +package org.withtime.be.withtimebe.domain.auth.controller; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.namul.api.payload.response.DefaultResponse; +import org.springframework.web.bind.annotation.*; +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.auth.service.command.OAuth2CommandService; + +@RestController +@RequestMapping("/api/v1/oauth2") +@RequiredArgsConstructor +public class OAuth2Controller { + + private final OAuth2CommandService oAuth2CommandService; + + @GetMapping("/callback/{provider}") + public DefaultResponse loginWithOAuth2(HttpServletRequest request, HttpServletResponse response, @PathVariable String provider, @RequestParam String code) { + return DefaultResponse.ok(oAuth2CommandService.login(request, response, provider, code)); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java new file mode 100644 index 0000000..229dfe4 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java @@ -0,0 +1,13 @@ +package org.withtime.be.withtimebe.domain.auth.converter; + +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; + +public class OAuth2Converter { + public static OAuth2ResponseDTO.Login toLogin(String email, boolean isFirst, Long socialId) { + return OAuth2ResponseDTO.Login.builder() + .email(email) + .socialId(socialId) + .isFirst(isFirst) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/response/OAuth2ResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/response/OAuth2ResponseDTO.java new file mode 100644 index 0000000..038e8cd --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/response/OAuth2ResponseDTO.java @@ -0,0 +1,26 @@ +package org.withtime.be.withtimebe.domain.auth.dto.response; + +import lombok.Builder; +import org.withtime.be.withtimebe.domain.member.entity.enums.SocialType; + +public record OAuth2ResponseDTO() { + + @Builder + public record Login( + String email, + Long socialId, + boolean isFirst + ) { + + } + + @Builder + public record GetUserInfo( + String email, + String providerId, + SocialType socialType + ) { + + } + +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoader.java new file mode 100644 index 0000000..583058c --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoader.java @@ -0,0 +1,8 @@ +package org.withtime.be.withtimebe.domain.auth.factory; + +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; + +public interface OAuth2UserLoader { + OAuth2ResponseDTO.GetUserInfo loadUser(String code); + String getSocialType(); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoaderFactory.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoaderFactory.java new file mode 100644 index 0000000..c917ef2 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoaderFactory.java @@ -0,0 +1,23 @@ +package org.withtime.be.withtimebe.domain.auth.factory; + +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class OAuth2UserLoaderFactory { + + private final Map oAuth2UserLoaderMap = new ConcurrentHashMap<>(); + + public OAuth2UserLoaderFactory(List oAuth2UserLoaders) { + oAuth2UserLoaders.forEach( + oAuth2UserLoader -> oAuth2UserLoaderMap.put(oAuth2UserLoader.getSocialType().toLowerCase(), oAuth2UserLoader) + ); + } + + public OAuth2UserLoader getUserLoader(String provider) { + return oAuth2UserLoaderMap.get(provider.toLowerCase()); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java new file mode 100644 index 0000000..7c87419 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java @@ -0,0 +1,25 @@ +package org.withtime.be.withtimebe.domain.auth.factory.support; + +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.auth.factory.OAuth2UserLoader; +import org.withtime.be.withtimebe.global.error.code.OAuthErrorCode; +import org.withtime.be.withtimebe.global.error.exception.OAuthException; + +public abstract class AbstractOAuth2UserLoader implements OAuth2UserLoader { + + @Override + public OAuth2ResponseDTO.GetUserInfo loadUser(String code) { + try { + String token = getAccessToken(code); + return getUserInfo(token); + } + catch (Exception e) { + throw new OAuthException(OAuthErrorCode.FAIL_TO_GET_USER_INFO); + } + } + + protected abstract String getAccessToken(String code); + + protected abstract OAuth2ResponseDTO.GetUserInfo getUserInfo(String token); + +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java new file mode 100644 index 0000000..877dbf9 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java @@ -0,0 +1,25 @@ +package org.withtime.be.withtimebe.domain.auth.factory.support; + +import org.springframework.stereotype.Component; +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.enums.SocialType; + +@Component +public class KakaoUserLoader extends AbstractOAuth2UserLoader { + + + @Override + protected String getAccessToken(String code) { + return ""; + } + + @Override + protected OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) { + return null; + } + + @Override + public String getSocialType() { + return SocialType.KAKAO.name(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandService.java new file mode 100644 index 0000000..cd81cd7 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandService.java @@ -0,0 +1,9 @@ +package org.withtime.be.withtimebe.domain.auth.service.command; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; + +public interface OAuth2CommandService { + OAuth2ResponseDTO.Login login(HttpServletRequest request, HttpServletResponse response, String provider, String code); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java new file mode 100644 index 0000000..357ad55 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java @@ -0,0 +1,64 @@ +package org.withtime.be.withtimebe.domain.auth.service.command; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.withtime.be.withtimebe.domain.auth.converter.OAuth2Converter; +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.auth.factory.OAuth2UserLoader; +import org.withtime.be.withtimebe.domain.auth.factory.OAuth2UserLoaderFactory; +import org.withtime.be.withtimebe.domain.member.converter.SocialConverter; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.entity.Social; +import org.withtime.be.withtimebe.domain.member.repository.SocialRepository; +import org.withtime.be.withtimebe.global.error.code.OAuthErrorCode; +import org.withtime.be.withtimebe.global.error.exception.OAuthException; +import org.withtime.be.withtimebe.global.security.domain.CustomUserDetails; +import org.withtime.be.withtimebe.global.security.manager.TokenManager; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class OAuth2CommandServiceImpl implements OAuth2CommandService { + + private final OAuth2UserLoaderFactory oAuth2UserLoaderFactory; + private final SocialRepository socialRepository; + private final TokenManager tokenManager; + + @Override + public OAuth2ResponseDTO.Login login(HttpServletRequest request, HttpServletResponse response, String provider, String code) { + OAuth2UserLoader userLoader = oAuth2UserLoaderFactory.getUserLoader(provider); + if (userLoader == null) { + throw new OAuthException(OAuthErrorCode.UNSUPPORTED_SOCIAL_TYPE); + } + OAuth2ResponseDTO.GetUserInfo userInfo = userLoader.loadUser(code); + return successfulOAuth2(request, response, userInfo); + } + + private OAuth2ResponseDTO.Login successfulOAuth2(HttpServletRequest request, HttpServletResponse response, OAuth2ResponseDTO.GetUserInfo userInfo) { + Optional socialOptional = socialRepository.findByProviderIdAndSocialType(userInfo.providerId(), userInfo.socialType()); + // 소셜로 첫 로그인 + if (socialOptional.isEmpty()) { + Social social = socialRepository.save(SocialConverter.toSocial(userInfo)); + return OAuth2Converter.toLogin(userInfo.email(), true, social.getId()); + } + // 소셜로 로그인 한 적은 있지만 사용자와 연결 X 즉, 최초 로그인 X + else if (socialOptional.get().getMember() == null) { + Social social = socialOptional.get(); + return OAuth2Converter.toLogin(userInfo.email(), true, social.getId()); + } + // 소셜로 로그인 한 적도 있고 사용자와도 연결된 경우 + else { + Social social = socialOptional.get(); + processToken(request, response, social.getMember()); + return OAuth2Converter.toLogin(userInfo.email(), false, social.getId()); + } + } + + private void processToken(HttpServletRequest request, HttpServletResponse response, Member member) { + CustomUserDetails customUserDetails = new CustomUserDetails(member); + tokenManager.addToken(request, response, customUserDetails); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/converter/SocialConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/SocialConverter.java new file mode 100644 index 0000000..b241da4 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/SocialConverter.java @@ -0,0 +1,13 @@ +package org.withtime.be.withtimebe.domain.member.converter; + +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Social; + +public class SocialConverter { + public static Social toSocial(OAuth2ResponseDTO.GetUserInfo userInfo) { + return Social.builder() + .socialType(userInfo.socialType()) + .providerId(userInfo.providerId()) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/repository/SocialRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/member/repository/SocialRepository.java new file mode 100644 index 0000000..e8dd17e --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/repository/SocialRepository.java @@ -0,0 +1,11 @@ +package org.withtime.be.withtimebe.domain.member.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.withtime.be.withtimebe.domain.member.entity.Social; +import org.withtime.be.withtimebe.domain.member.entity.enums.SocialType; + +import java.util.Optional; + +public interface SocialRepository extends JpaRepository { + Optional findByProviderIdAndSocialType(String providerId, SocialType socialType); +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/OAuthErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/OAuthErrorCode.java new file mode 100644 index 0000000..32a9aa9 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/OAuthErrorCode.java @@ -0,0 +1,26 @@ +package org.withtime.be.withtimebe.global.error.code; + +import lombok.AllArgsConstructor; +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.code.dto.supports.DefaultResponseErrorReasonDTO; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +public enum OAuthErrorCode implements BaseErrorCode { + + FAIL_TO_GET_USER_INFO(HttpStatus.INTERNAL_SERVER_ERROR, "OAUTH500_1", "사용자 정보를 가져오는 데 실패했습니다."), + UNSUPPORTED_SOCIAL_TYPE(HttpStatus.BAD_REQUEST, "OAUTH400_1", "지원하지 않는 소셜 로그인입니다.") + ; + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public DefaultResponseErrorReasonDTO getReason() { + return DefaultResponseErrorReasonDTO.builder() + .httpStatus(this.httpStatus) + .code(this.code) + .message(this.message) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/exception/OAuthException.java b/src/main/java/org/withtime/be/withtimebe/global/error/exception/OAuthException.java new file mode 100644 index 0000000..ccf6743 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/exception/OAuthException.java @@ -0,0 +1,10 @@ +package org.withtime.be.withtimebe.global.error.exception; + +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.error.exception.ServerApplicationException; + +public class OAuthException extends ServerApplicationException { + public OAuthException(BaseErrorCode code) { + super(code); + } +} From 5f308166e57ffe65d31c89913ff615c2b81272a8 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Mon, 14 Jul 2025 16:12:03 +0900 Subject: [PATCH 04/14] =?UTF-8?q?:sparkles:=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=20=EA=B0=80=EC=9E=85=20=EB=A1=9C=EC=A7=81=EC=97=90=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=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 --- .../auth/controller/AuthController.java | 9 ++++++- .../auth/dto/request/AuthRequestDTO.java | 3 ++- .../command/AuthCommandServiceImpl.java | 21 ++++++++-------- .../domain/member/entity/Social.java | 4 +++ .../global/error/code/SocialErrorCode.java | 25 +++++++++++++++++++ .../error/exception/SocialException.java | 11 ++++++++ 6 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/code/SocialErrorCode.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/exception/SocialException.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java index 2ad149b..f34509e 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java @@ -24,7 +24,7 @@ public class AuthController { private final AuthCommandService authCommandService; private final EmailCommandService emailCommandService; - @Operation(summary = "회원가입 API by 요시", description = "최초 회원가입 시 필요한 정보를 포함하여 회원가입 진행") + @Operation(summary = "회원가입 API by 요시", description = "최초 회원가입 시 필요한 정보를 포함하여 회원가입 진행, 소셜 로그인인 경우에만 socialId 포함하고 아닌 경우 제거하거나 null") @ApiResponses({ @ApiResponse(responseCode = "204", description = "회원가입 성공"), @ApiResponse( @@ -33,6 +33,13 @@ public class AuthController { 다음과 같은 이유로 실패할 수 있습니다: - AUTH400_1: 이미 존재하는 이메일입니다. """ + ), + @ApiResponse( + responseCode = "404", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - SOCIAL404_1: 소셜을 찾을 수 없습니다. + """ ) }) @PostMapping("/sign-up") diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/AuthRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/AuthRequestDTO.java index dca0fbf..858b1c3 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/AuthRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/AuthRequestDTO.java @@ -22,7 +22,8 @@ public record SignUp( String password, Gender gender, String phoneNumber, - LocalDate birth + LocalDate birth, + Long socialId ) { } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java index 48d7d10..cdd75fd 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java @@ -12,15 +12,11 @@ import org.withtime.be.withtimebe.domain.auth.service.query.TokenQueryService; import org.withtime.be.withtimebe.domain.auth.service.query.TokenStorageQueryService; import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.entity.Social; import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; -import org.withtime.be.withtimebe.global.error.code.AuthErrorCode; -import org.withtime.be.withtimebe.global.error.code.EmailErrorCode; -import org.withtime.be.withtimebe.global.error.code.MemberErrorCode; -import org.withtime.be.withtimebe.global.error.code.TokenErrorCode; -import org.withtime.be.withtimebe.global.error.exception.AuthException; -import org.withtime.be.withtimebe.global.error.exception.EmailException; -import org.withtime.be.withtimebe.global.error.exception.MemberException; -import org.withtime.be.withtimebe.global.error.exception.TokenException; +import org.withtime.be.withtimebe.domain.member.repository.SocialRepository; +import org.withtime.be.withtimebe.global.error.code.*; +import org.withtime.be.withtimebe.global.error.exception.*; import org.withtime.be.withtimebe.global.security.constants.AuthenticationConstants; import org.withtime.be.withtimebe.global.security.domain.CustomUserDetails; import org.withtime.be.withtimebe.global.util.CookieUtil; @@ -32,6 +28,7 @@ public class AuthCommandServiceImpl implements AuthCommandService { private final PasswordEncoder passwordEncoder; private final MemberRepository memberRepository; + private final SocialRepository socialRepository; private final TokenCommandService tokenCommandService; private final TokenStorageCommandService tokenStorageCommandService; private final TokenQueryService tokenQueryService; @@ -42,8 +39,12 @@ public class AuthCommandServiceImpl implements AuthCommandService { public void signUp(AuthRequestDTO.SignUp request) { validateSignUp(request); - Member member = AuthConverter.toLocalMember(request.email(), request.username(), passwordEncoder.encode(request.password()), request.phoneNumber(), request.gender(), request.birth()); - memberRepository.save(member); + Member member = memberRepository.save(AuthConverter.toLocalMember(request.email(), request.username(), passwordEncoder.encode(request.password()), request.phoneNumber(), request.gender(), request.birth())); + if (request.socialId() != null) { + Social social = socialRepository.findById(request.socialId()).orElseThrow(() -> + new SocialException(SocialErrorCode.NOT_FOUND_SOCIAL)); + social.addMember(member); + } } @Override diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Social.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Social.java index 5cadc14..b6e8fd3 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Social.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Social.java @@ -27,4 +27,8 @@ public class Social extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; + + public void addMember(Member member) { + this.member = member; + } } diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/SocialErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/SocialErrorCode.java new file mode 100644 index 0000000..4526c34 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/SocialErrorCode.java @@ -0,0 +1,25 @@ +package org.withtime.be.withtimebe.global.error.code; + +import lombok.AllArgsConstructor; +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.code.dto.supports.DefaultResponseErrorReasonDTO; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +public enum SocialErrorCode implements BaseErrorCode { + + NOT_FOUND_SOCIAL(HttpStatus.NOT_FOUND, "SOCIAL404_1", "해당 소셜을 찾을 수 없습니다."), + ; + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public DefaultResponseErrorReasonDTO getReason() { + return DefaultResponseErrorReasonDTO.builder() + .httpStatus(this.httpStatus) + .code(this.code) + .message(this.message) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/exception/SocialException.java b/src/main/java/org/withtime/be/withtimebe/global/error/exception/SocialException.java new file mode 100644 index 0000000..a733299 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/exception/SocialException.java @@ -0,0 +1,11 @@ +package org.withtime.be.withtimebe.global.error.exception; + +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.error.exception.ServerApplicationException; + +public class SocialException extends ServerApplicationException { + + public SocialException(BaseErrorCode code) { + super (code); + } +} From bb7ec1fffa1eb0e81095d65c57e66407eecd92a9 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Mon, 14 Jul 2025 16:32:26 +0900 Subject: [PATCH 05/14] =?UTF-8?q?:bug:=20fix:=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=9D=BC?= =?UTF-8?q?=EB=B6=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/OAuth2CommandServiceImpl.java | 30 ++++++++++--------- .../member/converter/SocialConverter.java | 9 ++++++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java index 357ad55..d67f6ba 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java @@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.withtime.be.withtimebe.domain.auth.converter.OAuth2Converter; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.auth.factory.OAuth2UserLoader; @@ -11,6 +12,7 @@ import org.withtime.be.withtimebe.domain.member.converter.SocialConverter; import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.member.entity.Social; +import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; import org.withtime.be.withtimebe.domain.member.repository.SocialRepository; import org.withtime.be.withtimebe.global.error.code.OAuthErrorCode; import org.withtime.be.withtimebe.global.error.exception.OAuthException; @@ -21,10 +23,12 @@ @Service @RequiredArgsConstructor +@Transactional public class OAuth2CommandServiceImpl implements OAuth2CommandService { private final OAuth2UserLoaderFactory oAuth2UserLoaderFactory; private final SocialRepository socialRepository; + private final MemberRepository memberRepository; private final TokenManager tokenManager; @Override @@ -39,25 +43,23 @@ public OAuth2ResponseDTO.Login login(HttpServletRequest request, HttpServletResp private OAuth2ResponseDTO.Login successfulOAuth2(HttpServletRequest request, HttpServletResponse response, OAuth2ResponseDTO.GetUserInfo userInfo) { Optional socialOptional = socialRepository.findByProviderIdAndSocialType(userInfo.providerId(), userInfo.socialType()); - // 소셜로 첫 로그인 - if (socialOptional.isEmpty()) { - Social social = socialRepository.save(SocialConverter.toSocial(userInfo)); - return OAuth2Converter.toLogin(userInfo.email(), true, social.getId()); - } - // 소셜로 로그인 한 적은 있지만 사용자와 연결 X 즉, 최초 로그인 X - else if (socialOptional.get().getMember() == null) { - Social social = socialOptional.get(); - return OAuth2Converter.toLogin(userInfo.email(), true, social.getId()); + Optional memberOptional = memberRepository.findByEmail(userInfo.email()); + + // 해당 이메일로 회원가입이 된 경우 + if (memberOptional.isPresent()) { + // 소셜이 있으면 가져오고 아니면 새로 만들기 + Social social = socialOptional.orElseGet(() -> socialRepository.save(SocialConverter.toSocial(userInfo, memberOptional.get()))); + processLogin(request, response, memberOptional.get()); + return OAuth2Converter.toLogin(userInfo.email(), false, social.getId()); } - // 소셜로 로그인 한 적도 있고 사용자와도 연결된 경우 + // 회원가입이 안 된 경우 else { - Social social = socialOptional.get(); - processToken(request, response, social.getMember()); - return OAuth2Converter.toLogin(userInfo.email(), false, social.getId()); + Social social = socialOptional.orElseGet(() -> socialRepository.save(SocialConverter.toSocial(userInfo))); + return OAuth2Converter.toLogin(userInfo.email(), true, social.getId()); } } - private void processToken(HttpServletRequest request, HttpServletResponse response, Member member) { + private void processLogin(HttpServletRequest request, HttpServletResponse response, Member member) { CustomUserDetails customUserDetails = new CustomUserDetails(member); tokenManager.addToken(request, response, customUserDetails); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/converter/SocialConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/SocialConverter.java index b241da4..89918a7 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/converter/SocialConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/SocialConverter.java @@ -1,6 +1,7 @@ package org.withtime.be.withtimebe.domain.member.converter; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.member.entity.Social; public class SocialConverter { @@ -10,4 +11,12 @@ public static Social toSocial(OAuth2ResponseDTO.GetUserInfo userInfo) { .providerId(userInfo.providerId()) .build(); } + + public static Social toSocial(OAuth2ResponseDTO.GetUserInfo userInfo, Member member) { + return Social.builder() + .socialType(userInfo.socialType()) + .providerId(userInfo.providerId()) + .member(member) + .build(); + } } From 685f392d49f18c68d79544f50b863f76ea881d1a Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Tue, 15 Jul 2025 22:42:37 +0900 Subject: [PATCH 06/14] =?UTF-8?q?:bug:=20fix:=20UserLoaderFactory=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EC=A4=91=EB=B3=B5=20=EC=8B=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8F=84=EB=A1=9D=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 --- .../domain/auth/factory/OAuth2UserLoaderFactory.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoaderFactory.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoaderFactory.java index c917ef2..9d834b8 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoaderFactory.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/OAuth2UserLoaderFactory.java @@ -13,7 +13,13 @@ public class OAuth2UserLoaderFactory { public OAuth2UserLoaderFactory(List oAuth2UserLoaders) { oAuth2UserLoaders.forEach( - oAuth2UserLoader -> oAuth2UserLoaderMap.put(oAuth2UserLoader.getSocialType().toLowerCase(), oAuth2UserLoader) + oAuth2UserLoader -> { + String socialType = oAuth2UserLoader.getSocialType().toLowerCase(); + if (oAuth2UserLoaderMap.get(socialType) != null) { + throw new IllegalStateException("OAuth2UserLoader social type 중복: " + socialType); + } + oAuth2UserLoaderMap.put(socialType, oAuth2UserLoader); + } ); } From faf473b9effe25b91bdf181fcf5b6a8d4cfe9770 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Tue, 15 Jul 2025 22:43:31 +0900 Subject: [PATCH 07/14] =?UTF-8?q?:bug:=20fix:=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/command/OAuth2CommandServiceImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java index d67f6ba..4c547ff 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/OAuth2CommandServiceImpl.java @@ -31,6 +31,8 @@ public class OAuth2CommandServiceImpl implements OAuth2CommandService { private final MemberRepository memberRepository; private final TokenManager tokenManager; + private final EmailVerificationCodeStorageCommandService emailVerificationCodeStorageCommandService; + @Override public OAuth2ResponseDTO.Login login(HttpServletRequest request, HttpServletResponse response, String provider, String code) { OAuth2UserLoader userLoader = oAuth2UserLoaderFactory.getUserLoader(provider); @@ -55,6 +57,7 @@ private OAuth2ResponseDTO.Login successfulOAuth2(HttpServletRequest request, Htt // 회원가입이 안 된 경우 else { Social social = socialOptional.orElseGet(() -> socialRepository.save(SocialConverter.toSocial(userInfo))); + emailVerificationCodeStorageCommandService.saveVerifiedEmail(userInfo.email()); return OAuth2Converter.toLogin(userInfo.email(), true, social.getId()); } } From 2c221b3c5cf6af53986574aca36f875a755b3739 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Tue, 15 Jul 2025 22:44:41 +0900 Subject: [PATCH 08/14] =?UTF-8?q?:sparkles:=20feat:=20=EC=86=8C=EC=85=9C?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B9=B4=EC=B9=B4=EC=98=A4=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 --- .../support/AbstractOAuth2UserLoader.java | 26 ++++++- .../auth/factory/support/KakaoUserLoader.java | 78 +++++++++++++++++-- .../support/dto/KakaoOAuth2ResponseDTO.java | 53 +++++++++++++ .../global/data/OAuth2ConfigData.java | 37 +++++++++ .../global/security/SecurityConfig.java | 4 + 5 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/KakaoOAuth2ResponseDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/data/OAuth2ConfigData.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java index 7c87419..3f637b2 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java @@ -1,12 +1,19 @@ package org.withtime.be.withtimebe.domain.auth.factory.support; +import lombok.RequiredArgsConstructor; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.auth.factory.OAuth2UserLoader; +import org.withtime.be.withtimebe.global.data.OAuth2ConfigData; import org.withtime.be.withtimebe.global.error.code.OAuthErrorCode; import org.withtime.be.withtimebe.global.error.exception.OAuthException; +import java.io.IOException; + +@RequiredArgsConstructor public abstract class AbstractOAuth2UserLoader implements OAuth2UserLoader { + private final OAuth2ConfigData oAuth2ConfigData; + @Override public OAuth2ResponseDTO.GetUserInfo loadUser(String code) { try { @@ -18,8 +25,23 @@ public OAuth2ResponseDTO.GetUserInfo loadUser(String code) { } } - protected abstract String getAccessToken(String code); + protected abstract String getAccessToken(String code) throws IOException; - protected abstract OAuth2ResponseDTO.GetUserInfo getUserInfo(String token); + protected abstract OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) throws IOException; + protected String getClientId() { + return this.oAuth2ConfigData.getRegistration().get(this.getSocialType()).getClientId(); + } + + protected String getRedirectUri() { + return this.oAuth2ConfigData.getRegistration().get(this.getSocialType()).getRedirectUri(); + } + + protected String getTokenUri() { + return this.oAuth2ConfigData.getProvider().get(this.getSocialType()).getTokenUri(); + } + + protected String getUserInfoUri() { + return this.oAuth2ConfigData.getProvider().get(this.getSocialType()).getUserInfoUri(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java index 877dbf9..d110fc8 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java @@ -1,25 +1,93 @@ package org.withtime.be.withtimebe.domain.auth.factory.support; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.auth.factory.support.dto.KakaoOAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.member.entity.enums.SocialType; +import org.withtime.be.withtimebe.global.data.OAuth2ConfigData; + +import java.io.IOException; @Component public class KakaoUserLoader extends AbstractOAuth2UserLoader { + private final SocialType socialType = SocialType.KAKAO; + + public KakaoUserLoader(OAuth2ConfigData oAuth2ConfigData) { + super(oAuth2ConfigData); + } @Override - protected String getAccessToken(String code) { - return ""; + protected String getAccessToken(String code) throws IOException { + // 인가코드 토큰 가져오기 + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + + httpHeaders.add("Content-Type", "application/x-www-form-urlencoded"); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("grant_type", "authorization_code"); + map.add("client_id", getClientId()); + map.add("redirect_uri", getRedirectUri()); + map.add("code", code); + HttpEntity request = new HttpEntity<>(map, httpHeaders); + + ResponseEntity response1 = restTemplate.exchange( + getTokenUri(), + HttpMethod.POST, + request, + String.class); + + ObjectMapper objectMapper = new ObjectMapper(); + KakaoOAuth2ResponseDTO.Token oAuth2TokenDTO = null; + + oAuth2TokenDTO = objectMapper.readValue(response1.getBody(), KakaoOAuth2ResponseDTO.Token.class); + return oAuth2TokenDTO.getAccess_token(); } @Override - protected OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) { - return null; + protected OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) throws IOException { + KakaoOAuth2ResponseDTO.KakaoProfile kakaoProfile = getKakaoProfile(token); + return OAuth2ResponseDTO.GetUserInfo.builder() + .email(kakaoProfile.getKakao_account().getEmail()) + .providerId(String.valueOf(kakaoProfile.getId())) + .socialType(SocialType.KAKAO) + .build(); } + @Override public String getSocialType() { - return SocialType.KAKAO.name(); + return this.socialType.name().toLowerCase(); + } + + private KakaoOAuth2ResponseDTO.KakaoProfile getKakaoProfile(String token) throws IOException{ + // 토큰으로 정보 가져오기 + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + + httpHeaders.add("Authorization", "Bearer " + token); + httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + + HttpEntity request1 = new HttpEntity<>(httpHeaders); + + ResponseEntity response2 = restTemplate.exchange( + getUserInfoUri(), + HttpMethod.GET, + request1, + String.class + ); + + ObjectMapper om = new ObjectMapper(); + + return om.readValue(response2.getBody(), KakaoOAuth2ResponseDTO.KakaoProfile.class); } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/KakaoOAuth2ResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/KakaoOAuth2ResponseDTO.java new file mode 100644 index 0000000..328d9df --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/KakaoOAuth2ResponseDTO.java @@ -0,0 +1,53 @@ +package org.withtime.be.withtimebe.domain.auth.factory.support.dto; + +import lombok.Getter; + +public class KakaoOAuth2ResponseDTO { + + @Getter + public static class Token { + String token_type; + String access_token; + String refresh_token; + Long expires_in; + Long refresh_token_expires_in; + String scope; + } + + @Getter + public static class KakaoProfile { + private Long id; + private String connected_at; + private Properties properties; + private KakaoAccount kakao_account; + + @Getter + public static class Properties { + private String nickname; + private String profile_image; + private String thumbnail_image; + } + + @Getter + public static class KakaoAccount { + private String email; + private Boolean is_email_verified; + private Boolean email_needs_agreement; + private Boolean has_email; + private Boolean profile_nickname_needs_agreement; + private Boolean profile_image_needs_agreement; + private Boolean email_needs_argument; + private Boolean is_email_valid; + private Profile profile; + + @Getter + public static class Profile { + private String nickname; + private String thumbnail_image_url; + private String profile_image_url; + private Boolean is_default_nickname; + private Boolean is_default_image; + } + } + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/data/OAuth2ConfigData.java b/src/main/java/org/withtime/be/withtimebe/global/data/OAuth2ConfigData.java new file mode 100644 index 0000000..8966056 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/data/OAuth2ConfigData.java @@ -0,0 +1,37 @@ +package org.withtime.be.withtimebe.global.data; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "spring.security.oauth2.client") +public class OAuth2ConfigData { + private Map registration; + private Map provider; + + @Getter + @Setter + public static class Registration { + private String clientId; + private String redirectUri; + private String authorizationGrantType; + private String clientAuthenticationMethod; + private List scope; + } + + @Getter + @Setter + public static class Provider { + private String authorizationUri; + private String tokenUri; + private String userInfoUri; + private String userNameAttribute; + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java index e0f9195..92b9450 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java @@ -7,6 +7,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -45,6 +46,8 @@ public class SecurityConfig { private String[] allowUrl = { API_PREFIX + "/auth/**", API_PREFIX + "/notices/**", + API_PREFIX + "/oauth2/**", + "/oauth2/authorization/**", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**" @@ -63,6 +66,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .csrf(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) + .oauth2Login(Customizer.withDefaults()) .exceptionHandling(exception -> exception .accessDeniedHandler(accessDeniedHandler()) .authenticationEntryPoint(authenticationEntryPoint()) From 4fb5e3fe2014d5c29fa0a9bbcbf97b3d152541b7 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Wed, 16 Jul 2025 18:49:25 +0900 Subject: [PATCH 09/14] =?UTF-8?q?:recycle:=20refactor:=20=EC=B9=B4?= =?UTF-8?q?=EC=B9=B4=EC=98=A4=20=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/converter/OAuth2Converter.java | 12 +++ .../auth/factory/support/KakaoUserLoader.java | 37 ++++----- .../support/dto/KakaoOAuth2ResponseDTO.java | 76 +++++++++---------- src/main/resources/application-develop.yml | 19 +++++ src/main/resources/application.yml | 19 +++++ 5 files changed, 107 insertions(+), 56 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java index 229dfe4..fa3ab51 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java @@ -1,6 +1,9 @@ package org.withtime.be.withtimebe.domain.auth.converter; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.auth.factory.support.dto.KakaoOAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.auth.factory.support.dto.NaverOAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.enums.SocialType; public class OAuth2Converter { public static OAuth2ResponseDTO.Login toLogin(String email, boolean isFirst, Long socialId) { @@ -10,4 +13,13 @@ public static OAuth2ResponseDTO.Login toLogin(String email, boolean isFirst, Lon .isFirst(isFirst) .build(); } + + public static OAuth2ResponseDTO.GetUserInfo toGetUserInfo(KakaoOAuth2ResponseDTO.KakaoProfile kakaoProfile) { + return OAuth2ResponseDTO.GetUserInfo.builder() + .email(kakaoProfile.kakao_account().email()) + .providerId(String.valueOf(kakaoProfile.id())) + .socialType(SocialType.KAKAO) + .build(); + } + } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java index d110fc8..a1f63de 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java @@ -9,6 +9,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; +import org.withtime.be.withtimebe.domain.auth.converter.OAuth2Converter; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.auth.factory.support.dto.KakaoOAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.member.entity.enums.SocialType; @@ -27,6 +28,23 @@ public KakaoUserLoader(OAuth2ConfigData oAuth2ConfigData) { @Override protected String getAccessToken(String code) throws IOException { + KakaoOAuth2ResponseDTO.Token oAuth2TokenDTO = getToken(code); + return oAuth2TokenDTO.access_token(); + } + + @Override + protected OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) throws IOException { + KakaoOAuth2ResponseDTO.KakaoProfile kakaoProfile = getKakaoProfile(token); + return OAuth2Converter.toGetUserInfo(kakaoProfile); + } + + + @Override + public String getSocialType() { + return this.socialType.name().toLowerCase(); + } + + private KakaoOAuth2ResponseDTO.Token getToken(String code) throws IOException { // 인가코드 토큰 가져오기 RestTemplate restTemplate = new RestTemplate(); HttpHeaders httpHeaders = new HttpHeaders(); @@ -49,24 +67,7 @@ protected String getAccessToken(String code) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); KakaoOAuth2ResponseDTO.Token oAuth2TokenDTO = null; - oAuth2TokenDTO = objectMapper.readValue(response1.getBody(), KakaoOAuth2ResponseDTO.Token.class); - return oAuth2TokenDTO.getAccess_token(); - } - - @Override - protected OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) throws IOException { - KakaoOAuth2ResponseDTO.KakaoProfile kakaoProfile = getKakaoProfile(token); - return OAuth2ResponseDTO.GetUserInfo.builder() - .email(kakaoProfile.getKakao_account().getEmail()) - .providerId(String.valueOf(kakaoProfile.getId())) - .socialType(SocialType.KAKAO) - .build(); - } - - - @Override - public String getSocialType() { - return this.socialType.name().toLowerCase(); + return objectMapper.readValue(response1.getBody(), KakaoOAuth2ResponseDTO.Token.class); } private KakaoOAuth2ResponseDTO.KakaoProfile getKakaoProfile(String token) throws IOException{ diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/KakaoOAuth2ResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/KakaoOAuth2ResponseDTO.java index 328d9df..e7b80f4 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/KakaoOAuth2ResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/KakaoOAuth2ResponseDTO.java @@ -2,51 +2,51 @@ import lombok.Getter; -public class KakaoOAuth2ResponseDTO { +public record KakaoOAuth2ResponseDTO () { - @Getter - public static class Token { - String token_type; - String access_token; - String refresh_token; - Long expires_in; - Long refresh_token_expires_in; - String scope; + public record Token ( + String token_type, + String access_token, + String refresh_token, + Long expires_in, + Long refresh_token_expires_in, + String scope + ) { } - @Getter - public static class KakaoProfile { - private Long id; - private String connected_at; - private Properties properties; - private KakaoAccount kakao_account; + public record KakaoProfile ( + Long id, + String connected_at, + Properties properties, + KakaoAccount kakao_account + ) { - @Getter - public static class Properties { - private String nickname; - private String profile_image; - private String thumbnail_image; + public record Properties ( + String nickname, + String profile_image, + String thumbnail_image + ) { } - @Getter - public static class KakaoAccount { - private String email; - private Boolean is_email_verified; - private Boolean email_needs_agreement; - private Boolean has_email; - private Boolean profile_nickname_needs_agreement; - private Boolean profile_image_needs_agreement; - private Boolean email_needs_argument; - private Boolean is_email_valid; - private Profile profile; + public record KakaoAccount ( + String email, + Boolean is_email_verified, + Boolean email_needs_agreement, + Boolean has_email, + Boolean profile_nickname_needs_agreement, + Boolean profile_image_needs_agreement, + Boolean email_needs_argument, + Boolean is_email_valid, + Profile profile + ) { - @Getter - public static class Profile { - private String nickname; - private String thumbnail_image_url; - private String profile_image_url; - private Boolean is_default_nickname; - private Boolean is_default_image; + public record Profile ( + String nickname, + String thumbnail_image_url, + String profile_image_url, + Boolean is_default_nickname, + Boolean is_default_image + ) { } } } diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index 353c6f3..dd61e4c 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -26,6 +26,25 @@ spring: auth: true starttls: enable: true + security: + oauth2: + client: + registration: + kakao: + authorization-grant-type: authorization_code + client-id: ${KAKAO_API_KEY} + redirect-uri: ${KAKAO_REDIRECT_URI} + client-authentication-method: client_secret_post + scope: + - profile_nickname + - profile_image + - account_email + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id jwt: secret: ${JWT_SECRET} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f630364..d824bdd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,6 +26,25 @@ spring: auth: true starttls: enable: true + security: + oauth2: + client: + registration: + kakao: + authorization-grant-type: authorization_code + client-id: ${KAKAO_API_KEY} + redirect-uri: ${KAKAO_REDIRECT_URI} + client-authentication-method: client_secret_post + scope: + - profile_nickname + - profile_image + - account_email + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id jwt: secret: ${JWT_SECRET} From 2327bac2ea717a0a6967c8b98729268d36a52a5d Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Wed, 16 Jul 2025 18:53:11 +0900 Subject: [PATCH 10/14] =?UTF-8?q?:sparkles:=20feat:=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B2=84=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/converter/OAuth2Converter.java | 7 ++ .../support/AbstractOAuth2UserLoader.java | 4 + .../auth/factory/support/NaverUserLoader.java | 95 +++++++++++++++++++ .../support/dto/NaverOAuth2ResponseDTO.java | 24 +++++ .../global/data/OAuth2ConfigData.java | 1 + src/main/resources/application-develop.yml | 16 +++- src/main/resources/application.yml | 16 +++- 7 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/NaverUserLoader.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/NaverOAuth2ResponseDTO.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java index fa3ab51..4e45c11 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java @@ -22,4 +22,11 @@ public static OAuth2ResponseDTO.GetUserInfo toGetUserInfo(KakaoOAuth2ResponseDTO .build(); } + public static OAuth2ResponseDTO.GetUserInfo toGetUserInfo(NaverOAuth2ResponseDTO.UserInfo.UserInfoData naver) { + return OAuth2ResponseDTO.GetUserInfo.builder() + .email(naver.email()) + .providerId(naver.id()) + .socialType(SocialType.NAVER) + .build(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java index 3f637b2..ede8b8f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java @@ -33,6 +33,10 @@ protected String getClientId() { return this.oAuth2ConfigData.getRegistration().get(this.getSocialType()).getClientId(); } + protected String getClientSecret() { + return this.oAuth2ConfigData.getRegistration().get(this.getSocialType()).getClientSecret(); + } + protected String getRedirectUri() { return this.oAuth2ConfigData.getRegistration().get(this.getSocialType()).getRedirectUri(); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/NaverUserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/NaverUserLoader.java new file mode 100644 index 0000000..5363c66 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/NaverUserLoader.java @@ -0,0 +1,95 @@ +package org.withtime.be.withtimebe.domain.auth.factory.support; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.withtime.be.withtimebe.domain.auth.converter.OAuth2Converter; +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.auth.factory.support.dto.NaverOAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.enums.SocialType; +import org.withtime.be.withtimebe.global.data.OAuth2ConfigData; + +import java.io.IOException; + +@Component +public class NaverUserLoader extends AbstractOAuth2UserLoader { + + private static final String AUTHORIZATION_TOKEN_PREFIX = "Bearer "; + + private final SocialType socialType = SocialType.NAVER; + + public NaverUserLoader(OAuth2ConfigData oAuth2ConfigData) { + super(oAuth2ConfigData); + } + + @Override + protected String getAccessToken(String code) throws IOException { + NaverOAuth2ResponseDTO.Token oAuth2TokenDTO = getToken(code); + return oAuth2TokenDTO.access_token(); + } + + @Override + protected OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) throws IOException { + NaverOAuth2ResponseDTO.UserInfo.UserInfoData userInfo = getNaverProfile(token); + return OAuth2Converter.toGetUserInfo(userInfo); + } + + @Override + public String getSocialType() { + return socialType.name().toLowerCase(); + } + + private NaverOAuth2ResponseDTO.Token getToken(String code) throws IOException { + // 인가코드 토큰 가져오기 + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + + httpHeaders.add("Content-Type", "application/x-www-form-urlencoded"); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("grant_type", "authorization_code"); + map.add("client_id", getClientId()); + map.add("client_secret", getClientSecret()); + map.add("code", code); + HttpEntity request = new HttpEntity<>(map, httpHeaders); + + ResponseEntity response1 = restTemplate.exchange( + getTokenUri(), + HttpMethod.POST, + request, + String.class); + + ObjectMapper objectMapper = new ObjectMapper(); + + return objectMapper.readValue(response1.getBody(), NaverOAuth2ResponseDTO.Token.class); + } + + private NaverOAuth2ResponseDTO.UserInfo.UserInfoData getNaverProfile(String token) throws IOException{ + // 토큰으로 정보 가져오기 + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + + httpHeaders.add("Authorization", AUTHORIZATION_TOKEN_PREFIX + token); + httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + + HttpEntity request1 = new HttpEntity<>(httpHeaders); + + ResponseEntity response2 = restTemplate.exchange( + getUserInfoUri(), + HttpMethod.GET, + request1, + String.class + ); + + ObjectMapper om = new ObjectMapper(); + + return om.readValue(response2.getBody(), NaverOAuth2ResponseDTO.UserInfo.class).response(); + } + +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/NaverOAuth2ResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/NaverOAuth2ResponseDTO.java new file mode 100644 index 0000000..9545438 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/NaverOAuth2ResponseDTO.java @@ -0,0 +1,24 @@ +package org.withtime.be.withtimebe.domain.auth.factory.support.dto; + +public record NaverOAuth2ResponseDTO() { + public record Token( + String access_token, + String refresh_token, + String token_type, + String expires_in + ) { + } + + public record UserInfo( + String resultcode, + String message, + UserInfoData response + ) { + public record UserInfoData( + String id, + String email + ) { + + } + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/data/OAuth2ConfigData.java b/src/main/java/org/withtime/be/withtimebe/global/data/OAuth2ConfigData.java index 8966056..81efa0c 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/data/OAuth2ConfigData.java +++ b/src/main/java/org/withtime/be/withtimebe/global/data/OAuth2ConfigData.java @@ -20,6 +20,7 @@ public class OAuth2ConfigData { @Setter public static class Registration { private String clientId; + private String clientSecret; private String redirectUri; private String authorizationGrantType; private String clientAuthenticationMethod; diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index dd61e4c..7bcd896 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -32,19 +32,29 @@ spring: registration: kakao: authorization-grant-type: authorization_code - client-id: ${KAKAO_API_KEY} + client-id: ${KAKAO_CLIENT_ID} redirect-uri: ${KAKAO_REDIRECT_URI} client-authentication-method: client_secret_post scope: - - profile_nickname - - profile_image - account_email + naver: + authorization-grant-type: authorization_code + redirect-uri: ${NAVER_REDIRECT_URI} + client-id: ${NAVER_CLIENT_ID} + client-secret: ${NAVER_SECRET} + scope: + - email provider: kakao: authorization-uri: https://kauth.kakao.com/oauth/authorize token-uri: https://kauth.kakao.com/oauth/token user-info-uri: https://kapi.kakao.com/v2/user/me user-name-attribute: id + naver: + authorization_uri: https://nid.naver.com/oauth2.0/authorize + token_uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + user_name_attribute: response jwt: secret: ${JWT_SECRET} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d824bdd..8b3d8d2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,19 +32,29 @@ spring: registration: kakao: authorization-grant-type: authorization_code - client-id: ${KAKAO_API_KEY} + client-id: ${KAKAO_CLIENT_ID} redirect-uri: ${KAKAO_REDIRECT_URI} client-authentication-method: client_secret_post scope: - - profile_nickname - - profile_image - account_email + naver: + authorization-grant-type: authorization_code + redirect-uri: ${NAVER_REDIRECT_URI} + client-id: ${NAVER_CLIENT_ID} + client-secret: ${NAVER_SECRET} + scope: + - email provider: kakao: authorization-uri: https://kauth.kakao.com/oauth/authorize token-uri: https://kauth.kakao.com/oauth/token user-info-uri: https://kapi.kakao.com/v2/user/me user-name-attribute: id + naver: + authorization_uri: https://nid.naver.com/oauth2.0/authorize + token_uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + user_name_attribute: response jwt: secret: ${JWT_SECRET} From 2a4a8f53e74e52e00f6f89816e04ae9fcd461ac6 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Thu, 17 Jul 2025 16:25:48 +0900 Subject: [PATCH 11/14] =?UTF-8?q?:sparkles:=20feat:=20=EA=B5=AC=EA=B8=80?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/converter/OAuth2Converter.java | 9 ++ .../support/AbstractOAuth2UserLoader.java | 8 ++ .../factory/support/GoogleUserLoader.java | 94 +++++++++++++++++++ .../auth/factory/support/KakaoUserLoader.java | 4 +- .../support/dto/GoogleOAuth2ResponseDTO.java | 23 +++++ src/main/resources/application-develop.yml | 16 +++- src/main/resources/application.yml | 16 +++- 7 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/GoogleUserLoader.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/GoogleOAuth2ResponseDTO.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java index 4e45c11..06e3ea7 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/converter/OAuth2Converter.java @@ -1,6 +1,7 @@ package org.withtime.be.withtimebe.domain.auth.converter; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.auth.factory.support.dto.GoogleOAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.auth.factory.support.dto.KakaoOAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.auth.factory.support.dto.NaverOAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.member.entity.enums.SocialType; @@ -29,4 +30,12 @@ public static OAuth2ResponseDTO.GetUserInfo toGetUserInfo(NaverOAuth2ResponseDTO .socialType(SocialType.NAVER) .build(); } + + public static OAuth2ResponseDTO.GetUserInfo toGetUserInfo(GoogleOAuth2ResponseDTO.UserInfo google) { + return OAuth2ResponseDTO.GetUserInfo.builder() + .email(google.email()) + .providerId(google.id()) + .socialType(SocialType.GOOGLE) + .build(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java index ede8b8f..724e051 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java @@ -1,8 +1,16 @@ package org.withtime.be.withtimebe.domain.auth.factory.support; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.auth.factory.OAuth2UserLoader; +import org.withtime.be.withtimebe.domain.auth.factory.support.dto.GoogleOAuth2ResponseDTO; import org.withtime.be.withtimebe.global.data.OAuth2ConfigData; import org.withtime.be.withtimebe.global.error.code.OAuthErrorCode; import org.withtime.be.withtimebe.global.error.exception.OAuthException; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/GoogleUserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/GoogleUserLoader.java new file mode 100644 index 0000000..d1eae64 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/GoogleUserLoader.java @@ -0,0 +1,94 @@ +package org.withtime.be.withtimebe.domain.auth.factory.support; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.withtime.be.withtimebe.domain.auth.converter.OAuth2Converter; +import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.auth.factory.support.dto.GoogleOAuth2ResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.enums.SocialType; +import org.withtime.be.withtimebe.global.data.OAuth2ConfigData; + +import java.io.IOException; + +@Component +public class GoogleUserLoader extends AbstractOAuth2UserLoader { + + private static final String AUTHORIZATION_TOKEN_PREFIX = "Bearer "; + private final SocialType socialType = SocialType.GOOGLE; + + public GoogleUserLoader(OAuth2ConfigData oAuth2ConfigData) { + super(oAuth2ConfigData); + } + + @Override + protected String getAccessToken(String code) throws IOException { + GoogleOAuth2ResponseDTO.Token token = getToken(code); + return token.access_token(); + } + + @Override + protected OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) throws IOException { + GoogleOAuth2ResponseDTO.UserInfo userInfo = getGoogleProfile(token); + return OAuth2Converter.toGetUserInfo(userInfo); + } + + @Override + public String getSocialType() { + return this.socialType.name().toLowerCase(); + } + + private GoogleOAuth2ResponseDTO.Token getToken(String code) throws IOException { + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + + httpHeaders.add("Content-Type", "application/x-www-form-urlencoded"); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("grant_type", "authorization_code"); + map.add("client_id", getClientId()); + map.add("client_secret", getClientSecret()); + map.add("redirect_uri", getRedirectUri()); + map.add("code", code); + HttpEntity request = new HttpEntity<>(map, httpHeaders); + + ResponseEntity response1 = restTemplate.exchange( + getTokenUri(), + HttpMethod.POST, + request, + String.class); + + ObjectMapper objectMapper = new ObjectMapper(); + + return objectMapper.readValue(response1.getBody(), GoogleOAuth2ResponseDTO.Token.class); + + } + + private GoogleOAuth2ResponseDTO.UserInfo getGoogleProfile(String token) throws IOException { + // 토큰으로 정보 가져오기 + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + + httpHeaders.add("Authorization", AUTHORIZATION_TOKEN_PREFIX + token); + httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + + HttpEntity request1 = new HttpEntity<>(httpHeaders); + + ResponseEntity response2 = restTemplate.exchange( + getUserInfoUri(), + HttpMethod.GET, + request1, + String.class + ); + + ObjectMapper om = new ObjectMapper(); + + return om.readValue(response2.getBody(), GoogleOAuth2ResponseDTO.UserInfo.class); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java index a1f63de..3bc233c 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java @@ -20,6 +20,7 @@ @Component public class KakaoUserLoader extends AbstractOAuth2UserLoader { + private static final String AUTHORIZATION_TOKEN_PREFIX = "Bearer "; private final SocialType socialType = SocialType.KAKAO; public KakaoUserLoader(OAuth2ConfigData oAuth2ConfigData) { @@ -65,7 +66,6 @@ private KakaoOAuth2ResponseDTO.Token getToken(String code) throws IOException { String.class); ObjectMapper objectMapper = new ObjectMapper(); - KakaoOAuth2ResponseDTO.Token oAuth2TokenDTO = null; return objectMapper.readValue(response1.getBody(), KakaoOAuth2ResponseDTO.Token.class); } @@ -75,7 +75,7 @@ private KakaoOAuth2ResponseDTO.KakaoProfile getKakaoProfile(String token) throws RestTemplate restTemplate = new RestTemplate(); HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add("Authorization", "Bearer " + token); + httpHeaders.add("Authorization", AUTHORIZATION_TOKEN_PREFIX + token); httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); HttpEntity request1 = new HttpEntity<>(httpHeaders); diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/GoogleOAuth2ResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/GoogleOAuth2ResponseDTO.java new file mode 100644 index 0000000..383adda --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/dto/GoogleOAuth2ResponseDTO.java @@ -0,0 +1,23 @@ +package org.withtime.be.withtimebe.domain.auth.factory.support.dto; + +public record GoogleOAuth2ResponseDTO() { + public record Token( + String access_token, + String refresh_token, + Long expires_in, + String token_type, + String scope, + String id_token + ) { + + } + + public record UserInfo( + String id, + String email, + Boolean verified_email, + String picture + ) { + + } +} diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index 7bcd896..064841c 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -41,7 +41,13 @@ spring: authorization-grant-type: authorization_code redirect-uri: ${NAVER_REDIRECT_URI} client-id: ${NAVER_CLIENT_ID} - client-secret: ${NAVER_SECRET} + client-secret: ${NAVER_CLIENT_SECRET} + scope: + - email + google: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + redirect-uri: ${GOOGLE_REDIRECT_URI} scope: - email provider: @@ -52,9 +58,13 @@ spring: user-name-attribute: id naver: authorization_uri: https://nid.naver.com/oauth2.0/authorize - token_uri: https://nid.naver.com/oauth2.0/token + token-uri: https://nid.naver.com/oauth2.0/token user-info-uri: https://openapi.naver.com/v1/nid/me - user_name_attribute: response + user-name-attribute: response + google: + authorization-uri: https://accounts.google.com/o/oauth2/v2/auth + token-uri: https://oauth2.googleapis.com/token + user-info-uri: https://www.googleapis.com/userinfo/v2/me jwt: secret: ${JWT_SECRET} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8b3d8d2..08bf185 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -41,7 +41,13 @@ spring: authorization-grant-type: authorization_code redirect-uri: ${NAVER_REDIRECT_URI} client-id: ${NAVER_CLIENT_ID} - client-secret: ${NAVER_SECRET} + client-secret: ${NAVER_CLIENT_SECRET} + scope: + - email + google: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + redirect-uri: ${GOOGLE_REDIRECT_URI} scope: - email provider: @@ -52,9 +58,13 @@ spring: user-name-attribute: id naver: authorization_uri: https://nid.naver.com/oauth2.0/authorize - token_uri: https://nid.naver.com/oauth2.0/token + token-uri: https://nid.naver.com/oauth2.0/token user-info-uri: https://openapi.naver.com/v1/nid/me - user_name_attribute: response + user-name-attribute: response + google: + authorization-uri: https://accounts.google.com/o/oauth2/v2/auth + token-uri: https://oauth2.googleapis.com/token + user-info-uri: https://www.googleapis.com/userinfo/v2/me jwt: secret: ${JWT_SECRET} From bde6eac8521e74c3e49ddf08542507bdf659457e Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 18 Jul 2025 14:40:53 +0900 Subject: [PATCH 12/14] =?UTF-8?q?:recycle:=20refactor:=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20OAuth2UserLoader=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/AbstractOAuth2UserLoader.java | 51 ++++++++++++++- .../factory/support/GoogleUserLoader.java | 63 ++---------------- .../auth/factory/support/KakaoUserLoader.java | 62 ++---------------- .../auth/factory/support/NaverUserLoader.java | 65 ++----------------- 4 files changed, 62 insertions(+), 179 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java index 724e051..430442d 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/AbstractOAuth2UserLoader.java @@ -6,16 +6,17 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.auth.factory.OAuth2UserLoader; -import org.withtime.be.withtimebe.domain.auth.factory.support.dto.GoogleOAuth2ResponseDTO; import org.withtime.be.withtimebe.global.data.OAuth2ConfigData; import org.withtime.be.withtimebe.global.error.code.OAuthErrorCode; import org.withtime.be.withtimebe.global.error.exception.OAuthException; import java.io.IOException; +import java.util.Optional; @RequiredArgsConstructor public abstract class AbstractOAuth2UserLoader implements OAuth2UserLoader { @@ -37,6 +38,54 @@ public OAuth2ResponseDTO.GetUserInfo loadUser(String code) { protected abstract OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) throws IOException; + protected T getToken(String code, Class clz) throws IOException { + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + + httpHeaders.add("Content-Type", "application/x-www-form-urlencoded"); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", getClientId()); + params.add("redirect_uri", getRedirectUri()); + params.add("code", code); + Optional.ofNullable(getClientSecret()).ifPresent(secret -> params.add("client_secret", secret)); + HttpEntity request = new HttpEntity<>(params, httpHeaders); + + ResponseEntity response = restTemplate.exchange( + getTokenUri(), + HttpMethod.POST, + request, + String.class); + + ObjectMapper objectMapper = new ObjectMapper(); + + return objectMapper.readValue(response.getBody(), clz); + + } + + protected T getProfile(String tokenPrefix, String token, Class clz) throws IOException { + // 토큰으로 정보 가져오기 + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + + httpHeaders.add("Authorization", tokenPrefix + token); + httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + + HttpEntity request1 = new HttpEntity<>(httpHeaders); + + ResponseEntity response = restTemplate.exchange( + getUserInfoUri(), + HttpMethod.GET, + request1, + String.class + ); + + ObjectMapper om = new ObjectMapper(); + + return om.readValue(response.getBody(), clz); + } + protected String getClientId() { return this.oAuth2ConfigData.getRegistration().get(this.getSocialType()).getClientId(); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/GoogleUserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/GoogleUserLoader.java index d1eae64..c6d71cd 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/GoogleUserLoader.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/GoogleUserLoader.java @@ -1,14 +1,6 @@ package org.withtime.be.withtimebe.domain.auth.factory.support; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestTemplate; import org.withtime.be.withtimebe.domain.auth.converter.OAuth2Converter; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.auth.factory.support.dto.GoogleOAuth2ResponseDTO; @@ -21,7 +13,7 @@ public class GoogleUserLoader extends AbstractOAuth2UserLoader { private static final String AUTHORIZATION_TOKEN_PREFIX = "Bearer "; - private final SocialType socialType = SocialType.GOOGLE; + private static final SocialType SOCIAL_TYPE = SocialType.GOOGLE; public GoogleUserLoader(OAuth2ConfigData oAuth2ConfigData) { super(oAuth2ConfigData); @@ -29,66 +21,19 @@ public GoogleUserLoader(OAuth2ConfigData oAuth2ConfigData) { @Override protected String getAccessToken(String code) throws IOException { - GoogleOAuth2ResponseDTO.Token token = getToken(code); + GoogleOAuth2ResponseDTO.Token token = super.getToken(code, GoogleOAuth2ResponseDTO.Token.class); return token.access_token(); } @Override protected OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) throws IOException { - GoogleOAuth2ResponseDTO.UserInfo userInfo = getGoogleProfile(token); + GoogleOAuth2ResponseDTO.UserInfo userInfo = super.getProfile(AUTHORIZATION_TOKEN_PREFIX, token, GoogleOAuth2ResponseDTO.UserInfo.class); return OAuth2Converter.toGetUserInfo(userInfo); } @Override public String getSocialType() { - return this.socialType.name().toLowerCase(); + return SOCIAL_TYPE.name().toLowerCase(); } - private GoogleOAuth2ResponseDTO.Token getToken(String code) throws IOException { - RestTemplate restTemplate = new RestTemplate(); - HttpHeaders httpHeaders = new HttpHeaders(); - - httpHeaders.add("Content-Type", "application/x-www-form-urlencoded"); - - MultiValueMap map = new LinkedMultiValueMap<>(); - map.add("grant_type", "authorization_code"); - map.add("client_id", getClientId()); - map.add("client_secret", getClientSecret()); - map.add("redirect_uri", getRedirectUri()); - map.add("code", code); - HttpEntity request = new HttpEntity<>(map, httpHeaders); - - ResponseEntity response1 = restTemplate.exchange( - getTokenUri(), - HttpMethod.POST, - request, - String.class); - - ObjectMapper objectMapper = new ObjectMapper(); - - return objectMapper.readValue(response1.getBody(), GoogleOAuth2ResponseDTO.Token.class); - - } - - private GoogleOAuth2ResponseDTO.UserInfo getGoogleProfile(String token) throws IOException { - // 토큰으로 정보 가져오기 - RestTemplate restTemplate = new RestTemplate(); - HttpHeaders httpHeaders = new HttpHeaders(); - - httpHeaders.add("Authorization", AUTHORIZATION_TOKEN_PREFIX + token); - httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); - - HttpEntity request1 = new HttpEntity<>(httpHeaders); - - ResponseEntity response2 = restTemplate.exchange( - getUserInfoUri(), - HttpMethod.GET, - request1, - String.class - ); - - ObjectMapper om = new ObjectMapper(); - - return om.readValue(response2.getBody(), GoogleOAuth2ResponseDTO.UserInfo.class); - } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java index 3bc233c..6d7d6af 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/KakaoUserLoader.java @@ -1,14 +1,6 @@ package org.withtime.be.withtimebe.domain.auth.factory.support; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestTemplate; import org.withtime.be.withtimebe.domain.auth.converter.OAuth2Converter; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.auth.factory.support.dto.KakaoOAuth2ResponseDTO; @@ -21,7 +13,7 @@ public class KakaoUserLoader extends AbstractOAuth2UserLoader { private static final String AUTHORIZATION_TOKEN_PREFIX = "Bearer "; - private final SocialType socialType = SocialType.KAKAO; + private static final SocialType SOCIAL_TYPE = SocialType.KAKAO; public KakaoUserLoader(OAuth2ConfigData oAuth2ConfigData) { super(oAuth2ConfigData); @@ -29,66 +21,20 @@ public KakaoUserLoader(OAuth2ConfigData oAuth2ConfigData) { @Override protected String getAccessToken(String code) throws IOException { - KakaoOAuth2ResponseDTO.Token oAuth2TokenDTO = getToken(code); + KakaoOAuth2ResponseDTO.Token oAuth2TokenDTO = getToken(code, KakaoOAuth2ResponseDTO.Token.class); return oAuth2TokenDTO.access_token(); } @Override protected OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) throws IOException { - KakaoOAuth2ResponseDTO.KakaoProfile kakaoProfile = getKakaoProfile(token); + KakaoOAuth2ResponseDTO.KakaoProfile kakaoProfile = super.getProfile(AUTHORIZATION_TOKEN_PREFIX, token, KakaoOAuth2ResponseDTO.KakaoProfile.class); return OAuth2Converter.toGetUserInfo(kakaoProfile); } @Override public String getSocialType() { - return this.socialType.name().toLowerCase(); + return SOCIAL_TYPE.name().toLowerCase(); } - private KakaoOAuth2ResponseDTO.Token getToken(String code) throws IOException { - // 인가코드 토큰 가져오기 - RestTemplate restTemplate = new RestTemplate(); - HttpHeaders httpHeaders = new HttpHeaders(); - - httpHeaders.add("Content-Type", "application/x-www-form-urlencoded"); - - MultiValueMap map = new LinkedMultiValueMap<>(); - map.add("grant_type", "authorization_code"); - map.add("client_id", getClientId()); - map.add("redirect_uri", getRedirectUri()); - map.add("code", code); - HttpEntity request = new HttpEntity<>(map, httpHeaders); - - ResponseEntity response1 = restTemplate.exchange( - getTokenUri(), - HttpMethod.POST, - request, - String.class); - - ObjectMapper objectMapper = new ObjectMapper(); - - return objectMapper.readValue(response1.getBody(), KakaoOAuth2ResponseDTO.Token.class); - } - - private KakaoOAuth2ResponseDTO.KakaoProfile getKakaoProfile(String token) throws IOException{ - // 토큰으로 정보 가져오기 - RestTemplate restTemplate = new RestTemplate(); - HttpHeaders httpHeaders = new HttpHeaders(); - - httpHeaders.add("Authorization", AUTHORIZATION_TOKEN_PREFIX + token); - httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); - - HttpEntity request1 = new HttpEntity<>(httpHeaders); - - ResponseEntity response2 = restTemplate.exchange( - getUserInfoUri(), - HttpMethod.GET, - request1, - String.class - ); - - ObjectMapper om = new ObjectMapper(); - - return om.readValue(response2.getBody(), KakaoOAuth2ResponseDTO.KakaoProfile.class); - } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/NaverUserLoader.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/NaverUserLoader.java index 5363c66..893a33a 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/NaverUserLoader.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/factory/support/NaverUserLoader.java @@ -1,14 +1,6 @@ package org.withtime.be.withtimebe.domain.auth.factory.support; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestTemplate; import org.withtime.be.withtimebe.domain.auth.converter.OAuth2Converter; import org.withtime.be.withtimebe.domain.auth.dto.response.OAuth2ResponseDTO; import org.withtime.be.withtimebe.domain.auth.factory.support.dto.NaverOAuth2ResponseDTO; @@ -21,8 +13,7 @@ public class NaverUserLoader extends AbstractOAuth2UserLoader { private static final String AUTHORIZATION_TOKEN_PREFIX = "Bearer "; - - private final SocialType socialType = SocialType.NAVER; + private static final SocialType SOCIAL_TYPE = SocialType.NAVER; public NaverUserLoader(OAuth2ConfigData oAuth2ConfigData) { super(oAuth2ConfigData); @@ -30,66 +21,18 @@ public NaverUserLoader(OAuth2ConfigData oAuth2ConfigData) { @Override protected String getAccessToken(String code) throws IOException { - NaverOAuth2ResponseDTO.Token oAuth2TokenDTO = getToken(code); + NaverOAuth2ResponseDTO.Token oAuth2TokenDTO = getToken(code, NaverOAuth2ResponseDTO.Token.class); return oAuth2TokenDTO.access_token(); } @Override protected OAuth2ResponseDTO.GetUserInfo getUserInfo(String token) throws IOException { - NaverOAuth2ResponseDTO.UserInfo.UserInfoData userInfo = getNaverProfile(token); + NaverOAuth2ResponseDTO.UserInfo.UserInfoData userInfo = super.getProfile(AUTHORIZATION_TOKEN_PREFIX, token, NaverOAuth2ResponseDTO.UserInfo.class).response(); return OAuth2Converter.toGetUserInfo(userInfo); } @Override public String getSocialType() { - return socialType.name().toLowerCase(); - } - - private NaverOAuth2ResponseDTO.Token getToken(String code) throws IOException { - // 인가코드 토큰 가져오기 - RestTemplate restTemplate = new RestTemplate(); - HttpHeaders httpHeaders = new HttpHeaders(); - - httpHeaders.add("Content-Type", "application/x-www-form-urlencoded"); - - MultiValueMap map = new LinkedMultiValueMap<>(); - map.add("grant_type", "authorization_code"); - map.add("client_id", getClientId()); - map.add("client_secret", getClientSecret()); - map.add("code", code); - HttpEntity request = new HttpEntity<>(map, httpHeaders); - - ResponseEntity response1 = restTemplate.exchange( - getTokenUri(), - HttpMethod.POST, - request, - String.class); - - ObjectMapper objectMapper = new ObjectMapper(); - - return objectMapper.readValue(response1.getBody(), NaverOAuth2ResponseDTO.Token.class); + return SOCIAL_TYPE.name().toLowerCase(); } - - private NaverOAuth2ResponseDTO.UserInfo.UserInfoData getNaverProfile(String token) throws IOException{ - // 토큰으로 정보 가져오기 - RestTemplate restTemplate = new RestTemplate(); - HttpHeaders httpHeaders = new HttpHeaders(); - - httpHeaders.add("Authorization", AUTHORIZATION_TOKEN_PREFIX + token); - httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); - - HttpEntity request1 = new HttpEntity<>(httpHeaders); - - ResponseEntity response2 = restTemplate.exchange( - getUserInfoUri(), - HttpMethod.GET, - request1, - String.class - ); - - ObjectMapper om = new ObjectMapper(); - - return om.readValue(response2.getBody(), NaverOAuth2ResponseDTO.UserInfo.class).response(); - } - } From 8860e830cdad8cd796a66e3a92d35d9d5c08fbe6 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 18 Jul 2025 15:04:24 +0900 Subject: [PATCH 13/14] =?UTF-8?q?:memo:=20docs:=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=20=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/OAuth2Controller.java | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java index 141c8a7..932cbe1 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java @@ -1,5 +1,10 @@ package org.withtime.be.withtimebe.domain.auth.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -11,12 +16,40 @@ @RestController @RequestMapping("/api/v1/oauth2") @RequiredArgsConstructor +@Tag(name = "소셜 로그인 API") public class OAuth2Controller { private final OAuth2CommandService oAuth2CommandService; + @Operation(summary = "소셜 로그인 API", description = "/oauth2/authorization/{provider}로 서버에 요청을 보낸 뒤 리다이렉트된 URI의 코드를 사용하여 요청, 리다이렉트되는 URI 의 Endpoint는 해당 API와 동일합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", + description = """ + 소셜 로그인 성공 + - isFirst: true 시 최초 회원가입 필요, 이메일은 인증된 상태로 1시간 유효 + - isFirst: false 시 최초 회원가입 필요 X, 로그인 처리 + """ + ), + @ApiResponse( + responseCode = "400", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - AUTH400_1: 지원하지 않는 소셜 로그인입니다. provider가 잘못되었거나 지원하지 않는 provider입니다. + """ + ), + @ApiResponse( + responseCode = "500", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - AUTH500_1: 사용자 정보를 가져오는데 실패했습니다. 인가코드가 잘못되었거나 OAuth2 인증 서버나 리소스 서버에 보낸 요청이 실패했습니다. + """ + ), + + }) @GetMapping("/callback/{provider}") - public DefaultResponse loginWithOAuth2(HttpServletRequest request, HttpServletResponse response, @PathVariable String provider, @RequestParam String code) { + public DefaultResponse loginWithOAuth2(HttpServletRequest request, HttpServletResponse response, + @Parameter(description = "소셜 로그인 플랫폼(대소문자 상관 없음), [kakao, google, naver]", example = "kakao") @PathVariable String provider, + @RequestParam String code) { return DefaultResponse.ok(oAuth2CommandService.login(request, response, provider, code)); } } From af38451b83e29c316256c1a5f48493b97cf8c574 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 18 Jul 2025 16:10:42 +0900 Subject: [PATCH 14/14] =?UTF-8?q?:memo:=20docs:=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=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 --- .../be/withtimebe/domain/auth/controller/OAuth2Controller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java index 932cbe1..ff11b3d 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/OAuth2Controller.java @@ -21,7 +21,7 @@ public class OAuth2Controller { private final OAuth2CommandService oAuth2CommandService; - @Operation(summary = "소셜 로그인 API", description = "/oauth2/authorization/{provider}로 서버에 요청을 보낸 뒤 리다이렉트된 URI의 코드를 사용하여 요청, 리다이렉트되는 URI 의 Endpoint는 해당 API와 동일합니다.") + @Operation(summary = "소셜 로그인 API by 요시", description = "/oauth2/authorization/{provider}로 서버에 요청을 보낸 뒤 리다이렉트된 URI의 코드를 사용하여 요청, 리다이렉트되는 URI 의 Endpoint는 해당 API와 동일합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = """