티스토리 뷰

회사에서 소셜 로그인을 구현해야 할 업무가 생겼는데 마침 진행하고 있는 프로젝트의 Spring Boot 버전이 3.0.4여서

Spring Boot 3.x 버전에서 OAuth2 라이브러리를 이용하여 카카오 로그인을 구현하고 동작 원리를 살펴보겠습니다.

제가 정리한 내용을 순차적으로 진행하시면 문제없이 카카오 로그인을 구현하실 수 있습니다.

 

Kakao Developers 설정

카카오 디벨로퍼 사이트에 접속하셔서 로그인하시고 우측상단에 보이는 '내 애플리케이션'을 클릭하고 애플리케이션을 생성합니다.

 

 

앱 설정 > 요약정보 페이지에 가시면 4가지의 앱 키가 있는데 REST API 키를 복사해 둡니다.

이 키가 서버에서 사용할 client-id 값이 됩니다. client-id 값은 밑에서 다시 설명하겠습니다.

 

 

앱 설정 > 플랫폼 페이지에 가셔서 하단에 Web 사이트 도메인을 등록하시면 됩니다. 도메인은 테스트 목적이기 때문에http://localhost:8080으로 등록하시면 됩니다. 

 

 

제품 설정 > 카카오 로그인 페이지에 가시면 위와 같은 사진을 볼 수 있는데요. 활성화 설정 상태를 ON으로 변경해 둡니다.

그리고 Redirect URI의 path는 /local/oauth2/code/kakao, host와 port는 각각 localhost8080으로 해둡니다.

 

host와 port는 정해진 값은 없습니다만 로컬에서 간단히 테스트하는 게 목적이므로 위처럼 해둡니다.

path는 다른 URI로도 할 수 있는데요. Spring OAuth2 Client 라이브러리를 사용하면 기본적으로 Redirect URI을 /local/oauth2/code/{registrationId}로 Redirect 해줍니다. registrationId도 밑에서 알아보겠습니다.

 

 

제품 설정 > 카카오 로그인 > 보안 페이지에 가시면 Client Secret을 발급받으실 수 있습니다. 발급받은 후 활성화 상태를 사용함으로 설정합니다. 이 키가 서버에서 client-secret 값이 됩니다. 이 값도 밑에서 다시 설명하겠습니다.

 

 

제품 설정 > 카카오 로그인 > 동의 항목 페이지에 가시면 사용자가 카카오 로그인을 할 때 수집해야 할 정보들의 목록이 나오는데 수집에 대한 동의 여부 항목입니다. 기본적으로 프로필 정보는 필수동의이며 이메일, 성별, 연령대, 생일은 선택 동의입니다. 저는 이메일을 선택적으로 받을 수 있게끔 하려고 선택 동의로 해두었습니다.

 

카카오 디벨로퍼 사이트 설정은 여기까지가 전부입니다. 

 

 

Spring Boot 설정 (디펜던시 추가 및 로그인 페이지)

1
2
3
4
5
6
7
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
cs

build.gradle에 위에 적힌 라이브러리들을 추가해 줍니다.

 

 

로그인 페이지를 렌더링하기 위해 Controller와 html 파일을 생성합니다.

html 파일의 위치는 src/main/resources/templates 디렉토리 하위에 생성하면 됩니다.

카카오 로그인을 클릭했을 때 /oauth2/autorization/kakao로 요청을 보내게끔 설정해 둡니다.

해당 경로도 Spring OAuth2 Client 라이브러리에서 기본으로 설정된 경로이며 이 경로 또한 커스텀하실 수 있습니다.

 

 

Spring Boot 설정 (application.yml + Spring Security)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
  security:
    oauth2:
      client:
        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
        registration:
          kakao:
            client-id: 클라이언트 ID값
            client-secret: 클라이언트 시크릿값
            client-authentication-method: client_secret_post
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            authorization-grant-type: authorization_code
            client-name: kakao
            scope:
              - profile
              - account_email
cs

application.yml에 다음과 같이 설정합니다. client-idclient-secret에 들어갈 값은 카카오 디벨로퍼 사이트에서 생성한 값을 세팅해서 주면 됩니다. redirect-uri은 카카오 디벨로퍼 사이트에서 설정한 redirect-uri 값을 세팅해서 주면 됩니다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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();
        
        // DB 저장로직이 필요하면 추가
 
        return new DefaultOAuth2User(authorities, oAuth2User.getAttributes(), userNameAttributeName);
    }
}
cs

DefaultOAuth2UserService 클래스를 상속한 OAuth2UserService 클래스를 생성해 줍니다.

loadUser 메서드가 실행될 시점엔 이미 Access Token이 정상적으로 발급된 상태이며 super.loadUser 메서드를 통해 Access Token으로 User 정보를 조회해 옵니다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeHttpRequests(config -> config.anyRequest().permitAll());
        http.oauth2Login(oauth2Configurer -> oauth2Configurer
                        .loginPage("/login")
                        .successHandler(successHandler())
                        .userInfoEndpoint()
                        .userService(oAuth2UserService));
 
        return http.build();
    }
 
    @Bean
    public 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();
        });
    }
}
cs

