diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberController.java b/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberController.java index d524b67d..b2c5f399 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberController.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberController.java @@ -1,6 +1,7 @@ package com.haejwo.tripcometrue.domain.member.controller; import com.haejwo.tripcometrue.domain.member.dto.request.SignUpRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.response.LoginResponseDto; import com.haejwo.tripcometrue.domain.member.dto.response.SignUpResponseDto; import com.haejwo.tripcometrue.domain.member.dto.response.TestUserResponseDto; import com.haejwo.tripcometrue.domain.member.entity.Member; @@ -28,8 +29,8 @@ public class MemberController { @PostMapping("/signup") public ResponseEntity> signup( @Valid @RequestBody SignUpRequestDto signUpRequestDto) { - SignUpResponseDto signupResponseDto = memberService.signup(signUpRequestDto); - ResponseDTO response = ResponseDTO.okWithData(signupResponseDto); + SignUpResponseDto signUpResponseDto = memberService.signup(signUpRequestDto); + ResponseDTO response = ResponseDTO.okWithData(signUpResponseDto); return ResponseEntity .status(response.getCode()) .body(response); @@ -57,4 +58,15 @@ public ResponseEntity> checkDuplicateEmail( .status(response.getCode()) .body(response); } + + @GetMapping("/oauth2/info") + public ResponseEntity> oauth2Test( + @RequestParam String token, @RequestParam String email, @RequestParam String name) { + LoginResponseDto loginResponseDto = new LoginResponseDto(email, name, token); + + ResponseDTO response = ResponseDTO.okWithData(loginResponseDto); + return ResponseEntity + .status(response.getCode()) + .body(response); + } } \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/entity/Member.java b/src/main/java/com/haejwo/tripcometrue/domain/member/entity/Member.java index d99a8059..cf2adf44 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/entity/Member.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/entity/Member.java @@ -34,7 +34,13 @@ public class Member extends BaseTimeEntity { private Double member_rating; @Builder - public Member(String email, String nickname, String password, String authority) { + public Member(String email, String nickname, String password, String authority, + String provider) { this.memberBase = new MemberBase(email, nickname, password, authority); + this.provider = provider; + } + + public void updateProfileImage(String profileImage){ + this.profile_image = profileImage; } } diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepository.java index d762be18..d6080660 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepository.java @@ -8,4 +8,6 @@ public interface MemberRepository extends JpaRepository { Optional findByMemberBaseEmail(String email); + Optional findByMemberBaseEmailAndProvider(String email, String provider); + } diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/GoogleUserInfo.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/GoogleUserInfo.java new file mode 100644 index 00000000..b11c4d90 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/GoogleUserInfo.java @@ -0,0 +1,42 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import java.util.Map; + +/** + * @author liyusang1 + * @implNote OAuth2 구글 로그인 후 받아온 값에서 사용자 정보를 저장하기 위한 클래스 + */ + +public class GoogleUserInfo implements OAuth2UserInfo { + + private Map attributes; + + public GoogleUserInfo(Map attributes) { + this.attributes = attributes; + } + + @Override + public String getName() { + return (String) attributes.get("name"); + } + + @Override + public String getPhoneNumber() { + return null; + } + + @Override + public String getProfileImage() { + return null; + } + + @Override + public String getEmail() { + return (String) attributes.get("email") + "GoogleOAuth2"; + } + + @Override + public String getProvider() { + return "google"; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/KakaoUserInfo.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/KakaoUserInfo.java new file mode 100644 index 00000000..b7cf2731 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/KakaoUserInfo.java @@ -0,0 +1,48 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import java.util.Map; + +/** + * @author liyusang1 + * @implNote OAuth2 카카오 로그인 후 받아온 값에서 사용자 정보를 저장하기 위한 클래스 + */ + +public class KakaoUserInfo implements OAuth2UserInfo { + + private Map attributes; + + public KakaoUserInfo(Map attributes) { + this.attributes = attributes; + } + + @Override + public String getName() { + // Kakao의 닉네임은 properties 안에 있습니다. + Map properties = (Map) attributes.get("properties"); + return (String) properties.get("nickname"); + } + + @Override + public String getPhoneNumber() { + return null; + } + + @Override + public String getEmail() { + // Kakao의 이메일은 kakao_account 안에 있습니다. + Map kakaoAccount = (Map) attributes.get("kakao_account"); + return (String) kakaoAccount.get("email") + "KaKaoOAuth2"; + } + + @Override + public String getProvider() { + return "kakao"; + } + + @Override + public String getProfileImage() { + // Kakao의 프로필 이미지는 properties 안에 있습니다. + Map properties = (Map) attributes.get("properties"); + return (String) properties.get("profile_image"); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/NaverUserInfo.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/NaverUserInfo.java new file mode 100644 index 00000000..6ee31953 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/NaverUserInfo.java @@ -0,0 +1,41 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import java.util.Map; + +/** + * @author liyusang1 + * @implNote OAuth2 네이버 로그인 후 받아온 값에서 사용자 정보를 저장하기 위한 클래스 + */ +public class NaverUserInfo implements OAuth2UserInfo { + + private Map attributes; + + public NaverUserInfo(Map attributes) { + this.attributes = attributes; + } + + @Override + public String getName() { + return (String) attributes.get("name"); + } + + @Override + public String getPhoneNumber() { + return (String) attributes.get("mobile"); + } + + @Override + public String getProfileImage() { + return null; + } + + @Override + public String getEmail() { + return (String) attributes.get("email") + "NaverOAuth2"; + } + + @Override + public String getProvider() { + return "naver"; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2LoginSuccessHandler.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2LoginSuccessHandler.java new file mode 100644 index 00000000..13fdc2e7 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2LoginSuccessHandler.java @@ -0,0 +1,45 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import com.haejwo.tripcometrue.global.jwt.JwtProvider; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +/** + * @author liyusang1 + * @implNote 해당 클래스는 SimpleUrlAuthenticationSuccessHandler를 상속받은 OAuth 로그인 성공 후 로직을 처리 하는 클래스 + * 로그인 성공 후 리디렉트 하게 설정 했습니다. + * 프론트 배포사이트 -> http://localhost:5173 + * 스프링 코드 내로 리디렉트 설정 하고 싶은 경우 + * String redirectUrl = "/user/oauth-success?token="+token; + */ +@Component +@RequiredArgsConstructor +public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtProvider jwtProvider; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + String token = jwtProvider.createToken(principalDetails.getMember()); + String email = principalDetails.getEmail(); + String name = principalDetails.getUsername(); + + //한국어 인코딩 설정 + String encodedName = URLEncoder.encode(name, StandardCharsets.UTF_8.toString()); + + String redirectUrl = "http://localhost:8080/v1/member/oauth2/info?token=" + token + + "&email=" + email + "&name=" + encodedName; + getRedirectStrategy().sendRedirect(request, response, redirectUrl); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2UserInfo.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2UserInfo.java new file mode 100644 index 00000000..bb3329e1 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2UserInfo.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +/** + * @author liyusang1 + * @implNote OAuth2.0 제공자들 마다 응답 해주는 속성 세부 값이 달라서 생성한 공통 interface + */ +public interface OAuth2UserInfo { + String getProvider(); + String getEmail(); + String getName(); + String getPhoneNumber(); + String getProfileImage(); +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/PrincipalOauth2UserService.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/PrincipalOauth2UserService.java new file mode 100644 index 00000000..1f8f4501 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/PrincipalOauth2UserService.java @@ -0,0 +1,67 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +/** + * @author liyusang1 + * @implNote OAuth2 client라이브러리에서 redirect된 경로의 로그인 성공 후 후처리를 하는 클래스, 로그인 성공 시 accesstoken과 사용자 정보를 + * 같이 지급받게 되며, 발급받은 accesstoken 및 사용자 정보를 아래와 같이 코드로 확인할 수 있다. + * System.out.println("getClientRegistration : " + userRequest.getClientRegistration ()); + * System.out.println("getAccessToken: " + userRequest.getAccessToken()); + * System.out.println("getAttributes: " + super.loadUser(userRequest).getAttributes()) + */ + +@Service +@RequiredArgsConstructor +public class PrincipalOauth2UserService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + + OAuth2User oauth2User = super.loadUser(userRequest); + OAuth2UserInfo oauth2Userinfo = null; + String provider = userRequest.getClientRegistration() + .getRegistrationId(); //google kakao facebook... + + if (provider.equals("google")) { + oauth2Userinfo = new GoogleUserInfo(oauth2User.getAttributes()); + } else if (provider.equals("naver")) { + oauth2Userinfo = new NaverUserInfo((Map) oauth2User.getAttributes().get("response")); + } else if (provider.equals("kakao")) { + oauth2Userinfo = new KakaoUserInfo(oauth2User.getAttributes()); + } + + Optional user = memberRepository.findByMemberBaseEmailAndProvider( + oauth2Userinfo.getEmail(), oauth2Userinfo.getProvider()); + + //이미 소셜로그인을 한적이 있는지 없는지 + if (user.isEmpty()) { + Member newUser = Member.builder() + .email(oauth2Userinfo.getEmail()) + .nickname(oauth2Userinfo.getName()) + .password("OAuth2") //Oauth2로 로그인을 해서 패스워드는 의미없음. + .authority("ROLE_USER") + .provider(provider) + .build(); + if (provider.equals("kakao")) { + newUser.updateProfileImage(oauth2Userinfo.getProfileImage()); + } + + memberRepository.save(newUser); + return new PrincipalDetails(newUser, oauth2User.getAttributes()); + } else { + return new PrincipalDetails(user.get(), oauth2User.getAttributes()); + } + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/SpringSecurityConfig.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/SpringSecurityConfig.java index c97d87bd..69674b7f 100644 --- a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/SpringSecurityConfig.java +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/SpringSecurityConfig.java @@ -24,6 +24,8 @@ public class SpringSecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtAuthorizationFilter jwtAuthorizationFilter; + private final PrincipalOauth2UserService principalOauth2UserService; + private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, @@ -68,10 +70,24 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, .requestMatchers(new AntPathRequestMatcher("/v1/member/signup")).permitAll() .requestMatchers(new AntPathRequestMatcher("/v1/member/test/jwt")).permitAll() .requestMatchers(new AntPathRequestMatcher("/v1/member/check-duplicated-email")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/member/oauth2/info/**")).permitAll() .requestMatchers(new AntPathRequestMatcher("/v1/places/**")).permitAll() .anyRequest().authenticated()); + /** + * @author liyusang1 + * @implNote 사용자 프로필 정보를 가져오고 그 정보를 토대로 회원가입을 자동으로 진행 + * 정보가 추가 적으로 필요하면 추가적으로 요구 받아야함 + * OAuth 완료가 되면 엑세스토큰 + 사용자 프로필 정보를 한번에 받음 로그인 성공시 principalOauth2UserService에서 처리 후 + * oAuth2LoginSuccessHandler에서 리디렉트 처리 + */ + http.oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint( + userInfoEndpoint -> userInfoEndpoint.userService(principalOauth2UserService)) + .successHandler(oAuth2LoginSuccessHandler) + ); + http.exceptionHandling(exceptionHandling -> { exceptionHandling.authenticationEntryPoint( (request, response, authException) -> CustomResponseUtil.fail(response, diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 3b649bbd..1e498b33 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -23,6 +23,51 @@ spring: host: localhost port: 6379 + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_ID} + client-secret: ${GOOGLE_KEY} + scope: + - email + - profile + + naver: + client-id: ${NAVER_ID} + client-secret: ${NAVER_KEY} + scope: + - name + - email + client-name: Naver + authorization-grant-type: authorization_code + redirect-uri: http://localhost:8080/login/oauth2/code/naver + + kakao: + client-id: ${KAKAO_ID} + client-secret: ${KAKAO_KEY} + client-authentication-method: client_secret_post + redirect-uri: http://localhost:8080/login/oauth2/code/kakao + authorization-grant-type: authorization_code + client-name: Kakao + scope: + - profile_nickname + - account_email + - profile_image + + provider: + 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 + 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 + logging: level: "[org.springframework.security]": DEBUG diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index daf15f58..df50c3b5 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -23,6 +23,51 @@ spring: host: ${REDIS_HOST} port: ${REDIS_PORT} + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_ID} + client-secret: ${GOOGLE_KEY} + scope: + - email + - profile + + naver: + client-id: ${NAVER_ID} + client-secret: ${NAVER_KEY} + scope: + - name + - email + client-name: Naver + authorization-grant-type: authorization_code + redirect-uri: http://tripcometrue.site:8080/login/oauth2/code/naver + + kakao: + client-id: ${KAKAO_ID} + client-secret: ${KAKAO_KEY} + client-authentication-method: client_secret_post + redirect-uri: http://tripcometrue.site:8080/login/oauth2/code/kakao + authorization-grant-type: authorization_code + client-name: Kakao + scope: + - profile_nickname + - account_email + - profile_image + + provider: + 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 + 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 + logging: level: "[org.springframework.security]": DEBUG diff --git a/src/test/http/member/login.http b/src/test/http/member/login.http index 6cdddb5f..65feff1e 100644 --- a/src/test/http/member/login.http +++ b/src/test/http/member/login.http @@ -5,4 +5,13 @@ Content-Type: application/json { "email": "liyusang1@naver.com", "password": "123456" -} \ No newline at end of file +} + +### google (Do not run it within an http file) +http://localhost:8080/oauth2/authorization/google + +### naver (Do not run it within an http file) +http://localhost:8080/oauth2/authorization/naver + +### kakao (Do not run it within an http file) +http://localhost:8080/oauth2/authorization/kakao \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 17937592..0e92b17c 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,4 +1,6 @@ spring: + config: + import: optional:file:.env[.properties] datasource: driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver url: jdbc:tc:mysql:8:///tripcometrue?characterEncoding=UTF-8&serverTimezone=Asia/Seoul @@ -17,6 +19,51 @@ spring: host: localhost port: 6379 + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_ID} + client-secret: ${GOOGLE_KEY} + scope: + - email + - profile + + naver: + client-id: ${NAVER_ID} + client-secret: ${NAVER_KEY} + scope: + - name + - email + client-name: Naver + authorization-grant-type: authorization_code + redirect-uri: http://localhost:8080/login/oauth2/code/naver + + kakao: + client-id: ${KAKAO_ID} + client-secret: ${KAKAO_KEY} + client-authentication-method: client_secret_post + redirect-uri: http://localhost:8080/login/oauth2/code/kakao + authorization-grant-type: authorization_code + client-name: Kakao + scope: + - profile_nickname + - account_email + - profile_image + + provider: + 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 + 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 + logging: level: "[org.springframework.security]": DEBUG