수업 자료에서 카카오 로그인의 과정이 너무 빨리 지나가서 하나하나 뜯어보고 기록
<<< 순서 >>>
1. 인가 코드 요청 GET
2. 인가 코드 요청 응답 -> Redirect_URI GET
3. 인가 코드로 액세스 토큰 요청 POST
4. access_token에서 카카오 사용자 정보 가져오기
5. 가져온 사용자 정보를 가져와서 추가로직 구현
6. User 정보로 JWT 토큰 만들어서 return.
7. JWT token 쿠키에 담아서 프론트에 넘겨주고 index.html로.
1. 프론트 단에서 버튼을 클릭할 때, 인가 코드를 받아오는 GET 요청을 보낸다.
이 때, client_id와 redirect_uri와 response_type을 쿼리 스트링으로 필수로 같이 보낸다. 사용자가 모든 필수 동의 항목에 동의하고 [동의하고 계속하기] 버튼을 누른 경우 redirect_uri로 인가 코드를 담은 쿼리 스트링 전달.
- client_id : 앱 REST API 키
- redirect_uri : 인가 코드를 받아서 토큰을 생성하는 URI(수업 자료에서는 "http://localhost:8080/api/user/kakao/callback"으로 설정.)
- response_type : code로 고정. 인가코드
<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'
">
카카오로 로그인하기
</button>
2. 인가 코드 받기 요청의 응답
인가 코드 받기 요청의 응답은 redirect_uri에 GET 요청으로 전달된다. 해당 요청은 아래와 같은 쿼리 파라미터를 포함한다.
query로 code가 들어오니까 @RequestParam으로 code를 받아서 kakaoService의 kakaoLogin 메서드로 code 전달.
@GetMapping("/api/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);
cookie.setPath("/");
response.addCookie(cookie); // 브라우저 쿠키 저장소에 쿠키 저장.
return "redirect:/";
}
3. 인가 코드로 액세스 토큰 요청
인가 코드 받기만으로는 카카오 로그인이 완료되지 않으며, 토큰 받기까지 마쳐야 카카로 로그인을 정상적으로 완료할 수 있다. 아래 URL로 POST 요청을 보내야 한다.
헤더에는 Content-type : application/x-www-form-urlencoded;charset=utf-8을 추가해줘야 한다.
바디에는 다음을 추가해야 한다.
그에 대한 응답을 String 형식의 ResponseEntity로 받고, 이 response의 바디 부분에서 다음과 같은 값들을 json화 해서 얻을 수 있다. 우리는 여기서 access_token만 얻자!
private String getToken(String code) throws JsonProcessingException {
log.info("인가코드 : " + code);
// 요청 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", "${REST_API_KEY}");
body.add("redirect_uri", "${REDIRECT_URI}");
body.add("code", code);
RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
.post(uri) // post방식
.headers(headers)
.body(body);
// HTTP 요청 보내기
ResponseEntity<String> response = restTemplate.exchange(
requestEntity,
String.class // String으로 받기.
);
// HTTP 응답 (JSON) -> 액세스 토큰 파싱
JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
return jsonNode.get("access_token").asText();
}
4. access_token에서 카카오 사용자 정보 가져오기
사용자 액세스 토큰을 헤더에 담아서 요청하는 방식과 어드민 키를 헤더에 담아서 요청하는 방식이 있는데 여기서는 액세스 토큰을 이용하여 사용자 정보를 가져온다.
액세스 토큰으로 카카오 사용자 정보를 가져오려면 아래 URL로 POST요청을 보내야 한다.
헤더에는 다음을 추가해줘야 한다.
그에 대한 응답을 String 형식의 ResponseEntity로 받고, 이 response의 바디 부분에서 다음과 같은 값들을 json화 해서 얻을 수 있다. 우리는 여기서 id, kakao_account의 profile의 nickname, kakao_account의 email을 가져올 것이다.
가져온 정보는 KakaoUserInfoDto 타입으로 저장하여 return해준다.
private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
log.info("access_token : " + accessToken);
// 요청 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("kakao_account")
.get("profile").get("nickname").asText();
String email = jsonNode.get("kakao_account")
.get("email").asText();
log.info("카카오 사용자 정보: " + id + ", " + nickname + ", " + email);
return new KakaoUserInfoDto(id, nickname, email);
}
@Getter
@NoArgsConstructor
public class KakaoUserInfoDto {
private Long id;
private String nickname;
private String email;
public KakaoUserInfoDto(Long id, String nickname, String email) {
this.id = id;
this.nickname = nickname;
this.email = email;
}
}
5. 가져온 사용자 정보를 가져와서 추가로직 구현
- 가져온 사용자 정보에 있는 kakaoId로 이미 kakao로 Login한 사람인지 확인.
- 있다면 유저 찾아서 return. 없다면 다음 로직
- 가져온 사용자 정보에 있는 email로 회원가입이 되어 있는 사람인지 확인
- 있다면 기존 유저에 kakaoId 업데이트 시켜주고 해당 유저 return 없다면 다음 로직
- 처음 온 사람이므로 가져온 사용자 정보로 회원가입 시키고 DB에 저장 후 해당 유저 return
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);
}
// 저장하기도 하지만 수정하기도 한다.
// @transaction 안 걸어도 되나? save는 그 때 그 때 알아서 수정/저장 해주기 때문에 안해줘도 됨.
// dirty checking을 하지 않아도 된다. 정확히는 우리가 수정을 하고 나서 save를 해주어도 된다.
userRepository.save(kakaoUser);
}
return kakaoUser;
}
6. User 정보로 JWT 토큰 만들어서 return.
public String kakaoLogin(String code) 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.getUsername(), kakaoUser.getRole());
String token = "Bearer%20" + createToken.substring(7);
return token;
}
7. JWT token 쿠키에 담아서 프론트에 넘겨주고 index.html로.
-> 가장 위에 controller 코드에서 처리
강의코드에서 수정한 점!
강의에서는 token을 줄 때 앞에 "Bearer "을 붙여서 주지 않아서 붙이는 코드를 한 줄 더 추가했다. 프론트단에서 백에서 넘어온 토큰에 "Bearer "을 추가하는 로직을 구현해주셨다고 했지만 내 프론트상에는 없기 때문에 내가 따로 "Bearer "을 붙여서 넘기는 코드를 추가했다.
강의에서는 카카오 사용자 정보의 닉네임을 가져올 때, properties의 nickname에서 가져왔는데 카카오 API 문서를 보니 kakao_account - profile - nickname에서 가져오는 것이 더 맞는 것 같아서 그 부분을 수정해 주었다.
처음에는 요청도 너무 많은 것 같고 뭐가 뭔지 좀 헷갈렸는데 차근차근 기록하니 과정을 이해하는데 많은 도움이 된 것 같다. 다음에는 nickname 뿐 아니라 생일이나 프로필 사진 같은 다른 정보도 가져와서 추후에 할 프로젝트에 적용을 시켜보면 좋을 것 같다.
'STUDY > SpringBoot' 카테고리의 다른 글
[TIL] Entity의 선형 구조 / 트리 구조에 따른 구현 방식 비교 - Trouble Shooting (0) | 2023.08.09 |
---|---|
[TIL] parent_id로 순서 바꾸는 로직 구현 (0) | 2023.08.08 |
[TIL] SpringBoot dependency 버전 맞추기! (0) | 2023.07.25 |
[TIL] SQLSyntaxError ( 컬럼명 like 문제) (0) | 2023.07.14 |
[TIL] AnnotationException 예외 발생 (mappedBy) (0) | 2023.07.13 |