[Spring Boot] Google OAuth2 로그인

Spring Boot

Language :

Google Cloud 콘솔

프로젝트가 없다면 프로젝트를 생성한다.

링크

OAuth 동의 화면 설정

API 및 서비스 > OAuth 동의 화면

  • UserType: 로그인을 모든 사용자가 이용하므로 외부를 선택한다.
google_oauth_00.png

  • 범위 추가: 사용할 API를 추가한다. 로그인만 사용하므로 userinfo.email, userinfo,profile, openid 선택한다.
google_oauth_02.png

  • 테스트 사용자: 테스트 모드일 때 다른 사용자도 테스트하려면 이메일 입력해서 등록하기
google_oauth_03.png

  • 완료
google_oauth_04.png

OAuth 2.0 클라이언트 ID 생성하기

API 및 서비스 > 사용자 인증 정보

  • 사용자 인증 정보 만들기 > OAuth 클라이언트 ID
google_oauth_05.png

  • URI 등록하기: 구글 로그인 결과 code를 받을 URI을 등록한다.
google_oauth_06.png

  • 생성된 클라이언트 ID와 클라이언트 보안 비밀번호 확인
google_oauth_07.png

API 문서 자료

구글 로그인창 호출

Spring Boot 코드

  • application.yml
google:
  client:
    id: "클라이언트 ID"
    secret: "클라이언트 보안 비밀번호"
  auth:
    url: "https://oauth2.googleapis.com"
    redirect-url: "OAuth 클라이언트 ID에 등록한 승인된 리다이렉트 URI"
  • Provider
@Component
class GoogleApiProvider(
    @Value("\${google.client.id}") private val clientId: String,
    @Value("\${google.client.secret}") private val clientSecret: String,
    @Value("\${google.auth.url}") private val authUrl: String,
    @Value("\${google.auth.redirect-url}") private val redirectUrl: String
) {

    /**
     * 구글 로그인 화면 URI
     */
    fun getGoogleAuthUrl(): String {
        return "https://accounts.google.com/o/oauth2/v2/auth?" +
            "scope=${URLEncoder.encode("openid email profile", Charsets.UTF_8)}" +
            "&access_type=offline" +
            "&response_type=code" +
            "&redirect_uri=$redirectUrl" +
            "&client_id=$clientId"
    }

}
  • Controller
@RestController
@RequestMapping("/api/auth")
class AuthController(
    private val googleApiProvider: GoogleApiProvider
) {

    /**
     * 구글 로그인창 호출
     */
    @GetMapping("/login/google-auth")
    fun redirectGoogleAuth(request: HttpServletRequest): ResponseEntity<Any> {
        val headers = HttpHeaders()
        headers.location = URI.create(googleApiProvider.getGoogleAuthUrl())
        return ResponseEntity(headers, HttpStatus.MOVED_PERMANENTLY)
    }

}

테스트

  • 브라우저 주소창에 http://localhost:8080/api/auth/login/google-auth 검색 또는 Front 화면에 버튼 생성해서 실행하기
test.png

로그인 화면

  • 로그인하면 동의 화면이 나온다. 여기서 계속을 누르면 구글 로그인창을 호출할 때 파라미터로 보낸 redirect_uri로 이동한다.
test2.png

동의 화면

리다이렉트 처리 / 로그인 처리

Spring Boot 코드

  • 토큰 받기 응답 DTO
data class GoogleTokenResponseDTO(
    @SerializedName("access_token")
    val accessToken: String,
    @SerializedName("expires_in")
    val expiresIn: String,
    @SerializedName("refresh_token")
    val refreshToken: String,
    val scope: String,
    @SerializedName("token_type")
    val tokenType: String
)
  • 유저 정보 응답 DTO
data class GoogleUserInfoResponseDTO(
    val sub: String,
    val name: String,
    @SerializedName("given_name")
    val givenName: String,
    val picture: String,
    val email: String,
    @SerializedName("email_verified")
    val emailVerified: String,
    val locale: String
)
  • Provider
@Component
class GoogleApiProvider {

    // ...

    /**
     * 토큰 받기
     */
    fun getToken(code: String): GoogleTokenResponseDTO {
        val url = "$authUrl/token"
        val param = LinkedMultiValueMap<String, String>()
        param["code"] = code
        param["client_id"] = clientId
        param["client_secret"] = clientSecret
        param["redirect_uri"] = redirectUrl
        param["grant_type"] = "authorization_code"

        val response = RestTemplate().postForObject(url, param, String::class.java)
            ?: throw GoogleApiException("token")
        return Gson().fromJson(response, GoogleTokenResponseDTO::class.java)
    }

    /**
     * 사용자 정보 조회
     */
    fun getUserInfo(accessToken: String): GoogleUserInfoResponseDTO {
        val url = "https://openidconnect.googleapis.com/v1/userinfo?access_token=$accessToken"

        val headers = HttpHeaders()
        headers.setBearerAuth(accessToken)

        val request = HttpEntity<MultiValueMap<String, String>>(LinkedMultiValueMap(), headers)

        val response = RestTemplate().exchange(url, HttpMethod.POST, request, String::class.java)
        return if (response.statusCode == HttpStatus.OK)
            Gson().fromJson(response.body, GoogleUserInfoResponseDTO::class.java)
        else throw GoogleApiException("user info")
    }

}
  • Controller
@RestController
@RequestMapping("/api/auth")
class AuthController(
    private val authService: AuthService,
    private val googleApiProvider: GoogleApiProvider
) {

    // ...

    /**
     * 구글 로그인 후 호출됨. 로그인 처리
     */
    @GetMapping("/login/google")
    fun loginGoogle(
        request: HttpServletRequest,
        @RequestParam code: String?,
        @RequestParam error: String?
    ): ResponseEntity<Any> {
        return code?.let {
            try {
                authService.loginGoogle(code)
                ResponseEntity.ok(HttpStatus.OK)
            } catch (e: Exception) {
                ResponseEntity.ok(HttpStatus.BAD_REQUEST)
            }
        } ?: ResponseEntity.ok(HttpStatus.BAD_REQUEST)
    }

}
  • Service
@Service
class AuthService(
    private val googleApiProvider: GoogleApiProvider
) {

    fun loginGoogle(code: String) {
        // 토큰 받아서
        val token = googleApiProvider.getToken(code)
        // 사용자 정보 조회
        val info = googleApiProvider.getUserInfo(token.accessToken)
        // 로그인 처리
    }

}

테스트

  • 위에 동의 화면에서 계속을 누르면 로그인 처리된다.

민갤

Back-End Developer

백엔드 개발자입니다.