diff --git a/src/main/java/org/capstone/maru/config/SecurityConfig.java b/src/main/java/org/capstone/maru/config/SecurityConfig.java index de10c72ad2..af5d657ebf 100644 --- a/src/main/java/org/capstone/maru/config/SecurityConfig.java +++ b/src/main/java/org/capstone/maru/config/SecurityConfig.java @@ -1,6 +1,7 @@ package org.capstone.maru.config; +import lombok.extern.slf4j.Slf4j; import org.capstone.maru.dto.security.KakaoOAuth2Response; import org.capstone.maru.dto.security.SharedPostPrincipal; import org.capstone.maru.service.MemberAccountService; @@ -19,6 +20,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.SecurityFilterChain; +@Slf4j @Configuration public class SecurityConfig { @@ -26,7 +28,7 @@ public class SecurityConfig { @ConditionalOnProperty(name = "spring.h2.console.enabled", havingValue = "true") public WebSecurityCustomizer configureH2ConsoleEnable() { return web -> web.ignoring() - .requestMatchers(PathRequest.toH2Console()); + .requestMatchers(PathRequest.toH2Console()); } @Bean @@ -34,6 +36,7 @@ public SecurityFilterChain securityFilterChain( HttpSecurity httpSecurity, OAuth2UserService oAuth2UserService ) throws Exception { + log.info("SecurityFilterChain 빈 등록 완료"); return httpSecurity .authorizeHttpRequests(auth -> auth .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() @@ -45,11 +48,13 @@ public SecurityFilterChain securityFilterChain( ) .oauth2Login(oAuth -> oAuth .userInfoEndpoint(userInfo -> userInfo - .userService(oAuth2UserService)) + .userService(oAuth2UserService) + ) ) .csrf( csrf -> csrf .ignoringRequestMatchers("/api/**") + .disable() ) .build(); } @@ -67,18 +72,20 @@ public OAuth2UserService oAuth2UserService( KakaoOAuth2Response kakaoOAuthResponse = KakaoOAuth2Response.from( oAuth2User.getAttributes()); + String registrationId = userRequest.getClientRegistration() - .getRegistrationId(); // "kakao" + .getRegistrationId(); // "kakao" + String providerId = String.valueOf(kakaoOAuthResponse.id()); - String userId = registrationId + "_" + providerId; + String memberId = registrationId + "_" + providerId; return memberAccountService - .searchMember(userId) + .searchMember(memberId) .map(SharedPostPrincipal::from) .orElseGet(() -> SharedPostPrincipal.from( memberAccountService.saveUser( - userId, + memberId, kakaoOAuthResponse.email(), kakaoOAuthResponse.nickname() ) diff --git a/src/main/java/org/capstone/maru/controller/MainController.java b/src/main/java/org/capstone/maru/controller/MainController.java index a0ba29d845..4c48fa81be 100644 --- a/src/main/java/org/capstone/maru/controller/MainController.java +++ b/src/main/java/org/capstone/maru/controller/MainController.java @@ -17,4 +17,5 @@ public String root() { public String test(@AuthenticationPrincipal SharedPostPrincipal sharedPostPrincipal) { return sharedPostPrincipal.getName(); } + } diff --git a/src/main/java/org/capstone/maru/domain/MemberAccount.java b/src/main/java/org/capstone/maru/domain/MemberAccount.java index 6daadc2207..4c6f643f11 100644 --- a/src/main/java/org/capstone/maru/domain/MemberAccount.java +++ b/src/main/java/org/capstone/maru/domain/MemberAccount.java @@ -7,10 +7,13 @@ import jakarta.persistence.Table; import java.util.Objects; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +import org.capstone.maru.dto.Role; +import org.capstone.maru.dto.SocialType; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -36,17 +39,24 @@ public class MemberAccount extends AuditingFields { @Column(length = 100) private String nickname; + private SocialType socialType; + + private Role role; + + @Builder private MemberAccount( String memberId, String email, String nickname, - String createdBy + String createdBy, + SocialType socialType ) { this.memberId = memberId; this.email = email; this.nickname = nickname; this.createdBy = createdBy; this.modifiedBy = createdBy; + this.socialType = socialType; } public static MemberAccount of( @@ -54,7 +64,7 @@ public static MemberAccount of( String email, String nickname ) { - return new MemberAccount(memberId, email, nickname, null); + return new MemberAccount(memberId, email, nickname, null, null); } public static MemberAccount of( @@ -63,7 +73,7 @@ public static MemberAccount of( String nickname, String createdBy ) { - return new MemberAccount(memberId, email, nickname, createdBy); + return new MemberAccount(memberId, email, nickname, createdBy, null); } @Override diff --git a/src/main/java/org/capstone/maru/dto/CustomOAuth2User.java b/src/main/java/org/capstone/maru/dto/CustomOAuth2User.java new file mode 100644 index 0000000000..d050359fb8 --- /dev/null +++ b/src/main/java/org/capstone/maru/dto/CustomOAuth2User.java @@ -0,0 +1,14 @@ +package org.capstone.maru.dto; + +import java.util.Collection; +import java.util.Map; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + +public class CustomOAuth2User extends DefaultOAuth2User { + + public CustomOAuth2User(Collection authorities, + Map attributes, String nameAttributeKey, String email) { + super(authorities, attributes, nameAttributeKey); + } +} diff --git a/src/main/java/org/capstone/maru/dto/KakaoOAuth2UserInfo.java b/src/main/java/org/capstone/maru/dto/KakaoOAuth2UserInfo.java new file mode 100644 index 0000000000..240431a460 --- /dev/null +++ b/src/main/java/org/capstone/maru/dto/KakaoOAuth2UserInfo.java @@ -0,0 +1,21 @@ +package org.capstone.maru.dto; + +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() { + return null; + } + +} diff --git a/src/main/java/org/capstone/maru/dto/NaverOAuth2UserInfo.java b/src/main/java/org/capstone/maru/dto/NaverOAuth2UserInfo.java new file mode 100644 index 0000000000..b1c0a11e7f --- /dev/null +++ b/src/main/java/org/capstone/maru/dto/NaverOAuth2UserInfo.java @@ -0,0 +1,20 @@ +package org.capstone.maru.dto; + +import java.util.Map; + +public class NaverOAuth2UserInfo extends OAuth2UserInfo { + + public NaverOAuth2UserInfo(Map attributes) { + super(attributes); + } + + @Override + public String getId() { + return String.valueOf(attributes.get("id")); + } + + @Override + public String getNickname() { + return null; + } +} diff --git a/src/main/java/org/capstone/maru/dto/OAuth2UserInfo.java b/src/main/java/org/capstone/maru/dto/OAuth2UserInfo.java new file mode 100644 index 0000000000..595161562d --- /dev/null +++ b/src/main/java/org/capstone/maru/dto/OAuth2UserInfo.java @@ -0,0 +1,17 @@ +package org.capstone.maru.dto; + +import java.util.Map; + +public abstract class OAuth2UserInfo { + + protected Map attributes; + + public OAuth2UserInfo(Map attributes) { + this.attributes = attributes; + } + + public abstract String getId(); //소셜 식별 값 : 구글 - "sub", 카카오 - "id", 네이버 - "id" + + public abstract String getNickname(); + +} diff --git a/src/main/java/org/capstone/maru/dto/OAuthAttributes.java b/src/main/java/org/capstone/maru/dto/OAuthAttributes.java new file mode 100644 index 0000000000..99104b527b --- /dev/null +++ b/src/main/java/org/capstone/maru/dto/OAuthAttributes.java @@ -0,0 +1,65 @@ +package org.capstone.maru.dto; + +import java.util.Map; +import lombok.Builder; +import lombok.Getter; +import org.capstone.maru.domain.MemberAccount; + +@Getter +public class OAuthAttributes { + + private String nameAttributeKey; // OAuth2 로그인 진행 시 키가 되는 필드 값, PK와 같은 의미 + private OAuth2UserInfo oauth2UserInfo; // 소셜 타입별 로그인 유저 정보(닉네임, 이메일, 프로필 사진 등등) + + @Builder + public OAuthAttributes(String nameAttributeKey, OAuth2UserInfo oauth2UserInfo) { + this.nameAttributeKey = nameAttributeKey; + this.oauth2UserInfo = oauth2UserInfo; + } + + /** + * SocialType에 맞는 메소드 호출하여 OAuthAttributes 객체 반환 파라미터 : userNameAttributeName -> OAuth2 로그인 시 + * 키(PK)가 되는 값 / attributes : OAuth 서비스의 유저 정보들 소셜별 of 메소드(ofGoogle, ofKaKao, ofNaver)들은 각각 소셜 + * 로그인 API에서 제공하는 회원의 식별값(id), attributes, nameAttributeKey를 저장 후 build + */ + public static OAuthAttributes of(SocialType socialType, + String userNameAttributeName, Map attributes) { + + if (socialType == SocialType.NAVER) { + return ofNaver(userNameAttributeName, attributes); + } + if (socialType == SocialType.KAKAO) { + return ofKakao(userNameAttributeName, attributes); + } + return null; + } + + private static OAuthAttributes ofKakao(String userNameAttributeName, + Map attributes) { + return OAuthAttributes.builder() + .nameAttributeKey(userNameAttributeName) + .oauth2UserInfo(new KakaoOAuth2UserInfo(attributes)) + .build(); + } + + public static OAuthAttributes ofNaver(String userNameAttributeName, + Map attributes) { + return OAuthAttributes.builder() + .nameAttributeKey(userNameAttributeName) + .oauth2UserInfo(new NaverOAuth2UserInfo(attributes)) + .build(); + } + + /** + * of메소드로 OAuthAttributes 객체가 생성되어, 유저 정보들이 담긴 OAuth2UserInfo가 소셜 타입별로 주입된 상태 OAuth2UserInfo에서 + * socialId(식별값), nickname을 가져와서 build + */ + public MemberAccount toEntity(SocialType socialType, OAuth2UserInfo oauth2UserInfo) { + return MemberAccount.builder() + .memberId(oauth2UserInfo.getId()) + .nickname(oauth2UserInfo.getNickname()) + .socialType(socialType) + .build(); + } + +} diff --git a/src/main/java/org/capstone/maru/dto/Role.java b/src/main/java/org/capstone/maru/dto/Role.java new file mode 100644 index 0000000000..4871045593 --- /dev/null +++ b/src/main/java/org/capstone/maru/dto/Role.java @@ -0,0 +1,14 @@ +package org.capstone.maru.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Role { + + GUEST("ROLE_GUEST"), USER("ROLE_USER"); + + private final String key; +} + diff --git a/src/main/java/org/capstone/maru/dto/SocialType.java b/src/main/java/org/capstone/maru/dto/SocialType.java new file mode 100644 index 0000000000..448e60a772 --- /dev/null +++ b/src/main/java/org/capstone/maru/dto/SocialType.java @@ -0,0 +1,5 @@ +package org.capstone.maru.dto; + +public enum SocialType { + KAKAO, NAVER +} diff --git a/src/main/java/org/capstone/maru/dto/security/SharedPostPrincipal.java b/src/main/java/org/capstone/maru/dto/security/SharedPostPrincipal.java index b477f0fdc9..b70a8f464a 100644 --- a/src/main/java/org/capstone/maru/dto/security/SharedPostPrincipal.java +++ b/src/main/java/org/capstone/maru/dto/security/SharedPostPrincipal.java @@ -19,6 +19,14 @@ public record SharedPostPrincipal( Map oAuth2Attributes ) implements UserDetails, OAuth2User { + /** + * of 파라미터를 받아서 현제 객체를 리턴합니다. + * + * @param memberId + * @param email + * @param nickname + * @return + */ public static SharedPostPrincipal of( String memberId, String email, @@ -48,6 +56,12 @@ public static SharedPostPrincipal of( ); } + /** + * MemberAccountDto를 받아서 현재 객체를 리턴합니다. + * + * @param dto + * @return + */ public static SharedPostPrincipal from(MemberAccountDto dto) { return SharedPostPrincipal.of( dto.memberId(), @@ -56,6 +70,11 @@ public static SharedPostPrincipal from(MemberAccountDto dto) { ); } + /** + * 현재 객체를 dto로 바꿔줍니다 + * + * @return + */ public MemberAccountDto toDto() { return MemberAccountDto.of( memberId, diff --git a/src/main/java/org/capstone/maru/repository/MemberAccountRepository.java b/src/main/java/org/capstone/maru/repository/MemberAccountRepository.java index 01e5743aa5..8774cd7f06 100644 --- a/src/main/java/org/capstone/maru/repository/MemberAccountRepository.java +++ b/src/main/java/org/capstone/maru/repository/MemberAccountRepository.java @@ -1,8 +1,11 @@ package org.capstone.maru.repository; +import java.util.Optional; import org.capstone.maru.domain.MemberAccount; +import org.capstone.maru.dto.SocialType; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberAccountRepository extends JpaRepository { + Optional findBySocialTypeAndSocialId(SocialType socialType, String id); } diff --git a/src/main/java/org/capstone/maru/service/CustomOAuth2UserService.java b/src/main/java/org/capstone/maru/service/CustomOAuth2UserService.java new file mode 100644 index 0000000000..dea770a352 --- /dev/null +++ b/src/main/java/org/capstone/maru/service/CustomOAuth2UserService.java @@ -0,0 +1,89 @@ +package org.capstone.maru.service; + +import java.util.Collections; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.capstone.maru.domain.MemberAccount; +import org.capstone.maru.dto.CustomOAuth2User; +import org.capstone.maru.dto.OAuthAttributes; +import org.capstone.maru.dto.SocialType; +import org.capstone.maru.repository.MemberAccountRepository; +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; + +@RequiredArgsConstructor +@Service +public class CustomOAuth2UserService implements OAuth2UserService { + + private final MemberAccountRepository userRepository; + private static final String NAVER = "naver"; + private static final String KAKAO = "kakao"; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(userRequest); + + /** + * userRequest에서 registrationId 추출 후 registrationId으로 SocialType 저장 + * http://localhost:8080/oauth2/authorization/kakao에서 kakao가 registrationId + * userNameAttributeName은 이후에 nameAttributeKey로 설정된다. + */ + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + SocialType socialType = getSocialType(registrationId); + String userNameAttributeName = userRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint() + .getUserNameAttributeName(); // OAuth2 로그인 시 키(PK)가 되는 값 + Map attributes = oAuth2User.getAttributes(); // 소셜 로그인에서 API가 제공하는 userInfo의 Json 값(유저 정보들) + + // socialType에 따라 유저 정보를 통해 OAuthAttributes 객체 생성 + OAuthAttributes extractAttributes = OAuthAttributes.of(socialType, userNameAttributeName, + attributes); + + MemberAccount createdUser = getUser(extractAttributes, socialType); + + // DefaultOAuth2User를 구현한 CustomOAuth2User 객체를 생성해서 반환 + return new CustomOAuth2User( + Collections.singleton(new SimpleGrantedAuthority(createdUser.getRole().getKey())), + attributes, + extractAttributes.getNameAttributeKey(), + createdUser.getEmail() + ); + } + + private SocialType getSocialType(String registrationId) { + if (NAVER.equals(registrationId)) { + return SocialType.NAVER; + } + if (KAKAO.equals(registrationId)) { + return SocialType.KAKAO; + } + return null; + } + + private MemberAccount getUser(OAuthAttributes attributes, SocialType socialType) { + MemberAccount findUser = userRepository.findBySocialTypeAndSocialId(socialType, + attributes.getOauth2UserInfo().getId()).orElse(null); + + if (findUser == null) { + return saveUser(attributes, socialType); + } + return findUser; + } + + /** + * OAuthAttributes의 toEntity() 메소드를 통해 빌더로 User 객체 생성 후 반환 생성된 User 객체를 DB에 저장 : socialType, + * socialId, email, role 값만 있는 상태 + */ + private MemberAccount saveUser(OAuthAttributes attributes, SocialType socialType) { + MemberAccount createdUser = attributes.toEntity(socialType, attributes.getOauth2UserInfo()); + return userRepository.save(createdUser); + } + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 6e6c71cf4e..5bc8c80370 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -43,9 +43,19 @@ spring: authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/kakao" client-authentication-method: client_secret_post + naver: + client-id: OvLyXvoT1NlEr8Wfiu2u + client-secret: U6Or_rMFOQ + authorization-grant-type: authorization_code + redirect-uri: http://localhost:8080/login/auth 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 \ No newline at end of file + 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