요즘 웹사이트 로그인 화면을 보면 아래처럼 다른 사이트의 계정을 연동하여 사용할 수 있도록 되어 있는 것을 흔히 볼 수 있다.
위와 같은 소셜 로그인은 기업 입장에서는 소규모 어플리케이션이나 스타트업 단계에 있어 인증/인가 과정에 들어가는 비용을 절감시키고, 사용자 입장에서 귀찮은 회원가입을 생략하여 클릭 몇 번으로 로그인 과정을 간소화하여 편리함을 높일 수 있다.
비품인 프로젝트에서도 구글 로그인을 사용하여 로그인 기능을 비교적 쉽게 구현해볼 수 있었다.
이러한 소셜 로그인 기능은 모두 Oauth 2.0 프로토콜을 사용하여 인증 / 인가가 이루어진다.
각 소셜 네트워크 플랫폼마다 인증코드를 발급받을 수 있는 방법은 상이하지만, 전체적인 프로세스는 위와 동일하게 이루어진다.
1. 소셜 로그인 서비스 제공자에게 Client 정보 등록 >> ClientId와 Secret Key 생성
2. 사용자가 로그인 버튼을 클릭하면 클라이언트 단에서 로그인 창으로 요청을 보낸다. 이 때, 각 서비스 제공자에게 제공받은 정보를 파라미터에 추가하여 요청을 보낸다.
doGoogleLogin() {
const url = 'https://accounts.google.com/o/oauth2/v2/auth?client_id=' +
process.env.VUE_APP_GOOGLE_CLIENT_ID +
'&redirect_uri=' +
process.env.VUE_APP_GOOGLE_REDIRECT_URL +
'&response_type=code' +
'&scope=email profile';
this.showSocialLoginPopup(url)
},
3. 정보가 맞다면, 로그인 창이 뜨고 사용자 정보를 입력하여 로그인을 진행한다.
4. 서비스 제공자는 인가코드를 설정된 RedirectUrl 로 Response한다.
5. 인가코드로 AccessToken을 요청한다.
// 1. "인가 코드"로 "액세스 토큰" 요청
private AccessTokenDto getToken(String code, String urlType, GoogleTokenType googleTokenType) throws JsonProcessingException {
String redirectUrl = "";
if (googleTokenType == GoogleTokenType.LOGIN) {
redirectUrl = urlType.equals("local") ? redirectLocalUrl : redirectServerUrl;
} else {
redirectUrl = urlType.equals("local") ? redirectLocalDeleteUrl : redirectServerDeleteUrl;
}
// 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("code", code);
body.add("client_id", clientId); // 클라이언트 Id
body.add("client_secret", clientSecret); // 클라이언트 Secret
body.add("redirect_uri", redirectUrl);
body.add("grant_type", "authorization_code");
body.add("access_type", "offline");
body.add("approval_prompt", "force");
// HTTP 요청 보내기1
HttpEntity<MultiValueMap<String, String>> googleTokenRequest =
new HttpEntity<>(body, headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
"https://oauth2.googleapis.com/token",
HttpMethod.POST,
googleTokenRequest,
String.class
);
// HTTP 응답 (JSON) -> 액세스 토큰 파싱
ObjectMapper objectMapper = new ObjectMapper(); // 받은 것을 Json형태로 파싱
return objectMapper.readValue(response.getBody(), AccessTokenDto.class);
}
6. 반환된 AccessToken으로 사용자 정보를 요청한다.
// 2. 토큰으로 구글 로그인 API 호출 : "액세스 토큰"으로 "구글 사용자 정보" 가져오기
private GoogleUserInfoDto getGoogleUserInfo(AccessTokenDto accessToken) throws JsonProcessingException {
// HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken.getAccess_token());
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HTTP 요청 보내기
HttpEntity<MultiValueMap<String, String>> googleUserInfoRequest = new HttpEntity<>(headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
"https://www.googleapis.com/oauth2/v2/userinfo",
HttpMethod.GET,
googleUserInfoRequest,
String.class
);
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(response.getBody(), GoogleUserInfoDto.class);
}
위의 과정까지가 구글 사용자 정보를 얻기까지의 과정이다. 해당 정보들을 모두 민감한 개인정보를 담고 있으므로, 클라이언트에게 Response할 때, 보안에 유의해야한다.
비품인 프로젝트의 경우에는 Spring Security + Jwt + Cookie를 사용하여 사용자 정보를 전달하였다.
Jwt + Cookie 의 취약점 보완을 위해 서버에서 쿠키를 생성하고, HTTP Only 설정으로 브라우저에서 쿠키에 접근할 수 없게 하였다. 또한 Secure 설정으로 HTTPS 통신에만 쿠키를 전송하도록 설정했다.
// 4. JWT 토큰 반환
ResponseCookie cookie = ResponseCookie.from(
JwtUtil.AUTHORIZATION_HEADER,
URLEncoder.encode(createdAccessToken, "UTF-8")).
path("/").
httpOnly(true).
sameSite("None").
secure(true).
maxAge(JwtUtil.ACCESS_TOKEN_TIME).
build();
httpServletResponse.addHeader("Set-Cookie", cookie.toString());
참고 자료
OAuth 프로토콜의 이해와 활용 2 - OAuth란 무엇인가?
앞에서는 OAuth가 왜 필요하고 어떻게 발전해 왔는지 알아보았습니다. 이번시간에는 OAuth가 무엇이고, 어떻게 흘러가는지 알아보겠습니다. OAuth는 위와 같은 플로우로 이루어 집니다.. 라고 하면
gdtbgl93.tistory.com