프로젝트에 open auth를 지원하는 4사(페이스북, 구글, 네이버, 카카오)에 대해 소셜로그인 기능을 추가해보았다.
각각의 개발자 센터에 들어가서 application을 만들고, client-id와 client-secret id를 받았다는 전제하에 진행할 것이다.
1. pom.xml에 oath 라이브러리 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
2. application.yml설정
- scope에 적힌 public_profile, email와 같은 명칭은 정해진 형식이니 개발자센터에서 확인해야 한다.
- scope에는 각 리소스 서버(구글, 카카오 등)의 user DB에서 가져오고 싶은 정보를 명시한다.
- 네이버, 카카오와 같은 경우는 provider를 따로 적어줘야 spring이 지원하는 oath2 login을 활용할 수 있다.
spring:
security:
oauth2:
client:
provider: #네이버, 카카오는 따로 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
registration:
facebook:
client-id: [Facebook 클라이언트 ID]
client-secret: [Facebook 클라이언트 시크릿 ID]
scope:
- public_profile
- email
google:
clientId: [Google 클라이언트 ID]
clientSecret: [Google 클라이언트 시크릿 ID]
redirect-uri: http://localhost:8080/login/oauth2/code/google
scope:
- email
- profile
naver:
clientId: [Naver 클라이언트 ID]
client-secret: [Naver 클라이언트 시크릿 ID]
redirect-uri: http://localhost:8080/login/oauth2/code/naver
authorization-grant-type: authorization_code
scope:
- name
- email
- profile_image
client-name: Naver
kakao:
clientId: [Kakao 클라이언트 ID]
client-secret: [Kakao 클라이언트 시크릿 ID]
redirect-uri: http://localhost:8080/login/oauth2/code/kakao
authorization-grant-type: authorization_code
scope:
- profile_nickname
- account_email
- profile_image
client-name: Kakao
client-authentication-method: POST
3. Security Class - configure수정
userService에는소셜 로그인 성공 시 진행할 OAuth2UserService 인터페이스의 구현체를 등록한다. 리소스 서버(네이버, 카카오, 페이스북 등) 에서 사용자 정보를 가져온 상태에서 추가 진행하고자 하는 기능을 구현하면 된다.
@Override
protected void configure(HttpSecurity http) throws Exception {
//super삭제 - 기존 시큐리티가 가지고 있는 기능이 다 비활성화됨.
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**","/chatroom/**","/image/**","/subscribe/**","/comment/**","/api/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/auth/signin")
.loginProcessingUrl("/auth/signin")
.defaultSuccessUrl("/")
.and()
.oauth2Login() //oauth2로그인도 추가로 진행
.userInfoEndpoint() //oauth2로그인 성공 후에 사용자 정보를 바로 가져온다.
.userService(oAuth2DetailsService);
}
위의 과정을 모두 마쳤다면, 이제 OAuth2UserService 인터페이스의 구현체(나의 경우, oAuth2DetailsService)를 직접 커스텀 하면된다.
바로 이 구현체에서 모든 소셜로그인 로직이 실행되게 되는데, 가령 사용자가 구글로그인을 할 경우에는 구글로그인이 되도록 해야 할 것이고, 카카오로그인을 할 경우에는 카카오 로그인이 진행될 수 있도록 구현해야 한다.
아래는 이미 완성된 코드인데, 전체적으로 어떻게 흘러가는지 파악하고 내려가자.
- 사용자를 저장할 userRepository를 불러온다.
- super.loadUser(userRequest)를 통해서 OAuth2User타입의 사용자 정보를 불러온다. (OAuth2User.getAttributes()를 사용하면 Map<String,Object>의 형태로 사용자 정보가 담겨진 것을 확인할 수 있다.)
- userRequest.getClientRegistration().getRegistrationId()로 리소스서버를 구분할 수 있다.
- OAuth2UserInfo(나중에 만들어야 하는 인터페이스)형의 참조변수를 선언해서 GoogleUserInfo, FacebookUserInfo, NaverUserInfo, KakaoUserInfo(나중에 만들어야 하는 oAuth2UserInfo의 구현체)의 인스턴스를 참조하게 한다.
- 최종적으로 oAuth2UserInfo에는 리소스 서버 중 하나의 사용자 정보가 들어가 있을것이다.
- oAuth2UserInfo에 담긴 정보를 이용해서 각자의 회원가입에 필요한 변수들을 setting한 다음, OAuth2User형태로 리턴해주면 된다. (나의 경우, 사전에 OAuth2User의 인스턴스로 PrincipalDetail이라는 클래스를 미리 커스텀했다.)
package com.cos.photogramstart.config.oauth;
import java.util.Map;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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;
import com.cos.photogramstart.config.auth.PrincipalDetail;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
@Slf4j
@RequiredArgsConstructor
@Service
public class OAuth2DetailsService extends DefaultOAuth2UserService{
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
OAuth2UserInfo oAuth2UserInfo = null;
if(userRequest.getClientRegistration().getRegistrationId().equals("google")){
log.info("구글 로그인 요청");
oAuth2UserInfo = new GoogleUserInfo(oauth2User.getAttributes());
}else if(userRequest.getClientRegistration().getRegistrationId().equals("facebook")){
log.info("페이스북 로그인 요청");
oAuth2UserInfo = new FacebookUserInfo(oauth2User.getAttributes());
}else if(userRequest.getClientRegistration().getRegistrationId().equals("naver")){
log.info("네이버 로그인 요청");
oAuth2UserInfo = new NaverUserInfo((Map)oauth2User.getAttributes().get("response"));
}else if(userRequest.getClientRegistration().getRegistrationId().equals("kakao")){
log.info("카카오 로그인 요청");
oAuth2UserInfo = new KakaoUserInfo(oauth2User.getAttributes());
}
String username = oAuth2UserInfo.getProvider() + oAuth2UserInfo.getUsername();
String password = new BCryptPasswordEncoder().encode(UUID.randomUUID().toString());
String email = oAuth2UserInfo.getEmail();
String name = oAuth2UserInfo.getName();
User userEntity = userRepository.findByUsername(username);
if(userEntity==null) { //최초 로그인 시
User user = User.builder()
.username(username)
.password(password)
.email(email)
.name(name)
.role("ROLE_USER")
.build();
return new PrincipalDetail(userRepository.save(user));
}else {
return new PrincipalDetail(userEntity);
}
}
}
회원가입을 하기 위해 필요로 하는 사항을 체크한다. 우선 나의 프로젝트에서 회원가입에 필요한 컬럼을 살펴보니, username, password, email, name 4가지이다. 따라서 어떤 리소스 서버인지를 나타내는 provider를 포함한 5가지에 대해서 정의할 수 있는 인터페이스를 생성한다.
public interface OAuth2UserInfo {
String getProvider();
String getUsername();
String getPassword();
String getEmail();
String getName();
}
이후, 이를 구현한 구현체 4가지를 만들어준다.(Google, KaKao, Naver, Facebook)
<GoogleUserInfo>
package com.cos.photogramstart.config.oauth;
import java.util.Map;
public class GoogleUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes;
public GoogleUserInfo(Map<String, Object> attributes){
this.attributes = attributes;
}
@Override
public String getProvider() {
return "google_";
}
@Override
public String getUsername() {
return (String) attributes.get("sub");
}
@Override
public String getPassword() {
return null;
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
}
<KakaoUserInfo>
package com.cos.photogramstart.config.oauth;
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 getProvider() {
return "kakao_";
}
@Override
public String getUsername() {
return attributes.get("id").toString();
}
@Override
public String getPassword() {
return null;
}
@Override
public String getEmail() {
return (String) attributesAccount.get("email");
}
@Override
public String getName() {
return (String) attributesProperties.get("nickname");
}
}
<NaverUserInfo>
package com.cos.photogramstart.config.oauth;
import java.util.Map;
public class NaverUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes;
public NaverUserInfo(Map<String,Object> attributes){
this.attributes = attributes;
}
@Override
public String getProvider() {
return "naver_";
}
@Override
public String getUsername() {
return (String) attributes.get("id");
}
@Override
public String getPassword() {
return null;
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
}
<FacebookUserInfo>
package com.cos.photogramstart.config.oauth;
import java.util.Map;
public class FacebookUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes;
public FacebookUserInfo(Map<String,Object> attributes){
this.attributes = attributes;
}
@Override
public String getProvider() {
return "facebook_";
}
@Override
public String getUsername() {
return (String) attributes.get("id");
}
@Override
public String getPassword() {
return null;
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
}
모든게 완료되었고, 각각의 소셜 로그인이 제대로 동작되는 지 프로젝트에서 확인해보자.
'Projects > SNS프로젝트' 카테고리의 다른 글
[채팅]오픈채팅 구현 과정(이슈&해결) (0) | 2022.12.23 |
---|---|
[채팅] @ServerEndpoint 사용 시 DI가 안되는 문제 (0) | 2022.12.12 |
[댓글] 댓글 구현하기(2) (0) | 2022.02.08 |
[댓글] 댓글 구현하기(1) (1) | 2022.02.07 |
[회원가입] 회원가입 구현 (0) | 2022.01.17 |
댓글