[개인 프로젝트] 카카오 소셜 로그인 구현

Turtle·2024년 8월 23일
0

1. Kakao 관련 설정

다른 블로그 및 강의에도 관련 설정 부분은 잘 나와있는 관계로 이 부분은 넘어가겠다.
설정이 끝났다면 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
        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

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

kakao developers

부가적으로 카카오 로그인 REST API 관련 문서를 보면 어떤 식으로 구성이 되어있는지 확인할 수 있다.

2. OAuth2UserInfo

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

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

3. KakaoUserInfo

@Slf4j 로그 어노테이션을 추가하여 카카오에서 어떤 정보를 제공하는지를 확인해보았다. 아래와 같은 정보들이 제공되는 것을 확인할 수 있다.

2024-08-23T22:31:54.778+09:00  INFO 12196 --- [nio-8080-exec-2] p.b.c.o.PrincipalOauthDetailsService     : 카카오 로그인 요청
2024-08-23T22:31:54.778+09:00  INFO 12196 --- [nio-8080-exec-2] p.b.c.o.PrincipalOauthDetailsService     : OAuth2User.Kakao={id= ~, connected_at= ~, properties= ~, kakao_account={profile_nickname_needs_agreement=false, profile_image_needs_agreement=true, profile={nickname= ~, is_default_nickname=false}, has_email=true, email_needs_agreement=false, is_email_valid=true, is_email_verified=true, email= ~}}

정확한 스펙을 참고하는 URL은 아래에 있다.

KakaoAccount 내용

public class KakaoUserInfo implements OAuth2UserInfo {

	private Map<String, Object> attributes;
	private Map<String, Object> attributesAccount;
	private Map<String, Object> attributesProfile;

	public KakaoUserInfo(Map<String, Object> attributes) {
		this.attributes = attributes;
		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 attributesAccount.get("email").toString();
	}

	@Override
	public String getName() {
		return attributesProfile.get("nickname").toString();
	}
}

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());
		}

		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개의 댓글