如何使用 Spring boot keycloak 适配器 + spring security 强制更新 oAuth 令牌(访问令牌 + 刷新令牌)。?

Posted

技术标签:

【中文标题】如何使用 Spring boot keycloak 适配器 + spring security 强制更新 oAuth 令牌(访问令牌 + 刷新令牌)。?【英文标题】:How to force an oAuth token renewal (access-token + refresh token) with Spring boot keycloak adapter + spring security.? 【发布时间】:2020-02-08 19:02:09 【问题描述】:

我有一个由 Keycloak 保护的多租户应用程序(springboot keycloak 适配器 + spring security)。鉴于项目的多租户性质,我编写了一个运行良好的多客户端连接器。

在官方 Keycloak 文档中,建议(对于多租户应用程序)将每个租户建模为一个新领域,但对我来说,在同一领域内拥有多个客户端会更好。这是由于以下优点:

可以共享客户端范围、组和其他配置 用户无需在 N 个不同领域重复 SSO 登录在同一领域客户端中完美运行(通过使用承载 服务 +CORS)

所以,除了一件事,我的初始 SSO access_token(然后通过 CORS 在所有仅承载服务之间共享)有点大(它显示了所有资源 - 租户 - 及其每个资源/租户中的角色)。

我想限制access_token 的大小,方法是使用“范围”将令牌中的角色限制为仅对我当时登录的租户有意义的角色。为此,我手动向身份验证服务器发出请求(在 springboot/spring security 提供的标准功能之外),目的是手动覆盖我的应用程序中存在的任何访问令牌,新的访问令牌由我的额外生成请求。

我的“新”令牌请求如下所示:

    SimpleKeycloakAccount currentUserAccount = (SimpleKeycloakAccount) auth.getDetails();
            String authServerUrl = currentUserAccount.getKeycloakSecurityContext().getDeployment().getAuthServerBaseUrl();
            String realm = currentUserAccount.getKeycloakSecurityContext().getDeployment().getRealm();
            String resource =  currentUserAccount.getKeycloakSecurityContext().getDeployment().getResourceName();
            String refreshToken = currentUserAccount.getKeycloakSecurityContext().getRefreshToken();
            String token = currentUserAccount.getKeycloakSecurityContext().getTokenString();
            
            
            Http http = new Http( new Configuration(authServerUrl, realm, resource,
                            currentUserAccount.getKeycloakSecurityContext().getDeployment().getResourceCredentials()
                           , null), 
                       (params, headers) -> );
            
            String url = authServerUrl + "/realms/" + realm + "/protocol/openid-connect/token";
            
            AccessTokenResponse response = http.<AccessTokenResponse>post(url)
                .authentication()
                    .client()
                .form()
                    .param("grant_type", "refresh_token")
                    .param("refresh_token", refreshToken)
                    .param("client_id", resource)
                    .param("client_secret", "SOME_SECRET")
                    .param("scope", "SOME_SCOPE_TO_RESTRICT_ROLES")
                .response()
                    .json(AccessTokenResponse.class)
                .execute();

// :) -  response.getToken() and response.getRefreshToken(), contain new successfully generated tokens
            

我的问题是,如何使用这些“自定义创建”令牌强制 my-app 更改/重置通过通常方式获得的标准 access-token 和 refresh_token?还是有可能?

感谢任何反馈!

更多信息

为了澄清更多,让我们分析一个典型的与 Keycloak 集成的 springboot/spring 安全项目的行为:

您可以通过配置(在 application.properties 或 SecurityContext 上)使用“角色”保护您的端点 你知道这个 Spring 应用程序在后台通道中与 Keycloak 授权服务器对话,这就是你成为 access_token 的方式(但这对开发人员来说是一个黑盒子,你只知道创建了一个 Principal,一个 Security Context,凭证;等等 - 一切都发生在幕后)

考虑到上述两点,假设您使用 Http 库基本上向身份验证服务器令牌端点请求一个新令牌,就像上面的代码一样(是的,按范围和所有内容过滤)。所以现在的情况是,虽然你已经创建了一个有效的access_token(和refresh_token);由于它们是通过向令牌端点发出请求“手动”创建的,因此该新令牌尚未“合并”到应用程序中,因为没有创建新的主体,没有生成新的安全上下文等。换句话说,对于 springboot 应用程序来说,这个新令牌是不存在的。

我想要完成的是告诉 sprinboot/spring security:“嘿,朋友,我知道你没有自己生成这个令牌,但请接受它并表现得好像你已经创建了它一样”。

我希望这能澄清我的问题的意图。

【问题讨论】:

由于您的客户端应用程序拥有的令牌现在无效,这不会在第一次请求时自动发生吗? 原始令牌不会因为发出新的令牌请求而失效。不,应用程序完全忽略了新的令牌,并且只会在 5 分钟(通常是 5 分钟)后变为无效时像在任何其他应用程序中一样刷新。 @Eugen Covaci 假设您有一个有效的令牌,然后您“刷新它”,这不会使原始令牌无效。原版过期或被明确撤销时失效。 @tony_008 登录时如何确定租户?这些信息存储在 keycloak 中的什么位置? @Boomer,每个租户只是一个标准的 keycloak 客户端。我有一个“主后端”,它也是一个 Keycloack 客户端。通过令牌元数据,我收到用户所在的租户。从那里只有承载的“多租户端点”,需要定制的多租户连接器,这不是开箱即用的。具体来说,您需要扩展“KeycloakSpringBootConfigResolver 实现 KeycloakConfigResolver”,其中可以对方法“createDeploymentJSON(AccessToken accessToken, String tennantNo)”进行参数化以使用不同的配置;以及 SecurityConfig "KeycloakConfigResolver" 【参考方案1】:

只是为了结束这个问题:

    不,似乎没有任何优雅或有意的方式通过使用 springboot/spring security keycloak 连接器来强制手动更新令牌。

    javascript 连接器可以像这样简单地做到这一点:

     // for creating your keycloak connector
     var keycloak = Keycloak(
                    url: 'http://localhost:8080/auth',
                    realm: '[YOUR_REALM]',
                    clientId: '[YOUR_CLIENT]'
                );
    
    
     // for login in (change scopes list to change access capabilities)
     var options = 
           scope: [EMPTY_STRING_SEPARATED_LIST_OF_SCOPES]  // <-- here specify valid scopes
         ;
    
     keycloak.login(options); // <-- receive a new token with correctly processed scopes
    

    鉴于使用 Keycloak 客户端 JS 适配器执行此操作是多么容易,而使用 springboot/spring 安全适配器执行此操作又是多么晦涩难懂,如下所示: 安全设计似乎打算有 2 个(Keycloak 安全)层;第一层是面向前端的公共客户端(通常受密码保护),第二层由几个通常只接受访问令牌的仅承载服务组成。如果对于那些仅承载的服务,您希望通过范围实现更精细的控制,您可以通过使用基于 javascript 的 Keycloak 客户端轻松实现这一点(解释的其他连接器无法很好地处理处理 OAuth2 范围所需的标头修改)。

【讨论】:

【参考方案2】:

如果每个租户都是单独的客户端,您可以在每个客户端使用 keycloak 的“范围”映射。只需关闭Full Scope Allowed,您的令牌将只包含该特定客户端(租户)的用户角色。

“范围映射”是一种不直观的说法,即“定义哪些角色应该进入访问令牌”:-)

当关闭 UI 更改时,您甚至可以配置其他客户端的其他角色应该额外进入访问令牌。

【讨论】:

感谢您的建议。实际上范围映射是我以前使用过的东西。这里唯一的问题是,由于我使用 CORS 进行令牌共享,我只有 2 个选项:要么我将所有 Tennant 的所有角色加载到现在正在工作的令牌中,但我试图避免。或者(类似于您的建议)按某些 Tennant 的范围过滤角色,以便创建的令牌仅限于 Tennant。为了实现这一点,我明确地向身份验证服务器请求一个限制在该 Tennant 范围内的令牌(有效),但我仍然需要 spring security 才能使用这个新令牌。 基本上你提到的是我想要完成的事情。我创建了一个新令牌,其角色受限于该 Tennant 的范围 - 它已成功创建,但我无法告诉“spring security”使用该新令牌,它继续使用旧令牌。 由于这里的空间有限,我编辑了原始帖子并添加了“更多信息”部分,以更清楚地解释我的问题。感谢您的支持:)。【参考方案3】:

您可以使用org.springframework.security.oauth2.provider.token.ConsumerTokenServices#revokeToken 方法撤销令牌。

在自动化服务器上​​:

@Resource(name="tokenServices")
ConsumerTokenServices tokenServices;

@RequestMapping(method = RequestMethod.POST, value = "/tokens/revoke/tokenId:.*")
@ResponseBody
public String revokeToken(@PathVariable String tokenId) 
    tokenServices.revokeToken(tokenId);
    return tokenId;

当然,您必须保护这种方法,因为它是一种非常敏感的方法。

【讨论】:

感谢您的评论@Eugen CovaciI :)。我真的不需要撤销原始令牌,我需要的是“人工创建”的令牌(在标准 springboot/keycloak 集成之外)被视为新的 access_token (因为它包含新的tennat角色)。即使旧令牌失效,应用程序也会用另一个不是我创建的令牌替换它。如果我可以用自定义创建的 access_token 替换原始 access_token(而不是 spring 安全性要求的典型的 access_token 缺少必要的范围),那就太好了。

以上是关于如何使用 Spring boot keycloak 适配器 + spring security 强制更新 oAuth 令牌(访问令牌 + 刷新令牌)。?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Angular -> Spring Boot Oauth2 -> Keycloak 获取 keycloak 令牌

如何使用 Keycloak 在 Spring Boot 中获取当前登录用户?

如何在 Spring Boot 应用程序中使用 Keycloak Policy Enforcer

如何使用 Spring Boot 在 Keycloak 中设置 redirect_uri

如何在嵌入式 Spring Boot Keycloak 中设置主题?

如何让我的 Spring Boot 应用程序使用给定的用户名和密码登录到 keycloak?