[Spring Boot] Google OAuth2 로그인
Spring BootGoogle Cloud 콘솔
프로젝트가 없다면 프로젝트를 생성한다.
OAuth 동의 화면 설정
API 및 서비스 > OAuth 동의 화면
- UserType: 로그인을 모든 사용자가 이용하므로 외부를 선택한다.
- 범위 추가: 사용할 API를 추가한다. 로그인만 사용하므로 userinfo.email, userinfo,profile, openid 선택한다.
- 테스트 사용자: 테스트 모드일 때 다른 사용자도 테스트하려면 이메일 입력해서 등록하기
- 완료
OAuth 2.0 클라이언트 ID 생성하기
API 및 서비스 > 사용자 인증 정보
- 사용자 인증 정보 만들기 > OAuth 클라이언트 ID
- URI 등록하기: 구글 로그인 결과 code를 받을 URI을 등록한다.
- 생성된 클라이언트 ID와 클라이언트 보안 비밀번호 확인
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 화면에 버튼 생성해서 실행하기
- 로그인하면 동의 화면이 나온다. 여기서 계속을 누르면 구글 로그인창을 호출할 때 파라미터로 보낸 redirect_uri로 이동한다.
리다이렉트 처리 / 로그인 처리
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)
// 로그인 처리
}
}
테스트
- 위에 동의 화면에서 계속을 누르면 로그인 처리된다.