요즘 SNS 소셜 로그인을 많이 이용하게 되는데 서비스를 만들기전에 직접 구글 로그인을 구현해 보기 위해 이 글을 작성하게 되었습니다. 일단 구글 로그인 기능 구현을 위해 선행적으로 알아야하는 개념은 Oauth, JWT 이 두가지를 알아야합니다. Oauth 부터 알아보도록 하겠습니다.
Oauth는 Open Authorization의 약자입니다. 일단 Oauth가 나온 배경부터 이야기하자면 현재 내가 사용하고 있는 서비스에서 제 3자 서비스에 대한 리소스를 가져오기 위해서는 사용자가 지금 사용하는 서비스에 제 3자 서비스에 대한 ID와 PW를 넘겨줘야 사용중인 서비스가 제 3자 서비스에 대한 리소스를 가져올 수 있습니다. 하지만 이 방법은 제 3자 서비스나 개인 입장에서는 별로 탐탁치 않은 방법일 것입니다. 왜냐하면 보안적인 문제 때문에 많이 찝찝하기 때문입니다. 그래서 Oauth라는 개념이 나오게 되었습니다. 이제 부터 제 3자 서비스는 “서비스 제공자”, 사용중인 서비스는 “소비자”, 서비스 제공자와 소비자에 대한 계정이 있는 개인을 “사용자”라고 정의하겠습니다. 소비자는 서비스 제공자에게 리소스를 요청하기 위한 권한이 있어야 하는데 이러한 권한을 얻기 위한 인증과정을 한번 살펴보겠습니다.
인증과정
이러한 과정을 통해 진행이 된다고 보면 됩니다. 과정이 많이 복잡해 보이는데 한번 직관적으로 접근해 보도록 하겠습니다. 1번과 2번 과정은 요청토큰을 요청하고 발급하는 과정이 있는데 이러한 과정은 소비자가 허가된 서비스 인지 검증하기 위해 사전에 서비스제공자에 등록하는 경우라고 보시면 될것 같습니다. 3번 과정을 통해 사용자는 서비스 제공자에 인증 요청을 보내게 됩니다. 그래서 인증이 완료되면 4번 과정을 통해 아까 1번 2번 과정에서 등록한 소비자인지 확인하고 등록한 소비자라고 하면 소비자로 이동시켜주게 됩니다. 그래서 5번 6번 과정을 통해 권한이 부여된 토큰을 발급하고 7번 과정을 통해 토큰을 통해 서비스 제공자에 있는 리소스를 가져올 수 있다고 보시면 될 것 같습니다.
JWT는 Json Web Token의 약자이다. JWT는 당사자 간의 정보를 JSON으로 안전하게 전송하기 위한 Claim기반의 Web Token 입니다. 이 정보는 HMAC 또는 RSA를 사용하는 공개키/개인키를 통해 서명할 수 있기 때문에 신뢰할 수 있습니다. 그래서 해당 토큰을 통해 사용자를 식별하고 리소스에 접근해서 리소스를 가져올 수 있습니다. JWT의 구조는 Header, Payload, Signature 3개로 나뉩니다. 이 3개는 “.” 구분자를 통해 구분됩니다. 그래서 각 부분은 Base64로 인코딩 됩니다. 각 구조에 대해 한번 알아보도록 하겠습니다.
Header는 토큰의 타입과 사용중인 서명 알고리즘에 대한 값이 들어가 있습니다. JWT에서 가장 첫번째 부분을 담당 합니다.
Payload는 Claim들을 포함하고 있고 JWT에서 두번째 부분을 담당 하고 있습니다. Claim은 사용자에 대한 데이터를 이야기합니다. 그래서 발급자, 토큰 만료시간, 식별자 등을 포함할 수 있습니다.
Singnature는 Base64로 인코딩된 Header와 Payload 그리고 secret을 헤더에 있는 알고리즘을 통한 암호화로 생성할 수 있고 중간에 메세지가 변경되지 않았는지 확인하는데 사용됩니다.
Base64를 통해 데이터가 인코딩 되었기 때문에 토큰에는 제3자가 알면 안되는 중요한 정보를 담지 말아야 합니다. 또한 일반적으로 헤더를 통해 전송이 되기 때문에 너무 큰 데이터를 담으면 안됩니다.
이제부터는 Google Oauth에 대해 직접 구현하면서 알아보도록 하겠습니다. 먼저 구글 클라우드에 접속합니다. https://console.cloud.google.com/welcome 해당 url로 접속하게 되면 아래와 같은 화면이 나오게 됩니다.

빨간 네모박스 안에 있는 프로젝트 선택을 클릭해주시면 프로젝트를 생성할 수 있습니다. 프로젝트 생성후에 선택해주게 되면 API 및 서비스 탭에 있는 사용자 인증 정보로 이동해주시면 아래와 같은 화면이 나오게 됩니다.

