[OAuth]Spring Security 없이 카카오 로그인 구현하기

김태희·2021년 7월 6일
4
post-thumbnail

OAuth(Open Authorization) 개념

산업 표준 프로토콜이다.

왜 사용하는가?

  • 사용자는 새로운 서비스에 회원가입(ID/PW 제공)하기 꺼려한다.
  • 기존에 가입되어있는 서비스(Kakao)에 로그인(인증)하고 정보를 선택적으로 제공(인가)하여 새로운 서비스에 로그인 할 수 있다.

OpenID와의 차이

OpenID도 인증을 위한 표준 프로토콜이고 HTTP를 사용한다는 점에서는 OAuth와 같다.

그러나 OpenID와 OAuth의 목적은 다르다. OpenID의 주요 목적은 인증(Authentication)이지만, OAuth의 주요 목적은 허가(Authorization)이다.

물론 OAuth에서도 인증 과정이 있다. 가령 Facebook의 OAuth를 이용한다면 Facebook의 사용자인지 인증하는 절차를 Facebook(Service Provider) 처리한다. 하지만 OAuth의 근본 목적은 해당 사용자의 담벼락(wall)에 글을 쓸 수 있는 API를 호출할 수 있는 권한이나, 친구 목록을 가져오는 API를 호출할 수 있는 권한이 있는 사용자인지 확인하는 것이다.

OAuth를 사용자 인증을 위한 방법으로 쓸 수 있지만, OpenID와 OAuth의 근본 목적은 다르다는 것을 알아야 한다.

OAuth 1.0의 단점

  • 웹 애플리케이션이 아닌 애플리케이션에서는 사용하기 곤란하다
  • 구현이 복잡하다.
  • 절차가 복잡하여 OAuth 구현 라이브러리를 제작하기 어렵다.

OAuth 2.0

  • 웹 애플리케이션이 아닌 애플리케이션 지원 강화
  • 암호화가 필요 없다. HTTPS를 사용하고 HMAC을 사용하지 않는다.
  • Siganature 단순화 정렬과 URL 인코딩이 필요 없다.

Access Token 갱신 OAuth 1.0에서 Access Token을 받으면 Access Token을 계속 사용할 수 있었다. 트위터의 경우에는 Access Token을 만료시키지 않는다. OAuth 2.0에서는 보안 강화를 위해 Access Token의 Life-time을 지정할 수 있도록 했다.

이외에도 OAuth 2.0에서 사용하는 용어 체계는 OAuth 1.0과 완전히 다르다. 같은 목적의 다른 프로토콜이라고 이해는 것이 좋다. 하지만 아직 최종안이 나오지 않았기 때문에, 현재로서는 OAuth 2.0의 특징만 파악하는 것으로도 충분할 듯 하다.

비교OAuth 1.0OAuth 2.0
유효기간Access 토큰의 유효기간 없음(무제한)- Access 토큰 유효기간 부여
- 만료 시 Refresh 토큰 이용하여 재발급
클라이언트웹 서비스웹, 앱 등


Kakao Developers 플랫폼 설정

애플리케이션 생성

Kakao Developers 에 접속해 로그인을 한다.

상단 메뉴 중에 "내 애플리케이션" 을 클릭한다.

가운데에 보이는 "애플리케이션 추가하기" 를 누른다.


"앱 이름""사업자명" 에 적당한 앱 이름을 입력하고, 저장을 누른다.


서비스 도메인 주소, REDIRECT URI 등록

앱을 클릭한다.

왼쪽 메뉴에서 "플랫폼" 을 클릭한다.


맨 아래에 있는 "Web 플랫폼 등록" 버튼을 누른다.


사이트 도메인 주소를 입력하고, 저장을 누른다.
여기에서는 로컬 테스트용이기 때문에 http://localhost:8080 로 입력했다.

그러면 이와 같은 상태가 된다. 그리고 "등록하러 가기" 링크를 클릭한다.

"활성화 설정" 의 상태 토글 버튼을 눌러 상태를 "OFF" 에서 "ON" 으로 바꾼다.

그 아래에 있는 "Redirect URI" 등록 버튼을 누른다.

현재는 로컬 테스트용이기 때문에 위와 같이 입력하고 저장을 누른다.

