[Kakao Login] 카카오 로그인 구현하기 - 로그인요청, 인가코드 받기, 토큰 받기

leeng·2023년 3월 30일
1

kakaoLogin

목록 보기
1/3

개인 프로젝트를 만들면서 소셜 로그인 기능도 구현해보고 싶었다. 그래서 일단 제일 대중적인 카카오 로그인을 구현해보기로 결정!
우선은 라이브러리 없이 로그인을 구현해보고 나중에 OAuth2도 적용해 볼 예정이다.
그리고 JWT 토큰이랑 Spring Security도 입혀가보겠다 ㅎㅎ

사싷 카카오 로그인을 구현하기 위해 가장 먼저 해야할 일은 kakao developers 계정을 만들고, 클라이언트 키를 발급받고 등등이 있다. 이건 다른 분들이 참고할 만한 글을 많이 작성해 놓아서 따로 여기에는 정리하지 않으려 한다.

아래 사진은 카카오 개발자 사이트에서 정리해운 카카오 로그인 과정 시퀀스 다이어그램이다.
이걸 참고해서 차근차근 하나씩 구현해나가보자.

본격적으로 구현하기 전에

사실 문서를 보고 개발해 보는 게 거의 처음이라 본인은 초반에 많이 헤맸는데, 한번 방법을 알고 나니까 그 다음부터는 어렵지 않았다. 그래서 혹시라도 감도 안오는 분들을 위해 대략적인 방법을 설명해 보려고 한다.

  1. 일단 각 기능에 필요한 url을 확인하고
  2. 요청 파라미터를 확인하여 필요한 파라미터 설정해주고
  3. 응답이 어떤 것이 오는지 확인하여 json으로 파싱하여 받아주면 된다.

Step 1: 인가코드 받기

1~5. 로그인 요청 ~ 동의 후 로그인

1~5번까지의 과정 중 사실 코드로 해야할 일은 로그인 요청을 보내는 것뿐이다.
아래는 카카오 로그인을 요청하는 클라이언트측 소스이다. (참고로 svelte로 작성한 코드이다.)

<script>
    import { beforeUpdate, afterUpdate, onMount, onDestroy } from "svelte";
    import { REST_API_KEY, REDIRECT_URI } from "../../store/store.js";
    import { checkLogIn } from "../../scripts/common.js";
    
    let loggedIn = checkLogIn();
    
    if (loggedIn) {
        // 로그인 상태면 메인으로 보내기
        location.replace('/main');
    }
    
    let scope = "profile_image,account_email,gender,age_range,birthday";
    
    const loginUrl =
        "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=" +
        $REST_API_KEY +
        "&redirect_uri=" +
        $REDIRECT_URI +
        "&scope=" +
        scope;
            
    const login = () => {
        location.href = loginUrl;
    }
</script>

"https://kauth.kakao.com/oauth/authorize" url에 필요한 파라미터들을 설정해주면 된다.
'response_type'은 code로 고정되어 있어서 url과 함께 적어주었고, 'client_id'와
인가 코드를 전달받을 'redirect_uri', 'scope'를 추가로 설정해주었다.
(scope는 사용자에게 동의받고자 하는 동의 항목이다. 수집하고자 하는 항목이라고 이해하면 좋을 듯 싶다.)

이제 로그인 요청 코드 작성은 끝났다.
로그인 요청을 보내면 사용자 화면에서는 아이디, 비밀번호를 입력 화면과 동의 화면이 출력된다.
그리고 사용자가 동의를 하면 파라미터로 전달해준 redirect_uri로 인가 코드가 전달된다.

6. 인가 코드 전달 받기(전달 받은 인가 코드 백엔드 서버로 전달하기)

아래는 redirect uri로 이동한 페이지의 코드이다.

<script>
  import axios from "axios";
  import queryString from "query-string";
  
  let parsed = {};
  let code = "";
  let message = "로그인 중...";
  
  if (typeof window !== "undefined") {
    parsed = queryString.parse(window.location.search);
    if (parsed.code != null) {
      code = parsed.code;
    } else if (parsed.error != null) { // 로그인 취소 시 다시 로그인 화면으로 돌아감      
      console.log("error: " +  parsed.error + ", error discription: " + parsed.error_description);
      location.href = "/login";
    }
  }
  
  if (code != "" && code != null) {
    axios
      .get("http://localhost:8080/api/login/token?code=" + code, {
        withCredentials: true, // cross domain에서 쿠키 받기 위해서  credentials 설정
      })
      .then((res) => {
        if (res.status == 200) {
          let propertyCookie =
            "; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/";
          let nickname = res.data.member.nickname;
          document.cookie = "nickname=" + nickname + propertyCookie;
          setTimeout(() => location.replace("/main"), 100);
        }
      })
      .catch((err) => {
        if (err.response) {
          alert("잘못된 접근입니다.");
          location.replace("/main");
        }
      });
  }
</script>

사용자가 동의 항목에 동의하여 정상적으로 로그인했다면 토큰 받기 요청에 필요한 인가 코드인 'code'가 응답으로 올 것이고, 로그인이 실패했다면 'error'가 응답으로 올 것이다.

