카카오 소셜 로그인 서비스를 가져와서 내가 만든 프로젝트에 카카오 로그인 기능을 적용시켰다.
평소에 접하던 카카오, 네이버 같은 로그인 기능이 이렇게 구현되는 것이란걸 알 수 있었다.
그리고 가끔 소셜 로그인을 했는데도 또 사이트의 로그인이나 회원가입이 나오는 이유가 궁금했었는데, 오늘 직접 구현해보면서 왜 그렇게 되었는지 이해는 해줄 수 있었다! (근데 굳이 그렇게 했어야했니...)
kakao developers > 로그인 REST API
카카오는 친절하게 그림까지 보여주며 이해하기 쉽게 로그인 과정을 설명해주지만, 더 간단히 나타낸 그림은 다음과 같다.
(프론트)앱에서 사용자가 카카오 로그인 ID, PW를 입력하면 인증 코드 요청이 된다.
그러면 카카오 서버는 인증 코드 전달을 주며, 이것을 Controller로 받아오면 된다.
@GetMapping("/user/kakao/callback") // kakao developers의 어플리케이션 등록 할 때 넣어줬던 path
public String kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException {
String token = kakaoService.kakaoLogin(code);
Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7)); // Cookie에는 띄어쓰기 못넣는다. -> "Bearer "로 시작하기 때문에 오류 -> substring해줬음
cookie.setPath("/");
response.addCookie(cookie); //successfulAuthentication과 같은 역할
return "redirect:/";
}
public String kakaoLogin(String code) throws JsonProcessingException {
// 1. "인가 코드"로 "액세스 토큰" 요청
String accessToken = getToken(code);
// 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);
... // 회원가입, JWT 토큰 반환 등 -> 필요한 로직으로 변경
return createToken;
}
private String getToken(String code) throws JsonProcessingException {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("https://kauth.kakao.com")
.path("/oauth/token") // REST API 문서에 따라 작성
.encode()
.build()
.toUri();
// HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HTTP Body 생성
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", "**rest API key 복붙**");
body.add("redirect_uri", "http://localhost:8080/api/user/kakao/callback");
body.add("code", code);
RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity // '이런 형태로 request'라고 고정됨.
.post(uri) //post로 고정되어있다.
.headers(headers)
.body(body);
// HTTP 요청 보내기
ResponseEntity<String> response = restTemplate.exchange(
requestEntity,
String.class
...
);
}
private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
...
}
RequestEntity<MultiValueMap<String, String>>
라고 request를 보낸 것.restTemplate.exchange
: restTemplate를 호출하여 카카오 서버를 호출하는 것이다! -> 이게 실행되면 인증코드로 토큰 요청을 하는 것.private String getToken(String code) throws JsonProcessingException {
...
// HTTP 응답 (JSON) -> 액세스 토큰 파싱
JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
return jsonNode.get("access_token").asText();
}
private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("https://kapi.kakao.com")
.path("/v2/user/me")
.encode()
.build()
.toUri();
// HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
.post(uri)
.headers(headers)
.body(new LinkedMultiValueMap<>()); //body는 보낼 필요가 없어서 그냥 생성만 해서 보냄
// HTTP 요청 보내기
ResponseEntity<String> response = restTemplate.exchange(
requestEntity,
String.class
);
...
}
private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
Long id = jsonNode.get("id").asLong();
String nickname = jsonNode.get("properties")
.get("nickname").asText();
String email = jsonNode.get("kakao_account")
.get("email").asText();
return new KakaoUserInfoDto(id, nickname, email);
}
// DB 에 중복된 Kakao Id 가 있는지 확인
Long kakaoId = kakaoUserInfo.getId();
User kakaoUser = userRepository.findByKakaoId(kakaoId).orElse(null);
if (kakaoUser == null) {
// 카카오 사용자 email 동일한 email 가진 회원이 있는지 확인
String kakaoEmail = kakaoUserInfo.getEmail();
User sameEmailUser = userRepository.findByEmail(kakaoEmail).orElse(null);
if (sameEmailUser != null) {
kakaoUser = sameEmailUser;
// 기존 회원정보에 카카오 Id 추가
kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId);
} else {
// 신규 회원가입
// password: random UUID
String password = UUID.randomUUID().toString();
String encodedPassword = passwordEncoder.encode(password);
// email: kakao email
String email = kakaoUserInfo.getEmail();
kakaoUser = new User(kakaoUserInfo.getNickname(), encodedPassword, email, UserRoleEnum.USER, kakaoId);
}
userRepository.save(kakaoUser); // 영속성
}
return kakaoUser;
kakaoUser.kakaoIdUpdate(kakaoId);
: Transactional이 필요 없게 하려고 return을 kakaoUser라는 객체를 반환했다.외부 API 받아오는 것도 처음엔 뭐가뭔지 도통 몰랐는데, 이렇게 하나씩 뜯어가며 공부해보니 받아와서 사용만 하면 되겠구나 싶다. 물론 막상 스스로 해보면 잘 될지는 모르겠지만... 아무튼 이번 기회에 그 구조에 대해 이해 할 수 있었다.
다른 API들도 받아와서 활용해보고싶다!!!