权限篇:初探权限系统基本模型及 Spring Security 权限控制方案

图1-1 文章脑图

Spring Security 主要的功能是 认证授权,认证系列篇基本完结,接下来将进入 Spring Security 的授权系列篇。

本文主要介绍 常规权限系统的基本设计模型以及 Spring Security 的权限控制方案,话不多说,Let’s Go !!!

常规权限系统设计模型

系统应用不做权限管控,就犹如在大街上裸奔一般。

至今为止最普及的权限模型是 RBAC模型,基于角色的访问控制(Role Based Access Control)。该模型主要含有3个实体,分别为: 用户角色权限

图1-2 RBAC模型

由模型图我们可以看出,三个实体分别是:用户、角色、以及权限;并且用户和角色之间是多对多的关系,角色和权限之间也是多对多的关系。

用户:发起操作的主体,比较常规的比如:系统的普通用户,管理员等。

角色: 拥有权限集的组合,用以连接用户和权限的桥梁;比如商城系统中管理员角色则有权限进行新增商品、下架商品等操作。

权限:用户可访问的资源,权限大体上可划分为:操作权限数据权限;操作权限是指页面上的功能按钮,例如:新增、删除、修改等。数据权限是指不同的用户在同一个页面下所看到的数据是不一样的。

可能有人会不理解,为什么在 用户权限之间多添加了一层 角色的概念,不能直接将权限给到具体的用户吗?在系统规模比较小的情况下确实可以这么做,但当系统的用户上来后,就变得难以维护了。

扫描二维码关注公众号,回复: 14657617 查看本文章

比如新增一个1000个用户,需要为新用户设置查看个人信息、编辑个人信息、修改密码等权限,将权限赋给用户的操作会产生将近3000条的记录;而如果引入 角色的概念,可以把查看个人信息、编辑个人信息、修改密码设置为 角色A,给1000个新增用户授予 角色A即可,这样1000个新增的用户都拥有了所需的权限,并且只产生1000条记录。

图1-3 对比图

权限模型基本介绍完毕,那该如何将模型落地到实践中呢?

随着前后端分离架构的逐步成熟,越来越多的系统在架构选型上都采取了前后端分离的架构;那么在前后端分离的架构下,权限设计有什么需要注意的地方呢?

在前后端分离的场景下,页面的跳转统一由前端控制,后端只负责提供数据。怎么友好的告诉前端某个用户是否拥有某项操作的权限呢?这里我们对 RBAC模型中的 权限实体做了略微的调整,引入 Rest风格,调整为 资源。正如我们所知,前端页面上的操作按钮会一一映射到后端的接口上,我们只需要在 资源表中维护相关接口的 URI即可。

基于上述结论,我们可以设计出最核心的几张数据库表:用户表、用户角色关系表、角色表、角色资源关系表、资源表

图 1-4 数据库模型

建表语句如下所示(省略其他信息,只关注权限相关):

CREATE TABLE `user_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `username` varchar(255) DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `enabled` tinyint(1) DEFAULT 1 COMMENT '0不可用 1可用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COMMENT '用户表'


CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name_en` varchar(64) NOT NULL DEFAULT '' COMMENT '角色名en',
  `name_cn` varchar(64) NOT NULL DEFAULT '' COMMENT '角色名cn',
  `enabled` tinyint(1) DEFAULT 1 COMMENT '0不可用 1可用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '角色表';


CREATE TABLE `resource` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `uri` varchar(64) NOT NULL DEFAULT '' COMMENT '接口uri',
    `method` tinyint NOT NULL DEFAULT 0 COMMENT '接口请求类型 0:GET 1:POST ...',
    `enabled` tinyint(1) DEFAULT 1 COMMENT '0不可用 1可用',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '资源表';


CREATE TABLE `user_role_relation` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `user_id` int(11) NOT NULL DEFAULT 0 COMMENT '用户id',
    `role_id` int(11) NOT NULL DEFAULT 0 COMMENT '角色id',
    `enabled` tinyint(1) DEFAULT 1 COMMENT '0不可用 1可用',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '用户角色关系表';


CREATE TABLE `role_resource_relation` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `role_id` int(11) NOT NULL DEFAULT 0 COMMENT '用户id',
    `resource_id` int(11) NOT NULL DEFAULT 0 COMMENT '角色id',
    `enabled` tinyint(1) DEFAULT 1 COMMENT '0不可用 1可用',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '角色资源关系表';

Spring Security权限控制方案

Spring Security 提供了默认的权限控制功能,需要预先分配给用户特定的权限(还记得我们之前在介绍 Spring Security 的 UserDetails接口吗?接口中有一个方法 Collection<? extends GrantedAuthority> getAuthorities()用以返回用户所拥有的权限),并指定各个 URL 执行所要求的权限。当用户在请求某资源路径时,Spring Security 会检查用户所拥有的权限是否可以访问该资源路径。

Spring Security 的权限控制粒度主要有 URL 级和方法级;主要是结合 SpEL 表达式来使用,如果表达式返回 true 则表示需要对应的权限,如果返回 false 则表示不需要相关权限;同时 Spring Security 也提供了一些注解搭配使用。

URL 级权限控制

URL 级的权限控制主要是通过在配置文件中进行配置,这种方式之前有看过,直接上代码 ~~

