Google Oauth - Token 획득하기 (Signup/Signin용)
728x90
반응형

 

 

구글 ID를 통해 회원가입 및 로그인을 하는 서비스를 많이 보았을 것이다. 이를 개발하는 방법을 여기서 다루어본다. 또한 필자가 당황했던 포인트도 같이 다룬다.

여기서 각자의 서비스에서 사용되는 로그인과 가입과 관련한 로직은 나오지 않는다. 오직 Oauth 토큰 획득에 포커싱이 맞춰져 있으니 Signup/Signin 로직을 찾는 분들은 다른 곳에 문을 두드리길 권한다.

 

 

들어가기 앞서

아래 사항이 이미 준비되어 있음을 가정하고 시작한다. 만약 준비가 안되어 있다면 Google 개발자 문서 혹은 다른 개발자의 자료를 통해 준비하는 방법을 확인하길 바란다.

  • 개발용 구글 계정
  • 사용자 인증 정보 생성
    • 아래의 클라이언트 ID를 획득하기 위함이다.
  • 웹 애플리케이션의 클라이언트 ID

 

 

 

인증요청 (Auth URI)

유저가 회원가입 혹은 로그인을 하기 위해 토큰을 얻겠다라는 요청하는 단계다. 화면으로 설명을 해보자면, 회원가입 버튼 또는 로그인 버튼을 누르면 구글 로그인 혹은 구글 계정 선택창이 나타나는 부분이 여기에 해당한다. URL에 파라미터만 잘 세팅해두면 된다.

"https://accounts.google.com/o/oauth2/v2/auth"를 Base URL로하여 파라미터를 추가하면 된다. 파라미터 정보는 아래와 같다. 구글 문서에서도 동일한 정보를 확인할 수 있다. "HTTP / REST" 탭을 선택해서 확인해 볼 것.

  • scope (필수값)
    • 어떤 범주의 정보를 획득하게 할 것인가
    • 회원가입 단계에서는 userinfo.profile 범주를 가장 많이 사용한다.
  • access_type
    • 실제 유저가 브라우저에 접근하고 있지 않아도 다른 프로그램으로 인해 토큰을 갱신할 수 있는지 여부를 결정한다.
    • offline인 경우에 이 여부가 가능하게 된다.
    • 다른 샘플코드에서도 offline으로 많이 사용한다.
  • include_granted_scopes
    • 애플리케이션이 컨텍스트에서 추가 범위에 대한 액세스를 요청하기 위해 추가 권한 부여를 사용할지를 결정한다.
    • 위에서 언급한 scope를 추가하기 위한 조건이 아닐까 추정해본다.
    • 많은 샘플코드에서 true로 사용하기에 나도 그렇게 사용해보았다.
  • response_type (필수값)
    • 인증코드를 받을 것인지, 어떻게 받을 것인지 선택하는 란이다.
    • 웹 서버라면 "code"를 입력하면 된다.
  • state
    • 뭔지 모르겠다. 정말. 공식문서에도 파라미터의 설명이 없다. 아는 분이 있다면 제보 부탁.
    • 일단 무지성으로 state_parameter_passthrough_value를 넣으면 된다.
  • redirect_uri (필수값)
    • 요청이 정상처리되면 code를 전달 받을 수 있는 Redirect URI
    • 구글 콘솔에 해당 URI가 추가되어 있어야 한다.
  • client_id (필수값)
    • 위에서 언급한 클라이언트 ID

 

아래는 샘플 Auth URI이다. scope와 redirect_uri, client_id는 독자들 각자에게 맞게 변경해야 한다.

https://accounts.google.com/o/oauth2/v2/auth?scope=https%3A//www.googleapis.com/auth/userinfo.profile&access\_type=offline&include\_granted\_scopes=true&response\_type=code&state=state\_parameter\_passthrough\_value&redirect\_uri=http%3A//localhost:4007/sample/oauth/google&client\_id=sample-client-id.apps.googleusercontent.com

 

위 URL을 브라우저에 입력하면 구글 로그인창 혹은 계정 선택창이 나오고 정상적인 구글 로그인 절차를 밟으면 redirect_uri에 입력된 API로 요청이 들어온다. 이제 그 API를 어떻게 작업할지 다뤄본다.

 

 

 

토큰 획득 방법

우선 redirect_uri에 삽입된 API를 살펴보자. snsApiClient 내부에 토큰을 얻을 로직을 넣으면 된다. snsApiClient는 생성자 주입받아서 사용하고 있다.

@RestController
@RequestMapping("sample")
@Api(description = "SignUp - 회원가입")
class UserSignUpController(private val snsApiClient: SnsApiClient) {

    // ... 일부 생략 ...

    @GetMapping("/oauth/google")
    @ApiOperation(value = "구글 - code를 통한 회원가입")
    fun oauthGoogle(@RequestParam code: String) {
        println(">>> START oauthGoogle")
        snsApiClient.getGoogleAccessToken(code)
    }

}

 

간단하게 Controller에 진입점을 만들어 두었다. Auth URI를 보면 Redirect URI가 위의 API임을 알 수 있다. 이제 Auth code를 받을 수 있다. 이 code로 토큰을 받아보자.

 

