[개인 프로젝트] 네이버 소셜 로그인 구현

Turtle·2024년 8월 23일
0

1. Naver 관련 설정

다른 블로그 및 강의에도 관련 설정 부분은 잘 나와있는 관계로 이 부분은 넘어가겠다.
설정이 끝났다면 application.properties 혹은 application.yml 파일에 네이버 소셜 로그인 관련 설정을 추가해준다.

server:
  port: 8080
  servlet:
    context-path: /
    encoding:
      charset: UTF-8
      enabled: true
      force: true

spring:
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:tcp://localhost/~/db
    username: sa
    password:
  h2:
    console:
      enabled: true
  jpa:
    defer-datasource-initialization: true
    hibernate:
      ddl-auto: update
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 구글 클라이언트 ID값
            client-secret: 구글 클라이언트 Secret값
            scope:
              - email
              - profile
          kakao:
            client-id: REST API키
            client-secret: Client Secret 코드
            scope:
              - account_email
              - profile_nickname
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            authorization-grant-type: authorization_code
            client-name: Kakao
            client-authentication-method: client_secret_post
          naver:
            client-id: 네이버 클라이언트 ID값
            client-secret: 네이버 클라이언트 Secret값
            scope:
              - name
              - email
              - profile_image
            client-name: Naver
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/naver
        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
          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	# 이 부분에 주목

logging:
  level:
    org:
      hibernate:
        sql: debug
        #type: trace

2. OAuth2UserInfo

OAuth 2.0 제공자들마다 제공해주는 값은 상이하다. 구현체 클래스에 직접적으로 의존하는 코드는 유지보수하기 어렵다. OAuth 2.0 제공자들이 공통으로 제공하는 값만을 추출하여 상위 인터페이스로 정의한 후 이 인터페이스를 구현하는 방법으로 코드를 구현했다.

public interface OAuth2UserInfo {
	String getProviderId();
	String getProvider();
	String getEmail();
	String getName();
}

3. NaverUserInfo

앞서 구현했던 부분에서 principalName cannot be empty 오류가 발생했다. 그래서 스프링 시큐리티에서 UserDetails 인터페이스를 구현한 로그인 사용자 정보를 담는 부분에서 OAuth 관련
부분에서 null로 처리해줬던 부분이 생각나 바꿔봤지만 그래도 오류가 발생했다.

앞선 소셜 로그인과 같이 @Slf4j 로그 어노테이션을 추가하여 네이버에서 어떤 정보를 제공하는지를 확인해보았는데 제공되는 데이터의 형식이 달라서 발생했던 문제였다.

2024-08-23T23:41:51.128+09:00  INFO 26208 --- [nio-8080-exec-2] p.b.c.o.PrincipalOauthDetailsService     : 네이버 로그인 요청
2024-08-23T23:41:51.129+09:00  INFO 26208 --- [nio-8080-exec-2] p.b.c.o.PrincipalOauthDetailsService     : OAuth2User.Naver={resultcode=00, message=success, response={id= ~, profile_image= ~, email= ~, name= ~}}

response라는 이름의 객체가 들어있고 이 안에 프로필 이미지, 이메일, 이름 등의 회원 정보가 포함된 것을 볼 수 있다. 따라서 NPE를 방지하기 위해 다운 캐스팅을 시도하여 직접 response에 접근하여 객체를 꺼내야 한다.

4. PrincipalOauthDetailsService

@Slf4j
@Service
@RequiredArgsConstructor
public class PrincipalOauthDetailsService extends DefaultOAuth2UserService {

	private final MemberRepository memberRepository;

	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		OAuth2User oAuth2User = super.loadUser(userRequest);
		return processOAuth2User(userRequest, oAuth2User);
	}

	private OAuth2User processOAuth2User(OAuth2UserRequest request, OAuth2User oAuth2User) {
		OAuth2UserInfo oAuth2UserInfo = null;

		if (request.getClientRegistration().getRegistrationId().equalsIgnoreCase("google")) {
			log.info("구글 로그인 요청");
			log.info("OAuth2User.Google={}", oAuth2User.getAttributes());
			oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
		} else if (request.getClientRegistration().getRegistrationId().equalsIgnoreCase("kakao")) {
			log.info("카카오 로그인 요청");
			log.info("OAuth2User.Kakao={}", oAuth2User.getAttributes());
			oAuth2UserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
		} else if (request.getClientRegistration().getRegistrationId().equalsIgnoreCase("naver")) {
			log.info("네이버 로그인 요청");
			log.info("OAuth2User.Naver={}", oAuth2User.getAttributes());
            // 다운 캐스팅
			oAuth2UserInfo = new NaverUserInfo((Map) oAuth2User.getAttributes().get("response"));
		}

		Optional<Member> findMember = memberRepository.findByProviderAndProviderId(oAuth2UserInfo.getProvider(), oAuth2UserInfo.getProviderId());
		Member member;

		// 소셜 로그인 시 계정 중복 여부를 검증
		if (findMember.isPresent()) {
			log.info("해당 이메일로 가입한 계정이 존재합니다.");
			member = findMember.get();
		} else {
			log.info("해당 이메일로 가입한 계정이 존재하지 않습니다. 소셜 로그인과 동시에 회원가입이 자동으로 진행됩니다.");
			// OAuth 2.0 유저의 경우 패스워드가 없음
			member = Member.builder()
					.username(oAuth2UserInfo.getProvider() + "_" + oAuth2UserInfo.getProviderId())
					.email(oAuth2UserInfo.getEmail())
					.role(Role.ROLE_USER)
					.provider(oAuth2UserInfo.getProvider())
					.providerId(oAuth2UserInfo.getProviderId())
					.build();
		}

		memberRepository.save(member);
		return new PrincipalDetails(member, oAuth2User.getAttributes());
	}
}

0개의 댓글