그러면 이와 같은 상태가 된다.


개인정보 제공 동의항목 설정

왼쪽 메뉴의 "카카오 로그인" 탭의 하위 항목들 중, "동의항목" 에 들어간다.

카카오로그인 시, 제공받고 싶은 개인정보들을 제공받을 수 있도록 설정한다.

닉네임, 프로필 사진, 이메일, 연령대에 대해 설정을 완료한 모습이다.


Client Secret 발급

왼쪽 메뉴의 "보안" 탭에 들어간다.

이 설정은 안 해도 동작은 한다.

토큰 발급 시, 보안을 강화하기 위해 추가 확인하는 코드를 발급받는 설정이다.

보안은 중요하니 설정해보도록 하자.

가운데에 있는 "코드 생성" 버튼을 눌러 생성한다.

그리고 "활성화 상태""사용" 으로 바꾸자.

이제 Kakao developers에서의 설정은 끝났다!!

이를 사용하는 서비스 구현부를 만들어 보자.


카카오 로그인(OAuth 2.0) 흐름에 따른 구현

사용자가 내 서비스에 접속한다.
"카카오 간편 로그인" 을 클릭한다.

1. 인증 코드 요청

  • REST API 키, REDIRECT URI를 같이 넘겨준다.
GET /oauth/authorize?client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&response_type=code HTTP/1.1
Host: kauth.kakao.com

사용자에게 카카오 로그인 화면이 뜬다.
사용자가 로그인을 한다.
사용자가 내 서비스에 제공할 항목들을 선택 후, 확인 버튼을 누른다.

2. 인증 코드 전달

  • 카카오 서버에서 REDIRECT URI 주소로 Redirect 한다.
  • GET 쿼리 파라미터로 code={AUTHORIZE_CODE} 를 보내준다.
HTTP/1.1 302 Found
Content-Length: 0
Location: {REDIRECT_URI}?code={AUTHORIZE_CODE}
인증 코드를 받는 컨트롤러 코드
@RestController
public class OAuthControllerV1 {

    @GetMapping("/auth/kakao/callback")
    public String home(String code) {
        return "인증 코드 : " + code;
    }
}

3. 인증 코드로 토큰 요청

위의 파라미터들을 모두 포함해 토큰 요청을 보낸다.

4. 토큰 전달

"access_token" 값을 받는다.

인증 코드를 받은 후, 위의 파라미터들을 모두 포함해 Access 토큰 요청을 보내고 응답을 받는 코드
@RequiredArgsConstructor
@RestController
public class OAuthControllerV2 {
    private final ObjectMapper objectMapper;