박스안에 있는 사용자 인증 정보 만들기를 클릭하게 되면 여러가지가 나오게 되는데 그중에서 저희는 OAuth를 구현할 것이기 때문에 OAuth 클라이언트 ID를 눌러줍니다. 동의 화면을 만들라고 하는데 해당 화면은 소비자가 인증할 때 보여줄 화면입니다. 그래서 User Type은 일단 외부로 설정해두고 필수 정보들만 입력해 주도록 합시다. 범위를 지정할 수 있는데 API 사용에 대한 권한을 이야기합니다. 기본적으로 로그인만 진행할 것이기 때문에 email과 profile만 추가하도록 하겠습니다.

이제 동의 화면을 다 만들었기 때문에 아까 사용자 인증 정보 만들기를 통해 다시 OAuth 클라이언트 ID를 눌러줍니다. 그러면 아래와 같은 화면이 나오게 되는데요 저희는 웹 어플리케이션을 만들것이기 때문에 웹 어플리케이션을 선택해줍니다.

웹 어플리케이션을 선택하게 되면 아래와 같은 화면이 나타나게 됩니다.

여기서 승인된 자바스크립트 원본에 본인의 웹 BaseUrl를 넣어주도록 합시다. 이때 https가 아닌 http로 설정하게 되면 동작하지 않습니다. 설정이 다 되었으면 이제 Code를 작성해 보도록 하겠습니다. html 부터 작성하게 될텐데 코드는 아래와 같습니다.
<html>
<body>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<div id="g_id_onload"
data-client_id="본인 OAuth Client ID"
data-login_uri="인증 완료후 돌아갈 화면">
</div>
<div class="g_id_signin"
data-type="standard"
data-size="large"
data-theme="outline"
data-text="sign_in_with"
data-shape="rectangular"
data-logo_alignment="left">
</div>
</body>
</html>
일단 g_id_onload가 핵심적인 부분입니다. data-client-id와 data-login-uri를 설정해 주었는데요 data-client_id는 아까 생성한 본인 OAuth Client ID를 넣어주시면 됩니다. 그리고 data-login_uri는 인증 완료후 돌아갈 화면에 대한 uri를 입력해 주시면 됩니다. 이렇게 설정이 되었으면 Controller 부분을 작성하도록 하겠습니다.
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping("/auth")
public class AuthController {
private final JwtDecoder jwtDecoder;
@PostMapping("/google")
public ResponseEntity<GoogleAuth> googleLogin(@RequestParam("credential") String jwt) {
GoogleAuth auth = jwtDecoder.decode(jwt, GoogleAuth.class);
return ResponseEntity.ok(auth);
}
}
저는 Redirect Uri를 /auth/google로 설정해주었습니다. 이때 credential(JWT)이 Form 형식으로 되어있기 때문에 RequestParam을 통해 받아올 수 있습니다. credential은 JWT 형태로 되어있기 때문에 decode하게 되면 아래와 같이 나오게 됩니다.
{
"iss": "https://accounts.google.com",
"azp": "1234987819200.apps.googleusercontent.com",
"aud": "1234987819200.apps.googleusercontent.com",
"sub": "10769150350006150715113082367",
"at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
"hd": "example.com",
"email": "jsmith@example.com",
"email_verified": "true",
"iat": 1353601026,
"exp": 1353604926,
"nonce": "0394852-3190485-2490358"
}
해당 데이터에 대한 정보는 https://developers.google.com/identity/openid-connect/openid-connect#sendauthrequest 해당링크에서 자세하게 확인 할 수 있습니다. 저희가 중점적으로 봐야하는건 sub와 email입니다. sub는 고유한 uuid값이고 email은 소비자 이메일을 나타냅니다. 그래서 로그인을 검증하기 위해서는 두 값이 가장 중요합니다. 다만 sub은 항상 제공되지만 email은 경우에 따라 제공되지 않을 수도 있습니다. 다시 돌아와서 JwtDecoder와 GoogleAuth의 코드를 보도록 하겠습니다.
@AllArgsConstructor
@Component
public class JwtDecoder {
private final ObjectMapper objectMapper;
public <T> T decode(String jwt, Class<T> type){
try{
String[] pieces = jwt.split("\\.");
String b64payload = pieces[1];
String jsonString = new String(Base64Utils.decode(b64payload.getBytes()));
return objectMapper.readValue(jsonString,type);
}catch (JsonProcessingException e){
return null;
}
}
}
먼저 JwtDecoder 부터 보면 jwt String을 받아와 “.” 구분자를 통해 분리를 하고 그 중 두번째에 해당하는 우리가 필요한 데이터가 있는 payload부분을 Base64Utils를 통해 decode를 시키게 되면 Json으로 반환하게 됩니다. 이러한 Json을 ObjectMapper를 통해 GoogleAuth Type으로 반환하도록 했습니다.
@Data
public class GoogleAuth {
private String sub;
private String email;
}
sub와 email만 사용할 것이기 때문에 두 개의 필드만 추가하게 되었습니다.
구글은 문서화가 잘 되어있어서 쉽게 적용할 수 있었던 것 같습니다. 해당 기능을 구현하면서 OAuth와 JWT의 개념을 알 수 있었고 https를 적용하기 위한 방법도 알게되어서 좋았습니다.