이전 포스팅에서 소개한 대표적인 로그인 방법 3가지 중 하나인 OAuth 2.0 방식으로 로그인을 구현해보겠다.
OAuth 2.0은 소셜 로그인 방식에 주로 사용되는데 무료로 해당 API를 제공해주는 카카오 로그인 API를 선택하였다.
카카오 로그인 구현
1. 카카오 로그인 API 구현
2. 카카오 토큰 갱신 API 구현
카카오 로그인 API를 사용하기 위해서는 Kakao developers에서 제공하는 개발 문서를 잘 참고하여야 한다. REST API에서 호출할 것이기 때문에 카카오 로그인 REST API 개발 문서를 살펴보면 된다.
로그인은 다음과 같은 과정으로 진행된다.
1-1. 일단, 카카오 로그인 API 시작을 위해 애플리케이션을 추가한다.
1-2. 플랫폼은 Web으로 설정하였다.
1-3. 앱 키 확인하기!
앱 키에는 네이티브 앱, REST API, 자바스크립트, Admin 키가 있는데 REST API에서 호출할 것이기 때문에 REST API 앱 키가 Client ID로 사용될 예정이다.
1-4. 동의 항목 설정
동의 항목 설정에서는 카카오 서버에 저장되어 있는 사용자의 개인 정보 중 어떤 것을 요청할지 설정하는 단계이다.
일단, 닉네임과 이메일을 받아오도록 하였다. 전화번호는 사업자 등록 번호가 있어야 권한이 주어지는 듯 하다..
1-5. 웹 도메인 및 Redirect URI 설정
웹 도메인과 Redirect URI는 테스트를 진행할 로컬 주소와 서버 주소 두 가지를 추가해 주었다.
1-6. 로그인 Controller 구현
카카오 로그인 요청 시 인가 코드 받기, 토큰 받기, 사용자 로그인 처리의 과정을 수행할 Controller 코드를 작성한다.
/**
* 카카오 로그인 API
* [GET] /app/login/kakao
* @return BaseResponse<String>
*/
@ResponseBody
@GetMapping("/kakao")
public BaseResponse<PostLoginRes> kakaoLogin(@RequestParam(required = false) String code) {
try {
// URL에 포함된 code를 이용하여 액세스 토큰 발급
String accessToken = loginService.getKakaoAccessToken(code);
System.out.println(accessToken);
// 액세스 토큰을 이용하여 카카오 서버에서 유저 정보(닉네임, 이메일) 받아오기
HashMap<String, Object> userInfo = loginService.getUserInfo(accessToken);
System.out.println("login Controller : " + userInfo);
PostLoginRes postLoginRes = null;
// 만일, DB에 해당 email을 가지는 유저가 없으면 회원가입 시키고 유저 식별자와 JWT 반환
// 현재 카카오 유저의 전화번호를 받아올 권한이 없어서 테스트를 하지 못함.
if(loginProvider.checkEmail(String.valueOf(userInfo.get("email"))) == 0) {
//PostLoginRes postLoginRes = 해당 서비스;
return new BaseResponse<>(postLoginRes);
} else {
// 아니면 기존 유저의 로그인으로 판단하고 유저 식별자와 JWT 반환
postLoginRes = loginProvider.getUserInfo(String.valueOf(userInfo.get("email")));
return new BaseResponse<>(postLoginRes);
}
} catch (BaseException exception) {
return new BaseResponse<>((exception.getStatus()));
}
}
1-7. 인가 코드 받기
인가 코드는 카카오에서 지정한 형식의 URI을 통해 받아올 수 있다.
예시)
https://kauth.kakao.com/oauth/authorize?client_id=REST_API_KEY입력&redirect_uri=http://localhost:8080/app/login/kakao&response_type=code
서버를 실행하고 해당 URI를 크롬 URL 창에 입력하면 카카오 인증 서버로부터 쿼리 스트링으로 인가 받은 코드를 확인할 수 있다.
1-8. 토큰 발급 (Service 구현)
인가 코드를 사용하여 카카오 인증 서버로부터 Access토큰과 Refresh 토큰을 발급받는다.
public String getKakaoAccessToken (String code) {
String accessToken = "";
String refreshToken = "";
String requestURL = "https://kauth.kakao.com/oauth/token";
try {
URL url = new URL(requestURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
// setDoOutput()은 OutputStream으로 POST 데이터를 넘겨 주겠다는 옵션이다.
// POST 요청을 수행하려면 setDoOutput()을 true로 설정한다.
conn.setDoOutput(true);
// POST 요청에서 필요한 파라미터를 OutputStream을 통해 전송
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
String sb = "grant_type=authorization_code" +
"&client_id=REST_API_KEY 입력" + // REST_API_KEY
"&redirect_uri=http://localhost:8080/app/login/kakao" + // REDIRECT_URI
"&code=" + code;
bufferedWriter.write(sb);
bufferedWriter.flush();
int responseCode = conn.getResponseCode();
System.out.println("responseCode : " + responseCode);
// 요청을 통해 얻은 데이터를 InputStreamReader을 통해 읽어 오기
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
StringBuilder result = new StringBuilder();
while ((line = bufferedReader.readLine()) != null) {
result.append(line);
}
System.out.println("response body : " + result);
JsonElement element = JsonParser.parseString(result.toString());
accessToken = element.getAsJsonObject().get("access_token").getAsString();
refreshToken = element.getAsJsonObject().get("refresh_token").getAsString();
System.out.println("accessToken : " + accessToken);
System.out.println("refreshToken : " + refreshToken);
bufferedReader.close();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
return accessToken;
}
1-7 단계에서 인가 받은 코드를 사용하여 토큰 발급이 정상적으로 수행되면 다음과 같이 반환 정보가 출력될 것이다.
1-9. 사용자 로그인 처리 (Service 구현)
토큰이 발급 되었으면 해당 토큰으로 사용자 정보(이메일)를 불러와 기존 회원인지 판단한다.
만일, 기존 회원이면 로그인 처리를 하여 사용자 식별자와 JWT를 반환한다.
기존 회원이 아니면 사용자 정보로 회원 가입을 진행해야 하는데 권한 문제로 전화번호를 불러올 수 없는 상황이니 생략하였다.
public HashMap<String, Object> getUserInfo(String accessToken) {
HashMap<String, Object> userInfo = new HashMap<>();
String postURL = "https://kapi.kakao.com/v2/user/me";
try {
URL url = new URL(postURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Authorization", "Bearer " + accessToken);
int responseCode = conn.getResponseCode();
System.out.println("responseCode : " + responseCode);
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
StringBuilder result = new StringBuilder();
while ((line = br.readLine()) != null) {
result.append(line);
}
System.out.println("response body : " + result);
JsonElement element = JsonParser.parseString(result.toString());
JsonObject properties = element.getAsJsonObject().get("properties").getAsJsonObject();
JsonObject kakaoAccount = element.getAsJsonObject().get("kakao_account").getAsJsonObject();
String nickname = properties.getAsJsonObject().get("nickname").getAsString();
String email = kakaoAccount.getAsJsonObject().get("email").getAsString();
userInfo.put("nickname", nickname);
userInfo.put("email", email);
} catch (IOException exception) {
exception.printStackTrace();
}
return userInfo;
}
발급 받은 토큰으로 사용자 정보를 조회해 보았다. nickname과 이메일이 잘 조회된 것을 확인할 수 있다.
Access 토큰 갱신은 카카오 개발 문서의 토큰 갱신하기 부분을 참고하였다.
구현 방식은 로그인 방식과 거의 비슷하여 요청 URL과 파라미터, 반환 데이터들을 문서에서 참고하였고, 해당 정보를 기반으로 갱신 Controller와 Service를 구현하였다.
Controller 작성
/**
* 카카오 토큰 갱신 API
* [GET] /app/login/kakao/:userId
* @return BaseResponse<String>
*/
@ResponseBody
@GetMapping("/kakao/{userId}")
public BaseResponse<String> updateKakaoToken(@PathVariable int userId) {
String result = "";
try {
//jwt에서 id 추출.
int userIdxByJwt = jwtService.getUserIdx();
//userIdx와 접근한 유저가 같은지 확인
if(userId != userIdxByJwt){
return new BaseResponse<>(INVALID_USER_JWT);
}
//같으면 토큰 갱신
loginService.updateKakaoToken(userId);
return new BaseResponse<>(result);
} catch (BaseException exception) {
return new BaseResponse<>((exception.getStatus()));
}
}
Service 작성
public void updateKakaoToken(int userId) throws BaseException {
KakaoToken kakaoToken = loginProvider.getKakaoToken(userId);
String postURL = "https://kauth.kakao.com/oauth/token";
KakaoToken newToken = null;
try {
URL url = new URL(postURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
// POST 요청에 필요한 파라미터를 OutputStream을 통해 전송
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
String sb = "grant_type=refresh_token" +
"&client_id=REST_API_KEY 입력" + // REST_API_KEY
"&refresh_token=" + kakaoToken.getRefresh_token() + // REFRESH_TOKEN
"&client_secret=시크릿 키 입력";
bufferedWriter.write(sb);
bufferedWriter.flush();
// 요청을 통해 얻은 데이터를 InputStreamReader을 통해 읽어 오기
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
StringBuilder result = new StringBuilder();
while ((line = bufferedReader.readLine()) != null) {
result.append(line);
}
System.out.println("response body : " + result);
JsonElement element = JsonParser.parseString(result.toString());
Set<String> keySet = element.getAsJsonObject().keySet();
// 새로 발급 받은 accessToken 불러오기
String accessToken = element.getAsJsonObject().get("access_token").getAsString();
// refreshToken은 유효 기간이 1개월 미만인 경우에만 갱신되어 반환되므로,
// 반환되지 않는 경우의 상황을 if문으로 처리해주었다.
String refreshToken = "";
if(keySet.contains("refresh_token")) {
refreshToken = element.getAsJsonObject().get("refresh_token").getAsString();
}
if(refreshToken.equals("")) {
newToken = new KakaoToken(accessToken, kakaoToken.getRefresh_token());
} else {
newToken = new KakaoToken(accessToken, refreshToken);
}
bufferedReader.close();
bufferedWriter.close();
} catch (IOException exception) {
exception.printStackTrace();
}
try{
int result = 0;
if (newToken != null) {
result = loginDao.updateKakaoToken(userId, newToken);
}
if(result == 0){
throw new BaseException(UPDATE_FAIL_TOKEN);
}
} catch(Exception exception){
throw new BaseException(DATABASE_ERROR);
}
}
테스트 화면
Access 토큰 갱신 전
토큰 갱신 요청
Access 토큰 갱신 후