(24.12.29) 카카오 로그인 구현(Rest API, Spring Boot) : 밑준비
서술을 시작하기 앞서, 해당 내용은 KaKao Developers의 카카오 로그인 서비스를 기반으로 서술됨을 알린다.
(※ url : https://developers.kakao.com/docs/latest/ko/index)
요즘 지인과 프로젝트로 앱을 만들고 있으며, 나는 현재 백엔드 포지션을 잡고 개발을 맡고 있다.
그 과정에서 필요한 기능 중 하나가 SNS 로그인인데, 크게 카카오, 네이버, 구글 애플로 잡았다.
이 중 애플을 제외한 카카오, 네이버, 구글은 다 똑같이 OAuth 2.0 기반의 로그인 방식을 사용하고 세세한 부분에서만 다르기 때문에 카카오 로그인 기능 구현 하나만 제대로 알고 간다면 다른 두 포털의 로그인 API도 무난하게 구현할 수 있을 거라고 생각한다.(실제로 본인도 카카오 로그인 소스코드를 조금만 바꿔가면서 다른 두 포탈의 로그인 API를 구현했다.)
우선 카카오 API 로그인의 로직부터 알아보자.
간단히 요약하면,
- (클라이언트)카카오 로그인 요청
- (서버)사전 발급해준 인가 코드 확인 및 인가 토큰(Access Token) 발급
- (플랫폼) 인가 토큰 확인 및 사용자 정보 서버로 발급
- (서버 > 클라이언트) 사용자 정보 제공 및 로그인 완료.
크고 단순하게만 보자면 다음과 같고, 이젠 조금 디테일하게 살펴보자.
0. 로그인 구현 시작 전 밑준비
카카오나 네이버나 구글에서 제공하는 로그인 서비스의 공통점을 2가지 정도만 언급해보자면,
- OAuth 2.0 기반의 서비스라는 것
- 다음과 같은 요소가 사전에 준비되어 있어야 한다는 것
>> Client Id, Client Secret, Redirect URI
(※ 이외에도 받아야 할 파라미터가 더 있지만... 중요하지 않으니 넘어가자.)
위 세 요소에 대해 간단히 설명을 하자면,
- Client ID : 앱의 Rest API 키
- Client Secret : 토큰 발급과 갱신을 위해 사용되는 코드
- Redirect URI : 서비스에서 요청한 인가 코드와 토큰을 전달하기 위한 URI
OAuth 2.0은 Spring Security를 활용하여 별도 구현하면 되고, 그 아래의 3가지 정보는 각 서비스의 개발 지원 포탈에서 일련의 과정을 거쳐 발급받을 수 있다.
(※ 네이버 : https://developers.naver.com/main/)
(※ 구글 : https://cloud.google.com/?hl=ko > 콘솔)
(※ 카카오 : https://developers.kakao.com/)
구글과 네이버에 대한 설명은 생략하고, 여기엔 카카오 로그인에 대한 과정만 알아보자.
1) 카카오 디벨로퍼스에서 내 애플리케이션으로 이동
2) 애플리케이션 추가 진행
애플리케이션 등록이 끝났다면, Client Id와 Client Secret를 확인할 수 있다.
Client Secret의 정보 조회 및 변경은 해당 애플리케이션을 만든 유저 본인만 가능하며, 아래의 '활성화 상태' 옵션을 '사용함'으로 바꿔야 Client Secret이 사용 가능해진다.
사이트 도메인엔 현재 Rest API를 구현하고 있는 백엔드 도메인을 등록한 뒤 등록 가능하다.
위의 과정을 통해 Client ID, Client Secret, Redirect URI를 얻을 수 있었다.
이제 이 데이터를 이용해서 Spring Boot에서의 밑준비를 진행해보자.
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // (2024.11.17) OAuth 2.0
implementation 'org.springframework.boot:spring-boot-starter-security' // (2024.11.17)
//thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// json 파싱 목적으로 jackson 선언
implementation 'com.fasterxml.jackson.core:jackson-databind'
//webClient 사용을 위한 Webflux 추가
implementation 'org.springframework.boot:spring-boot-starter-webflux'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
}
다음과 같이 선언해주자.
Application.properties
spring.security.oauth2.client.registration.kakao.client-id=
spring.security.oauth2.client.registration.kakao.client-secret=
spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post
spring.security.oauth2.client.registration.kakao.redirect-uri=
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.client-name=kakao
spring.security.oauth2.client.registration.kakao.scope=
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute=id
위에서 공백인 client-id와 client-secret은 이전 단계에서 본인이 설정한 데이터를 집어넣으면 되고,
Scope의 경우 https://developers.kakao.com/console/app/1088566/product/login/scope 을 참조하여 입력하자.
(ex. spring.security.oauth2.client.registration.kakao.scope=profile_nickname, account_email)
Spring Security
카카오 로그인은 OAuth2.0 기반의 서비스이므로 SecurityConfig를 선언해주자.
import kr.trablock.backend.api.auth.service.OAuth2UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
private final OAuth2UserService oAuth2UserService;
public SecurityConfig(OAuth2UserService oAuth2UserService) {
this.oAuth2UserService = oAuth2UserService;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf((csrf) ->
csrf.disable());
http.authorizeHttpRequests((config) -> config.anyRequest().permitAll());
http.oauth2Login((oauth2Configurer) -> oauth2Configurer
.loginPage("/login")
.successHandler(successHandler())
.userInfoEndpoint(userInfoEndpointConfig ->
userInfoEndpointConfig.userService(oAuth2UserService)));
return http.build();
}
@Bean
AuthenticationSuccessHandler successHandler() {
return((request, response, authentication) -> {
DefaultOAuth2User defaultOAuth2User = (DefaultOAuth2User) authentication.getPrincipal();
String id = defaultOAuth2User.getAttributes().get("id").toString();
String body = """
{"id":"%s"}
""".formatted(id);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
PrintWriter writer = response.getWriter();
writer.println(body);
writer.flush();
});
}
}
OAuth2UserService
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class OAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
// Role Generate
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_ADMIN");
// nameAttributeKey
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
return new DefaultOAuth2User(authorities, oAuth2User.getAttributes(), userNameAttributeName);
}
}
이것으로 밑준비는 다 끝났고, 이후부터 본격적으로 카카오 로그인 서비스를 구현해보자.