ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [OAuth] 소셜 로그인 구현: 2. 카카오 로그인 구현
    Today I Learned 2023. 8. 1. 21:51

    본 글에서는 Spring Boot, React를 사용하는 백엔드/프론트엔드 애플리케이션에서 카카오 로그인을 구현하는 과정을 정리했다.
     
    소셜 로그인을 구현하는 전체 과정은 다음의 순서에 걸쳐서 작성될 예정이다.

    1. OAuth 이해하기
    2. 카카오 로그인 구현
    3. 네이버 로그인 구현

     

    카카오 소셜 로그인 플로우

    카카오에서 제공하는 소셜 로그인을 위한 플로우는 다음과 같다.
     

     
    해당 플로우를 OAuth 프로토콜 플로우에 대입하여 살펴보았다. 로그인 방식으로는 사용자 서버 애플리케이션이 카카오 계정 소유자의 정보를 이용해 생성한 액세스 토큰을 사용자 클라이언트 애플리케이션에 반환하는 방식을 적용했다.
     

    권한 부여

    1. 사용자 클라이언트 애플리케이션에서 카카오 계정 소유자의 개인정보 접근 권한을 획득하기 위해 카카오 계정 소유자를 카카오 인증 서버로 안내한다.
    2. 카카오 계정 소유자는 카카오 계정으로 로그인한 뒤, 사용자 애플리케이션이 지정한 개인정보를 제공하기 위한 동의를 진행한다.
    3. 모든 필수 동의조건에 동의했을 경우, 카카오 인증 서버는 302 Found 응답에 지정된(개발자가 등록한) 리다이렉트 URL와 인증 코드가 쿼리 파라미터로 포함된 URI를 Location 속성으로 포함하여 사용자 클라이언트 애플리케이션에 반환한다.
     

    액세스 토큰 발급

    4. 사용자 클라이언트 애플리케이션은 등록했던 리다이렉트 URI에서 사용자 서버 애플리케이션에 로그인하기 위한 액세스 토큰 요청을 송신한다. 이때 요청 본문에 인증 코드를 포함한다.
    5. 사용자 서버 애플리케이션은 전달받은 해당 인증 코드를 이용해 카카오 인증 서버에 액세스 토큰 발급을 요청하는 POST 요청을 송신한다.
    6. 카카오 인증 서버는 인증 코드가 유효할 경우 카카오 계정 소유자의 허가된 개인정보에 접근할 수 있는 액세스 토큰 및 관련 내용들을 본문에 포함하여 응답으로 반환한다.
     

    리소스 획득

    7. 사용자 서버 애플리케이션은 카카오 계정 소유자의 허가된 개인정보를 카카오 API 서버에 요청한다. 요청 헤더에는 카오 인증 서버로부터 반환받은 세스 토큰을 포함한다.
    8. 카카오 API 서버는 액세스 토큰이 유효할 경우 카카오 계정 소유자의 허가된 개인정보를 본문에 포함해 응답으로 반환한다.
    9. 사용자 서버 애플리케이션은 반환받은 카카오 계정 소유자의 개인정보에서 식별자 등을 이용해 사용자 애플리케이션에서 사용할 액세스 토큰을 생성해 반환한다. 이 과정에서 필요하다면 사용자 식별을 위한 리소스를 생성해 서버에 저장한다.
     
    해당 플로우를 따르는 간단한 클라이언트, 서버 애플리케이션을 구현해보았다.
     
     

    Kakao Developers에 애플리케이션 등록

    다음의 링크를 통해 Kakao Developers의 '내 애플리케이션'으로 이동한다.
    https://developers.kakao.com/console/app
     
    구현하는 소셜 로그인 애플리케이션과 연동하기 위해 '애플리케이션 추가하기'를 수행한다.
     

    사업자명에는 본인의 이름을 입력한다.

     
    저장하면, 다음과 같이 구현하는 애플리케이션과 연동할 수 있는 새로운 애플리케이션이 생성된 것을 확인할 수 있다.
     

     
    생성한 애플리케이션 대시보드에 들어가면, 카카오 서버가 구현할 애플리케이션을 식별하기 위한 앱 키가 부여되어 있는 것을 확인할 수 있다.
     

     
    '플랫폼 설정하기'에 진입해 플랫폼을 등록한다. 구현할 애플리케이션은 웹 애플리케이션이므로, 'Web 플랫폼 등록'을 진행한다.
     

     
    애플리케이션을 로컬 서버에서만 실행할 것이므로, 프로토콜은 http, 호스트명은 localhost, 포트 주소는 8080을 사용했다. 배포된 애플리케이션일 경우, 배포된 애플리케이션에 맞게 사이트 도메인을 등록해야 한다.
     

     

     
    등록을 마칠 경우, 다음과 같이 대시보드의 플랫폼 란이 변경되는 것을 확인할 수 있다.
     

     
    이제 구현할 애플리케이션이 카카오 로그인 API를 통해 사용자의 개인정보에 접근하는 권한을 갖는 인증 코드를 받을 수 있도록 하기 위해 카카오 로그인을 활성화하는 절차를 진행한다.
     

     
    활성화 설정 상태를 OFF에서 ON으로 토글한다.
     

     

     
    이제 구현할 애플리케이션이 정상적으로 인증 코드를 획득하는 경우 이동해야 하는 Redirect URI를 등록한다. 이 역시 배포된 애플리케이션인 경우에는 배포된 애플리케이션에 맞게 URI를 등록해야 한다.

     
    사용자가 카카오 인가 서버에서 개인정보 제공에 동의해 애플리케이션에 권한 부여가 성공적으로 진행되었을 경우, 카카오 인가 서버는 인증 코드를 해당 URI에 Query Parameter 형식으로 제공하게 된다.
     
    만약 액세스 토큰을 이용해 사용자 계정의 식별자 값뿐만 아니라 다른 추가적인 정보를 획득하고자 하는 경우, 메뉴에서 '동의항목' 란으로 이동해 사용자에게 제공되는 동의 상태 설정을 변경해준다.
     

     
    그 외에 애플리케이션 설정에 대해 추가적인 정보가 필요한 경우, 다음의 링크를 참고할 수 있다.
    https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite
     
     
    다음으로는 애플리케이션 구현에 관련된 내용이다. 본 글에서는 가급적 카카오 로그인에 관련된 소스코드만을 드러내고자 했기 때문에 본 글에서 기술한 내용들은 개인에 맞게 최적화해 적용하는 과정이 필요하다.
     

    클라이언트 애플리케이션 구현

    클라이언트 애플리케이션은 React를 사용해 구현했다. 전체 소스코드는 다음의 링크에서 확인할 수 있다.
    https://github.com/hsjkdss228/tech-studies/tree/main/20230721-spring-oauth2/client
     

    주요 추가 의존성

    parcel
    클라이언트 애플리케이션을 빌드한다.

    react-router-dom
    URL 링크에 따라 화면에 표출시킬 컴포넌트를 선택한다.

    usehooks-ts
    useLocalStorage Custom Hook을 사용할 수 있다. 브라우저의 로컬 스토리지에 존재하는 Key-Value 쌍들 중 지정한 Key에 해당하는 Value를 useLocalStorage의 setValue 형식 함수를 호출해 변경했을 경우, 브라우저에 표출되고 있는 컴포넌트들 중 로컬 스토리지에서 해당 Key-Value 쌍을 사용하고 있는 컴포넌트를 리렌더링한다. 본 애플리케이션에서는 서버 애플리케이션에서 반환하는 액세스 토큰을 로컬 스토리지에 저장해 사용한다.

    axios
    Promise를 기반으로 HTTP 요청을 생성해 지정한 URL에 송신하고, 응답을 반환받는다.

     

    Kakao SDK for JavaScript

    본 애플리케이션은 카카오가 보유한 사용자의 개인정보에 대한 접근 권한을 부여하는 과정을 카카오에서 제공하는 로그인 함수를 호출하는 방식을 따른다. 따라서 클라이언트 애플리케이션에 다음과 같이 JavaScript SDK 스크립트를 포함(include)시켰다.
     

    index.html

    진입점을 정의하는 <body> 요소 내부에 다음과 같이 <script> 요소들을 추가했다.

    <body>
      <div id="app">
        Now Loading...
      </div>
      <!-- React -->
      <script type="module" src="./src/index.jsx"></script>
      <!-- Kakao SDK for JavaScript -->
      <script
        src="https://t1.kakaocdn.net/kakao_js_sdk/2.3.0/kakao.min.js"
        integrity="sha384-70k0rrouSYPWJt7q9rSTKpiTfX6USlMYjZUtr1Du+9o4cGvhPAWxngdtVZDdErlh"
        crossorigin="anonymous"
      ></script>
    </body>

     
    만약 src 속성에 포함되어 있는 SDK 버전이나 intregrity 속성 값이 변경되었을 경우, 다음의 링크에서 확인할 수 있다.
     
    https://developers.kakao.com/docs/latest/ko/sdk-download/js
     

    src/index.jsx

    애플리케이션 진입점에서 window.Kakao 객체를 활성화시키기 위해 다음과 같은 소스코드를 추가했다.

    const kakao = window.Kakao;
    // 내 애플리케이션 → 애플리케이션 → 요약 정보 → 앱 키의 JavaScript 키에서 해당 키를 확인할 수 있다.
    const kakaoJavaScriptKey = 'JAVASCRIPT_KEY';
    
    if (!kakao.isInitialized()) {
      kakao.init(kakaoJavaScriptKey);
    }

     

    권한 부여

    사용자가 '카카오 아이디로 로그인' 버튼을 눌러 카카오 로그인을 시작하면, 클라이언트 애플리케이션은 Kakao.Auth.authorize() 함수를 호출한다. 이때 인자 객체의 요소로는 클라이언트 애플리케이션에 대한 권한 부여가 성공했을 경우 리다이렉트할 URI인 redirectUri 값을 제공한다. 해당 URI로는 카카오에 등록한 리다이렉트 URI를 부여해야 한다.
     
    authorize() 함수의 자세한 레퍼런스는 다음의 링크에서 확인할 수 있다.
    https://developers.kakao.com/sdk/reference/js/release/Kakao.Auth.html#.authorize
     

    src/pages/LoginPage.jsx

    import useKakao from '../hooks/useKakao';
    
    export default function LoginPage() {
      // 브라우저의 로컬 스토리지에 액세스 토큰이 존재하는 경우, /profile로 리다이렉트한다.
      // ...
    
      const kakao = useKakao();
    
      const host = 'http://localhost:8080';
    
      const handleClickKakaoLoginButton = async () => {
        kakao.Auth.authorize({
          redirectUri: `${host}/oauth`,
        });
      };
    
      return (
        <div>
          <button
            type="button"
            onClick={handleClickKakaoLoginButton}
          >
            카카오 아이디로 로그인
          </button>
        </div>
      );
    }

     

    src/hooks/useKakao.js

    export default function useKakao() {
      return window.Kakao;
    }

     

    액세스 토큰 발급

    리다이렉트된 페이지에는 쿼리 파라미터로 인증 코드가 전달된다. 해당 인증 코드를 추출한 뒤, 서버 애플리케이션에 액세스 토큰 발급을 요청한다. 서버로부터 액세스 토큰이 전달되면 액세스 토큰을 브라우저의 로컬 스토리지에 저장한 뒤, 액세스 토큰을 이용해 서버 애플리케이션에 API 요청을 송신하는 페이지로 이동한다.
     

    src/pages/OAuthPage.jsx

    import { useEffect } from 'react';
    import { useLocation, useNavigate } from 'react-router-dom';
    
    import { useLocalStorage } from 'usehooks-ts';
    
    import { apiService } from '../services/ApiService';
    
    export default function OAuthPage() {
      const [, setAccessToken] = useLocalStorage('accessToken', '');
    
      const { search } = useLocation();
    
      const queryParams = search.substring(1);
    
      if (queryParams.startsWith('error')) {
        return (
          <p>로그인을 취소했습니다.</p>
        );
      }
    
      const authorizationCode = queryParams.split('=')[1];
    
      const navigate = useNavigate();
    
      const requestAccessToken = async () => {
        const { accessToken } = await apiService.requestAccessToken(authorizationCode);
    
        if (accessToken) {
          setAccessToken(accessToken);
          apiService.setAccessToken(accessToken);
          navigate('/profile');
        }
      };
    
      useEffect(() => {
        requestAccessToken();
      }, []);
    
      return (
        <p>로그인을 진행하는 중입니다...</p>
      );
    }

     

    src/services/ApiService.js

    import axios from 'axios';
    
    export default class ApiService {
      async requestAccessToken(authorizationCode) {
        const { data } = await axios.post(
          'http://localhost:8000/oauth/token',
          { authorizationCode },
        );
        return data;
      }
    }
    
    export const apiService = new ApiService();

     
    서버로부터 전달받은 액세스 토큰은 브라우저의 로컬 스토리지에 저장하고 있는 방식으로 사용자는 브라우저에 로그인 상태를 유지할 수 있다.
     
     

    서버 애플리케이션

    서버 애플리케이션은 Spring Boot를 사용해 구현했다. 전체 소스코드는 다음의 링크에서 확인할 수 있다.
    https://github.com/hsjkdss228/tech-studies/tree/main/20230721-spring-oauth2/backend
     

    주요 추가 의존성

    Spring Web
    Spring MVC를 사용해 RESTful을 포함한 웹 애플리케이션을 빌드한다. 서버 내에서 요청을 송신하기 위한 클라이언트 역할을 수행할 수 있는 RestTemplate을 내재하고 있다.

    Lombok
    생성자, Getter와 같이 반복적으로 재사용되는 코드들을 어노테이션을 부여하는 방식으로 정의할 수 있다.

     
    본 서버 애플리케이션에서는 카카오 인가 서버로부터 반환받은 액세스 토큰을 클라이언트 애플리케이션에 직접 반환해 사용하는 방식으로 구현하였으나, 서버가 전달받은 액세스 토큰을 이용해 카카오 API 서버에 사용자의 정보를 요청해 반환받은 뒤 해당 정보를 조합하거나 별도의 비즈니스 로직을 거쳐 서버에서 사용할 리소스로 변환해 영속화하고, 해당 리소스를 인코딩하는 식으로 자체적으로 액세스 토큰을 만들어 반환하는 방식으로 구현할 수도 있다.
     

    액세스 토큰 발급

    클라이언트 애플리케이션으로부터 액세스 토큰 발급을 위한 요청이 전달되면, 서버 애플리케이션은 해당 요청을 Controller에게 전달한다. 비즈니스 로직에는 카카오 인가 서버로부터 액세스 토큰을 전달받기 위한 요청을 송신하는 과정이 포함된다.
     
    요청 본문에는 사용자가 전달한 인증 코드, 애플리케이션 식별을 위한 REST API 키, 인가 코드를 리다이렉트받았던 URI 등을 레퍼런스에 맞게 포함한다.
     
    액세스 토큰 발급을 위한 API 요청의 자세한 레퍼런스는 다음의 링크를 참고할 수 있다.
    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token
     

    utils/KakaoApiService.java

    @Service
    public class KakaoApiService {
        private final String clientId;
        private final String redirectUrl;
        private final RestTemplate restTemplate;
        
        public KakaoApiService(@Value("${kakao.clientId}") String clientId,
                               @Value("${kakao.redirectUrl}") String redirectUrl,
                               RestTemplate restTemplate) {
            this.clientId = clientId;
            this.redirectUrl = redirectUrl;
            this.restTemplate = restTemplate;
        }
        
        public String requestAccessToken(String authorizationCode) {
            String url = "https://kauth.kakao.com/oauth/token";
    
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    
            MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
            formData.add("grant_type", "authorization_code");
            formData.add("client_id", clientId);
            formData.add("redirect_url", redirectUrl);
            formData.add("code", authorizationCode);
    
            HttpEntity<MultiValueMap<String, String>> request
                = new HttpEntity<>(formData, headers);
    
            ResponseEntity<KakaoOAuthAccessTokenDto> response = restTemplate
                .exchange(
                    url,
                    HttpMethod.POST,
                    request,
                    KakaoOAuthAccessTokenDto.class
                );
    
            String accessToken = response.getBody().getAccessToken();
    
            if (!response.getStatusCode().is2xxSuccessful()
                || accessToken == null) {
                throw new KakaoRequestAccessTokenFailed();
            }
    
            return accessToken;
        }
    }
    
    // dtos/KakaoOAuthAccessTokenDto.java
    @NoArgsConstructor
    @Getter
    public class KakaoOAuthAccessTokenDto {
        @JsonProperty("token_type")
        private String tokenType;
    
        @JsonProperty("access_token")
        private String accessToken;
    
        @JsonProperty("expires_in")
        private String expiresIn;
    }
    
    // exceptions/KakaoRequestAccessTokenFailed.java
    public class KakaoRequestAccessTokenFailed extends RuntimeException {
        public KakaoRequestAccessTokenFailed() {
            super("카카오 Access Token 획득 요청에 실패했습니다.");
        }
    }

     
    KakaoApiService에서 사용할 REST API 키, 리다이렉트 URI, RestTemplate 인스턴스는 다음과 같이 주입받을 수 있다.
     

    // main/resources/application.properties
    // 각 Value 값들에는 값들을 직접 입력하거나, 환경변수를 부여하는 방식으로 전달한다.
    kakao.clientId=REST_API_KEY
    kakao.redirectUrl=REDIRECT_URL
    // config/RestTemplateConfig.java
    @Configuration
    public class RestTemplateConfig {
        @Bean
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
    }

     

    리소스 획득

    서버 애플리케이션이 카카오 인가 서버로부터 반환받은 액세스 토큰을 이용해 카카오 API 서버에 요청을 송신하고, 사용자의 개인정보 리소스를 응답으로 반환받는 로직은 다음과 같다.
     
    사용자의 개인정보 데이터를 얻기 위한 API 요청의 자세한 레퍼런스는 다음 링크를 참고할 수 있다. 
    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info
     

    utils/KakaoApiService.java

    @Service
    public class KakaoApiService {
        // KakaoApiService에 다음의 메서드가 추가된다.
        public KakaoUserProfileDto requestUserProfile(String accessToken) {
            String url = "https://kapi.kakao.com/v2/user/me";
    
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            headers.set("Authorization", "Bearer " + accessToken);
    
            HttpEntity<?> request = new HttpEntity<>(headers);
    
            ResponseEntity<KakaoUserProfileDto> response = restTemplate.exchange(
                url,
                HttpMethod.GET,
                request,
                KakaoUserProfileDto.class
            );
    
            return response.getBody();
        }
    }
    
    // dtos/KakaoUserProfileDto.java
    @NoArgsConstructor
    @Getter
    public class KakaoUserProfileDto {
        @JsonProperty("id")
        private String id;
    
        @JsonProperty("kakao_account")
        private KakaoAccountDto kakaoAccount;
    }
    
    // dtos/KakaoAccountDto.java
    @NoArgsConstructor
    @Getter
    public class KakaoAccountDto {
        @JsonProperty("profile")
        private KakaoProfileDto kakaoProfile;
    
        @JsonProperty("email")
        private String email;
    }

     
    응답 DTO에서는 @JsonProperty("key") 어노테이션을 통해 JSON 요소의 Key에 해당하는 값의 필드를 가져올 수 있다. 만약 응답 본문에 포함되는 요소의 타입이 객체 타입인 경우, DTO 내 해당 요소의 타입은 해당 객체를 나타내기 위한 객체 타입으로 정의되어야 한다.
     
     

    References

    - 카카오 로그인 - 이해하기
    - 카카오 로그인 - 설정하기
    카카오 로그인 - REST API
    - 카카오 로그인 - JavaScript
    Kakao SDK for JavaScript - 시작하기
    Kakao SDK for JavaScript - SDK 다운로드
    [React] 카카오 로그인 (JavaScript SDK 초기화)
    Kakao.Auth.authorize()
    location.pathname
    - RestTemplate Post Request with a JSON
     
     
     

    댓글

Designed by Tistory.