[Spring Boot]카카오 로그인 구현
지난 포스팅에 이어 카카오, 네이버 로그인까지 하나의 예제를 만들어보았습니다. 테스트 환경은 동일합니다.
※ 테스트 환경
- Spring Boot
- JDK 1.8
- Gradle
- Thymeleaf
1. 카카오 개발자 설정
1) 애플리케이션 추가
Kakao Developers 에 접속해 애플리케이션을 추가해줍니다. 여기서 REST API 키가 사용됩니다.
2) 메뉴 - 플랫폼 - Web 플랫폼 등록
애플 로그인과 다르게 카카오와 네이버는 localhost를 등록해 테스트할 수 있습니다. 아래와 같이 포트번호와 함께 테스트 도메인을 입력합니다.
3) 리다이렉트 URI 를 등록
4) 동의항목 설정
비즈니스 설정을 한 경우에는 전화번호를 제공받을 수 있습니다. 본 예제에서는 닉네임과 카카오계정(이메일)을 사용했습니다.
5) SECRET 코드 생성
메뉴 - 보안에서 Client Secret을 발급받고 활성화시켜줍니다. 1)에서 생성된 REST API 키는 외부로 노출되어도 되는 키지만 Client Secret 키는 절대 노출되서는 안됩니다.
2. 로그인 구현
카카오 로그인도 애플 로그인과 비슷하게 동작합니다. 카카오 인증 서버로부터 인증 코드를 내려받고 인증 코드를 이용해 액세스 토큰을 요청합니다. 그리고 카카오 인증 서버로부터 내려받은 액세스 토큰으로 사용자 정보를 가져옵니다.
- 인증 코드 요청
- 액세스 토큰을 이용해 사용자 정보 가져오기
* 여기서부터 이전 포스팅에서의 소스와 이어지는 부분이 있습니다.
1) 인증 코드 요청
1-1) application.properties 정의
# 카카오 로그인 설정
kakao.client.id=e9887fe498f68de5f200a52f1c4a9890
kakao.client.secret=dsHwYcO83G3KuW7Bx6sNLUQg4kmnbKU9
kakao.redirect.url=http://localhost:7070/kakao/callback
- kakao.client.id : 1.1-1)에서 확인한 REST API 키
- kakao.client.secret : 1.1-5)에서 생성한 Client Secret 키
- kakao.redirect.url : 1.1-3)에서 생성한 Redirect URI
1-2) Controller 생성
@RequiredArgsConstructor
@Controller
public class HomeController {
private final AppleService appleService;
private final KakaoService kakaoService;
@RequestMapping(value="/", method= RequestMethod.GET)
public String login(Model model) {
model.addAttribute("appleUrl", appleService.getAppleLogin());
model.addAttribute("kakaoUrl", kakaoService.getKakaoLogin());
return "index";
}
}
1-3) Service 생성
@Service
public class KakaoService {
@Value("${kakao.client.id}")
private String KAKAO_CLIENT_ID;
@Value("${kakao.client.secret}")
private String KAKAO_CLIENT_SECRET;
@Value("${kakao.redirect.url}")
private String KAKAO_REDIRECT_URL;
private final static String KAKAO_AUTH_URI = "https://kauth.kakao.com";
private final static String KAKAO_API_URI = "https://kapi.kakao.com";
public String getKakaoLogin() {
return KAKAO_AUTH_URI + "/oauth/authorize"
+ "?client_id=" + KAKAO_CLIENT_ID
+ "&redirect_uri=" + KAKAO_REDIRECT_URL
+ "&response_type=code";
}
}
1-4) index.html 생성
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<title>소셜로그인 예제</title>
</head>
<body>
<div>
<p><a th:href="@{|${appleUrl}|}">애플로그인</a></p>
<p><a th:href="@{|${kakaoUrl}|}">카카오로그인</a></p>
</div>
</body>
</html>
2) 액세스 토큰 및 유저 정보 요청
이제 인증 코드를 전달받을 redirect_uri에 매핑될 Controller를 만들어보겠습니다.
2-1) Controller 생성
request 안에는 카카오 인증 서버로부터 내려 받은 인증 코드가 존재합니다. 여기서 추가로 커스텀 파라미터를 사용해야 하는 일이 있다면 1-3)에서 로그인 주소 생성시 state값을 추가로 사용하면 됩니다. 그럼 request 안에 state값을 받아 사용할 수 있습니다.
https://kauth.kakao.com/oauth/authorize?client_id=xxx&redirect_uri=xxx&response_type=code&state=리다이렉트 URL로 넘겨야 하는 값
@RestController
@RequiredArgsConstructor
@RequestMapping("kakao")
public class KakaoController {
private final KakaoService kakaoService;
@GetMapping("/callback")
public ResponseEntity<MsgEntity> callback(HttpServletRequest request) throws Exception {
KakaoDTO kakaoInfo = kakaoService.getKakaoInfo(request.getParameter("code"));
return ResponseEntity.ok()
.body(new MsgEntity("Success", kakaoInfo));
}
}
2-2) Service 수정
@Service
public class KakaoService {
@Value("${kakao.client.id}")
private String KAKAO_CLIENT_ID;
@Value("${kakao.client.secret}")
private String KAKAO_CLIENT_SECRET;
@Value("${kakao.redirect.url}")
private String KAKAO_REDIRECT_URL;
private final static String KAKAO_AUTH_URI = "https://kauth.kakao.com";
private final static String KAKAO_API_URI = "https://kapi.kakao.com";
public String getKakaoLogin() {
return KAKAO_AUTH_URI + "/oauth/authorize"
+ "?client_id=" + KAKAO_CLIENT_ID
+ "&redirect_uri=" + KAKAO_REDIRECT_URL
+ "&response_type=code";
}
public KakaoDTO getKakaoInfo(String code) throws Exception {
if (code == null) throw new Exception("Failed get authorization code");
String accessToken = "";
String refreshToken = "";
try {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded");
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type" , "authorization_code");
params.add("client_id" , KAKAO_CLIENT_ID);
params.add("client_secret", KAKAO_CLIENT_SECRET);
params.add("code" , code);
params.add("redirect_uri" , KAKAO_REDIRECT_URL);
RestTemplate restTemplate = new RestTemplate();
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
ResponseEntity<String> response = restTemplate.exchange(
KAKAO_AUTH_URI + "/oauth/token",
HttpMethod.POST,
httpEntity,
String.class
);
JSONParser jsonParser = new JSONParser();
JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody());
accessToken = (String) jsonObj.get("access_token");
refreshToken = (String) jsonObj.get("refresh_token");
} catch (Exception e) {
throw new Exception("API call failed");
}
return getUserInfoWithToken(accessToken);
}
private KakaoDTO getUserInfoWithToken(String accessToken) throws Exception {
//HttpHeader 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
//HttpHeader 담기
RestTemplate rt = new RestTemplate();
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(headers);
ResponseEntity<String> response = rt.exchange(
KAKAO_API_URI + "/v2/user/me",
HttpMethod.POST,
httpEntity,
String.class
);
//Response 데이터 파싱
JSONParser jsonParser = new JSONParser();
JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody());
JSONObject account = (JSONObject) jsonObj.get("kakao_account");
JSONObject profile = (JSONObject) account.get("profile");
long id = (long) jsonObj.get("id");
String email = String.valueOf(account.get("email"));
String nickname = String.valueOf(profile.get("nickname"));
return KakaoDTO.builder()
.id(id)
.email(email)
.nickname(nickname).build();
}
}
추가된 메소드는 총 2개입니다.
getKakaoInfo(String code)
getUserInfoWithToken(String accessToken)
getKakaoInfo(String code)는 컨트롤러에서 리턴받은 인증 코드값을 통해 카카오 인증 서버에 액세스 토큰을 요청합니다. 토큰은 액세스 토큰과 리프레쉬 토큰 두가지가 있는데, 리프레쉬 토큰은 액세스 토큰을 갱신할 때 사용됩니다. 액세스 토큰은 만료 시간이 존재하기 때문에 발급받은 리프레쉬 토큰을 저장해두고 액세스 토큰을 갱신하는 형태로 사용하게 됩니다. 본 예제에서는 발급받은 액세스 토큰을 통해 사용자 정보를 가져오는데까지만 다루고 있습니다.
getUserInfoWithToken(String accessToken)은 전달받은 액세스 토큰을 통해 사용자 정보를 가져옵니다. 사용자 정보는 사용자가 동의한 내역에 한해서 가져올 수 있습니다. 각 정보의 응답 키는 문서에 자세하게 나와있습니다.
RestTemplate을 이용해 https://kauth.kakao.com/oauth/token 주소로 토큰을 요청해봅니다.
RestTemplate restTemplate = new RestTemplate();
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
ResponseEntity<String> response = restTemplate.exchange(
KAKAO_AUTH_URI + "/oauth/token",
HttpMethod.POST,
httpEntity,
String.class
);
JSONParser jsonParser = new JSONParser();
JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody());
accessToken = (String) jsonObj.get("access_token");
refreshToken = (String) jsonObj.get("refresh_token");
액세스 토큰을 정상적으로 발급받았다면 이제 사용자 정보를 가져올 수 있습니다. 이 때 Header에 Bearer 액세스 토큰 을 담아 요청합니다.
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
//HttpHeader 담기
RestTemplate rt = new RestTemplate();
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(headers);
ResponseEntity<String> response = rt.exchange(
KAKAO_API_URI + "/v2/user/me",
HttpMethod.POST,
httpEntity,
String.class
);
//Response 데이터 파싱
JSONParser jsonParser = new JSONParser();
JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody());
JSONObject account = (JSONObject) jsonObj.get("kakao_account");
JSONObject profile = (JSONObject) account.get("profile");
응답받은 Response 안에는 id값이 존재하는데 이것은 회원 고유의 값입니다.
kakao_acount - profile에서 nickname 값과 kakao_acount에서 email 값을 확인할 수 있습니다.
2-3) DTO 생성
@Builder
@Data
public class KakaoDTO {
private long id;
private String email;
private String nickname;
}
2-4) 결과
* 사용자 동의 항목은 최초 연결 성공 이후에는 조회되지 않는데 연결끊기를 따로 구현하거나 Postman을 이용해 연결을 끊어주면 됩니다.
참고자료
+ 피드백은 언제나 환영입니다 :)
전체 소스는 GitHub에서 확인하실 수 있습니다.