如何在 Spring Security 中从 oauth 服务器返回的用户声明中设置用户权限

Posted

技术标签:

【中文标题】如何在 Spring Security 中从 oauth 服务器返回的用户声明中设置用户权限【英文标题】:How to set user authorities from user claims return by an oauth server in spring security 【发布时间】:2019-08-31 16:58:51 【问题描述】:

我最近写了一个使用spring security oauth2的spring boot项目,由于某种原因,身份验证服务器是IdentityServer4,我可以在我的项目中成功登录并获取用户名,但是我找不到任何设置用户权限/角色的方法。

request.isUserInRole 总是返回 false。 @PreAuthorize("hasRole('rolename')") 总是把我带到 403。

我在哪里可以放置一些代码来设置权限?

服务器通过 userinfo 端点返回了一些用户声明,我的项目收到了它们,我什至可以在控制器的原理参数中看到它。

这个方法总是返回 403

@ResponseBody
@RequestMapping("admin")
@PreAuthorize("hasRole('admin')")
public String admin(HttpServletRequest request)
    return "welcome, you are admin!" + request.isUserInRole("ROLE_admin");

application.properties

spring.security.oauth2.client.provider.test.issuer-uri = http://localhost:5000
spring.security.oauth2.client.provider.test.user-name-attribute = name

spring.security.oauth2.client.registration.test.client-id = java
spring.security.oauth2.client.registration.test.client-secret = secret
spring.security.oauth2.client.registration.test.authorization-grant-type = authorization_code
spring.security.oauth2.client.registration.test.scope = openid profile

我打印声明

@ResponseBody
@RequestMapping()
public Object index(Principal user)
    OAuth2AuthenticationToken token = (OAuth2AuthenticationToken)user;
    return token.getPrincipal().getAttributes();

并得到结果显示有一个名为“角色”的声明

"key":"value","role":"admin","preferred_username":"bob"

谁能帮帮我,请给我一个解决方案?

编辑 1: 原因是 oauth2 客户端已经移除了提取器,我必须实现 userAuthoritiesMapper。

最后我通过添加以下类得到了这项工作:

@Configuration
public class AppConfig extends WebSecurityConfigurerAdapter 
    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(this.userAuthoritiesMapper());
        //.oidcUserService(this.oidcUserService());
        super.configure(http);
    

    private GrantedAuthoritiesMapper userAuthoritiesMapper() 
        return (authorities) -> 
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            authorities.forEach(authority -> 
                if (OidcUserAuthority.class.isInstance(authority)) 
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;

                    OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
                    if (userInfo.containsClaim("role"))
                        String roleName = "ROLE_" + userInfo.getClaimAsString("role");
                        mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
                    
                 else if (OAuth2UserAuthority.class.isInstance(authority)) 
                    OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
                    Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
                    
                    if (userAttributes.containsKey("role"))
                        String roleName = "ROLE_" + (String)userAttributes.get("role");
                        mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
                    
                
            );

            return mappedAuthorities;
        ;
    

框架变化太快,网上的demo太老了!

【问题讨论】:

我想我在这里找到了原因:github.com/spring-projects/spring-security/issues/5625 docs.spring.io/spring-security/site/docs/current/reference/… 【参考方案1】:

我花了几个小时找到了解决方案。问题在于 spring oauth 安全性,默认情况下,它使用密钥“权限”从令牌中获取用户角色。所以,我实现了一个自定义令牌转换器。

你需要的第一个是自定义用户令牌转换器,这里是类:

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

