【问题标题】:Spring Security: mapping OAuth2 claims with roles to secure Resource Server endpointsSpring Security:将 OAuth2 声明与角色映射到保护资源服务器端点
【发布时间】:2020-01-31 23:15:09
【问题描述】:

我正在使用 Spring Boot 设置资源服务器并保护我使用 Spring Security 提供的 OAuth2 的端点。所以我使用的是 Spring Boot 2.1.8.RELEASE,例如使用 Spring Security 5.1.6.RELEASE

作为授权服务器,我正在使用 Keycloak。资源服务器中身份验证、颁发访问令牌和验证令牌之间的所有过程都正常工作。下面是一个发行和解码的令牌的例子(有些部分被剪掉了):

{
  "jti": "5df54cac-8b06-4d36-b642-186bbd647fbf",
  "exp": 1570048999,
  "aud": [
    "myservice",
    "account"
  ],
  "azp": "myservice",
  "realm_access": {
    "roles": [
      "offline_access",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "myservice": {
      "roles": [
        "ROLE_user",
        "ROLE_admin"
      ]
    },
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "openid email offline_access microprofile-jwt profile address phone",
}

如何配置 Spring Security 以使用访问令牌中的信息为不同的端点提供条件授权?

最终我想写一个这样的控制器:

@RestController
public class Controller {

    @Secured("ROLE_user")
    @GetMapping("userinfo")
    public String userinfo() {
        return "not too sensitive action";
    }

    @Secured("ROLE_admin")
    @GetMapping("administration")
    public String administration() {
        return "TOOOO sensitive action";
    }
}

【问题讨论】:

  • 我遇到了同样的问题,我正在尝试使用自定义的 JwtDecoder 和 CustomAccessTokenConverter,但仍然没有运气。关键应该是将 resource_access 角色放置在 Spring Security 可以获取它们的地方,但到目前为止,我没有运气,即使遵循互联网上的多个教程/示例。
  • 一些见解可以在这个视频中找到:youtube.com/… 这个播放列表包含实际上很好的材料,可以更好地了解如何构建 SpringBoot 应用程序。

标签: spring rest spring-boot spring-security authorization


【解决方案1】:

经过一番折腾,我找到了一个实现自定义 jwtAuthenticationConverter 的解决方案,它能够将特定于资源的角色附加到权限集合中。

    http.oauth2ResourceServer()
                .jwt()
                .jwtAuthenticationConverter(new JwtAuthenticationConverter()
                {
                    @Override
                    protected Collection<GrantedAuthority> extractAuthorities(final Jwt jwt)
                    {
                        Collection<GrantedAuthority> authorities = super.extractAuthorities(jwt);
                        Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
                        Map<String, Object> resource = null;
                        Collection<String> resourceRoles = null;
                        if (resourceAccess != null &&
                            (resource = (Map<String, Object>) resourceAccess.get("my-resource-id")) !=
                            null && (resourceRoles = (Collection<String>) resource.get("roles")) != null)
                            authorities.addAll(resourceRoles.stream()
                                                            .map(x -> new SimpleGrantedAuthority("ROLE_" + x))
                                                            .collect(Collectors.toSet()));
                        return authorities;
                    }
                });

其中 my-resource-id 既是出现在 resource_access 声明中的资源标识符,也是与 ResourceServerSecurityConfigurer 中的 API 关联的值 em>。

请注意,extractAuthorities 实际上已被弃用,因此更面向未来的解决方案应该是实现一个成熟的转换器

    import org.springframework.core.convert.converter.Converter;
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.oauth2.jwt.Jwt;
    import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
    import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;

    import java.util.Collection;
    import java.util.Collections;
    import java.util.Map;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

    public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>
    {
        private static Collection<? extends GrantedAuthority> extractResourceRoles(final Jwt jwt, final String resourceId)
        {
            Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
            Map<String, Object> resource;
            Collection<String> resourceRoles;
            if (resourceAccess != null && (resource = (Map<String, Object>) resourceAccess.get(resourceId)) != null &&
                (resourceRoles = (Collection<String>) resource.get("roles")) != null)
                return resourceRoles.stream()
                                    .map(x -> new SimpleGrantedAuthority("ROLE_" + x))
                                    .collect(Collectors.toSet());
            return Collections.emptySet();
        }

        private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();

        private final String resourceId;

        public CustomJwtAuthenticationConverter(String resourceId)
        {
            this.resourceId = resourceId;
        }

        @Override
        public AbstractAuthenticationToken convert(final Jwt source)
        {
            Collection<GrantedAuthority> authorities = Stream.concat(defaultGrantedAuthoritiesConverter.convert(source)
                                                                                                       .stream(),
                                                                     extractResourceRoles(source, resourceId).stream())
                                                             .collect(Collectors.toSet());
            return new JwtAuthenticationToken(source, authorities);
        }
    }

我已经使用 Spring Boot 2.1.9.RELEASE、Spring Security 5.2.0.RELEASE 和官方 Keycloak 7.0.0 Docker 映像测试了这两种解决方案。

一般来说,我认为无论实际的授权服务器(即 IdentityServer4、Keycloak...)如何,这似乎都是将声明转换为 Spring Security 授权的合适位置。

【讨论】:

  • 我刚刚做了一个基本的测试,它似乎真的有效!!非常感谢!!我不明白为什么在 Spring 世界中很难找到一些答案。如果 Spring 提供了实际的代码示例,有时会非常有帮助。而且我们在互联网上可以找到的信息已经过时了很多次,因此很难理解它是否正确。当然,我在 Spring 的初学者水平也没有帮助。
  • 你能告诉我,我需要在 spring 安全配置中使用这个 'CustomJwtAuthenticationConverter' 自定义转换器吗?
  • 它是第一个框中使用的 JwtAuthenticationConverter 的替代品,因此您应该像 http.oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(new CustomJwtAuthenticationConverter(...)) 一样使用它...
  • 这对我有帮助。谢谢:)
【解决方案2】:

这是另一个解决方案

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
    }

