在使用OAuth2的spring资源服务器中,我如何向keycloat服务器提供clientid?

ozxc1zmp  于 2023-08-02  发布在  Spring
关注(0)|答案(2)|浏览(122)

我有下面的场景(缩小了一点,使事情更容易):

  • 我有一个keycloak服务器,有一个领域,和两个客户端,一个“基础”客户端,一个“规范”客户端。基本客户端未启用身份验证,规范客户端启用了身份验证。基本客户端有一个主URL(http://localhost:4200),规范客户端没有。
  • 我有一个有Angular 的Web应用程序(http://localhost:4200),用户通过keycloak登录。角web应用具有客户端ID库。
  • 我有一个Spring应用程序,它应该是一个资源服务器。它应该有一个客户端规范。

现在一切都按预期工作,当在Spring使用keycloak适配器时。正如我所读到的,这是不推荐的,所以我决定将其更改为OAuth 2.0(使用UMA)。当我在浏览器中访问spec部分时,angular向资源服务器请求资源,资源服务器检查没有rpt令牌,并开始与keycloak通信,请求票证。钥匙斗篷回答:“客户端应用程序[基]未注册为资源服务器。”这当然是正确的,基客户端不是资源服务器。但是我如何在Spring中提供正确的客户端ID呢?在旧版本中,使用keycloak适配器,我有属性(applications.properties):resource = spec这样,keycloak适配器就能够与keycloak进行通信,以接收客户端的权限票据。在OAuth 2.0中有类似的东西吗?或者我必须以另一种方式配置keycloat服务器才能工作?任何帮助都是感激的。
亲切的问候

9cbw7uwe

9cbw7uwe1#

通常,带有JWT解码器的资源服务器只需要联系授权服务器(在您的情况下是Keycloak)来获取publictokens签名密钥(在Spring中,这在应用程序启动时完成一次),并且这不需要客户端ID。
只有在为令牌自检配置资源服务器时才需要客户端ID和机密(自检请求应该使用某种形式的机密进行保护)。

资源服务器不应将未经授权的请求重定向到登录。这是客户端的责任,需要会话(当对资源服务器的请求使用访问令牌保护时,对OAuth2客户端的请求使用会话cookie保护)。阅读我的教程中的OAuth2要点部分以了解更多背景知识。

使用Keycloak,默认情况下,角色是访问令牌声明的一部分:

  • realm_access.roles用于“领域”角色
  • ressource_access.{client_id}.roles用于“客户端”角色(这需要手动激活客户端角色Map程序)

Spring Security没有内置的东西来将Keycloak角色Map到“authorities”(在Spring Security世界中最接近“role”):default converter只使用scope声明作为构建Spring权限的源代码。
如果您需要将客户端角色Map到Spring权限,官方的方法是在配置安全过滤器链中的资源服务器时提供自己的Converter<Jwt, JwtAuthenticationToken>。就像这样:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
    
    @Bean
    Converter<Jwt, List<SimpleGrantedAuthority>> authoritiesConverter(@Value("oauth2-client-id") String clientId) {
        return jwt -> {
            final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
            final var clientAccess = (Map<String, Object>) resourceAccess.getOrDefault(clientId, Map.of());
            final var clientRoles = (List<String>) clientAccess.getOrDefault("roles", List.of());
            
            // Concat more claims if you need roles for other clients or realm ones
            return clientRoles.stream().map(SimpleGrantedAuthority::new).toList();
        };
    }
    
    @Bean
    Converter<Jwt, JwtAuthenticationToken> authenticationConverter(Converter<Jwt, ? extends Collection<? extends GrantedAuthority>> authoritiesConverter) {
        return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt), jwt.getClaimAsString(StandardClaimNames.PREFERRED_USERNAME));
    }
    
    @Bean
    SecurityFilterChain securityFilterChain(
            HttpSecurity http,
            Converter<Jwt, ? extends AbstractAuthenticationToken> authenticationConverter)
            throws Exception {
        http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwtResourceServer -> jwtResourceServer.jwtAuthenticationConverter(authenticationConverter)));

        // State-less session (state in access-token only)
        http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        // Disable CSRF because of state-less session-management
        http.csrf(csrf -> csrf.disable());

        // Return 401 (unauthorized) instead of 302 (redirect to login) when
        // authorization is missing or invalid
        http.exceptionHandling(eh -> eh.authenticationEntryPoint((request, response, authException) -> {
            response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        }));

        http.authorizeHttpRequests(requests -> requests
            .requestMatchers(
                "/actuator/health/readiness",
                "/actuator/health/liveness",
                "/v3/api-docs/**").permitAll()
            .anyRequest().authenticated());

        return http.build();
    }
}
oauth2-issuer: https://localhost:8443/realms/master
# Used in authorities converter
oauth2-client-id: change-me

spring:
  security:
    oauth2:
      resourceserver:
        issuer-uri: ${oauth2-issuer}

上面的配置还使用preferred_username(Keycloak使用用户登录名填充)作为Spring的Authentication名称。
但是你可以使用spring-addons-starter-oidc I wrote在没有Java conf的情况下做同样的事情:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-starter-oidc</artifactId>
</dependency>
oauth2-issuer: https://localhost:8443/realms/master
oauth2-client-id: change-me

com:
  c4-soft:
    springaddons:
      oidc:
        ops:
        # you can add more entries if using several realms or instances
        - iss: ${oauth2-issuer}
          username-claim: preferred_username
          authorities:
          # you can add as many entries as you like for more clients or realm roles
          # you may also define a prefix (for instance "ROLE_") or case transformation (for instance `caze: UPPER`) for each claim
          - path: $.resource_access.${oauth2-client-id}.roles
        resourceserver:
          # entries listed here are accessible to anonymous
          permit-all:
          - "/actuator/health/readiness"
          - "/actuator/health/liveness"
          - "/v3/api-docs/**"
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
}
jhdbpxl9

jhdbpxl92#

是的,在Spring OAuth 2.0中,您可以提供所需的一切。将下一个依赖项添加到项目中

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

字符串
则应设置应用程序属性

security.oauth2.client.client-id=<client_id>
security.oauth2.client.client-secret=<client_secret>


希望这对你有帮助

相关问题