Рубрики
Без рубрики

Spring boot + Spring Security 5 + Клиент OAuth2/OIDC – Глубокое погружение

Магия декодирования, лежащая в основе клиента spring boot oauth2. Помеченный oauth2, java, spring, spring security.

В моем предыдущем посте мы видели, как легко защитить ваше приложение с помощью входа в Google.

Теперь давайте посмотрим, какие все компоненты отвечают за то, чтобы это работало.

Запрос на доступ к защищенной конечной точке и запуск процесса аутентификации Google

Предпосылки

  1. application.yml настроен настроен со значениями клиента и поставщика
  2. Имя поставщика в свойстве 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”