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;
    }

}