如何通过 spring 安全中的 oauth 服务器根据用户声明 return 设置用户权限

How to set user authorities from user claims return by an oauth server in spring security

我最近写了一个spring启动项目,使用了spring security oauth2,由于某种原因,auth服务器是IdentityServer4,我可以成功登录并在我的项目中获取用户名,但我找不到任何设置用户 authority/role.

的方法

request.isUserInRole 总是 return 错误。 @PreAuthorize("hasRole('rolename')") 总是把我带到 403.

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

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

这个方法总是return 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();
}

并得到结果显示有一个名为 'role'

的声明
{"key":"value","role":"admin","preferred_username":"bob"}

有人可以帮助我并给我一个解决方案吗?

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

最后我通过添加以下 class:

完成了这项工作
@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;
        };
    }
}

The framework changes so fast and the demos on the web is too old!

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

您首先需要的是自定义用户令牌转换器,这里是 class:

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;
    }

}

显然 @wjsgzcn 的回答 (EDIT 1) 由于以下原因无效

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

  2. 因此以下代码行将不起作用,因为 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 class 就像这样

@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());
    }