거의 대부분의 서비스는 로그인 서비스를 제공하는데 이러한 서비스는 유출이 된다면 엄청난 피해가 발생하므로 굉장히 안전해야 한다. 그런데 이러한 로그인 서비스를 자체적으로 구현하여 높은 수준의 보안을 구현하기란 서비스 로직보다 로그인 구현에 더 비중이 많아지는 상황이 생기게 된다. 따라서 이미 검증된 구글, 카카오, 네이버 등의 다양한 플랫폼에 이러한 서비스를 맡기는 것이다. 이번 시간에는 OAuth를 이용한 카카오 로그인 서비스를 구현해 보자.
그렇다면 어떻게 외부 서비스에서 카카오 유저인지 어떻게 알 수 있을까?
로그인을 하기 위해선 ID/PW를 전달해서 로그인을 하면 되는데 이것을 직접 전달받아 전달하는 것은 보안상 매우 취약하다. 따라서 카카오에서 로그인을 하고 인증절차를 거쳐서 사용자 정보를 전달받도록 한다.
클라이언트가 카카오 로그인을 하면 카카오에서 Authorization Cdoe를 발급해주고 이 Authorization Code를 통해 카카오로부터 AccessToken을 발급받게 된다. 그러면 이 AccessToken을 통해 사용자 정보를 전달받게 된다. 여기서 Authorization Code는 유저가 복붙으로 우리에게 전달해주기는 매우 어려우므로, Redirect를 사용해서 구현하게된다.
로그인에 성공하면 해당 URL로 자동 리다이렉트 해주고 우리는 URI의 쿼리에서 인가 코드를 가져올 수 있고, 이 인가 코드를 통해 AccessToken을 얻을 수 있다. 그런데 이 redirect URI의 주소가 조작되거나 변조될 수 있다면 중간 탈취자가 나의 인가 코드를 가지고 액세스 토큰을 발급할 수 있게 된다.
따라서 OAuth 를 사용하게 위해서는 카카오에 redirect URI를 등록해야한다.
이렇게 하면 누군가 리다이렉트 주소를 변경해도 이미 등록된 주소가 아니면 조작되었다고 판단하고 반환하지 않게 된다.
정리하자면
1. 사용자가 서비스에서 카카오 로그인을 누르면 카카오 로그인 창이 뜬다.
2. 카카오 로그인이 정상적으로 수행되면 카카오가 인가 코드(Authorization Code)를 미리 설정한 redirect URI로 되돌려준다.
3. 서비스는 redirect URI로 들어온 url에서 인가코드를 얻어 kakao server에 AccessToken을 요청한다.
4. 카카오는 인카코드를 확인하고 AccessToken을 돌려준다.
5. 사용자가 AccessToken을 얻고 이걸로 서비스에 가입한다.
6. 서비스는 AccessToken으로 카카오 서버에 사용자 정보를 요청하고 값을 가져올 수 있으면 JWT를 발급 시켜준다.
7. 사용자는 발급받은 JWT로 서비스의 API를 요청한다.
그렇다면 이제 코드에 적용시켜보자.
https://developers.kakao.com/console/app 카카오 Developer 사이트에서 App을 생성하고 앱의 주소, redirect URI를 설정하자.
카카오와 통신하기 위해 RestTemplate을 빈으로 등록한다.
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
카카오와의 연동 결과를 Json을 객체로 매핑하기 위해 gson 라이브러리를 사용하였다.
implementation 'com.google.code.gson:gson'
다음으론 카카오 api와 연동하기 위해 application.yml을 작성한다.
(참고) https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
social:
kakao:
client-id: 4dbcb6576cb9e895c6419116dd540374
redirect: /oauth/kakao/redirect
url:
login: https://kauth.kakao.com/oauth/authorize
token: https://kauth.kakao.com/oauth/token
profile: https://kapi.kakao.com/v2/user/me
unlink: https://kapi.kakao.com/v1/user/unlink
spring:
url:
base: http://localhost:8080
로그인을 하는 방식은
1. [http://localhost:8080/oauth/kakao/login] 으로 접속한다.
2. 카카오 로그인 버튼 창을 띄어준다. 해당 버튼을 누르면 요청 URL을 보내고 카카오 로그인 창이 뜬다.
3. 요청 URL은 인가 코드 받기에 있는 URL이다.
그렇다면 카카오 서버에 나의 {REST_API_KEY}와 {REDIRECT_URI}를 담아서 GET요청을 보내는 코드를 작성하자
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/oauth/kakao")
public class KOAuthController {
private final RestTemplate restTemplate;
private final Environment env;
private final KakaoService kakaoService;
@Value("${spring.url.base}")
private String baseUrl;
@Value("${social.kakao.client-id}")
private String kakaoClientId;
@Value("${social.kakao.redirect}")
private String kakaoRedirectUri;
@GetMapping("/login")
public ModelAndView socialLogin(ModelAndView mav) {
StringBuilder loginUri = new StringBuilder()
.append(env.getProperty("social.kakao.url.login"))
.append("?response_type=code")
.append("&client_id=").append(kakaoClientId)
.append("&redirect_uri=").append(baseUrl).append(kakaoRedirectUri);
mav.addObject("loginUrl", loginUri);
mav.setViewName("social/login");
return mav;
}
}
카카오톡 로그인 버튼을 위한 view를 만들어 주자(login.ftl)
<button onclick="popupKakaoLogin()">KakaoLogin</button>
<script>
function popupKakaoLogin() {
window.open('${loginUrl}', 'popupKakaoLogin', 'width=730,height=400,scrollbars=0,toolbar=0,menubar=no')
}
</script>
카카오 로그인 버튼을 누르면 loginUrl로 요청이 가고 카카오톡 로고인 창이 뜨고 사용자는 로그인 후 동의항목을 체크하고 진행하게 된다.
이때 URL에는 client_id와 redirect_uri를 가지고있고 카카오 서버가 검증 후 redirect_uri로 사용자의 인가 코드를 쿼리 파라미터로 포함해서 리다이렉트한다.
카카오가 돌려주는 Authorization Code를 통해 토큰을 받기 위해 redirect_uri로 get요청을 보내자
@GetMapping(value = "/redirect")
public ModelAndView redirectKakao(
ModelAndView mav,
@RequestHeader("Authorization Code")
@RequestParam String code) {
mav.addObject("authInfo", kakaoService.getKakaoTokenInfo(code));
mav.setViewName("social/redirectKakao");
return mav;
}
쿼리 파라미터에서 인가 코드를 얻어서 카카오에 토큰을 요청한다.
해당 토큰을 매핑하기위한 RetKakaoOAuth 객체를 만들자.
@Getter
public class RetKakaoOAuth {
private String token_type;
private String access_token;
private Integer expires_in;
private String refresh_token;
private String refresh_token_expires_in;
private String scope;
}
카카오 토큰 데이터를 RestKakaoToken으로 매핑하자.
public RetKakaoOAuth getKakaoTokenInfo(String code) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", kakaoClientId);
params.add("redirect_uri", baseUrl + kakaoRedirectUri);
params.add("code", code);
String requestUri = env.getProperty("social.kakao.url.token");
if (requestUri == null) throw new AccountsExceptionHandler(ErrorCode._INTERNAL_SERVER_ERROR);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
ResponseEntity<String> response = restTemplate.postForEntity(requestUri, request, String.class);
if (response.getStatusCode() == HttpStatus.OK)
return gson.fromJson(response.getBody(), RetKakaoOAuth.class);
throw new AccountsExceptionHandler(ErrorCode._INTERNAL_SERVER_ERROR);
}
응답이 정상적으로 온다면 gson을 이용하여 전달받은 Json 데이터를 RetKakaoOAuth로 매핑한다.
발급받은 토큰을 확인하기 위한 view도 만들어보자.(redirectKakao.ftl)
<ol>
<li>token_type : ${authInfo.token_type}</li>
<li>access_token : ${authInfo.access_token}</li>
<li>expires_in : ${authInfo.expires_in}</li>
<li>refresh_token : ${authInfo.refresh_token}</li>
<li>refresh_token_expires_in : ${authInfo.refresh_token_expires_in}</li>
<li>scope : ${authInfo.scope}</li>
</ol>
이렇게 하면 로그인시에 다음 화면이 반환된다.
다음시간에는 이토큰을 이용한 회원가입 및 로그인을 구현해 보겠다.