【讨论】:

  • 这个解决方案是我正在寻找的解决方案:)。在阅读了来自 Spring Security Docu on Extracting Authorities 的如何使用自定义声明和权限后,我发现这很有帮助
  • 这在某些情况下可能是一个解决方案,但它在 OP 描述的场景中无济于事。使用“角色”作为当局声称的名称是行不通的。这种方法不会处理位于 resource_access->client_id 下的角色
【解决方案3】:

正如@hillel_guy 的回答已经提到的那样,使用AbstractHttpConfigurer 应该是要走的路。这对我来说与 spring-boot 2.3.4 和 spring-security 5.3.4 无缝协作。 参考 spring-security API 文档:OAuth2ResourceServerConfigurer

更新

完整示例,如 cmets 中所要求的:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final String JWT_ROLE_NAME = "roles";
    private static final String ROLE_PREFIX = "ROLES_";

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().anyRequest().authenticated()
                .and().csrf().disable()
                .cors()
                .and().oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
    }

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        // create a custom JWT converter to map the roles from the token as granted authorities
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(JWT_ROLE_NAME); // default is: scope, scp
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix(ROLE_PREFIX ); // default is: SCOPE_

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }
}

就我而言,我想从 JWT 映射 roles 而不是 scope
希望这会有所帮助。

【讨论】:

  • 您好,您能提供更多信息吗?我正在尝试做同样的事情,我尝试了不同的实现,但没有奏效。
  • 嗨@GuilhermeBernardi,我更新了我的答案。希望这对您有所帮助。
  • 如何为您的解决方案指定 .mvcMatchers(...) 或 .antMatchers(...)?我将我的用户定义为 john / doe 和 .roles("ADMIN")。使用您的解决方案,我仍然得到 403。我在您的 configure(...) 中看不到任何路径限制,例如角色为“ADMIN”的“/articles”。
【解决方案4】:

您遇到的困难部分是由于您的角色定位在 JWT 中的 resource_server->client_id 下。这需要一个自定义令牌转换器来提取它们。

您可以将 keycloak 配置为使用客户端映射器,该映射器将在顶级声明名称下显示角色,例如“角色”。这使得 Spring Security 配置更简单,因为您只需要 JwtGrantedAuthoritiesConverter 并设置了 authorityClaimName,如@hillel_guy 采用的方法所示。

keycloak 客户端映射器的配置如下:

【讨论】:

    【解决方案5】:

    如果您使用的是 Azure AD Oath,现在有一个更简单的方法:

            http 
            .cors()
            .and()
            .authorizeRequests()
            .anyRequest()
            .authenticated() 
            .and()
            .oauth2ResourceServer()
            .jwt()
            .jwtAuthenticationConverter(new AADJwtBearerTokenAuthenticationConverter("roles", "ROLE_")); 
    

    ADDJwtBearerTokenAuthenticationConverter 允许您将声明名称添加为第一个参数,并将您希望角色前缀的内容作为第二个参数添加。

    我的导入,以便您可以找到库:

    import com.azure.spring.aad.webapi.AADJwtBearerTokenAuthenticationConverter;
    

    【讨论】:

    • 您的回答是主观的,因为没有任何来源可以支持您的话。这些类型的答案被认为对 SO 不利,并且往往被否决甚至删除。请参考How do I write a good answer?章节:为链接提供上下文
    猜你喜欢
    • 2013-12-21
    • 2019-11-27
    • 1970-01-01
    • 2017-03-25
    • 2018-08-17
    • 2017-12-15
    • 1970-01-01
    • 2018-07-08
    • 2018-05-17
    相关资源
    最近更新 更多