준비 단계
1. Kakao Developer 에서 어플리케이션을 생성하고 Web 플랫폼 등록 에서 사이트 도메인을 입력해준다.
2. 카카오 로그인을 했을 때 인가코드를 전달 받을 Redirect URI ( callback ) 을 설정해준다.
인가코드를 전달받을 Controller 의 요청으로 보내주면 된다.
3. 사용자로부터 받을 정보를 선택해준다.
https://kauth.kakao.com/oauth/authorize?client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&response_type=code
인가코드를 받기 위해서 해당 REST_API_KEY 와 Redirect_URI 를 해당 애플리케이션과 callback 설정한 uri 를 맞춰서 넣어준다.
RestTemplateConfig
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
// RestTemplate 으로 외부 API 호출 시 일정 시간이 지나도 응답이 없을 때
// 무한 대기 상태 방지를 위해 강제 종료 설정
.setConnectTimeout(Duration.ofSeconds(5)) // 5초
.setReadTimeout(Duration.ofSeconds(5)) // 5초
.build();
}
}
RestTemplate 을 사용하기 위해 RestTemplateConfig 를 만들어주고,
KakaoUserController
@GetMapping("/login-kakao")
public String kakaoLogin(@RequestParam("code") String code, HttpServletResponse response) throws JsonProcessingException {
// code: 카카오 서버로부터 받은 인가 코드
String token = kakaoUserService.kakaoLogin(code, response);
// Cookie 생성 및 직접 브라우저에 Set
Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7));
cookie.setPath("/");
response.addCookie(cookie);
return "main";
}
전달받은 인가코드를 code 변수로 전달받고 HttpServletResponse 와 함께 kakaoUserService.kakaoLogin 으로 전달시킨다.
KakaoUserService
@Slf4j(topic = "KAKAO Login")
@Service
@RequiredArgsConstructor
public class KakaoUserService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
private final RestTemplate restTemplate;
private final JwtUtil jwtUtil;
public String kakaoLogin(String code, HttpServletResponse response) throws JsonProcessingException {
// 1. "인가 코드"로 "액세스 토큰" 요청
String accessToken = getToken(code);
// 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);
// 3. 필요시에 회원가입
User kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo);
// 4. JWT 토큰 반환
String createToken = jwtUtil.createToken(kakaoUser.getId());
log.info(createToken);
return createToken;
// return accessToken;
}
private String getToken(String code) throws JsonProcessingException {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("https://kauth.kakao.com")
.path("/oauth/token")
.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", "aeeffb48d21d2171981162a755a54141");
body.add("redirect_uri", "http://localhost:8080/login-kakao");
body.add("code", code);
RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
.post(uri)
.headers(headers)
.body(body);
// HTTP 요청 보내기
ResponseEntity<String> response = restTemplate.exchange(
requestEntity,
String.class
);
// HTTP 응답 (JSON) -> 액세스 토큰 파싱
JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
return jsonNode.get("access_token").asText();
}
kakao 로 인가코드를 보내서 액세스 토큰을 요청해 받아왔다.
Http Body 를 생성하는 쪽에서 보면
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", "aeeffb48d21d2171981162a755a54141");
body.add("redirect_uri", "http://localhost:8080/login-kakao");
body.add("code", code);
client_id 와 redirect_uri 를 알맞게 수정해주어야 한다.
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<>());
// 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);
}
받아온 액세스토큰으로 카카오 사용자 정보를 불러온다.
현재는 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(email, kakaoUserInfo.getNickname(), encodedPassword, UserRole.GUEST, UserStatus.ACTIVE, kakaoId);
}
userRepository.save(kakaoUser);
}
return kakaoUser;
}
이미 기존에 등록되어있는 회원이 있다면 그대로 로그인을 진행하고,
회원이 없다면 신규등록으로 user 객체를 만들었다.
전체 코드
더보기
package com.sparta.sweethoney.domain.user.kakaoUser.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.sweethoney.config.PasswordEncoder;
import com.sparta.sweethoney.domain.common.dto.AuthUser;
import com.sparta.sweethoney.domain.common.exception.user.NotFoundUserException;
import com.sparta.sweethoney.domain.user.entity.User;
import com.sparta.sweethoney.domain.user.entity.UserRole;
import com.sparta.sweethoney.domain.user.entity.UserStatus;
import com.sparta.sweethoney.domain.user.kakaoUser.dto.KakaoRegisterRequestDto;
import com.sparta.sweethoney.domain.user.kakaoUser.dto.KakaoUserInfoDto;
import com.sparta.sweethoney.domain.user.repository.UserRepository;
import com.sparta.sweethoney.util.JwtUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
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 KakaoUserService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
private final RestTemplate restTemplate;
private final JwtUtil jwtUtil;
public String kakaoLogin(String code, HttpServletResponse response) throws JsonProcessingException {
// 1. "인가 코드"로 "액세스 토큰" 요청
String accessToken = getToken(code);
// 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);
// 3. 필요시에 회원가입
User kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo);
// 4. JWT 토큰 반환
String createToken = jwtUtil.createToken(kakaoUser.getId());
log.info(createToken);
return createToken;
// return accessToken;
}
private String getToken(String code) throws JsonProcessingException {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("https://kauth.kakao.com")
.path("/oauth/token")
.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", "aeeffb48d21d2171981162a755a54141");
body.add("redirect_uri", "http://localhost:8080/login-kakao");
body.add("code", code);
RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
.post(uri)
.headers(headers)
.body(body);
// HTTP 요청 보내기
ResponseEntity<String> response = restTemplate.exchange(
requestEntity,
String.class
);
// 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<>());
// 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(email, kakaoUserInfo.getNickname(), encodedPassword, UserRole.GUEST, UserStatus.ACTIVE, kakaoId);
}
userRepository.save(kakaoUser);
}
return kakaoUser;
}
}
'컴퓨터 프로그래밍 > Spring' 카테고리의 다른 글
[Spring] Lazy Loading 과 Eager Loading 의 차이, N+1 Problem 과 해결 방법 (1) | 2024.10.03 |
---|---|
[Spring] Password Encoder 사용법 (0) | 2024.09.29 |
[Spring] S3 기능 구현 (1) | 2024.09.28 |
[Spring] Entity 연관관계 설정 코드 (0) | 2024.09.27 |
[Spring] GlobalExceptionHandler (0) | 2024.09.22 |