다른 블로그 및 강의에도 관련 설정 부분은 잘 나와있는 관계로 이 부분은 넘어가겠다.
설정이 끝났다면 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
부가적으로 카카오 로그인 REST API 관련 문서를 보면 어떤 식으로 구성이 되어있는지 확인할 수 있다.
OAuth 2.0 제공자들마다 제공해주는 값은 상이하다. 구현체 클래스에 직접적으로 의존하는 코드는 유지보수하기 어렵다. OAuth 2.0 제공자들이 공통으로 제공하는 값만을 추출하여 상위 인터페이스로 정의한 후 이 인터페이스를 구현하는 방법으로 코드를 구현했다.
public interface OAuth2UserInfo {
String getProviderId();
String getProvider();
String getEmail();
String getName();
}
@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은 아래에 있다.
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();
}
}
@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());
}
}