当我在授权 header 中发送有效的 bearer-token 时,为什么我的 Spring-Cloud 网关/OAuth2-Client 没有通过我的身份验证?

Why am I not getting authenticated by my Spring-Cloud Gateway / OAuth2-Client when I am sending my valid bearer-token in the authorization header?

我正在开发微服务基础设施,并开始实施 Spring 云网关来代理我的所有请求。我通过 spring-boot-starter-oauth2-client 依赖项使用 keycloak 保护我的网关。 我使用 TokenRelay 过滤器将 Bearer 附加到我的代理请求。 我基本上关注了这个博客https://blog.jdriven.com/2019/11/spring-cloud-gateway-with-openid-connect-and-token-relay/

spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8090/auth/realms/testrealm
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
spring.security.oauth2.client.registration.keycloak.client-id=yyy
spring.security.oauth2.client.registration.keycloak.client-secret=xxx
spring.cloud.gateway.default-filters[0]=TokenRelay

现在我正在路由所有与 /ping 匹配的请求到我的 Ping 微服务。

spring.cloud.gateway.routes[0].id=some-id
spring.cloud.gateway.routes[0].uri=http://localhost:8079
spring.cloud.gateway.routes[0].predicates[0]=Path=/ping

Ping 微服务是一个 spring-boot-starter-oauth2-resource-server,并按此配置,仅公开一个测试端点 /ping。

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8090/auth/realms/testrealm

@Configuration
@EnableWebSecurity
@RestController
public class SecurityConfig extends KeyCloakSecurityConfig {
    @GetMapping("/ping")
    public String ping() {
        return "hello";
    }
    // ... resource server configuration in KeyCloakSecurityConfig
}

现在我通过 http://localhost:8765/ping 访问我的网关(8765 上的 运行),我被正确地重定向到 keycloak 的 login-page。我用我的测试用户登录,然后被重定向到我的网关,然后它将把我的请求代理到 Ping 微服务。 PingMicroService 再次针对 Keycloak 验证 AccessToken,我收到了“你好”。

但是如果我想用例如 Postman 测试我的 API,我会假设,我可以在调用我的 / 之前将 Bearer-Token 添加到授权 Header平端点。 因此,我使用 Token-Endpoint http://localhost:8090/auth/realms/testrealm/protocol/openid-connect/token 获取我的访问令牌,并将其复制到我的 Header 的授权中=56=]。 但是发送后,我得到的是login-page,而不是请求的资源。

为什么当我在授权 header 中发送有效的 bearer-token 时,我的网关没有通过验证?

当我直接调用我的资源服务器时,bearer-token 被接受。这是有道理的,因为 api-gateway 除了将我的 access-token 转发给 resource-server 之外没有做任何其他事情,所以他可以进行必要的令牌内省。

经过一番研究,我发现当我通过浏览器执行 api-call 时,Spring 实际上会向我发送一个 SESSION 作为 cookie。 当我将此 SESSION 作为 cookie 复制到邮递员的请求中时,一切正常。 是否有一些文档说明为什么 Spring 使用 SESSION,或者这基本上是 Auth-Code 因为我们在这里使用授权代码流?

更新: 经过一些研究,我发现我的网关确实已经是无状态的,所以 sessioncookie 不是来自我的网关,而是来自 oauth2-client 依赖项(参见 How to make API Gateway Stateless for Authentication/Authorization Process Using Oauth2?)。 所以我重现了我的问题,使用一个简单的 Spring 启动应用程序,使用 keycloak 设置 OAuth2login 并公开一个端点 /ping。 参见 https://github.com/smotastic/spring-oauth2-client-keycloak 如果您使用有效的 Bearer Access-Token 调用 /ping,应用程序会将您重定向到 keycloak 的 Login-Page,不接受令牌,因为它需要一个有效的 SESSION-Cookie

对于遇到类似问题的任何人。 问题出在 spring-boot-starter-oauth2-client 依赖项中。 这使我的网关有状态,通过从授权服务器发回 SESSION-Cookie 而不是 Access-Token。

不幸的是,我无法使用由 Keycloak (https://www.keycloak.org/docs/latest/securing_apps/#_spring_boot_adapter) 提供的官方 Spring-Boot-Adapter,因为此适配器具有一些网络依赖性,并且由于 spring-cloud-gateway 是基于 webflux 构建的, keycloak需要的web依赖不能结合使用

我的解决方案 是,不再使用 spring-cloud-gateway,而是使用 spring-cloud-starter-netflix-zuul 网关 。 这是建立在网络上,而不是在 webflux 上,所以我可以通过 keycloak 使用官方 Spring-Boot-Adapter。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>

Zuul 也发送一个 JSessionId,但仍然接受请求中的 accesstoken(如果已设置)header 作为有效授权。

请注意,Zuul 已进入维护模式 (https://cloud.spring.io/spring-cloud-netflix/multi/multi__modules_in_maintenance_mode.html)。 所以它不会获得任何新功能,建议使用 spring-cloud-gateway 代替。 但是对于我的用例来说,这是不可行的,所以我将来可能会回到这个网关,如果 oauth2-client 发布更新,我也可以在那里进行无状态身份验证。

这是一个示例存储库,它使用 zuul-gateway,使用官方 Spring-Boot-Keycloak-Adapter

受 keycloak 保护

https://github.com/smo-snippets/spring-cloud-microservices/tree/master/api-gateway