SpringSecurity - 用户动态授权 及 动态角色权限

Posted 小毕超

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity - 用户动态授权 及 动态角色权限相关的知识,希望对你有一定的参考价值。

一、SpringSecurity 动态授权

上篇文章我们介绍了SpringSecurity的动态认证,上篇文章就说了SpringSecurity 的两大主要功能就是认证和授权,既然认证以及学习了,那本篇文章一起学习了SpringSecurity 的动态授权。

上篇文章地址:https://blog.csdn.net/qq_43692950/article/details/122393435

二、SpringSecurity 授权

我们接着上篇文章的项目继续修改,上篇文章中有说到我们WebSecurityConfig配制类中的configure(HttpSecurity http)这个方法就是用来做授权的,现在就可以来体验一下了,比如我们修改以admin为开头的接口,权限或角色中需要有admin

 @Override
 protected void configure(HttpSecurity http) throws Exception 
     http.authorizeRequests()
             .antMatchers("/admin/**").hasAuthority("admin")
             .antMatchers("/**").fullyAuthenticated()
             .and()
             .formLogin()
             .permitAll()
             .and()
             .csrf().disable();
 

下面使用admin用户访问admin/test接口:

报了403无权限的错误,因为我们设置了admin/**接口必须要有admin这个权限,可以看下上篇文章中写的UserService类:

这边直接给用户设定死了一个admin角色,这里就有个问题了权限和角色有什么区别,其实在SpringSecurity 中权限和角色都放在了一起,可以说概念上是一样的,但角色是以ROLE_开头的。

其中还需注意的是如果授权角色可以使用hasRole()hasAnyRole(),如果是授权权限则使用hasAuthority()hasAnyAuthority()

角色授权:授权代码需要加ROLE_前缀,controller上使用时不要加前缀。
权限授权:设置和使用时,名称保持一至即可。

所以可以修改UserService类:

在此请求接口:

现在就有权限访问了,但是写死肯定不是我们要的效果,所以此时可以将角色放在数据库中,通过查询数据库动态获取用户的角色。

下面就需要在数据库中创建role角色表:

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role` varchar(255) NOT NULL,
  `role_describe` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

角色肯定是和人有关系的,而且有时多对多的关系,所以根据关系模型我们要抽取出一个角色用户关系表:

CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `userid` int(11) NOT NULL,
  `roleid` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;


对于角色的新增和关联用户,无非就是数据库的增删改,这里不做演示了,直接在创建好表可以在表中添加几条角色,并关联用户:


添加RoleEntity实体

@Data
@TableName("/role")
public class RoleEntity 
    private Long id;
    private String role;
    @TableField("role_describe")
    private String roleDescribe;

RoleMapper类,并写根据用户id查询全部角色的接口:

@Mapper
@Repository
public interface RoleMapper extends BaseMapper<RoleEntity> 

    @Select("SELECT r.id,r.role,r.role_describe FROM user_role u,role r where u.roleid = r.id AND u.userid = #userId")
    List<RoleEntity> getAllRoleByUserId(@Param("userId") Integer userId);

修改UserService类:

@Service
public class UserService implements UserDetailsService 
    @Autowired
    UserMapper userMapper;

    @Autowired
    RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
        LambdaQueryWrapper<UserEntity> wrapper = new LambdaQueryWrapper<UserEntity>()
                .eq(UserEntity::getUsername, username);
        UserEntity userEntity = userMapper.selectOne(wrapper);
        if (userEntity == null) 
            throw new UsernameNotFoundException("用户不存在!");
        
        List<GrantedAuthority> auths = roleMapper.getAllRoleByUserId(userEntity.getId())
                .stream()
                .map(r -> new SimpleGrantedAuthority(r.getRole()))
                .collect(Collectors.toList());
        userEntity.setRoles(auths);
        return userEntity;
    

    public boolean register(String userName, String password) 
        UserEntity entity = new UserEntity();
        entity.setUsername(userName);
        entity.setPassword(new BCryptPasswordEncoder().encode(password));
        entity.setEnabled(true);
        entity.setLocked(false);
        return userMapper.insert(entity) > 0;
    

下面就可以测试了,在浏览器再次访问上面的接口:

但是发现是403,原因是我们给admin设置的是权限admin,不是角色,数据库中存的是ROLE_admin,这里是想让大家对两者的区别更加深刻下,修改数据库为admin

重新启动再次访问:

已经可以访问了。上面大家应该对权限和角色有了一定的了解,下面对授权和授予角色的方法做下说明:

  • hasRole
    如果用户具备给定角色就允许访问,否则出现 403。给接口授权时无需写ROLE_开头,因为底层代码会自动添加与之进行匹配,用户添加角色时必须写ROLE_

  • hasAnyRole
    表示用户具备任何一个条件都可以访问。

  • hasAuthority
    如果当前的主体具有指定的权限,则返回 true,否则返回 false

  • hasAnyAuthority
    如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返true

现在我们已经了解怎么样给用户授权了,也知道怎么给接口赋予权限了,但是还是有个问题:

这个都在代码里面写死也不合适呀,其实这里有两种方案,一种是地址和角色的固定变化不大的场景下,可以在这里从数据库中读取出来通过HttpSecurity对象映射角色,但这种方案不太好在项目运行期间动态添加角色。还有一种方案就是实现FilterInvocationSecurityMetadataSource接口,在这里面根据当前访问的url返回该url所具有的全部角色。显然后者更为灵活,但每次访问一次接口都取获取全部的角色肯定性能有所损失。

下面分别实现下这两种情况:

三、数据库读取通过HttpSecurity授权

上面已经创建了role角色表,现在要做urlrole的关联,所以添加一个menu表用来存放url

CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pattern` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

menurole也都是多对多的关系,所以也需要建一个menu_role关系表:

CREATE TABLE `menu_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `menu_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

还是在表中添加一些数据:


创建MeunEntity实体类:

@Data
@TableName("menu")
public class MeunEntity 
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String pattern;

MeunMapper 继承BaseMapper

@Mapper
@Repository
public interface MeunMapper extends BaseMapper<MeunEntity> 

修改RoleMapper

@Mapper
@Repository
public interface RoleMapper extends BaseMapper<RoleEntity> 

    @Select("SELECT r.id,r.role,r.role_describe FROM user_role u,role r where u.roleid = r.id AND u.userid = #userId")
    List<RoleEntity> getAllRoleByUserId(@Param("userId") Integer userId);

    @Select("SELECT r.id,r.role,r.role_describe FROM menu_role m,role r where m.role_id = r.id AND m.menu_id = #menuId")
    List<RoleEntity> getAllRoleByMenuId(@Param("menuId") Integer menuId);


修改WebSecurityConfig

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

    @Autowired
    private UserService userService;

    @Autowired
    MeunMapper meunMapper;

    @Autowired
    RoleMapper roleMapper;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userService).passwordEncoder(password());
    

    @Bean
    PasswordEncoder password() 
        return new BCryptPasswordEncoder();
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http
                .authorizeRequests();
        List<MeunEntity> meunEntities = meunMapper.selectList(null);
        meunEntities.forEach(m -> 
            authorizeRequests.antMatchers(m.getPattern()).hasAnyAuthority(roleMapper.getAllRoleByMenuId(m.getId())
                    .stream()
                    .map(RoleEntity::getRole).toArray(String[]::new));
        );
        authorizeRequests.antMatchers("/**").fullyAuthenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
    

    @Override
    public void configure(WebSecurity web) throws Exception 
        web.ignoring().antMatchers("/register/**");
    

重启项目,然后再次访问测试接口,已经实现和上面相同的效果:

四、通过FilterInvocationSecurityMetadataSource 动态角色

上面已经实现了第一种方案,下面继续实现第二中方案,下面创建一个类实现FilterInvocationSecurityMetadataSource 接口:

@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource 

    @Autowired
    MeunMapper meunMapper;

    @Autowired
    RoleMapper roleMapper;

    //用来实现ant风格的Url匹配
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException 
        //获取当前请求的Url
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        List<MeunEntity> list = meunMapper.selectList(null);
        List<ConfigAttribute> roles = new ArrayList<>();
        list.forEach(m -> 
            if (antPathMatcher.match(m.getPattern(), requestUrl)) 
                List<ConfigAttribute> allRoleByMenuId = roleMapper.getAllRoleByMenuId(m.getId())
                        .stream()
                        .map(r -> new SecurityConfig(r.getRole()))
                        .collect(Collectors.toList());
                roles.addAll(allRoleByMenuId);
            
        );
        if (!roles.isEmpty()) 
            return roles;
        
        return SecurityConfig.createList("ROLE_LOGIN");
    

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() 
        return null;
    

    @Override
    public boolean supports(Class<?> clazz) 
        return true;
    

还需创建一个CustomAccessDecisionManager用来实现AccessDecisionManager

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager 

    @Override
    public void decide(Authentication auth, Object object, Collection<ConfigAttribute> ca) throws AccessDeniedException, InsufficientAuthenticationException 
        for (ConfigAttribute configAttribute : ca) 
        	//如果请求Url需要的角色是ROLE_LOGIN,说明当前的Url用户登录后即可访问
            if ("ROLE_LOGIN".equals(configAttribute.getAttribute()) && auth instanceof UsernamePasswordAuthenticationToken) 
                return;
            
            Collection<? extends GrantedAuthority> auths = auth.getAuthorities(); //获取登录用户具有的角色
            for (GrantedAuthority grantedAuthority : auths) 
                if (configAttribute.getAttribute().equals(grantedAuthority.getAuthority()))
                    return;
                
            
        
        throw new AccessDeniedException("权限不足");
    

    @Override
    public boolean supports(ConfigAttribute configAttribute) 
        return true;
    

    @Override
    public boolean supports(Class<?> aClass) 
        return true;
    

修改WebSecurityConfig

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

    @Autowired
    private UserService userService;

    @Autowired
    CustomAccessDecisionManager customAccessDecisionManager;

    @Autowired
    CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userService).passwordEncoder(password());
    

    @Bean
    PasswordEncoder password() 
        return new BCryptPasswordEncoder();
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() 
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) 
                        o.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        o.setAccessDecisionManager(customAccessDecisionManager);
                        return o;
                    
                )
                .antMatchers("/**").fullyAuthenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
    

    @Override
    public void configure(WebSecurity web) throws Exception 
        web.ignoring().antMatchers("/register/**");
    

    @Bean
    RoleHierarchy roleHierarchy() 
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        String hierarchy = "ROLE_admin > ROLE_user > ROLE_common";
        roleHierarchy.setHierarchy(hierarchy);
        return roleHierarchy;
    

再次测试上面的测试接口,可以发现也达到了相同的效果:

但是此时是动态角色的,我们可以创建一个新用户,给新用户一个新的角色,再给该角色赋予admin/**的权限。

创建用户adc

添加角色:

角色绑定用户:

角色绑定menu:


下面清楚浏览器的缓存,使用abc用户登录:


成功访问接口,说明动态角色权限已经生效了。


喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!

以上是关于SpringSecurity - 用户动态授权 及 动态角色权限的主要内容,如果未能解决你的问题,请参考以下文章

SpringSecurity - 用户动态认证

SpringSecurity - WebFlux环境下实现用户动态认证

SpringSecurity - WebFlux环境下动态角色权限

SpringSecurity - 整合JWT使用 Token 认证授权

2.springsecurity——授权

springsecurity 的学习授权与认证