OAuth(카카오)
OAuth 카카오 로그인
korea-wongi
2024. 8. 21. 00:03
카카오 로그인 사용 승인 받기
- 1. 카카오 개발자센터 회원가입
- 2. 내 애플리케이션 메뉴 선택 > 애플리케이션 추가하기
- 3. 사이트 도메인 등록하기
- 애플리케이션 선택
- 플랫폼 메뉴 선택 > 플랫폼 설정하기 클릭
- Web 플랫폼 등록
- 사이트 도메인 입력 (개발중인 로컬환경의 서버 주소 입력)
- 4. 인가토큰을 받게 될 Redirect URI 설정하기
- 카카오 로그인 메뉴 클릭
- Redirect URI 등록 클릭
- 활성화 설정 ON
- 5. 동의 항목 설정하기
Front 인가 코드 요청 방법
<button id="login-kakao-btn" onclick="location.href='https://kauth.kakao.com/oauth/authorize?client_id={REST API KEY}&redirect_uri={REDIRECT URI}&response_type=code'">
Controller 코드
더보기
@GetMapping("/user/kakao/callback")
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.setPath("/");
response.addCookie(cookie);
return "redirect:/";
}
Service 코드
더보기
package com.sparta.myselectshop.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.myselectshop.dto.KakaoUserInfoDto;
import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.entity.UserRoleEnum;
import com.sparta.myselectshop.jwt.JwtUtil;
import com.sparta.myselectshop.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.UUID;
@Slf4j(topic = "KAKAO Login")
@Service
@RequiredArgsConstructor
public class KakaoService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
private final RestTemplate restTemplate;
private final JwtUtil jwtUtil;
public String kakaoLogin(String code) throws JsonProcessingException {
// 1. 인가 코드로 액세스 토큰 요청
String accessToken = getToken(code);
// 2. 토큰으로 카카오 API 호출 : 엑세스 토큰으로 카카오 사용자 정보 가져오기
KakaoUserInfoDto kakaoUserInfoDto = getKakaoUserInfo(accessToken);
// 3. 필요시에 회원가입
User kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfoDto);
// 4. JWT 토큰 반환
String token = jwtUtil.createToken(kakaoUser.getUsername(), kakaoUser.getRole());
return token;
}
private String getToken(String code) throws JsonProcessingException {
log.info("인가코드 : " + code);
// 요청 URL 만들기
// UriComponentsBuilder는 이 기본 URI를 바탕으로 URI를 구성하는 도구
URI uri = UriComponentsBuilder
// 기본 URI 문자열을 설정
.fromUriString("https://kauth.kakao.com")
// 기본 URI에 추가할 경로를 설정
.path("/oauth/token")
// URI를 인코딩합니다. URI에는 특수 문자나 공백이 포함될 수 있는데, 이러한 문자는 URL 인코딩을 통해 안전하게 변환되어야 합니다.
.encode()
// UriComponentsBuilder에서 설정한 모든 구성 요소를 기반으로 UriComponents 객체를 생성
.build()
// UriComponents 객체를 실제 URI 객체로 변환
.toUri();
// HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=UTF-8");
// HTTP Body 생성
// MultiValueMap 은 동일한 키에 대해 여러 값을 저장할 수 있습니다.
// LinkedMultiValueMap 은 MultiValueMap 의 구현체로, 키와 값의 쌍을 저장합니다. 이 구현체는 입력 순서를 유지합니다.
// 이 객체는 HTTP POST 요청의 바디에 포함될 파라미터를 구성하는 데 사용됩니다.
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", "591549ea1d3f8c3987a80fff1e2da2f0");
body.add("redirect_uri", "http://localhost:8080/api/user/kakao/callback");
body.add("code", code);
// RequestEntity는 HTTP 요청의 메소드, URI, 헤더, 바디를 모두 포함하는 객체를 생성합니다.
// 이 객체를 통해 HTTP 요청을 RestTemplate으로 보낼 수 있습니다.
RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
.post(uri)
.headers(headers)
.body(body);
// HTTP 요청 보내기
// String.class = 응답을 String 으로 받겠다는 의미이다.
ResponseEntity<String> response = restTemplate.exchange(requestEntity, String.class);
// HTTP 응답 (JSON) -> 액세스 토큰 파싱
JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
log.info("인가코드 수행 성공");
return jsonNode.get("access_token").asText();
}
private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
log.info("액세스 토큰 : " + accessToken);
URI uri = UriComponentsBuilder
.fromUriString("https://kapi.kakao.com")
.path("/v2/user/me")
.encode().build().toUri();
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<>());
// HTTP 요청 보내기
ResponseEntity<String> response = restTemplate.exchange(requestEntity, String.class);
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();
log.info("카카오 사용자 정보: " + id + ", " + nickname + ", " + email);
return new KakaoUserInfoDto(id, nickname, email);
}
private User registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) {
// 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;
}
}