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