ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-01-09 17:33:15  阅读:178  来源: 互联网

标签:动态 角色 admin SpringSecurity Override role id 权限 public


一、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用户登录:
在这里插入图片描述
在这里插入图片描述
成功访问接口,说明动态角色权限已经生效了。

在这里插入图片描述
喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!

标签:动态,角色,admin,SpringSecurity,Override,role,id,权限,public
来源: https://blog.csdn.net/qq_43692950/article/details/122394611

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有