public class CustomUserTokenConverter implements UserAuthenticationConverter 
    private Collection<? extends GrantedAuthority> defaultAuthorities;
    private UserDetailsService userDetailsService;

    private final String AUTHORITIES = "role";
    private final String USERNAME = "preferred_username";
    private final String USER_IDENTIFIER = "sub";

    public CustomUserTokenConverter() 
    

    public void setUserDetailsService(UserDetailsService userDetailsService) 
        this.userDetailsService = userDetailsService;
    

    public void setDefaultAuthorities(String[] defaultAuthorities) 
        this.defaultAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(defaultAuthorities));
    

    public Map<String, ?> convertUserAuthentication(Authentication authentication) 
        Map<String, Object> response = new LinkedHashMap();
        response.put(USERNAME, authentication.getName());
        if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) 
            response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
        

        return response;
    

    public Authentication extractAuthentication(Map<String, ?> map) 
        if (map.containsKey(USER_IDENTIFIER)) 
            Object principal = map.get(USER_IDENTIFIER);
            Collection<? extends GrantedAuthority> authorities = this.getAuthorities(map);
            if (this.userDetailsService != null) 
                UserDetails user = this.userDetailsService.loadUserByUsername((String)map.get(USER_IDENTIFIER));
                authorities = user.getAuthorities();
                principal = user;
            

            return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
         else 
            return null;
        
    

    private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) 
        if (!map.containsKey(AUTHORITIES)) 
            return this.defaultAuthorities;
         else 
            Object authorities = map.get(AUTHORITIES);
            if (authorities instanceof String) 
                return AuthorityUtils.commaSeparatedStringToAuthorityList((String)authorities);
             else if (authorities instanceof Collection) 
                return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection)authorities));
             else 
                throw new IllegalArgumentException("Authorities must be either a String or a Collection");
            
        
    

您需要一个自定义令牌转换器,这里是:

import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter 

    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> claims) 
        OAuth2Authentication authentication = super.extractAuthentication(claims);
        authentication.setDetails(claims);
        return authentication;
    



最后你的 ResourceServerConfiguration 看起来像这样:

import hello.helper.CustomAccessTokenConverter;
import hello.helper.CustomUserTokenConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter 
    @Override
    public void configure(final HttpSecurity http) throws Exception 
        // @formatter:off
        http.authorizeRequests()
                .anyRequest().access("hasAnyAuthority('Admin')");
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception 
        resources.resourceId("arawaks");
    

    @Bean
    @Primary
    public RemoteTokenServices tokenServices() 
        final RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId("resourceId");
        tokenServices.setClientSecret("resource.secret");
        tokenServices.setCheckTokenEndpointUrl("http://localhost:5001/connect/introspect");
        tokenServices.setAccessTokenConverter(accessTokenConverter());
        return tokenServices;
    


    @Bean
    public CustomAccessTokenConverter accessTokenConverter() 
        final CustomAccessTokenConverter converter = new CustomAccessTokenConverter();
        converter.setUserTokenConverter(new CustomUserTokenConverter());
        return converter;
    


【讨论】:

您的回答似乎是对的!我稍后会测试它,谢谢! 我正在尝试通过添加一个名为“authorities”且值 = ["ROLE_admin"] 的声明来解决它,但是 spring security 仍然返回 403,令牌是:"preferred_username":"bob" ,"authorities":"[\"ROLE_admin\"]" 您不需要将前缀 ROLE_ 添加到您的角色中,我的第一个解决方案是这样的,在 IdentityServer4 中,在 ProfileService 实现中,我添加了声明“权限”并且它无需更改spring security oauth2的默认TokenConverter,角色名称相同,不带ROLE_前缀。 当局声称似乎被忽略了,我尝试了许多格式的声称值,但没有运气。而我使用的是 spring-boot-starter-oauth2-client 包,没有 UserAuthenticationConverter 类。 我已经解决了这个问题,看看我的帖子的编辑。【参考方案2】:

显然@wjsgzcn 回答(EDIT 1) 不起作用,原因如下

    如果您打印Oauth2UserAuthirty class 返回的属性,您很快就会注意到JSON 数据的内容没有role 键,而是有一个authorities 键,因此您需要使用该键进行迭代在权限(角色)列表上获取实际的角色名称。

    因此以下代码行将不起作用,因为oauth2UserAuthority.getAttributes(); 返回的JSON 数据中没有role

     OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
     Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
     if (userAttributes.containsKey("role"))
         String roleName = "ROLE_" + (String)userAttributes.get("role");
         mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
     
    