로그인에 성공하여 인가코드(code)를 받았다면, 토큰을 받기 위해 백엔드 서버로 전달해야한다.
여기서는 "http://localhost:8080/api/login/token"이 토큰을 발급받기 위한 백엔드 서버의 url이다.

Step 2: 토큰 받기

백엔드 서버는 SpringBoot로 만들어졌다. 아래 login 메소드가 인가코드를 받아 token을 발급받고 그 이후의 로그인 처리까지하는 메소드이다.

@Slf4j
@RestController
@RequiredArgsConstructor
public class LoginController {

    private final MemberService memberService;

@GetMapping("/api/login/token")
    public LoginMemberResponse login(@RequestParam String code, HttpServletRequest request, HttpServletResponse response) throws IOException {
        String authorize_code = "";
        String accessToken = "";
        Member member = null;
        boolean newMember = false;

        if (code != null) {
            authorize_code = code;
        }
        
        // 토큰 받기
        accessToken = getToken(authorize_code);
        // 토큰 유효성 검증
        validateToken(accessToken);
        // 토큰으로 회원정보 조회
        KakaoMemberInfo memberInfo = getKakaoUserInfo(accessToken);
        // 조회한 회원정보로 회원가입 여부 확인
        boolean isExist = memberService.isExistingMember(memberInfo.getKakaoMemberId());

        if (isExist) { // 회원이면 회원 조회
            member = memberService.findOne(memberInfo.getKakaoMemberId());
        } else { // 회원 아니면 회원가입
            member = join(memberInfo);
            newMember = true;
        }
        
        login(request, response, member, accessToken); // 로그인 처리(세션에 토큰이랑 회원정보 저장 후 쿠키)
		......

실질적으로 토큰을 받아오는 메소드는 getToken()이다. 우선 getToken 메소드부터 살펴보자.
HttpURLConnection을 생성한 후 필요한 파라미터들을 설정해준다. 참고로 파라미터의 redirect_uri은 클라이언트에서 인가코드를 받을 때 사용했던 redirect_uri을 적어주면 된다.
그리고 요청을 보낸 후, ResponseCode에 따른 로직을 작성해준다.

    private String getToken(String authorize_code) throws IOException {
        String access_token = "";
        HttpURLConnection connection = getConnection(KakaoApiConstants.URLs.GET_TOKEN_URL, "POST", true);

        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
        StringBuilder sb = new StringBuilder();

        sb.append("grant_type=authorization_code");
        sb.append("&client_id=" + KakaoApiKey.REST_API_KEY);
        sb.append("&redirect_uri=" + KakaoApiConstants.URLs.REDIRECT_URI);
        sb.append("&code=" + authorize_code);
        bw.write(sb.toString());
        bw.flush();

        int responseCode = connection.getResponseCode();
        log.info("getToken response code : {}", responseCode);

아래는 ResponseCode가 200,즉 HTTP_OK일 때의 로직이다.
받아온 응답을 json으로 파싱하여 필요한 데이터들을 꺼내면 된다.
참고로 토큰 갱신 로직은 일단 보류하고 accessToken만 return할 것이다.

        if (responseCode == HttpURLConnection.HTTP_OK) {
            String result = getResultString(connection.getInputStream());

            JsonParser jsonParser = new JacksonJsonParser();

            Map<String, Object> map = jsonParser.parseMap(result);
            accessToken = (String) map.get("access_token");

            /**
             * 토큰 만료 및 refresh는 나중에 처리
             */
            refresh_token = (String) map.get("refresh_token");
            expires_in = (int) map.get("expires_in");
            refresh_token_expires_in = (int) map.get("refresh_token_expires_in");

            log.info("token : {} ", access_token);
            log.info("expires_id  : {}", expires_in);

        } else {

아래는 에러가 발생했을 때의 로직이다.
인가코드가 유효하지 않은 경우(KOE320), Redirect URI가 유효하지 않은 경우(KOE303), client_id가 존재하지 않는 경우(KOE101) 등이 있다.
필자는 각 경우에 해당하는 exception을 만들어서 처리했는데, 각자의 스타일에 맞게 처리하면 될 것이다.

			responseCode == HttpURLConnection.HTTP_OK일 때 로직...
   } else {
            String result = getResultString(connection.getErrorStream());

            JsonParser jsonParser = new JacksonJsonParser();
            /**
             * map에 error, error_description, error_code 들어있음
             */
            Map<String, Object> map = jsonParser.parseMap(result);
            String error = (String) map.get("error");
            String error_code = (String) map.get("error_code");
            String error_description = (String) map.get("error_description");

            log.error("error: {} ", error);
            log.error("error_code: {} ", error_code);
            log.error("error_Description: {} ", error_description);

            if (error_code.equals("KOE320")) { // authorize_code not found
                throw new AuthorizationCodeNotFoundException(error_description);
            }else if(error_code.equals("KOE303")){ // Redirect URI mismatch.
                throw new RuntimeException("Redirect URI mismatch");
            }else if(error_code.equals("KOE101")){ // Not exist client_id
                throw new RuntimeException("Not exist client_id");
            }
        }

        return accessToken;
    }

다음 포스트에서는 토큰의 유효성을 검증한 후 사용자 정보를 받아오는 로직을 살펴볼 것이다.

profile
기술블로그보다는 기록블로그

0개의 댓글