[Spring] Kakao의 API 활용하여 소셜 로그인 구현

Kakao 로그인 API를 구현하는 방법은 생각보다 많이 복잡해서 구현하는데 오류도 많았고, 그러다보니 정상적으로 구현하는데 시간이 좀 오래 걸렸던것 같다. 이번에는 동작원리, 코드, 수정한 오류 사항을 바탕으로 글을 작성해보고자 한다.

 

Kakao 로그인 API 구현하기

개발에 앞서 Kakao 로그인 API를 구현하는 방법은 카카오 개발자 홈페이지에 상세하게 설명되어있다.

https://developers.kakao.com/

 

Kakao Developers

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

developers.kakao.com

* 진행순서

1) 카카오 개발자 홈페이지에 로그인

2) 내 애플리케이션 - 애플리케이션 추가하기 

 

3) 애플리케이션 정보를 입력하여 애플리케이션 추가

 

4) 문서 - 카카오 로그인 - 설정하기를 참고하여 필수로 지정되어 있는 내용을 설정하고, 선택 사항은 서비스에 맞게 설정

 

5) 내 애플리케이션 - 카카오 로그인으로 이동

 

6) 카카오 로그인 활성화 및

 

7) Redirect URI를 등록 = 내가 만든 URI : http:.//localhost/member/kakao

 

8) 동의항목에서는 기본적인 필수 동의 항목이 지정되어 있다.추가적으로 개인정보 동의항목 심사 신청을 통해 제공 받을 수 있는 항목의 수가 늘어난다. 실습을 위해 닉네임만 제공 받도록 설정하였다.

 

9) 도구 - JS SDK 데모 - 카카오로그인 - 로그인으로 이동하여 카카오에서 제공하는 기본 프론트 코드를 복사

이때, 수정을 해줘야하는 곳이 2군데 있다.

- 4번째 줄의 Kakao.init() -> 내 애플리케이션에 작성되어있는 Js키를 입력해주어야함

- 16번째 줄의 redirectUri -> 동일하게 내가 지정한 Uri를 작성해주어야 함 

 

10) 카카오에서 제공하는 기본 프론트 페이지 

11) 카카오 로그인 버튼을 누른 뒤 

 

12) 로그인 후 - 개인 정보 동의 화면 출력

15) 로그인 후 백엔드 서버에서 JWT 토큰을 제공

 

* 동작원리

1. 클라이언트가 프론트서버에서 제공하는 카카오 로그인 버튼을 누름으로써 시작
2. 프론트서버는 카카오 로그인 화면을 제공해주고
3. 클라이언트는 정보를 입력후 로그인 버튼을 누른다. 이 신호를 받은 카카오는
4. 카카오에서 개인 정보 동의 화면을 프론트한테 전달하고
5. 프론트에서 해당 동의화면을 동의 후 버튼을 누른다. 이 과정이 카카오에게 인증코드를 요청하는 과정이다.
6. 카카오는 백앤드에게 인증코드를 전달한다
7. 인증코드를 넘겨 받은 백은 카카오에게 토큰을 요청하고
8. 카카오는 백에게 토큰을 전달한다.
9. 이후 백은 카카오에게 전달 받은 토큰을 통해 회원 정보를 본인 DB에서 조회하고
10. 정보가 조회된다면 로그인 , 없다면 회원 가입 절차를 진행한다

 

* 동작 코드

위의 과정을 코드로 구현하면 다음과 같다.

 

/*CustomerController*/

@GetMapping(value = "/member/kakao")
    // 인가 코드 받아오는 코드
    public ResponseEntity kakao(String code) {
        String accessToken = kakaoService.getKakaoToken(code);
        KakaoEmailReq kakaoEmailReq = kakaoService.getUserInfo(accessToken);
        Customer customer = memberService.getCustomerByCustomerId(kakaoEmailReq.getEmail());
        if(customer == null) {
            customer = kakaoService.kakaoSignup(kakaoEmailReq);
        }
        return ResponseEntity.ok().body(kakaoService.kakaoLogin(customer));
    }
    }
    
    
    
    
/*KakaoService*/
@CrossOrigin("*")
@Service
@RequiredArgsConstructor
public class KakaoService {
    private final CustomerRepository customerRepository;
    private final PasswordEncoder passwordEncoder;
    @Value("${jwt.secret-key}")
    private String secretKey;

    @Value("${jwt.token.expired-time-ms}")
    private int expiredTimeMs;

    public KakaoEmailReq getUserInfo(String accessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
        headers.add(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8");
        HttpEntity request = new HttpEntity<>(headers);
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<Object> response = restTemplate.exchange(
                "https://kapi.kakao.com/v2/user/me",
                HttpMethod.GET,
                request,
                Object.class
        );

        String str = ("" + response.getBody());
        System.out.println(str);
        Gson gson = new Gson();
        System.out.println(str.split(",")[3]);
        Map<String,Object> result = gson.fromJson("{"+str.split(",")[3]+"}", Map.class);
        Map<String, Object> properties = (Map<String, Object>)result.get("properties");


        System.out.println("username : " + (String)properties.get("nickname"));

        KakaoEmailReq kakaoEmailReq = KakaoEmailReq.builder()
                .customerName((String)properties.get("nickname"))
                .email((String)properties.get("nickname")+"@kakao.com")
                .build();

        return kakaoEmailReq;
    }

    public String getKakaoToken(String code) {
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8");

        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("client_id", "02fd054905fd7c0efa8cd84edb2f1234"); //Rest API키
        params.add("redirect_uri", "http://localhost:8080/member/kakao");
        params.add("code", code);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<Object> response = restTemplate.exchange(
                "https://kauth.kakao.com/oauth/token",
                HttpMethod.POST,
                request,
                Object.class
        );

        Gson gson = new Gson();
        Map<String, Object> result = gson.fromJson("" + response.getBody(), Map.class);
        String accessToken = "" + result.get("access_token");

        return accessToken;
    }

    public String kakaoLogin(Customer customer) {
        return JwtUtils.generateAccessToken(customer, secretKey, expiredTimeMs);
    }

    public Customer kakaoSignup(KakaoEmailReq kakaoEmailReq) {
        Customer customer = customerRepository.save(Customer.builder()
                .customerEmail(kakaoEmailReq.getEmail())
                .customerPassword(passwordEncoder.encode("kakao"))
                        .customerName(kakaoEmailReq.getCustomerName())
                        .customerAddress(null)
                        .customerPNum("")
                .customerAuthority("Customer")
                        .socialLogin(true)
                        .status(true)
                .build());
        return customer;
    }
}

 

* 오류 사항 수정

- 기존에 사용했던 코드

Map<String, Object> result = gson.fromJson(str.split(",")[0]+","+str.split(",")[2]+"}", Map.class);

 

- 에러 발생 코드

 

 

- 오류 발생 원인

: MalformedJsonException 은 Retrofit과 Gson을 사용하여 서버로부터 JSON 데이터를 파싱할 때 발생할 수 있는 예외다.

 

- 오류 해결 방법

: 카카오에서 제공한 데이터를 파악해보니 다음과 같다.

{setPrivacyInfo=false, id=3305257474, connected_at=2024-01-28T03:05:21Z, properties={nickname=현범}, kakao_account={profile_nickname_needs_agreement=false, profile={nickname=현범}}}

: 내가 필요로하는 정보는 properies안 nickname 정보이므로 필요한 데이터만  사용하는 방법을 통해 오류가 해결됬다.

 

- 수정 후 코드

Map<String, Object> result = gson.fromJson("{"+str.split(",")[3]+"}", Map.class);