@Component
class SnsApiClient(
    @Value("\${google.client-id}")
    private val googleClientId: String,
    @Value("\${google.client-secret}")
    private val googleClientSecret: String,
    @Value("\${google.redirect-uri}")
    private val googleRedirectUri: String,

    private val restTemplate: RestTemplate,
    private val objectMapper: ObjectMapper
) {
    private val SIGNUP_API_GOOGLE = "https://oauth2.googleapis.com/token"

    // ... 일부 생략 ...

    @Throws(Exception::class)
    fun getGoogleAccessToken(authorizeCode: String) {
        println("##### authorizeCode ::: " + authorizeCode)
        val headers = HttpHeaders()
        headers.contentType = MediaType.APPLICATION_FORM_URLENCODED
        val parameters: MultiValueMap<String, String> = LinkedMultiValueMap()
        parameters.add("client_id", googleClientId)
        parameters.add("client_secret", googleClientSecret)
        parameters.add("code", authorizeCode)
        parameters.add("grant_type", "authorization_code")
        parameters.add("redirect_uri", googleRedirectUri)

        val req = HttpEntity(parameters, headers)
        try {
            val res: ResponseEntity<String> = restTemplate.exchange(
                SIGNUP_API_GOOGLE,
                HttpMethod.POST,
                req,
                String::class.java
            )

            val googleAuth = objectMapper.readValue(res.body, GoogleAuth::class.java)
            println("##### token ::: " + googleAuth.access_token)
        } catch (e: Exception) {
            println("e.message >>> ${e.message}")
        }
    }
}

data class GoogleAuth(
    val access_token: String,
    val expires_in: String,
    val token_type: String,
    val scope: String,
    val refresh_token: String
)

 

각 파라미터의 설명은 생략한다. 궁금하신 분들은 잘 설명한 분들의 글 혹은 구글 문서를 참고하자. 참고로 파라미터 중 redirect_uri가 있는데 이것은 앞에 등장했던 Auth URI의 Redirect URI와 동일한 값을 넣으면 된다. 또한 code를 통한 토큰 획득이기에 grant_type은 반드시 "authorization_code"로 해야한다.

이렇게하여 원하는 토큰을 얻었다. 그래서 끝인가? 물론 아니다. 토큰으로부터 유저 정보를 획득해야 한다. 필자는 유저 정보 획득하는 방법까지 공개하도록 하겠다.

 

 

 

 

개인정보, 유저정보는 조심히 다룹시다. 이 이미지를 직관하고 싶지 않다면.

 

토큰을 통한 유저정보 획득

유저 정보가 아닌 다른 정보를 획득하고 싶다면 이전 단계에서 scope 설정 변경과 아래 코드에서 USER_API_GOOGLE를 변경해야 한다. restTemplate과 objectMapper는 생성자 주입으로 받은 변수이다.

private val USER_API_GOOGLE = "https://openidconnect.googleapis.com/v1/userinfo?access_token="

fun getSnsUserModel(oauthToken: String): UserModel {
    val headers = HttpHeaders()
    headers.set("Authorization", "bearer $oauthToken")

    val req = HttpEntity<MultiValueMap<String, String>>(LinkedMultiValueMap(), headers)
    val res = restTemplate.exchange(
        USER_API_GOOGLE + oauthToken,
        HttpMethod.POST,
        req,
        String::class.java
    )

    return objectMapper.readValue(res.body, UserModel::class.java)
}

class UserModel(
    val sub: String? = null,
    val name: String? = null,
    val given_name: String? = null,
    val family_name: String? = null,
    val picture: String? = null,
    val email: String? = null,
    val email_verified: Boolean? = false,
    val locale: String? = null,
)

이로서 원하는 정보를 모두 획득했다. 이제 개발 중 난감했던 순간을 공유해본다.

 

 

 

HttpClientErrorException - contentType 이슈

개발을 하다가 아래와 같은 에러를 볼 경우가 있다.

HttpClientErrorException$BadRequest: 400 Bad Request
"code": 400, "message": "Invalid JSON payload received. Unknown name \"client_id\": Proto field is not repeating, cannot start list.\nInvalid JSON payload received.

 

Unknown name "client_id" ??????

 

본인 생각에는 client_id를 제대로 입력했다고 생각했을텐데 Unknown이라고 나타나니깐 난감할 수 밖에 없다. 포커싱을 다른 곳에 맞춰야 한다. "Invalid JSON payload received". 필자는 Content-Type을 아래와 같이 입력을 했었다.

headers.contentType = MediaType.APPLICATION_JSON // 잘못 입력했다.

위의 실습코드를 잘 따라오신 분이라면 타입이 APPLICATION_JSON이 아님을 알 것이다.

headers.contentType = MediaType.APPLICATION_FORM_URLENCODED // 이게 맞지

 

 

 

Oauth를 다루는 일은 처음의 진입장벽이 높아서 그렇지 몇번하다보면 다른 SNS를 하더라도 비슷하여 쉽게 다룰수 있게 된다. 그 자신감이 생길 수 있게되는 실습코드를 제공했길 바래본다. 오늘은 여기까지. 끄읏

728x90
반응형