@Configuration
public class AuthorityConfig extends WebSecurityConfigurerAdapter {
    
    

    /**
     * 权限控制
     * @param http
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.authorizeRequests()
                // 注意: spring security 的角色role前缀为 ROLE_
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasAnyRole("admin", "user")
                .anyRequest().authenticated() ;
    }
}

这段代码理解起来很简单,意思是:访问 /admin/**路径需要 admin角色权限, 访问 /user/**路径需要 admin或者 user的角色权限。

方法级权限控制

方法级的权限控制即根据权限来控制用户是否有权限访问某个方法,Spring Security 提供了几个注解搭配 SpEL 来使用。

首先需要在启动类配置 @EnableGlobalMethodSecurity(prePostEnabled = true)来启动方法级别的权限控制,然后在具体需要进行权限控制的方法上使用注解搭配生效。

  • @PreAuthorize: 方法执行前进行权限检查

  • @Secured: 方法执行前进行权限检查

  • @PostAuthorize:方法执行后进行权限检查

启动类配置生效方法级权限控制

@SpringBootApplication
@MapperScan("com.kylin.demo.security.dao")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(SecurityApplication.class, args);
    }
}

/**
 * test
 * @author 小奇
 */
@Service
public class TestService {
    
    

    /**
     * 需要ROLE_ADMIN权限才可访问
     *
     * @return String
     */
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String admin() {
    
    
        return "has admin role";
    }

    /**
     * 允许任何正常登录用户访问,无需权限
     * @return String
     */
    @PreAuthorize("permitAll()")
    public String permitAll() {
    
    
        return "允许任何正常用户访问,无需权限";
    }

}

赋予权限的时机

我们了解了 Spring Security 提供的权限控制的方案,但是似乎忘记了一点: 什么时候给予用户各种权限呢?或者应该表述为: 什么时候给予用户各种拥有权限的角色呢?

还记得我们之前介绍过的 UserDetailsUserDetailsService接口吗? UserDetails接口中有一个方法 Collection<? extends GrantedAuthority> getAuthorities()用以获取当前用户的所有拥有的权限;而 UserDetailsService接口中的 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException方法,根据用户名加载用户信息。

我们可以在 LoadUserByUsername的时候,将用户所拥有的权限信息一并填充返回。

UserInfo.java

@Setter
public class UserInfo implements UserDetails {
    
    

    private String username;

    private String password;

    /**
     * 角色集合
     */
    private List<Role> roleList;

    /**
     * UserDetails的接口
     * 用户权限集,默认需要添加ROLE_作为前缀
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        List<SimpleGrantedAuthority> authorityList = new ArrayList<>(roleList.size());
        // 注: Spring Security 角色以 ROLE_ 作为前缀
        roleList.forEach(role -> authorityList.add(new SimpleGrantedAuthority(role.getNameEn())));
        return authorityList;
    }

    // 省略其他方法...
}

UserInfo类实现 UserDetails接口,并且重写 getAuthorities()方法,用以设置当前用户所拥有的权限集。

/**
 * 用户信息service模块
 *
 * UserDetailsService接口为SpringSecurity内置接口,内部有方法:
 * UserDetails loadUserByUsername(String username):如名所得 根据用户名加载用户
 * 该方法主要是在:DaoAuthenticationProvider中被调用,获取用户的信息
 *
 * @author 小奇
 */
@Slf4j
@Service
public class UserInfoServiceImpl implements UserDetailsService, UserInfoService {
    
    


    @Autowired
    private PasswordEncoder passwordEncoder;

    @Resource
    private UserInfoDAO userInfoDAO;

    /**
     * 角色service
     */
    @Autowired
    private RoleService roleService;

    /**
     * 用户角色关系 service
     */
    @Autowired
    private UserRoleRelationService userRoleRelationService;

    /**
     * 根据用户名查找用户 & 设置用户权限集
     *
     * @param username
     * @return UserDetails
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        Optional<UserInfo> userInfoOpt = Optional.ofNullable(userInfoDAO.loadUserByUsername(username));
        UserInfo user = userInfoOpt.orElseThrow(() ->
                new UsernameNotFoundException("can't not load user by username"));
        log.info("根据用户名:{}查询用户成功", user.getUsername());

        // 设置用户的权限集
        List<UserRoleRelation> relationList = userRoleRelationService.queryByUserId(user.getId());
        List<Role> roleList = new ArrayList<>(relationList.size());
        relationList.forEach(relation -> {
    
    
            Role role = roleService.queryById(relation.getRoleId());
            if (!Objects.isNull(role)) {
    
    
                roleList.add(role);
            }
        });
        user.setRoleList(roleList);
        return user;
    }

}

自定义UserInfoServiceImpl类实现 UserDetailsService接口,重写 loadUserByUsername方法,并且实现装载用户的权限集。

总结

本文主要介绍 Spring Security 的权限控制方案以及基本的权限系统设计模型;Spring Security 的权限控制粒度有 URL级 和方法级,主要通过 SpEL 表达式来实现。至今为止最为普及的权限模型是RBAC模型,核心主体模块有 用户角色权限

文章到这就结束拉,欢迎大家扫码关注小奇公众号~
请添加图片描述

猜你喜欢

转载自blog.csdn.net/weixin_46920376/article/details/109555720