В моем предыдущем посте мы видели, как легко защитить ваше приложение с помощью входа в Google.
Теперь давайте посмотрим, какие все компоненты отвечают за то, чтобы это работало.
Запрос на доступ к защищенной конечной точке и запуск процесса аутентификации Google
Предпосылки
application.yml
настроен настроен со значениями клиента и поставщика- Имя поставщика в свойстве
spring.security.oauth2.client.registration.провайдер
настроен на google
Когда доступ к http://localhost:8080/me
запрашивается, если у вас настроен только один поставщик удостоверений, Spring автоматически перенаправит вас на http://localhost:8080/oauth2/authorization/google
. Фильтр перенаправления запроса авторизации OAuth2
, который зарегистрирован в шаблоне url /oauth2/authorization/*
, загрузит соответствующую конфигурацию и перенаправит к Поставщику удостоверений. В нашем случае Google.
Если вы хотите, чтобы ваши пользователи могли выбирать между несколькими поставщиками, затем настройте свой application.yml
для нескольких поставщиков, но вам нужна страница входа в систему, где у вас может быть несколько ссылок для пользователей на выбор.
При успешной аутентификации Google перенаправляет на URL-адрес перенаправления приложения
Как только пользователь успешно проходит аутентификацию в Google, Google now перенаправляет на URL-адрес перенаправления приложения, настроенный в консоли разработчика Google. В нашем примере мы выбрали определенный URL-адрес http://localhost:8080/login/oauth2/code/google
. Это связано с тем, что фильтр обработки аутентификации для OAuth2 Фильтр аутентификации входа OAuth2
зарегистрирован для прослушивания /login/oauth2/code/*
.
Фильтр аутентификации входа в систему OAuth2 делегирует аутентификацию OidcAuthorizationCodeAuthenticationProvider
, который выполняет 3 действия:
- Обменивает код на токен
- Проверяет id_token
- Заполняет информацию о пользователе, вызывая конечную точку Информации о пользователе из хорошо известной конфигурации Google
Теперь вы можете спросить, что, если я зарегистрировал другой URL-адрес перенаправления и хочу, чтобы фильтр аутентификации входа в систему OAuth2 прослушивал это. Это довольно просто, все, что вам нужно сделать имеет следующую конфигурацию безопасности
@Configuration @EnableWebSecurity class SecurityConfiguration: WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http .authorizeRequests() .anyRequest() .authenticated() .and() .oauth2Login() .redirectionEndpoint() .baseUri("/oauth/callback/*") } }
Что Дальше
В результате успешной аутентификации вы получите объект аутентификации типа Токен аутентификации OAuth2
. Этот токен будет содержать всю необходимую информацию из id_token
и конечная точка userinfo
.
Вы можете получить доступ ко всем данным о вошедшем в систему пользователе с помощью
SecurityContextHolder.getContext().authentication as OAuth2AuthenticationToken
или
@GetMapping("/me") fun hello(currentUser: OAuth2AuthenticationToken): ResponseEntity{ return ResponseEntity.ok(currentUser) }
Но как насчет токенов доступа и обновления?
В результате успешного потока подключения OpenID клиентское приложение получает три токена, access_token, refresh_token и id_token
. Возможно, мы захотим использовать этот токен доступа для доступа к какому-либо защищенному ресурсу с сервера ресурсов , например, tasks API google . Авторизованная клиентская служба OAuth2
отслеживает токены, связанные с пользователем.
val currentUser = SecurityContextHolder.getContext().authentication as OAuth2AuthenticationToken val currentUserClientConfig = oAuth2AuthorizedClientService.loadAuthorizedClient( authorizedClientRegistrationId, currentUser.name) println("AccessToken: ${currentUserClientConfig.accessToken.tokenValue}") println("RefreshToken: ${currentUserClientConfig.refreshToken.tokenValue}")
Но Срок действия токенов доступа может истечь
Когда токены доступа истекают, сервер ресурсов, подобный like tasks API google , вернет HTTP-статус 401, самое простое решение – выдать OAuth2AuthorizationException
, который является типом AuthenticationException
, который снова запустит поток входа в систему.
Но мы также можем использовать токены обновления для автоматического обновления наших токенов, настроив RestTemplate
с помощью перехватчика запросов, который обновит токены по истечении срока действия
class BearerTokenInterceptor(private val oAuth2AuthorizedClientService: OAuth2AuthorizedClientService) : ClientHttpRequestInterceptor { companion object { val log: Logger = LoggerFactory.getLogger(BearerTokenInterceptor::class.java) } private var accessTokenExpiresSkew = Duration.ofMinutes(1) private val clock = Clock.systemUTC() override fun intercept(request: HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse { val currentUser = SecurityContextHolder.getContext().authentication as OAuth2AuthenticationToken val currentUserClientConfig = currentUser.clientConfig() if (isExpired(accessToken = currentUserClientConfig.accessToken)) { log.info("AccessToken expired, refreshing automatically") refreshToken(currentUserClientConfig, currentUser) } request.headers[AUTHORIZATION] = "Bearer ${currentUserClientConfig.accessToken.tokenValue}" return execution.execute(request, body) } private fun OAuth2AuthenticationToken.clientConfig(): OAuth2AuthorizedClient { return oAuth2AuthorizedClientService.loadAuthorizedClient( authorizedClientRegistrationId, name) ?: throw CredentialsExpiredException("could not load client config for $name, reauthenticate") } private fun refreshToken(currentClient: OAuth2AuthorizedClient, currentUser: OAuth2AuthenticationToken) { val atr = refreshTokenClient(currentClient) if (atr == null || atr.accessToken == null) { log.info("Failed to refresh token for ${currentUser.name}") return } val refreshToken = atr.refreshToken ?: currentClient.refreshToken val updatedClient = OAuth2AuthorizedClient( currentClient.clientRegistration, currentClient.principalName, atr.accessToken, refreshToken ) oAuth2AuthorizedClientService.saveAuthorizedClient(updatedClient, currentUser) } private fun refreshTokenClient(currentClient: OAuth2AuthorizedClient): OAuth2AccessTokenResponse? { val formParameters = LinkedMultiValueMap() formParameters.add(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.REFRESH_TOKEN.value) formParameters.add(OAuth2ParameterNames.REFRESH_TOKEN, currentClient.refreshToken?.tokenValue) formParameters.add(OAuth2ParameterNames.REDIRECT_URI, currentClient.clientRegistration.redirectUriTemplate) val requestEntity = RequestEntity .post(URI.create(currentClient.clientRegistration.providerDetails.tokenUri)) .header(CONTENT_TYPE, APPLICATION_FORM_URLENCODED_VALUE) .body(formParameters) return try { val r = restTemplate(currentClient.clientRegistration.clientId, currentClient.clientRegistration.clientSecret) val responseEntity = r.exchange(requestEntity, OAuth2AccessTokenResponse::class.java) responseEntity.body } catch (e: OAuth2AuthorizationException) { log.error("Unable to refresh token ${e.error.errorCode}") throw OAuth2AuthenticationException(e.error, e) } } private fun isExpired(accessToken: OAuth2AccessToken): Boolean { val now = this.clock.instant() val expiresAt = accessToken.expiresAt ?: return false return now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew)) } private fun restTemplate(clientId: String, clientSecret: String): RestTemplate { return RestTemplateBuilder() .additionalMessageConverters( FormHttpMessageConverter(), OAuth2AccessTokenResponseHttpMessageConverter()) .errorHandler(OAuth2ErrorResponseErrorHandler()) .basicAuthentication(clientId, clientSecret) .build() } }
До сих пор я не обнаружил, что oauth2-клиент может автоматически обновлять токены в пользовательском сеансе, дайте мне знать, если это так:)
Я попытался собрать воедино все части, пожалуйста, дайте мне отзыв, если я что-то пропустил:)
Оригинал: “https://dev.to/shyamala_u/spring-boot–spring-security-5–oauth2oidc-client—deep-dive-261l”