Security Config는 위 처럼 작성합니다. 로그인 페이지는 /login으로 해두고 (기본값도 /login)

로그인이 정상적으로 이뤄지면 사용자의 id를 응답하는 간단한 코드입니다.

 

 

테스트

이제 로그인 페이지로 접속 후 카카오 로그인을 해보겠습니다.

 

 

로그인이 정상적으로 완료되었고 응답으로 사용자 id가 반환되었습니다.

 

 

동작원리 살펴보기

카카오 로그인은 OAuth 2.0 기반의 서비스입니다. OAuth 2.0에 대한 자세한 내용은 여기에서 확인하실 수 있습니다.

 

 

위 사진은 카카오 로그인부터 회원 확인 및 가입의 프로세스입니다.

단계별로 Spring OAuth2에서는 이를 어떻게 구현하였는지 차근차근 살펴보겠습니다.

 

로그인 요청

사용자는 카카오로 로그인하기 위해 서버에 요청을 보냅니다. 이때 서버의 엔드포인트는 /oauth2/authorization/kakao입니다. 사용자는 이 엔드포인트로 요청을 보내면 카카오 로그인 페이지로 Redirect가 되는데 한번 살펴보겠습니다.

 

 

OAuth2AuthorizationRequestRedirectFilter#doFitlerInteral 메서드가 호출되며 DefaultOAuth2AuthorizationRequestResolver#resolve 메서드를 내부적으로 호출하고 결과가 null이 아니면 인가요청이 필요로 하는 페이지로 redirect 시켜줍니다.

 

 

registrationId의 메서드가 null이면 그대로 null을 반환합니다.

resolveRegistrationId 메서드와 resolve 메서드를 차례대로 확인해 보겠습니다.

 

 

resolveRegistrationId 메서드에서는 요청 url이 해당 pattern과 일치하면 uri variable name의 값을 꺼내서 반환하게 되어있습니다. 여기에서 REGISTRATION_ID_URI_VARIABLE_NAME 변수에는 'registrationId' 라는 값이 저장되어 있습니다.

 

 

authorizationRequestMatcher의 url pattern은 autorizationRequestBaseUri + / + {registrationId} 입니다.

authorizationRequestBaseUri의 값을 어디서 주입해 주는지 확인해 보겠습니다.

 

 

OAuth2ClientConfigurer 파일에서 authorizationRequestBaseUri를 주입해 주는데 해당 값은 /oauth2/authorization으로 되어있습니다.

 

 

resolve 메서드에서는 redirect-uri을 만들어서 가공합니다. 위 코드에서 findByRegistrationId로 ClientRegistration을 조회해 오는데 저 값이 application.yml에 정의되있는 registrationId (kakao)에 해당하는 registration 값을 가져옵니다.

 

정리하자면

1. 사용자가 /oauth2/authorization/kakao로 요청
2. DefaultOAuth2AuthorizationRequestResolver에서 url pattern 검사를 함
3. /oauth2/authorization/{registrationId} pattern에 맞으면 registrationId 변수에 바인딩 된 uri 변수를 가져옴
4. uri변수값을 가져와서 ClientRegistration을 조회
5. ClientRegistration에 들어있는 정보로 redirect-uri을 가공
6. 사용자한테 가공된 redirect-uri로 해당 페이지로 redirect를 함

이게 로그인 요청에 대한 흐름입니다.

 

 

인가코드 요청

해당 페이지로 redirect 되었으면 사용자는 이메일, 패스워드를 입력 후 로그인을 합니다.

로그인하면 위에 화살표가 가리키는 query string에서 redirect-uri 파라미터에 정의된 주소로 이동하는데 이때 인가 코드를 넘겨줍니다.

 

 

OAuth2LoginAuthenticationFilter#attemptAuthentication 메서드가 호출되며 내부에서 ProviderManger의 authenticate 메서드를 호출합니다.

 

 

ProviderManager는 AuthenticationProvider를 가지고 있는데 그 중 OAuth2LoginAuthenticationProvider의 authenticate가 호출되며 authenticate 메서드 내부에서는 OAuth2AuthorizationCodeAuthenticationProvider의 authenticate를 한 번 더 호출합니다.

 

 

OAuth2AuthorizationCodeAuthenticationProvider에서는 인가받은 코드로 Access Token API를 호출합니다.

 

 

Access Token을 받아오면 OAuth2User#loadUser 메서드를 호출합니다. 이때 userService에는 위에 Security Config 파일에서 정의했던 DefaultOAuth2UserService를 상속한 OAuth2UserService#loadUser 메서드가 호출됩니다.

 

 

여기서 super.loadUser를 통해서 Access Token으로 사용자 정보를 조회합니다.

 

 

받아온 정보를 로그를 통해 확인해 보니 사용자 정보를 정상적으로 조회한 것 같습니다.

 

 

로그인이 정상적으로 완료되었으니 사용자의 id 값을 응답 body에 추가되게끔 작성하였는데 잘 된 것 같습니다.

 

 

참고

https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html

 

OAuth2 :: Spring Security

Spring Security provides comprehensive OAuth 2 support. This section discusses how to integrate OAuth 2 into your servlet based application.

docs.spring.io

 

https://developers.kakao.com/docs/latest/ko/kakaologin/common

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함