【问题标题】:Retrieve Keycloak Roles in reactive Spring Gateway security在反应式 Spring Gateway 安全性中检索 Keycloak 角色
【发布时间】:2021-07-16 10:36:47
【问题描述】:

我从 Zuul Gateway 迁移到 Spring Gateway。这迫使我放弃了 Webflux 的 Servlet。我使用 KeyCloak 和 KeyCloak 角色进行身份验证和授权。

没有官方的响应式 KeyCloak 实现,所以我使用 Spring OAuth2 代替。除了检索角色之外,它工作正常。

我不能使用 servlet 拦截器,因为 WebFlux 不允许使用 servlet。此外,似乎 Spring Gateway 通常不允许拦截响应体。

因此我的问题仍然存在:如何在 Spring Gateway 中检索 KeyCloak 角色,以便其安全性使用它们?

这是我使用的一些示例代码: 在 SecurityConfig.java 类中:

@Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.csrf().disable().authorizeExchange(exchanges -> exchanges.pathMatchers("/**").hasAnyRole("DIRECTOR")); }

application.yml:

spring.security.oauth2.client.provider.keycloak.issuer-uri: ..../realms/default

【问题讨论】:

    标签: keycloak spring-cloud-gateway security-roles


    【解决方案1】:

    我自己也有同样的问题。我得到的问题之一是获取JWT 标签之类的副本,即Keycloak 对您的设置进行编码的文本

        @GetMapping("/whoami")
        @ResponseBody
        public Map<String, Object> index(
                @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
                        Authentication auth) {
                log.error("XXAuth is {}",auth);
                log.error("XXClient is {}", authorizedClient.getClientRegistration());
                log.error("XXClient access  is {}", authorizedClient.getAccessToken());
                log.error("Token {}",authorizedClient.getAccessToken().getTokenValue());
       }
    

    此代码将为您提供对话中的一些值,令牌部分是 JWT 令牌,您可以将其复制并粘贴到 jwt.io 并找出 Keycloak 实际发送的内容.

    这通常看起来像

    {
      "exp": 1622299931,
      "iat": 1622298731,
      "auth_time": 1622298258,
      "jti": "635ca59f-c87b-40da-b4ae-39774ed8098a",
      "iss": "http://clunk:8080/auth/realms/spring-cloud-gateway-realm",
      "sub": "6de0d95f-95b0-419d-87a4-b2862e8d0763",
      "typ": "Bearer",
      "azp": "spring-cloud-gateway-client",
      "nonce": "2V8_3siQjTOIRbfs68BHwzvz3-dWeqXGUultzhJUWrA",
      "session_state": "dd226823-90bc-429e-9cac-bb575b7d4fa0",
      "acr": "0",
      "realm_access": {
          "roles": [
              "ROLE_ANYONE"
          ]
      },
      "resource_access": {
          "spring-cloud-gateway-client": {
              "roles": [
                  "ROLE_ADMIN_CLIENT"
              ]
          }
      },
      "scope": "openid email profile roles",
      "email_verified": true,
      "preferred_username": "anon"
    

    }

    如您所见,Keycloak 支持两种不同类型的 ROLE 令牌,但它们没有在顶层定义,而是在 realm_accessresource_access 下定义,不同之处在于资源访问定义了作为资源一部分的 ROLE real_access 定义了在所有领域中定义的角色。

    要定义这些值,需要定义一个Mapper,如下

    要将这些值加载到 Spring 安全性中,您需要定义一个 userAuthoritiesMapper Bean 并将属性中的设置导出为 SimpleGrantedAuthority,如下所示。

    package foo.bar.com;
    
    import lombok.extern.slf4j.Slf4j;
    import net.minidev.json.JSONArray;
    import net.minidev.json.JSONObject;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
    import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
    import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    @Slf4j
    @EnableWebFluxSecurity
    @EnableReactiveMethodSecurity
    
    public class RoleConfig {
            
    
            
            @Bean
            GrantedAuthoritiesMapper userAuthoritiesMapper() {
                    String ROLES_CLAIM = "roles";
                    return authorities -> {
                            
                            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
                            
                            for (Object authority : authorities) {
                                    boolean isOidc = authority instanceof OidcUserAuthority;
                                    
                                    if (isOidc) {
                                            log.error("Discovered an Oidc type of object");
                                            var oidcUserAuthority = (OidcUserAuthority) authority;
                                            java.util.Map<String, Object> attribMap = oidcUserAuthority.getAttributes();
                                            JSONObject jsonClaim;
                                            for (String attrib : attribMap.keySet()) {
                                                    log.error("Attribute name  {}  type {} ", attrib, attrib.getClass().getName());
                                                    Object claim = attribMap.get(attrib);
                                                    if (attrib.equals("realm_access")) {
                                                            log.error("Define on roles for  entire client");
                                                            
                                                            jsonClaim = (JSONObject) claim;
                                                            if (!jsonClaim.isEmpty()) {
                                                                    log.error("JobClaim is {}", jsonClaim);
                                                                    Object roleStr = jsonClaim.get("roles");
                                                                    if (roleStr != null) {
                                                                            log.error("Role String {}", roleStr.getClass().getName());
                                                                            JSONArray theRoles = (JSONArray) roleStr; //jsonClaim.get("roles");
                                                                            for (Object roleName : theRoles) {
                                                                                    log.error("Name {} ", roleName);
                                                                            }
                                                                            
                                                                    }
                                                            }
                                                    }
                                                    if (attrib.equals("resource_access")) {
                                                            log.error("Unique to attrib client");
                                                            jsonClaim = (JSONObject) claim;
                                                            if (!jsonClaim.isEmpty()) {
                                                                    log.error("Job is {}", jsonClaim);
                                                                    
                                                                    String clientName = jsonClaim.keySet().iterator().next();
                                                                    log.error("Client name {}", clientName);
                                                                    JSONObject roleObj = (JSONObject) jsonClaim.get(clientName);
                                                                    Object roleNames = roleObj.get("roles");
                                                                    log.error("Role names {}", roleNames.getClass().getName());
                                                                    JSONArray theRoles = (JSONArray) roleObj.get("roles");
                                                                    for (Object roleName : theRoles) {
                                                                            log.error("Name {} ", roleName);
                                                                    }
                                                                    
                                                            }
                                                    }
                                                    
                                            }
                                            
                                            var userInfo = oidcUserAuthority.getUserInfo();
                                            log.error("UserInfo {}", userInfo);
                                            for (String key : userInfo.getClaims().keySet()) {
                                                    log.error("UserInfo keys {}", key);
                                            }
                                            if (userInfo.containsClaim(ROLES_CLAIM)) {
                                                    var roles = userInfo.getClaimAsStringList(ROLES_CLAIM);
                                                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                                            } else {
                                                    log.error("userInfo DID NOT FIND A claim");
                                            }
                                    } else {
                                            
                                            var oauth2UserAuthority = (SimpleGrantedAuthority) authority;
                                            log.error("Authority name " + authority.getClass().getName());
                                    }
                            }
                            
                            return mappedAuthorities;
                    };
            }
            
            private Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
                    return roles.stream()
                            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                            .collect(Collectors.toList());
            }
    }
    

    请注意,此代码基于OAuth2 Login with custom granted authorities from UserInfo 上的示例 获取属性是我自己的工作。

    注意如果没有找到realm_accessresource_access,将在最高级别生成错误消息,因为我假设想要解码Keycloak 引用是使用的原因这段代码。

    正常工作时,会产生如下输出

    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Discovered an Oidc type of object
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  at_hash  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  sub  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  resource_access  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Unique to attrib client
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Job is {"spring-cloud-gateway-client":{"roles":["ROLE_ADMIN_CLIENT"]}}
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Client name spring-cloud-gateway-client
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Role names net.minidev.json.JSONArray
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Name ROLE_ADMIN_CLIENT 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  email_verified  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  iss  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  typ  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  preferred_username  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  nonce  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  aud  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  acr  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  realm_access  type java.lang.String 
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Define on roles for  entire client
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : JobClaim is {"roles":["ROLE_ANYONE"]}
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Role String net.minidev.json.JSONArray
    2021-05-29 15:32:11.249 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Name ROLE_ANYONE 
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  azp  type java.lang.String 
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  auth_time  type java.lang.String 
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  exp  type java.lang.String 
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  session_state  type java.lang.String 
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  iat  type java.lang.String 
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Attribute name  jti  type java.lang.String 
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : UserInfo org.springframework.security.oauth2.core.oidc.OidcUserInfo@8be9a0b8
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : UserInfo keys sub
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : UserInfo keys email_verified
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : UserInfo keys preferred_username
    2021-05-29 15:32:11.250 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : userInfo DID NOT FIND A claim
    2021-05-29 15:32:11.252 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Authority name org.springframework.security.core.authority.SimpleGrantedAuthority
    2021-05-29 15:32:11.252 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Authority name org.springframework.security.core.authority.SimpleGrantedAuthority
    2021-05-29 15:32:11.252 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Authority name org.springframework.security.core.authority.SimpleGrantedAuthority
    2021-05-29 15:32:11.252 ERROR 7394 --- [or-http-epoll-5] com.jdriven.gateway.RoleConfig           : Authority name org.springframework.security.core.authority.SimpleGrantedAuthority
    2021-05-29 15:32:11.252 DEBUG 7394 --- [or-http-epoll-5] o.s.w.r.f.client.ExchangeFunctions       : [34ff3355] Cancel signal (to close connection)
    2021-05-29 15:32:11.252 DEBUG 7394 --- [or-http-epoll-5] o.s.w.r.f.client.ExchangeFunctions       : [1b083d68] Cancel signal (to close connection)
    2021-05-29 15:32:11.254 DEBUG 7394 --- [or-http-epoll-5] ebSessionServerSecurityContextRepository : Saved SecurityContext 'SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=Name: [anon], Granted Authorities: [[ROLE_USER, SCOPE_email, SCOPE_openid, SCOPE_profile, SCOPE_roles]], User Attributes: [{at_hash=GCz2JybWiLc-42ACnjLJ6w, sub=6de0d95f-95b0-419d-87a4-b2862e8d0763, resource_access={"spring-cloud-gateway-client":{"roles":["ROLE_ADMIN_CLIENT"]}}, email_verified=true, iss=http://clunk:8080/auth/realms/spring-cloud-gateway-realm, typ=ID, preferred_username=anon, nonce=2V8_3siQjTOIRbfs68BHwzvz3-dWeqXGUultzhJUWrA, aud=[spring-cloud-gateway-client], acr=0, realm_access={"roles":["ROLE_ANYONE"]}, azp=spring-cloud-gateway-client, auth_time=2021-05-29T14:24:18Z, exp=2021-05-29T14:52:11Z, session_state=dd226823-90bc-429e-9cac-bb575b7d4fa0, iat=2021-05-29T14:32:11Z, jti=7d479a85-d76e-4930-9c86-b384a56d7af5}], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[]]]' in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@69c3d462'
    

    【讨论】:

      【解决方案2】:

      @戴夫 谢谢你提醒我这个问题。从那以后,我在 WebFlux 中找到了一种解决方法。我已经覆盖了 ReactiveOAuth2UserService。默认情况下,它有两种风格,一种是 OAuth,一种是 Oidc。就我而言,我已经覆盖了 Oidc:

      @Component public class ReactiveKeycloakUserService extends OidcReactiveOAuth2UserService {
       @Override
       public Mono<OidcUser> loadUser(OidcUserRequest userRequest) throws ... {
        // Call super and then replace result with roles
       }
      }
      

      Spring 将注入我的实例而不是默认实例。从 userRequest 中,您可以检索角色,并在超类上调用相同的方法后,您可以截取结果并在其上添加角色。

      【讨论】:

      • 另一种选择是提供 GrantedAuthoritiesMapper 的实现。与使用 GrantedAuthoritiesMapper 相比,使用 OAuth2UserService 的基于委托的策略更先进,也更灵活,因为它允许您访问 OidcUserRequest 和 OidcUser(使用 OpenID Connect 1.0 UserService 时)
      猜你喜欢
      • 2020-07-26
      • 2012-09-06
      • 2015-08-26
      • 2012-04-08
      • 1970-01-01
      • 2015-05-22
      • 2023-04-05
      • 2015-12-03
      • 2012-03-21
      相关资源
      最近更新 更多