所以改为使用以下方法从 getAttributes 中获取实际角色

if (userAttributes.containsKey("authorities"))
   ObjectMapper objectMapper = new ObjectMapper();
   ArrayList<Role> authorityList = 
   objectMapper.convertValue(userAttributes.get("authorities"), new 
   TypeReference<ArrayList<Role>>() );
   log.info("authList: ", authorityList);
   for(Role role: authorityList)
      String roleName = "ROLE_" + role.getAuthority();
      log.info("role: ", roleName);
      mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
   

Role 是一个像这样的 pojo 类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role 
    @JsonProperty
    private String authority;

这样您将能够获得ROLE_ post 前缀,这是在成功通过授权服务器身份验证后授予用户的实际角色,并且客户端返回授予权限(角色)的LIST

现在完整的GrantedAuthoritesMapper 如下所示:

private GrantedAuthoritiesMapper userAuthoritiesMapper() 
    return (authorities) -> 
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            authorities.forEach(authority -> 
                if (OidcUserAuthority.class.isInstance(authority)) 
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;

                    OidcIdToken idToken = oidcUserAuthority.getIdToken();
                    OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
                    
                    // Map the claims found in idToken and/or userInfo
                    // to one or more GrantedAuthority's and add it to mappedAuthorities
                    if (userInfo.containsClaim("authorities"))
                        ObjectMapper objectMapper = new ObjectMapper();
                        ArrayList<Role> authorityList = objectMapper.convertValue(userInfo.getClaimAsMap("authorities"), new TypeReference<ArrayList<Role>>() );
                        log.info("authList: ", authorityList);
                        for(Role role: authorityList)
                            String roleName = "ROLE_" + role.getAuthority();
                            log.info("role: ", roleName);
                            mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
                         
                    

                 else if (OAuth2UserAuthority.class.isInstance(authority)) 
                    OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
                    Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
                    log.info("userAttributes: ", userAttributes);
                    // Map the attributes found in userAttributes
                    // to one or more GrantedAuthority's and add it to mappedAuthorities
                    if (userAttributes.containsKey("authorities"))
                        ObjectMapper objectMapper = new ObjectMapper();
                        ArrayList<Role> authorityList = objectMapper.convertValue(userAttributes.get("authorities"), new TypeReference<ArrayList<Role>>() );
                        log.info("authList: ", authorityList);
                        for(Role role: authorityList)
                            String roleName = "ROLE_" + role.getAuthority();
                            log.info("role: ", roleName);
                            mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
                         
                    
                
            );
            log.info("The user authorities: ", mappedAuthorities);
            return mappedAuthorities;
    ;

现在您可以在oauth2Login 中使用userAuthorityMapper,如下所示

@Override
    public void configure(HttpSecurity http) throws Exception 
        http.antMatcher("/**").authorizeRequests()
            .antMatchers("/", "/login**").permitAll()
            .antMatchers("/clientPage/**").hasRole("CLIENT")
            .anyRequest().authenticated()
            .and()
            .oauth2Login()
                .userInfoEndpoint()
                .userAuthoritiesMapper(userAuthoritiesMapper());
    

【讨论】:

以上是关于如何在 Spring Security 中从 oauth 服务器返回的用户声明中设置用户权限的主要内容,如果未能解决你的问题,请参考以下文章

Apache Oltu Spring Security OAuth2 和 Google 集成

Spring security oauth 2 简单示例

如何在 JSP 页面中从 Java 类中设置 hashmap 的值以便在控制器中进一步使用

如何在 Javascript 中从 Spring 获取 JSON 响应

如何在 Hibernate、Spring、JSP 中从 db 中获取多行?

如何在apache中从角度调用spring boot api