스프링 게시판 만들기 - 8 (1) 편과 이어지는 내용입니다.
각각의 개발자 센터에 들어가서
클라이언트 Id 와 비밀번호를 받았다는 가정하에
진행합니다!
구글
CustomOAuth2MemberService.java
package toyproject.board.config.auth;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import toyproject.board.config.auth.provider.FacebookUserInfo;
import toyproject.board.config.auth.provider.GoogleUserInfo;
import toyproject.board.config.auth.provider.OAuth2UserInfo;
import toyproject.board.detail.PrincipalDetails;
import toyproject.board.domain.Member;
//import toyproject.board.dto.OAuthAttributes;
//import toyproject.board.dto.SessionMember;
import toyproject.board.domain.Role;
import toyproject.board.dto.MemberDto;
import toyproject.board.repository.MemberRepository;
import toyproject.board.service.MemberService;
import javax.servlet.http.HttpSession;
import java.util.Collections;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class CustomOAuth2MemberService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberRepository memberRepository;
private final HttpSession httpSession;
private final MemberService memberService;
// 함수 종료시 @AuthenticationPrincipal 어노테이션이 만들어진다.
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
System.out.println("userRequest = " + userRequest.getAccessToken().getTokenValue());
// 구글 로그인버튼 클릭 -> 구글 로그인창 -> 로그인을 완료 -> code 를 리턴(Oauth-client 라이브러리) -> AccessToken 요청
// userRequest 정보 -> loadUser함수 호출 -> 구글로부터 회원프로필을 받아준다.
System.out.println("userRequest = " + userRequest.getClientRegistration());
System.out.println("oAuth2User = " + userRequest.getAdditionalParameters());
System.out.println("oAuth2User.getAttributes() = " + oAuth2User.getAttributes());
System.out.println("oAuth2User.getAuthorities() = " + oAuth2User.getAuthorities());
System.out.println("oAuth2User.getName() = " + oAuth2User.getName());
// OAuth2UserInfo oAuth2UserInfo = null;
// if(userRequest.getClientRegistration().getRegistrationId().equals("google")){
// System.out.println("구글 로그인 요청");
// oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
// }else if(userRequest.getClientRegistration().getRegistrationId().equals("facebook")){
// System.out.println("페이스북 로그인 요청");
// oAuth2UserInfo = new FacebookUserInfo(oAuth2User.getAttributes());
// }else{
// System.out.println("구글과 페이스북만 지원");
// }
String provider = userRequest.getClientRegistration().getRegistrationId(); // google
String providerId = oAuth2User.getAttribute("sub");
String username = provider + "_" + providerId; // google_1923912312312
String password = "dodokong";
String email = oAuth2User.getAttribute("email");
String role = Role.USER.getValue();
// String provider = oAuth2UserInfo.getProvider(); // google
// String providerId = oAuth2UserInfo.getProviderId();
// String username = provider + "_" + providerId; // google_1923912312312
// String password = "dodokong";
// String email = oAuth2UserInfo.getEmail();
// String role = Role.USER.getValue();
Member member = memberRepository.findByUsername(username);
if (member == null) {
System.out.println("최초 가입");
member = Member.builder()
.username(username)
.password(password)
.email(email)
.role(role)
.provider(provider)
.providerId(providerId)
.build();
memberService.joinUserWithMember(member);
}else {
System.out.println("이미 가입한 적이 있습니다.");
}
return new PrincipalDetails(member, oAuth2User.getAttributes());
}
}
주석 처리한 부분은 제외하고 확인하면, 이전에 로그를 찍었을 때 sub 가 primary key 값으로 들어가 있었고,
email 은 email 정보를 가져오고.. 결국 로그의 값에 맞추어서 값을 꺼내오고
해당 값을 builder 를 이용해서 Join 을 했다.
로그인 하면 위와 같이 정보가 저장됨을 확인할 수 있다!
페이스북
application.properties 에 본인의 클라이언트 id 와 패스워드를 등록하고!!
페이스북 로그인 버튼을 새로 만들어준다.
로그인을 테스트 해보면 위와 같은 결과가 나온다.
구글과 차이점은 sub 가 id 로 바뀌었다는 것이다.
구글에서 작성한 코드를 복사 붙여넣기로
만들어도 좋지만, 클린코딩을 위해서 다른 방법을 써보고자 합니다.
OAuth2UserInfo.interface
public interface OAuth2UserInfo {
String getProviderId();
String getProvider();
String getEmail();
String getName();
}
우리가 회원가입에 필요한 값은 위의 4개이다.
각각의 플랫폼 별로 모두 가져와야 하는 값이기에 인터페이스로 만들고,
GoogleUserInfo.java
public class GoogleUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes; // getAttributes
public GoogleUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public String getProviderId() {
return (String) attributes.get("sub");
}
@Override
public String getProvider() {
return "google";
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
}
FacebookUserInfo.java
public class FacebookUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes; // getAttributes
public FacebookUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public String getProviderId() {
return (String) attributes.get("id");
}
@Override
public String getProvider() {
return "facebook";
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
}
CustomOAuth2MemberService 에서 attributes 값을 각각의 Info 클래스에 넘겨받아서
메소드를 구현한다.
이 때, getProvider() 값을 각각 정의해서 넣어주면 끝!
package toyproject.board.config.auth;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import toyproject.board.config.auth.provider.FacebookUserInfo;
import toyproject.board.config.auth.provider.GoogleUserInfo;
import toyproject.board.config.auth.provider.OAuth2UserInfo;
import toyproject.board.detail.PrincipalDetails;
import toyproject.board.domain.Member;
//import toyproject.board.dto.OAuthAttributes;
//import toyproject.board.dto.SessionMember;
import toyproject.board.domain.Role;
import toyproject.board.dto.MemberDto;
import toyproject.board.repository.MemberRepository;
import toyproject.board.service.MemberService;
import javax.servlet.http.HttpSession;
import java.util.Collections;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class CustomOAuth2MemberService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberRepository memberRepository;
private final HttpSession httpSession;
private final MemberService memberService;
// 함수 종료시 @AuthenticationPrincipal 어노테이션이 만들어진다.
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
System.out.println("userRequest = " + userRequest.getAccessToken().getTokenValue());
// 구글 로그인버튼 클릭 -> 구글 로그인창 -> 로그인을 완료 -> code 를 리턴(Oauth-client 라이브러리) -> AccessToken 요청
// userRequest 정보 -> loadUser함수 호출 -> 구글로부터 회원프로필을 받아준다.
System.out.println("userRequest = " + userRequest.getClientRegistration());
System.out.println("oAuth2User = " + userRequest.getAdditionalParameters());
System.out.println("oAuth2User.getAttributes() = " + oAuth2User.getAttributes());
System.out.println("oAuth2User.getAuthorities() = " + oAuth2User.getAuthorities());
System.out.println("oAuth2User.getName() = " + oAuth2User.getName());
OAuth2UserInfo oAuth2UserInfo = null;
if(userRequest.getClientRegistration().getRegistrationId().equals("google")){
System.out.println("구글 로그인 요청");
oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
}else if(userRequest.getClientRegistration().getRegistrationId().equals("facebook")){
System.out.println("페이스북 로그인 요청");
oAuth2UserInfo = new FacebookUserInfo(oAuth2User.getAttributes());
}else{
System.out.println("구글과 페이스북만 지원");
}
// String provider = userRequest.getClientRegistration().getRegistrationId(); // google
// String providerId = oAuth2User.getAttribute("sub");
// String username = provider + "_" + providerId; // google_1923912312312
// String password = bCryptPasswordEncoder.encode("dodokong");
// String email = oAuth2User.getAttribute("email");
// String role = Role.USER.getValue();
String provider = oAuth2UserInfo.getProvider(); // google
String providerId = oAuth2UserInfo.getProviderId();
String username = provider + "_" + providerId; // google_1923912312312
String password = "dodokong";
String email = oAuth2UserInfo.getEmail();
String role = Role.USER.getValue();
Member member = memberRepository.findByUsername(username);
if (member == null) {
System.out.println("최초 가입");
member = Member.builder()
.username(username)
.password(password)
.email(email)
.role(role)
.provider(provider)
.providerId(providerId)
.build();
memberService.joinUserWithMember(member);
}else {
System.out.println("이미 가입한 적이 있습니다.");
}
return new PrincipalDetails(member, oAuth2User.getAttributes());
}
}
그러면 각각 구글 로그인과 페이스북 로그인이 들어올 때마다
DB 에 저장되는 값을 손쉽게 설정할 수 있다!
앞으로도 네이버와 카카오 Info 를 추가해주기만 하면 끝!!
네이버
네이버는 설정 값이 조금 다르다.
글로벌한 서비스인 구글, 페이스북 등 과는 달리, 네이버나 카카오의 경우
기본 설정 값이 없기 때문에 다음과 같이 provider 정보까지 모두 적어줘야 한다.
scope 를 제외한 값들은 모두 기본 설정 값이므로 변경 없이 사용하면 됩니다!
여기서 우리가 눈여겨 봐야할 부분은 바로 response 이다.
웬 뜬금없는 response ? 하고 회원 가입 url 을 만든 후 로그인을 진행하면
Missing Attribute Response 라는 오류가 발생하게 된다.
이는 네이버가 유저 정보를 json 형태로 response 안에 담아줘서 발생하는 오류다.
이처럼, 유저 정보를 response 안에 담아줘서 전송하기 때문에, 우리가 꺼낼 때 또한 response 안에서 꺼내줘야 한다.
NaverUserInfo.java
package toyproject.board.config.auth.provider;
import java.util.Map;
public class NaverUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes; // getAttributes
public NaverUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public String getProviderId() {
return (String) attributes.get("id");
}
@Override
public String getProvider() {
return "naver";
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
}
CustomOAuth2MemberService.java
정상적으로 DB 에 저장됨을 알 수 있다.
카카오
카카오도 앞선 작업들과 동일하게 카카오 개발자 센터에서 앱을 등록하고,
application.properties 에 등록하고
html 파일에 링크를 추가해준 뒤, 어떤 값이 넘어오는지 확인해본다.
로그를 찍어보면 위와 같은 값이 나온다. 카카오의 경우에 값이 좀 많다!..
우리가 필요한 정보는 id, nickname, 그리고 email 이다.
id 는 그냥 attributes.get()으로 꺼내면 될 것 같고, nickname 은 attributes.get(properties)
email 은 attributes.get(kakao_account) 로 꺼내면 될 것 같다.
KakaoUserInfo.java
package toyproject.board.config.auth.provider;
import java.util.Map;
public class KakaoUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes; // getAttributes
private Map<String, Object> attributesProperties; // getAttributes
private Map<String, Object> attributesAccount; // getAttributes
private Map<String, Object> attributesProfile; // getAttributes
public KakaoUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
this.attributesProperties = (Map<String, Object>) attributes.get("properties");
this.attributesAccount = (Map<String, Object>) attributes.get("kakao_account");
this.attributesProfile = (Map<String, Object>) attributesAccount.get("profile");
}
@Override
public String getProviderId() {
return attributes.get("id").toString();
}
@Override
public String getProvider() {
return "kakao";
}
@Override
public String getEmail() {
return (String) attributesAccount.get("email");
}
@Override
public String getName() {
return (String) attributesProperties.get("nickname");
}
}
똑같이 Info 를 작성해주고, 다른 점은 앞서 말했듯 attributes.get(properties) 요렇게
값을 꺼내오는 부분만 차이가 난다.
서비스 부분에 카카오만 추가해주면 완료!!
사담
내용이 내가 예상한 것보다 더 방대해서 조금 놀랐다..
처음에 어떻게 해야할지 갈피를 못잡아서 많이 헤맸는데 다행히 구현을 잘 완료해서 다행이다.
앞으로 진행할 토이프로젝트나 기존 프로젝트들에도 잘 적용할 수 있을 것 같은 느낌!..
지금 내가 구현한 코드는 여러 Oauth 방식 중에서도
Authorization Code Grant(권한 부여 승인 코드 방식) 이다.
다른 방법에 대해서도 좀 더 알아보고, JWT 를 이용한
액세스 토큰과 리프레시 토큰도 적용해보면 좋겠다는 생각중이다!..
(게시판 구현이라고 해놓고, 로그인만 주구장창 구현하는 것 같아서 문제..)
'ToyProject' 카테고리의 다른 글
스프링 게시판 만들기 - 8 (1) (소셜 로그인 [구글,네이버,카카오,페이스북] ) (1) | 2022.08.01 |
---|---|
스프링 게시판 만들기 - 7 (2) (동시성 feat.ThreadLocal) (0) | 2022.07.13 |
스프링 게시판 만들기 - 7 (1) (조회수, 댓글 작성) (10) | 2022.04.04 |
스프링 게시판 만들기 - 6 (게시판 구현 & 페이징 처리 & 검색) (3) | 2022.03.31 |
스프링 게시판 만들기 - 5 (2) ( 로그인 처리 feat.스프링 시큐리티) (0) | 2022.01.22 |
댓글