From 7e2a8903add5b5bc62c3acdd2b1fad96f7871570 Mon Sep 17 00:00:00 2001 From: dogsub Date: Sun, 19 Jan 2025 03:28:53 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20#17=20oauth2=20=EA=B4=80=EB=A0=A8=5FCu?= =?UTF-8?q?stomOAuth2User,=20OAuthAttributes,=20Service,=20userInfo=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../weditapp/domain/member/domain/Member.java | 2 +- .../weditapp/global/error/ErrorCode.java | 1 + .../oauth2/domain/CustomOAuth2User.java | 26 ++++++ .../global/oauth2/dto/OAuthAttributes.java | 43 +++++++++ .../service/CustomOAuth2UserService.java | 93 +++++++++++++++++++ .../oauth2/userInfo/KakaoOAuth2UserInfo.java | 34 +++++++ .../oauth2/userInfo/OAuth2UserInfo.java | 19 ++++ 7 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/wedit/weditapp/global/oauth2/domain/CustomOAuth2User.java create mode 100644 src/main/java/com/wedit/weditapp/global/oauth2/dto/OAuthAttributes.java create mode 100644 src/main/java/com/wedit/weditapp/global/oauth2/service/CustomOAuth2UserService.java create mode 100644 src/main/java/com/wedit/weditapp/global/oauth2/userInfo/KakaoOAuth2UserInfo.java create mode 100644 src/main/java/com/wedit/weditapp/global/oauth2/userInfo/OAuth2UserInfo.java diff --git a/src/main/java/com/wedit/weditapp/domain/member/domain/Member.java b/src/main/java/com/wedit/weditapp/domain/member/domain/Member.java index 5932bb4..382013d 100644 --- a/src/main/java/com/wedit/weditapp/domain/member/domain/Member.java +++ b/src/main/java/com/wedit/weditapp/domain/member/domain/Member.java @@ -26,7 +26,7 @@ public class Member extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(nullable = false, unique = true) private String email; @Column(nullable = false) diff --git a/src/main/java/com/wedit/weditapp/global/error/ErrorCode.java b/src/main/java/com/wedit/weditapp/global/error/ErrorCode.java index 91acb4b..072b3ff 100644 --- a/src/main/java/com/wedit/weditapp/global/error/ErrorCode.java +++ b/src/main/java/com/wedit/weditapp/global/error/ErrorCode.java @@ -28,6 +28,7 @@ public enum ErrorCode { EXPIRED_JWT_TOKEN(401, "EXPIRED_JWT_TOKEN", "만료된 JWT 토큰입니다."), UNSUPPORTED_JWT_TOKEN(401, "UNSUPPORTED_JWT_TOKEN", "지원되지 않는 JWT 토큰입니다."), ILLEGAL_JWT(400, "ILLEGAL_JWT", "JWT 토큰 핸들러 컴팩트 오류입니다."), + EMAIL_NOT_PROVIDED(400, "EMAIL_NOT_PROVIDED", "이메일이 제공되지 않았습니다."), //작업 DELETE_FORBIDDEN(403, "DELETE_FORBIDDEN", "삭제 권한이 없습니다."); diff --git a/src/main/java/com/wedit/weditapp/global/oauth2/domain/CustomOAuth2User.java b/src/main/java/com/wedit/weditapp/global/oauth2/domain/CustomOAuth2User.java new file mode 100644 index 0000000..58fb239 --- /dev/null +++ b/src/main/java/com/wedit/weditapp/global/oauth2/domain/CustomOAuth2User.java @@ -0,0 +1,26 @@ +package com.wedit.weditapp.global.oauth2.domain; + +import com.wedit.weditapp.domain.shared.MemberRole; +import com.wedit.weditapp.domain.shared.MemberStatus; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + +import javax.management.relation.Role; +import java.util.Collection; +import java.util.Map; + +@Getter +public class CustomOAuth2User extends DefaultOAuth2User { + private String email; + private MemberRole role; + private MemberStatus status; + public CustomOAuth2User(Collection authorities, + Map attributes, String nameAttributeKey, + String email, MemberRole role, MemberStatus status) { + super(authorities, attributes, nameAttributeKey); + this.email = email; + this.role = role; + this.status = status; + } +} diff --git a/src/main/java/com/wedit/weditapp/global/oauth2/dto/OAuthAttributes.java b/src/main/java/com/wedit/weditapp/global/oauth2/dto/OAuthAttributes.java new file mode 100644 index 0000000..4ba1e23 --- /dev/null +++ b/src/main/java/com/wedit/weditapp/global/oauth2/dto/OAuthAttributes.java @@ -0,0 +1,43 @@ +package com.wedit.weditapp.global.oauth2.dto; + +import com.wedit.weditapp.global.oauth2.userInfo.KakaoOAuth2UserInfo; +import com.wedit.weditapp.global.oauth2.userInfo.OAuth2UserInfo; +import lombok.Builder; +import lombok.Getter; +import org.springframework.security.core.userdetails.User; + +import java.util.Map; +import java.util.UUID; + +@Getter +public class OAuthAttributes { + + private String nameAttributeKey; // OAuth2 로그인 진행 시 키가 되는 필드 값, PK와 같은 의미 + private OAuth2UserInfo oauth2UserInfo; // 소셜 타입별 로그인 유저 정보(닉네임, 이메일, 프로필 사진 등등) + + @Builder + private OAuthAttributes(String nameAttributeKey, OAuth2UserInfo oauth2UserInfo) { + this.nameAttributeKey = nameAttributeKey; + this.oauth2UserInfo = oauth2UserInfo; + } + + public static OAuthAttributes ofKakao(String userNameAttributeName, Map attributes) { + return OAuthAttributes.builder() + .nameAttributeKey(userNameAttributeName) + .oauth2UserInfo(new KakaoOAuth2UserInfo(attributes)) + .build(); + } + + // 이건 나중에 서비스나 엔티티 생성 로직에서 처리하는 게 더 나음 +// public User toEntity(SocialType socialType, OAuth2UserInfo oauth2UserInfo) { +// return User.builder() +// .socialType(socialType) +// .socialId(oauth2UserInfo.getId()) +// .email(UUID.randomUUID() + "@socialUser.com") +// .nickname(oauth2UserInfo.getNickname()) +// .imageUrl(oauth2UserInfo.getImageUrl()) +// .role(Role.GUEST) +// .build(); +// } + +} diff --git a/src/main/java/com/wedit/weditapp/global/oauth2/service/CustomOAuth2UserService.java b/src/main/java/com/wedit/weditapp/global/oauth2/service/CustomOAuth2UserService.java new file mode 100644 index 0000000..72173d6 --- /dev/null +++ b/src/main/java/com/wedit/weditapp/global/oauth2/service/CustomOAuth2UserService.java @@ -0,0 +1,93 @@ +package com.wedit.weditapp.global.oauth2.service; + +import com.wedit.weditapp.domain.member.domain.Member; +import com.wedit.weditapp.domain.member.domain.repository.MemberRepository; +import com.wedit.weditapp.global.error.ErrorCode; +import com.wedit.weditapp.global.error.exception.CommonException; +import com.wedit.weditapp.global.oauth2.domain.CustomOAuth2User; +import com.wedit.weditapp.global.oauth2.dto.OAuthAttributes; +import com.wedit.weditapp.global.oauth2.userInfo.KakaoOAuth2UserInfo; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService implements OAuth2UserService { + + private final MemberRepository memberRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + log.info("CustomOAuth2UserService.loadUser() - 카카오 OAuth2 로그인 요청 진입"); + + // 1. DefaultOAuth2UserService를 사용해 카카오에서 사용자 정보(attributes)를 가져옴 + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(userRequest); + + // 2. registrationId, userNameAttributeName 추출 (카카오인 경우) + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + String userNameAttributeName = userRequest + .getClientRegistration().getProviderDetails() + .getUserInfoEndpoint().getUserNameAttributeName(); + + // 3. 카카오 사용자 정보(attributes) 가져오기 + Map attributes = oAuth2User.getAttributes(); + + // 카카오 전용 OAuthAttributes 생성 (ofKakao) + OAuthAttributes extractAttributes = + OAuthAttributes.ofKakao(userNameAttributeName, attributes); + + // 4. DB에서 Member 조회 or 생성 + Member member = getOrSaveMember(extractAttributes); + + // 5. DefaultOAuth2User를 상속한 CustomOAuth2User 반환 + USER로 설정 + return new CustomOAuth2User( + Collections.singleton(new SimpleGrantedAuthority("ROLE_" + member.getRole().name())), + attributes, + extractAttributes.getNameAttributeKey(), + member.getEmail(), + member.getRole(), + member.getStatus() + ); + } + + // DB에서 카카오 소셜 회원 식별자를 통해 Member 조회 + // 없으면 새로 생성 후 저장 + private Member getOrSaveMember(OAuthAttributes attributes) { + KakaoOAuth2UserInfo kakaoInfo = (KakaoOAuth2UserInfo) attributes.getOauth2UserInfo(); + + String nickname = validateOrSetDefaultNickname(kakaoInfo.getNickname()); + String email = validateOrThrowEmail(kakaoInfo.getEmail()); + + return memberRepository.findByEmail(email).orElseGet(() -> saveNewMember(email, nickname)); + } + + // 닉네임 검증 메서드 + private String validateOrSetDefaultNickname(String nickname) { + return (nickname == null || nickname.isEmpty()) ? "카카오사용자" : nickname; + } + + // 이메일 검증 메서드 + private String validateOrThrowEmail(String email) { + if (email == null || email.isEmpty()) { + throw new CommonException(ErrorCode.EMAIL_NOT_PROVIDED); + } + return email; + } + + // 새 멤버 저장 메서드 + private Member saveNewMember(String email, String nickname) { + return memberRepository.save(Member.createUser(email, nickname)); + } +} diff --git a/src/main/java/com/wedit/weditapp/global/oauth2/userInfo/KakaoOAuth2UserInfo.java b/src/main/java/com/wedit/weditapp/global/oauth2/userInfo/KakaoOAuth2UserInfo.java new file mode 100644 index 0000000..4298b15 --- /dev/null +++ b/src/main/java/com/wedit/weditapp/global/oauth2/userInfo/KakaoOAuth2UserInfo.java @@ -0,0 +1,34 @@ +package com.wedit.weditapp.global.oauth2.userInfo; + +import java.util.Map; + +public class KakaoOAuth2UserInfo extends OAuth2UserInfo { + + public KakaoOAuth2UserInfo(Map attributes) { + super(attributes); + } + + @Override + public String getId() { + return String.valueOf(attributes.get("id")); + } + + @Override + public String getNickname() { + Map account = (Map) attributes.get("kakao_account"); + if (account == null) return null; + + Map profile = (Map) account.get("profile"); + if (profile == null) return null; + + return (String) profile.get("nickname"); + } + + @Override + public String getEmail() { + Map account = (Map) attributes.get("kakao_account"); + if (account == null) return null; + + return (String) account.get("email"); + } +} diff --git a/src/main/java/com/wedit/weditapp/global/oauth2/userInfo/OAuth2UserInfo.java b/src/main/java/com/wedit/weditapp/global/oauth2/userInfo/OAuth2UserInfo.java new file mode 100644 index 0000000..258ff07 --- /dev/null +++ b/src/main/java/com/wedit/weditapp/global/oauth2/userInfo/OAuth2UserInfo.java @@ -0,0 +1,19 @@ +package com.wedit.weditapp.global.oauth2.userInfo; + +import java.util.Map; + +public abstract class OAuth2UserInfo { + + protected Map attributes; + + public OAuth2UserInfo(Map attributes) { + this.attributes = attributes; + } + + // 소셜 식별 값 : 카카오 - "id" + public abstract String getId(); + + public abstract String getNickname(); + + public abstract String getEmail(); +}