From f6d4e8d64d9e17fcc0430bdc9788617254c10664 Mon Sep 17 00:00:00 2001 From: Kang Dong Hyeon Date: Thu, 19 Jun 2025 15:27:45 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat=20:=20=EB=84=A4=EC=9D=B4=EB=B2=84=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20OIDC=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 네이버 소셜 로그인을 시스템에 통합하여 사용자의 접근성을 높였습니다. - `NaverUser`와 `OAuth2NaverProviderUserConverter`를 구현하여 네이버 계정 연동을 처리합니다. - 네이버 API의 중첩된 응답(`response`) 구조를 파싱하기 위해 `OAuth2Utils`에 유틸리티 메서드를 추가했습니다. 더불어, 최신 인증 프로토콜을 지원하기 위해 OIDC(OpenID Connect) 설정을 추가했습니다. - `SecurityConfig`의 `oauth2Login` 설정에 `CustomOidcUserService`를 등록하여 OIDC 기반 인증을 활성화했습니다. 회원가입 로직을 개선하여 사용자 경험과 데이터 무결성을 향상했습니다. - 기존에 이메일과 사용자명을 별도로 검사하던 로직을 `findByUsernameAndEmail` 쿼리로 통합하여 중복 검사 효율을 높였습니다. --- .../config/SecurityConfig.java | 7 ++++- .../DelegatingProviderUserConverter.java | 4 ++- .../OAuth2NaverProviderUserConverter.java | 22 +++++++++++++++ .../domain/enums/OAuth2Config.java | 4 ++- .../domain/socials/NaverUser.java | 28 +++++++++++++++++++ .../exception/ExceptionType.java | 1 + .../repository/MemberRepository.java | 3 ++ .../service/AccountService.java | 7 ++--- .../account_service/util/OAuth2Utils.java | 9 ++++++ .../src/main/resources/application-local.yml | 2 +- .../src/main/resources/application.yml | 1 + 11 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 account-service/src/main/java/com/synapse/account_service/convert/OAuth2NaverProviderUserConverter.java create mode 100644 account-service/src/main/java/com/synapse/account_service/domain/socials/NaverUser.java diff --git a/account-service/src/main/java/com/synapse/account_service/config/SecurityConfig.java b/account-service/src/main/java/com/synapse/account_service/config/SecurityConfig.java index dbadb18..a75c7d9 100644 --- a/account-service/src/main/java/com/synapse/account_service/config/SecurityConfig.java +++ b/account-service/src/main/java/com/synapse/account_service/config/SecurityConfig.java @@ -18,6 +18,7 @@ import com.synapse.account_service.convert.authority.CustomAuthorityMapper; import com.synapse.account_service.filter.JwtAuthenticationFilter; import com.synapse.account_service.service.CustomOAuth2UserService; +import com.synapse.account_service.service.CustomOidcUserService; import com.synapse.account_service.service.CustomUserDetailsService; import com.synapse.account_service.service.handler.LoginFailureHandler; import com.synapse.account_service.service.handler.LoginSuccessHandler; @@ -32,6 +33,7 @@ public class SecurityConfig { private final LoginSuccessHandler loginSuccessHandler; private final LoginFailureHandler loginFailureHandler; private final CustomOAuth2UserService customOAuth2UserService; + private final CustomOidcUserService customOidcUserService; private final ObjectMapper objectMapper; private final PasswordEncoder passwordEncoder; @@ -48,7 +50,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticat .addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .oauth2Login(oauth2 -> oauth2 - .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService) + .oidcUserService(customOidcUserService) + ) .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler) ) diff --git a/account-service/src/main/java/com/synapse/account_service/convert/DelegatingProviderUserConverter.java b/account-service/src/main/java/com/synapse/account_service/convert/DelegatingProviderUserConverter.java index b8e63fe..163a8b6 100644 --- a/account-service/src/main/java/com/synapse/account_service/convert/DelegatingProviderUserConverter.java +++ b/account-service/src/main/java/com/synapse/account_service/convert/DelegatingProviderUserConverter.java @@ -22,7 +22,9 @@ public DelegatingProviderUserConverter() { new UserDetailsProviderUserConverter(), new OAuth2GoogleProviderUserConverter(), new OAuth2KakaoProviderUserConverter(), - new OAuth2KakaoOidcProviderUserConverter()); + new OAuth2KakaoOidcProviderUserConverter(), + new OAuth2NaverProviderUserConverter() + ); this.converters = Collections.unmodifiableList(new LinkedList<>(providerUserConverters)); } diff --git a/account-service/src/main/java/com/synapse/account_service/convert/OAuth2NaverProviderUserConverter.java b/account-service/src/main/java/com/synapse/account_service/convert/OAuth2NaverProviderUserConverter.java new file mode 100644 index 0000000..11945a4 --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/convert/OAuth2NaverProviderUserConverter.java @@ -0,0 +1,22 @@ +package com.synapse.account_service.convert; + +import com.synapse.account_service.domain.ProviderUser; +import com.synapse.account_service.domain.enums.OAuth2Config; +import com.synapse.account_service.domain.socials.NaverUser; +import com.synapse.account_service.util.OAuth2Utils; + +public final class OAuth2NaverProviderUserConverter implements ProviderUserConverter { + + @Override + public ProviderUser convert(ProviderUserRequest providerUserRequest) { + + if (!providerUserRequest.clientRegistration().getRegistrationId().equals(OAuth2Config.SocialType.NAVER.getSocialName())) { + return null; + } + + return new NaverUser(OAuth2Utils.getSubAttributes( + providerUserRequest.oAuth2User(), "response"), + providerUserRequest.oAuth2User(), + providerUserRequest.clientRegistration()); + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/domain/enums/OAuth2Config.java b/account-service/src/main/java/com/synapse/account_service/domain/enums/OAuth2Config.java index 737297b..8947651 100644 --- a/account-service/src/main/java/com/synapse/account_service/domain/enums/OAuth2Config.java +++ b/account-service/src/main/java/com/synapse/account_service/domain/enums/OAuth2Config.java @@ -3,7 +3,9 @@ public class OAuth2Config { public enum SocialType { GOOGLE("google"), - KAKAO("kakao"); + KAKAO("kakao"), + NAVER("naver") + ; private final String socialName; diff --git a/account-service/src/main/java/com/synapse/account_service/domain/socials/NaverUser.java b/account-service/src/main/java/com/synapse/account_service/domain/socials/NaverUser.java new file mode 100644 index 0000000..be6b816 --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/domain/socials/NaverUser.java @@ -0,0 +1,28 @@ +package com.synapse.account_service.domain.socials; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import com.synapse.account_service.domain.Attributes; + +public class NaverUser extends OAuth2ProviderUser { + + public NaverUser(Attributes attributes, OAuth2User oAuth2User, ClientRegistration clientRegistration) { + super(attributes.getSubAttributes(), oAuth2User, clientRegistration); + } + + @Override + public String getId() { + return (String) getAttributes().get("id"); + } + + @Override + public String getUsername() { + return (String) getAttributes().get("name"); + } + + @Override + public String getPicture() { + return (String) getAttributes().get("profile_image"); + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/exception/ExceptionType.java b/account-service/src/main/java/com/synapse/account_service/exception/ExceptionType.java index 3c5fa8c..f24b6c9 100644 --- a/account-service/src/main/java/com/synapse/account_service/exception/ExceptionType.java +++ b/account-service/src/main/java/com/synapse/account_service/exception/ExceptionType.java @@ -18,6 +18,7 @@ public enum ExceptionType { DUPLICATED_USERNAME(CONFLICT, "002", "이미 존재하는 사용자 이름입니다."), EXCEPTION(INTERNAL_SERVER_ERROR, "003", "예상치 못한 오류가 발생했습니다."), NOT_FOUND_MEMBER(NOT_FOUND, "004", "존재하지 않는 사용자입니다."), + DUPLICATED_USERNAME_AND_EMAIL(CONFLICT, "005", "이미 존재하는 사용자 이름과 이메일입니다."), INVALID_TOKEN(UNAUTHORIZED, "005", "유효하지 않은 토큰입니다."), EXPIRED_TOKEN(UNAUTHORIZED, "006", "만료된 토큰입니다."), diff --git a/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java b/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java index ced555f..0a197a7 100644 --- a/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java +++ b/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java @@ -14,6 +14,9 @@ public interface MemberRepository extends JpaRepository { Optional findByUsername(String username); Optional findByProviderAndRegistrationId(String provider, String registrationId); + @Query("SELECT m FROM Member m WHERE m.username = :username AND m.email = :email") + Optional findByUsernameAndEmail(@Param("username") String username, @Param("email") String email); + @Query("SELECT m FROM Member m WHERE (m.provider = :provider AND m.registrationId = :registrationId) OR m.email = :email OR m.username = :username") Optional findBySocialIdOrEmailOrUsername(@Param("provider") String provider, @Param("registrationId") String registrationId, @Param("email") String email, @Param("username") String username); } diff --git a/account-service/src/main/java/com/synapse/account_service/service/AccountService.java b/account-service/src/main/java/com/synapse/account_service/service/AccountService.java index f48173e..1cdf086 100644 --- a/account-service/src/main/java/com/synapse/account_service/service/AccountService.java +++ b/account-service/src/main/java/com/synapse/account_service/service/AccountService.java @@ -48,11 +48,8 @@ public SignUpResponse registerMember(SignUpRequest request) { private Member createAndSaveNewMember(String email, String username, String password, String provider, String registrationId) { // 중복 검사 - memberRepository.findByEmail(email).ifPresent(m -> { - throw new DuplicatedException(ExceptionType.DUPLICATED_EMAIL); - }); - memberRepository.findByUsername(username).ifPresent(m -> { - throw new DuplicatedException(ExceptionType.DUPLICATED_USERNAME); + memberRepository.findByUsernameAndEmail(username, email).ifPresent(m -> { + throw new DuplicatedException(ExceptionType.DUPLICATED_USERNAME_AND_EMAIL); }); Member member = Member.builder() diff --git a/account-service/src/main/java/com/synapse/account_service/util/OAuth2Utils.java b/account-service/src/main/java/com/synapse/account_service/util/OAuth2Utils.java index f1de400..377029f 100644 --- a/account-service/src/main/java/com/synapse/account_service/util/OAuth2Utils.java +++ b/account-service/src/main/java/com/synapse/account_service/util/OAuth2Utils.java @@ -15,6 +15,15 @@ public static Attributes getMainAttributes(OAuth2User oAuth2User) { .build(); } + @SuppressWarnings("unchecked") + public static Attributes getSubAttributes(OAuth2User oAuth2User, String subAttributesKey) { + + Map subAttributes = (Map) oAuth2User.getAttributes().get(subAttributesKey); + return Attributes.builder() + .subAttributes(subAttributes) + .build(); + } + @SuppressWarnings("unchecked") public static Attributes getOtherAttributes(OAuth2User oAuth2User, String subAttributesKey, String otherAttributesKey) { diff --git a/account-service/src/main/resources/application-local.yml b/account-service/src/main/resources/application-local.yml index bd9cfe1..0622468 100644 --- a/account-service/src/main/resources/application-local.yml +++ b/account-service/src/main/resources/application-local.yml @@ -13,7 +13,7 @@ spring: highlight: sql: true hbm2ddl: - auto: create-drop + auto: create dialect: org.hibernate.dialect.PostgreSQLDialect open-in-view: false show-sql: true diff --git a/account-service/src/main/resources/application.yml b/account-service/src/main/resources/application.yml index 09b442c..c9a94eb 100644 --- a/account-service/src/main/resources/application.yml +++ b/account-service/src/main/resources/application.yml @@ -15,3 +15,4 @@ spring: import: - security/application-db.yml - security/application-jwt.yml + - security/application-oauth2.yml From ce670e300d37280e2544a9fd2c09b510ee147b2d Mon Sep 17 00:00:00 2001 From: Kang Dong Hyeon Date: Thu, 19 Jun 2025 15:30:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix=20:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD=20=EC=82=AC=ED=95=AD?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=B4=EC=84=9C=20test=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account_service/service/AccountServiceTest.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java b/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java index 8bf5c7e..4ef6bee 100644 --- a/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java +++ b/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java @@ -41,8 +41,7 @@ void signUp_success() { // given: 테스트 준비 SignUpRequest request = new SignUpRequest("test@example.com", "테스트유저", "password123"); - given(memberRepository.findByEmail(anyString())).willReturn(Optional.empty()); - given(memberRepository.findByUsername(anyString())).willReturn(Optional.empty()); + given(memberRepository.findByUsernameAndEmail(anyString(), anyString())).willReturn(Optional.empty()); given(passwordEncoder.encode(anyString())).willReturn("encodedPassword"); given(memberRepository.save(any(Member.class))).willAnswer(invocation -> { Member memberToSave = invocation.getArgument(0); @@ -61,15 +60,15 @@ void signUp_success() { } @Test - @DisplayName("이메일 중복으로 회원가입 실패") - void signUp_fail_withDuplicateEmail() { + @DisplayName("이메일 또는 사용자명 중복으로 회원가입 실패") + void signUp_fail_withDuplicateUsernameAndEmail() { // given SignUpRequest request = new SignUpRequest("test@example.com", "테스트유저", "password123"); - // memberRepository.findByEmail이 호출되면, 이미 존재하는 Member 객체를 반환하도록 설정 - given(memberRepository.findByEmail(anyString())).willReturn(Optional.of(Member.builder().build())); + // memberRepository.findByUsernameAndEmail이 호출되면, 이미 존재하는 Member 객체를 반환하도록 설정 + given(memberRepository.findByUsernameAndEmail(anyString(), anyString())).willReturn(Optional.of(Member.builder().build())); - // when & then: BusinessException이 발생하는지 검증 + // when & then: DuplicatedException이 발생하는지 검증 assertThrows(DuplicatedException.class, () -> { accountService.registerMember(request); });