下面是这篇由两部分组成的教程。
- 第1部分
- 第2部分
描述的目标:
API网关将作为执行OAuth 2的示例基于授权代码流的登录和会话管理。此外,它还将显示如何使用资源服务器所需的适当OAuth承载令牌。钥匙获取的访问/刷新令牌Keycloft永远不会暴露在浏览器中。
不同的是,我没有利用Keycloft,我使用的是Login.gov,根据我的经验,尽管它们的文档不错,但与它们集成可能会非常痛苦
问题:
教程认为我可以逃脱一些基本的东西:
...
.oauth2Login()
以前,当我不使用网关范式或反应式编程模型时,我在安全配置上对oauth进行了大量调整,以与Login.gov集成,例如:
...
.oauth2Login()
.loginPage(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL)
.authorizationEndpoint()
.authorizationRequestResolver(new LoginGovAuthorizationRequestResolver(clientRegistrationRepository))
.authorizationRequestRepository(authorizationRequestRepository())
.and()
.tokenEndpoint()
.accessTokenResponseClient(accessTokenResponseClient())
.and()
.failureHandler(new LoginGovAuthenticationFailureHandler())
.successHandler(new LoginGovAuthenticationSuccessHandler())
很明显,这里发生了很多事情,但重点是:所有这些跳跃主要是为了做几件事(我还记得(:
- 在请求解析程序中添加2个参数
acr_values
(常量:可在配置authorization_uri
中添加(nonce
(最少22个字符,至少需要一个解析器(
additionalParameters.put("acr_values", LoginGovConstants.LOGIN_GOV_LOA1)
additionalParameters.put("nonce", nonce)
此外,转换器显然需要我签署自己的JWT,并为其提供client_assertion
和恒定的client_assertion_type
参数:
@Override
RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
RequestEntity<?> originalRequestEntity = super.convert(authorizationCodeGrantRequest)
String registrationId = resolveRegistrationId(authorizationCodeGrantRequest)
if(registrationId == LoginGovConstants.LOGIN_GOV_REGISTRATION_ID) {
ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(registrationId)
String clientId = clientRegistration.clientId
String clientSecret = clientRegistration.clientSecret
String tokenUri = clientRegistration.providerDetails.tokenUri
Long expirationTime = LoginGovConstants.LOGIN_GOV_TOKEN_EXPIRATION_TIME
String jwt = JWT.create()
.withSubject(clientId)
.withIssuer(clientId)
.withAudience(tokenUri)
// Should be an un-guessable, random string generated by the client
.withJWTId(UUID.randomUUID().toString())
.withExpiresAt(new Date(System.currentTimeMillis() + expirationTime))
.sign(Algorithm.RSA256(keystoreUtil.rsaPublicKey(), keystoreUtil.rsaPrivateKey()))
HttpHeaders headers = originalRequestEntity.headers
MultiValueMap<String, String> formParameters = originalRequestEntity.body as MultiValueMap<String, String>
URI uri = originalRequestEntity.url
formParameters.add("client_assertion", jwt)
formParameters.add("client_assertion_type", LoginGovConstants.LOGIN_GOV_CLIENT_ASSERTION_TYPE)
return new RequestEntity<?>(formParameters, headers, HttpMethod.POST, uri)
}
}
只需说一句,在我实现之前的解决方案时,我认为Spring Security默认情况下应该提供一些额外的URL参数,这似乎是一个很大的倒退。因此,如果我们能在我之前的学徒基础上有所改进,我将不胜感激。
尽管如此,对于这种新的API网关方法,考虑到Webflux和一种反应式范式,我进一步感到困惑。
例如:
OAuth2AuthorizationCodeGrantRequestEntityConverter
与ServerOAuth2AuthorizationCodeAuthenticationTokenConverter
OAuth2AuthorizationRequestResolver
与ServerOAuth2AuthorizationRequestResolver
这些类之间有什么重叠(如果有的话(?
有没有一种更简单的方法来完成我以前用这些不同的Server*
类所做的事情?
当前行为:
按照我提到的教程,我有一个部分流程与我的网关,但图片并不完整:
spring:
application:
name: gateway
security:
oauth2:
client:
registration:
login-gov:
client-id: ${LOGIN_GOV_CLIENT_ID}
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
scope:
- openid
- email
provider:
login-gov:
authorization-uri: https://idp.int.identitysandbox.gov/openid_connect/authorize
token-uri: https://idp.int.identitysandbox.gov/api/openid_connect/token?client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
user-info-uri: https://idp.int.identitysandbox.gov/api/openid_connect/userinfo
jwk-set-uri: https://idp.int.identitysandbox.gov/api/openid_connect/certs
user-name-attribute: sub
Login.gov使用private_key_jwt
身份验证方法,这似乎意味着我没有"客户端机密"。这就是为什么我有必要在之前的实现中创建并签署自己的JWT的原因吗?
这是当我到达网关端点(/api/user/api/v1/savesearchs/(时网关的输出,该端点最终应该重定向到实际服务。
登录流按预期启动,我在integration login.gov页面上输入凭据。提交后,我最终被重定向回错误页面/login?error
2020-04-20 14:10:30.934 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-10] HTTP GET "/api/user/api/v1/savesearches/"
2020-04-20 14:10:30.983 DEBUG 15352 --- [oundedElastic-2] o.s.w.s.s.DefaultWebSessionManager : Created new WebSession.
2020-04-20 14:10:30.995 DEBUG 15352 --- [oundedElastic-2] .s.u.m.MediaTypeServerWebExchangeMatcher : httpRequestMediaTypes=[text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8]
2020-04-20 14:10:30.996 DEBUG 15352 --- [oundedElastic-2] .s.u.m.MediaTypeServerWebExchangeMatcher : Processing text/html
2020-04-20 14:10:30.996 DEBUG 15352 --- [oundedElastic-2] .s.u.m.MediaTypeServerWebExchangeMatcher : text/html .isCompatibleWith text/html = true
2020-04-20 14:10:30.997 DEBUG 15352 --- [oundedElastic-2] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-10] Completed 302 FOUND
2020-04-20 14:10:31.006 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-11] HTTP GET "/oauth2/authorization/login-gov"
2020-04-20 14:10:31.017 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-11] Completed 302 FOUND
2020-04-20 14:10:31.853 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-12] HTTP GET "/api/user/api/v1/savesearches/"
2020-04-20 14:10:31.861 DEBUG 15352 --- [ctor-http-nio-5] .s.u.m.MediaTypeServerWebExchangeMatcher : httpRequestMediaTypes=[text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8]
2020-04-20 14:10:31.861 DEBUG 15352 --- [ctor-http-nio-5] .s.u.m.MediaTypeServerWebExchangeMatcher : Processing text/html
2020-04-20 14:10:31.861 DEBUG 15352 --- [ctor-http-nio-5] .s.u.m.MediaTypeServerWebExchangeMatcher : text/html .isCompatibleWith text/html = true
2020-04-20 14:10:31.861 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-12] Completed 302 FOUND
2020-04-20 14:10:31.868 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-13] HTTP GET "/oauth2/authorization/login-gov"
2020-04-20 14:10:31.874 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-13] Completed 302 FOUND
2020-04-20 14:10:44.361 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-14] HTTP GET "/login/oauth2/code/login-gov?code=d2236ca8-1458-4631-b067-057f461d2a71&state=pp0V-IpV75MSgq995i5IUoQvuaFGhBXBJyRiHsAhQvM%3D"
2020-04-20 14:10:44.382 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.r.f.client.ExchangeFunctions : [312e7ca5] HTTP POST https://idp.int.identitysandbox.gov/api/openid_connect/token?client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
2020-04-20 14:10:44.950 DEBUG 15352 --- [ctor-http-nio-5] org.springframework.web.HttpLogging : [312e7ca5] Writing form fields [grant_type, code, redirect_uri, client_id, code_verifier] (content masked)
2020-04-20 14:10:45.080 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.r.f.client.ExchangeFunctions : [312e7ca5] Response 400 BAD_REQUEST
2020-04-20 14:10:45.094 DEBUG 15352 --- [ctor-http-nio-5] org.springframework.web.HttpLogging : [312e7ca5] Decoded [{error=Client assertion Nil JSON web token}]
2020-04-20 14:10:45.095 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-14] Completed 302 FOUND
2020-04-20 14:10:45.099 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-15] HTTP GET "/login?error"
2020-04-20 14:10:45.101 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [eae7892d-15] Completed 200 OK
对我来说,突出的地方在这里非常清楚。我可以看到授权步骤成功,如重定向到具有code
和state
参数的/login/oauth2/code/login-gov?code=d2236ca8-1458-4631-b067-057f461d2a71&state=pp0V-IpV75MSgq995i5IUoQvuaFGhBXBJyRiHsAhQvM%3D
所示。
令人困惑的是提供给令牌端点的参数:它只有我在上面的配置中提供的client_assertion_type
,但它也应该有实际的client_assertion
(JWT(。我想,这就是我之前为在自定义转换器中创建JWT所做的所有跳跃,但我一直不明白为什么Spring不为我做这件事?
我是不是错过了一些基本的配置步骤?如果没有,我可以使用一些帮助以响应式(ServerWebExchange(的方式重新实现转换器。
谢谢你的耐心。
FYI Spring Security 5还不支持private_key_jwt
,这就是为什么您必须做额外的工作才能使其工作。
(添加一个答案,因为我没有足够的声誉来评论(