    @GetMapping("/auth/kakao/callback")
    public OAuthToken home(String code) {

        // 3. 인증 코드를 받은 후, 파라미터들을 포함해 토큰 요청
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code"); // 고정값
        params.add("client_id", "{REST API 키}");
        params.add("redirect_uri", "http://localhost:8080/auth/kakao/callback");
        params.add("code", code);
        params.add("client_secret", "{Client Secret}");

        // HttpHeader 오브젝트 생성
        HttpHeaders headersForAccessToken = new HttpHeaders();
        headersForAccessToken.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        // HttpHeader와 HttpBody를 하나의 오브젝트에 담기
        HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = new HttpEntity<>(params, headersForAccessToken);

        // POST방식으로 key-value 데이터를 요청(카카오쪽으로)
        RestTemplate rt = new RestTemplate(); //http 요청을 간단하게 해줄 수 있는 클래스

        // 실제로 요청하기
        // Http 요청하기 - POST 방식으로 - 그리고 response 변수에 응답을 받음.
        ResponseEntity<String> accessTokenResponse = rt.exchange(
            "https://kauth.kakao.com/oauth/token",
            HttpMethod.POST,
            kakaoTokenRequest,
            String.class
        );

        // JSON 응답을 객체로 변환
        OAuthToken oauthToken = null;
        try {
            oauthToken = objectMapper.readValue(accessTokenResponse.getBody(), OAuthToken.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        return oauthToken;
    }
}
@Getter
public class OAuthToken {
    private String access_token;
    private String token_type;
    private String refresh_token;
    private int expires_in;
    private String scope;
    private int refresh_token_expires_in;
}

5. Access 토큰으로 API 호출

위의 요청으로 카카오 서버에게 사용자 정보를 요청한다.

카카오 서버는 "6.토큰 유효성" 을 확인한다.

7. 응답 전달

사용자의 정보를 응답으로 받는다.

발급받은 Access 토큰으로 API를 호출해서 사용자의 정보를 응답으로 받는 코드
@RequiredArgsConstructor
@RestController
public class OAuthControllerV3 {
    private final ObjectMapper objectMapper;

    @GetMapping("/auth/kakao/callback")
    public KakaoProfile home(String code) {

        // 3, 4 : 인증 코드를 받은 후, 위의 파라미터들을 모두 포함해 Access 토큰 요청을 보내고 응답을 받는 코드
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code"); // 고정값
        params.add("client_id", "{REST API 키}");
        params.add("redirect_uri", "http://localhost:8080/auth/kakao/callback");
        params.add("code", code);
        params.add("client_secret", "{Client Secret}");

        // HttpHeader 오브젝트 생성
        HttpHeaders headersForAccessToken = new HttpHeaders();
        headersForAccessToken.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        // HttpHeader와 HttpBody를 하나의 오브젝트에 담기
        HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = new HttpEntity<>(params, headersForAccessToken);

        //POST방식으로 key-value 데이터를 요청(카카오쪽으로)
        RestTemplate rt = new RestTemplate(); //http 요청을 간단하게 해줄 수 있는 클래스

        // 실제로 요청하기
        // Http 요청하기 - POST 방식으로 - 그리고 response 변수의 응답을 받음.
        ResponseEntity<String> accessTokenResponse = rt.exchange(
            "https://kauth.kakao.com/oauth/token",
            HttpMethod.POST,
            kakaoTokenRequest,
            String.class
        );

        OAuthToken oauthToken = null;
        try {
            oauthToken = objectMapper.readValue(accessTokenResponse.getBody(), OAuthToken.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        // 토큰 전달 받기 완료

        // 5, 6, 7 : 발급받은 Access 토큰으로 API를 호출해서 사용자의 정보를 응답으로 받는 코드
        HttpHeaders headersForRequestProfile = new HttpHeaders();
        headersForRequestProfile.add("Authorization", "Bearer " + Objects.requireNonNull(oauthToken).getAccess_token());
        headersForRequestProfile.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        HttpEntity<MultiValueMap<String, String>> kakaoResourceProfileRequest = new HttpEntity<>(headersForRequestProfile);

        // Http 요청하기 - POST 방식으로 - 그리고 response 변수의 응답을 받음.
        ResponseEntity<String> resourceProfileResponse = rt.exchange(
            "https://kapi.kakao.com/v2/user/me",
            HttpMethod.POST,
            kakaoResourceProfileRequest,
            String.class
        );

        KakaoProfile profile = null;
        try {
            profile = objectMapper.readValue(resourceProfileResponse.getBody(), KakaoProfile.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        return profile;
    }
}
@Getter
public class KakaoProfile {
    private int id;
    private String connected_at;
    private Properties properties;
    private KakaoAccount kakao_account;

    @Getter
    public static class Properties {
        private String nickname;
        private String profile_image;
        private String thumbnail_image;
    }

    @Getter
    public static class KakaoAccount {
        private Boolean profile_needs_agreement;
        private Profile profile;
        private Boolean has_email;
        private Boolean email_needs_agreement;
        private Boolean is_email_valid;
        private Boolean is_email_verified;
        private String email;
        private Boolean has_age_range;
        private Boolean age_range_needs_agreement;
        private Boolean has_birthday;
        private Boolean birthday_needs_agreement;
        private Boolean has_gender;
        private Boolean gender_needs_agreement;

        @Getter
        public static class Profile {
            private String nickname;
            private String thumbnail_image_url;
            private String profile_image_url;
        }
    }
}

참조 :
https://d2.naver.com/helloworld/24942
https://developers.kakao.com/

profile
Web Back-End (Spring, JPA, AWS)

1개의 댓글

comment-user-thumbnail
2021년 8월 23일

정말 멋져요!

답글 달기