Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] oauth2 로그인 기능 추가 (구글,네이버,카카오) #37

Merged
merged 14 commits into from
Jan 8, 2024
Merged
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예전 커밋이지만 지금 발견해서 말씀드립니다.
32번째 줄을 보면 메소드명은 signup인데, Dto명에선 signUp으로 되어 있어요.
일반적으로 사용하는 signUp으로 통일하면 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 이부분 반영하겠습니다 실수한거같네요

Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -57,4 +58,15 @@ public ResponseEntity<ResponseDTO<Void>> checkDuplicateEmail(
.status(response.getCode())
.body(response);
}

@GetMapping("/oauth2/info")
public ResponseEntity<ResponseDTO<LoginResponseDto>> oauth2Test(
@RequestParam String token, @RequestParam String email, @RequestParam String name) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가독성을 위해 한 칸 내린 매개변수들은 한 번의 탭을 추가하는 건 어떨까요? 바로 밑 코드들과 같은 라인이라 구분이 되면 좋을듯 합니다.
예를 들어,

@GetMapping("/test/jwt")
public ResponseEntity<ResponseDTO<TestUserResponseDto>> test(
           @AuthenticationPrincipal PrincipalDetails principalDetails) {
      Member member = principalDetails.getMember();
      ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctrl alt l로 google code java convetion에 맞춰 적용할 경우 자동으로 다시 수정되는 부분입니다.
google code java convetion을 사용하고 있기 때문에 이대로 하는게 맞는거같습니다.

LoginResponseDto loginResponseDto = new LoginResponseDto(email, name, token);

ResponseDTO<LoginResponseDto> response = ResponseDTO.okWithData(loginResponseDto);
return ResponseEntity
.status(response.getCode())
.body(response);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memberBase 필드를 protected로 한 이유가 있나요? private으로 해도 되지 않나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private 보다 더 넓은 범위에서 쓰기 위해 protected가 맞는거 같습니다.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByMemberBaseEmail(String email);
Optional<Member> findByMemberBaseEmailAndProvider(String email,String provider);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

매개변수 사이에 한 칸 띄어쓰기하면 좋을듯 합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

google code java convetion을 적용하지 않았네요 반영해서 수정했습니다


}
Original file line number Diff line number Diff line change
@@ -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<String, Object> attributes;

public GoogleUserInfo(Map<String, Object> 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";
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> attributes;

public KakaoUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}

@Override
public String getName() {
// Kakao의 닉네임은 properties 안에 있습니다.
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
return (String) properties.get("nickname");
}

@Override
public String getPhoneNumber() {
return null;
}

@Override
public String getEmail() {
// Kakao의 이메일은 kakao_account 안에 있습니다.
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
return (String) kakaoAccount.get("email") + "KaKaoOAuth2";
}

@Override
public String getProvider() {
return "kakao";
}

@Override
public String getProfileImage() {
// Kakao의 프로필 이미지는 properties 안에 있습니다.
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
return (String) properties.get("profile_image");
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> attributes;

public NaverUserInfo(Map<String, Object> 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";
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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<Member> 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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
45 changes: 45 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading