通过 Spring Boot Keycloak 集成的 OAuth2 客户端凭据流

Posted

技术标签:

【中文标题】通过 Spring Boot Keycloak 集成的 OAuth2 客户端凭据流【英文标题】:OAuth2 client credentials flow via Spring Boot Keycloak integration 【发布时间】:2020-01-18 08:10:21 【问题描述】:

我的申请包括:

后端/资源服务器 UI 网页应用 钥匙斗篷

UI 正在通过 RESTful API 使用带有授权码授予流程的 keycloak 客户端与后端服务器通信。这工作正常。

现在,我需要额外的可能性来使用系统/服务帐户访问后端资源(通常具有比用户更多的权限)。你将如何实现这个要求?我认为客户端凭据流程在这里会很有用。

是否可以将 OAuth2 客户端凭据流与用于 Spring Boot 的 keycloak 客户端一起使用?我找到了使用 Spring Security OAuth2 客户端功能来实现客户端凭据流的示例,但这感觉很奇怪,因为我已经将 keycloak 客户端用于 OAuth。

编辑:解决方案

感谢您的回答,这对我帮助很大。在我的 UI webapp 中,我现在可以使用经过身份验证的用户 OAuth2 令牌或使用我的 UI 服务帐户的客户端凭据流中的令牌与后端进行通信。每种方式都有自己的RestTemplate,第一种是通过 keycloak 集成完成,第二种是由 Spring Security OAuth2 完成,如 here 所述。

【问题讨论】:

是的,Keycloak 客户端应该支持 client_credentials 身份验证流程。确保您在 Keycloak 客户端设置中打开了 Service Accounts Enabled @qdivision 如何使用 Spring Boot keycloak 集成配置客户端凭据流,以及如何在用户身份验证和客户端身份验证 REST 调用之间切换? 【参考方案1】:

是的,您可以使用 OAuth 2.0 客户端凭据流和服务帐户。

Keycloak 提出了 3 种保护 SpringBoot REST 服务的方法:

    带有 Keycloak Spring Boot 适配器 使用 keycloak Spring 安全适配器 使用 OAuth2 / OpenID 连接

下面是一个很好的解释,用 OAuth2/OIDC 方式的例子:

Tutorial by Arun B Chandrasekaran Code sample by Arun B Chandrasekaran

如果您遵循此示例,请记住:

注意将您的客户端配置为:

访问类型:机密 授权:启用 服务帐户(OAuth 客户端凭据流):已启用

注意将您的目标服务配置为:

访问类型:仅承载

所以,调用者应该是confidential,目标服务应该是bearer-only

创建您的用户、角色、映射器...并将角色分配给您的用户。

检查你的 spring 项目中是否有这个依赖项:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security.oauth.boot</groupId>
  <artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>

配置要在 REST 客户端中使用的身份验证 (application.properties) 例如:

security.oauth2.client.client-id=employee-service
security.oauth2.client.client-secret=68977d81-c59b-49aa-aada-58da9a43a850
security.oauth2.client.user-authorization-uri=$rest.security.issuer-uri/protocol/openid-connect/auth
security.oauth2.client.access-token-uri=$rest.security.issuer-uri/protocol/openid-connect/token
security.oauth2.client.scope=openid
security.oauth2.client.grant-type=client_credentials

像 Arun 的示例一样实现您的 JwtAccessTokenCustomizerSecurityConfigurer (ResourceServerConfigurerAdapter)。

最后实现你的服务控制器:

@RestController
@RequestMapping("/api/v1/employees")
public class EmployeeRestController 

  @GetMapping(path = "/username")
  @PreAuthorize("hasAnyAuthority('ROLE_USER')")
  public ResponseEntity<String> getAuthorizedUserName() 
    return ResponseEntity.ok(SecurityContextUtils.getUserName());
  

  @GetMapping(path = "/roles")
  @PreAuthorize("hasAnyAuthority('ROLE_USER')")
  public ResponseEntity<Set<String>> getAuthorizedUserRoles() 
    return ResponseEntity.ok(SecurityContextUtils.getUserRoles());
  

如需完整教程,请阅读参考的 Arun 教程。

希望对你有帮助。

【讨论】:

您的回答建议使用 Spring OAuth2 库。那么,不能使用 keycloak Spring Boot 与客户端凭据流集成? 您好,我认为弹簧适配器不支持它。您需要自定义它或使用这样的 RestTemplate:***.com/a/46400975 Spring OAuth2 库是一种比 keycloak-spring boot 更优雅的身份验证方式。你应该试试看。这是我第一次为我尝试每种配置的时候。 顺便说一句,我编辑了您的回复以改进格式和可读性。希望你不要介意。 嗨@CarlosCavero,您的回答还不错,这与我之前在 cmets 中建议的内容非常相似(使用休息模板和带有示例的帖子)。【参考方案2】:

按照@dmitri-algazin 来实施工作流程,您基本上有两个选择:

    如果你想覆盖除 Keycloak 之外的其他 IdM,它以某种方式解决了 Single Responsibility principle,我会使用 RestTemplate。您可以在下面找到变量:
    //Constants
    @Value("$keycloak.url")
    private String keycloakUrl;

    @Value("$keycloak.realm")
    private String keycloakRealm;

    @Value("$keycloak.client_id")
    private String keycloakClientId;

    RestTemplate restTemplate = new RestTemplate();
    private static final String BEARER = "BEARER ";

首先你需要生成访问令牌:

    @Override
    public AccessTokenResponse login(KeycloakUser user) throws NotAuthorizedException 
        try 
            String uri = keycloakUrl + "/realms/" + keycloakRealm + 
                    "/protocol/openid-connect/token";
            String data = "grant_type=password&username="+
                    user.getUsername()+"&password="+user.getPassword()+"&client_id="+
                    keycloakClientId;

            HttpHeaders headers = new HttpHeaders();
            headers.set("Content-Type", "application/x-www-form-urlencoded");

            HttpEntity<String> entity = new HttpEntity<String>(data, headers);
            ResponseEntity<AccessTokenResponse> response = restTemplate.exchange(uri, 
                    HttpMethod.POST, entity, AccessTokenResponse.class);            

            if (response.getStatusCode().value() != HttpStatus.SC_OK) 
                log.error("Unauthorised access to protected resource", response.getStatusCode().value());
                throw new NotAuthorizedException("Unauthorised access to protected resource");
            
            return response.getBody();
         catch (Exception ex) 
            log.error("Unauthorised access to protected resource", ex);
            throw new NotAuthorizedException("Unauthorised access to protected resource");
         
    

然后您可以使用令牌从用户那里检索信息:

    @Override
    public String user(String authToken) throws NotAuthorizedException 

        if (! authToken.toUpperCase().startsWith(BEARER)) 
            throw new NotAuthorizedException("Invalid OAuth Header. Missing Bearer prefix");
        

        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", authToken);

        HttpEntity<String> entity = new HttpEntity<>(headers);

        ResponseEntity<AccessToken> response = restTemplate.exchange(
                keycloakUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/userinfo", 
                HttpMethod.POST, 
                entity, 
                AccessToken.class);

        if (response.getStatusCode().value() != HttpStatus.SC_OK) 
            log.error("OAuth2 Authentication failure. "
                    + "Invalid OAuth Token supplied in Authorization Header on Request. Code ", response.getStatusCode().value());
            throw new NotAuthorizedException("OAuth2 Authentication failure. "
                    + "Invalid OAuth Token supplied in Authorization Header on Request.");
        

        log.debug("User info: ", response.getBody().getPreferredUsername());
        return response.getBody().getPreferredUsername();
    

您可以用@dimitri-algazin 提供的网址替换此网址,以检索所有用户信息。

    可以使用 Keycloak 依赖项:
        <!-- keycloak -->
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-admin-client</artifactId>
            <version>3.4.3.Final</version>
        </dependency>

        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-client</artifactId>
            <version>3.1.4.Final</version>
        </dependency>

并使用类生成令牌:

            Keycloak keycloak = KeycloakBuilder
                    .builder()
                    .serverUrl(keycloakUrl)
                    .realm(keycloakRealm)
                    .username(user.getUsername())
                    .password(user.getPassword())
                    .clientId(keycloakClientId)
                    .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(10).build())
                    .build();

            return keycloak.tokenManager().getAccessToken();

示例摘自here。我们还上传了image to Docker Hub,方便与Keycloak的交互。出于这个原因,我们从选项 2) 开始。现在我们正在处理其他 IdM,我们选择了选项 1),以避免包含额外的依赖项。结论:

如果您坚持使用 Keycloak,我会选择 选项 2,因为类包含 Keycloak 工具的额外功能。 我会选择选项 1 以获得进一步的报道和其他 OAuth 2.0 工具。

【讨论】:

【参考方案3】:

我们有类似的需求,通过用户 uuid 获取用户电子邮件。

创建服务用户,确保用户具有角色“realm-management”->“view-users”(也可能是查询用户)

过程很简单:使用服务用户登录 keycloak(将密码和/或用户名编码在属性文件中),使用授权标头中的 accessToken 向 keycloak 发出请求

GET http://yourdomainadress/auth/admin/realms/yourrealmname/users/userId

一种使用 REST API 登录 keycloak 的方法:

POST http://yourdomainadress/auth/realms/yourrealmname/protocol/openid-connect/token

标题:

内容类型:application/x-www-form-urlencoded

正文 x-www-form-urlencoded:

client_id: 你的客户

用户名:你正在使用的用户

密码:用户密码

grant_type: 密码

client_secret:11112222-3333-4444-5555-666666666666(如果客户端“访问类型”=“机密”,则需要客户端密码)

很快:确保您的服务用户具有正确的角色来执行操作, 登录,查询 keycloak(检查文档以获取正确的查询 url 和参数,总是具有挑战性)

【讨论】:

你能分享一些代码和允许客户端凭据流动的 application.properties 文件吗?

以上是关于通过 Spring Boot Keycloak 集成的 OAuth2 客户端凭据流的主要内容,如果未能解决你的问题,请参考以下文章

403 spring-boot-2-keycloak-适配器

Keycloak,如何保护不提供网页的 Spring Boot 后端?

通过 Spring Boot Keycloak 集成的 OAuth2 客户端凭据流

是否可以告诉 Spring Boot Keycloak 适配器 Keycloak 服务器可能有多个别名?

Spring Boot 中的 Keycloak 无法应用 Liquibase 补丁

Spring-boot 和 Keycloak 集成