Spring Security 主要的功能是 认证
和 授权
,认证系列篇基本完结,接下来将进入 Spring Security 的授权系列篇。
本文主要介绍 常规权限系统的基本设计模型以及 Spring Security 的权限控制方案,话不多说,Let’s Go !!!
常规权限系统设计模型
系统应用不做权限管控,就犹如在大街上裸奔一般。
至今为止最普及的权限模型是 RBAC模型
,基于角色的访问控制(Role Based Access Control)。该模型主要含有3个实体,分别为: 用户
、角色
、权限
。
由模型图我们可以看出,三个实体分别是:用户、角色、以及权限;并且用户和角色之间是多对多的关系,角色和权限之间也是多对多的关系。
用户:发起操作的主体,比较常规的比如:系统的普通用户,管理员等。
角色: 拥有权限集的组合,用以连接用户和权限的桥梁;比如商城系统中管理员角色则有权限进行新增商品、下架商品等操作。
权限:用户可访问的资源,权限大体上可划分为:操作权限
、 数据权限
;操作权限是指页面上的功能按钮,例如:新增、删除、修改等。数据权限是指不同的用户在同一个页面下所看到的数据是不一样的。
可能有人会不理解,为什么在 用户
和 权限
之间多添加了一层 角色
的概念,不能直接将权限给到具体的用户吗?在系统规模比较小的情况下确实可以这么做,但当系统的用户上来后,就变得难以维护了。
比如新增一个1000个用户,需要为新用户设置查看个人信息、编辑个人信息、修改密码等权限,将权限赋给用户的操作会产生将近3000条的记录;而如果引入 角色
的概念,可以把查看个人信息、编辑个人信息、修改密码设置为 角色A
,给1000个新增用户授予 角色A
即可,这样1000个新增的用户都拥有了所需的权限,并且只产生1000条记录。
权限模型基本介绍完毕,那该如何将模型落地到实践中呢?
随着前后端分离架构的逐步成熟,越来越多的系统在架构选型上都采取了前后端分离的架构;那么在前后端分离的架构下,权限设计有什么需要注意的地方呢?
在前后端分离的场景下,页面的跳转统一由前端控制,后端只负责提供数据。怎么友好的告诉前端某个用户是否拥有某项操作的权限呢?这里我们对 RBAC模型
中的 权限
实体做了略微的调整,引入 Rest
风格,调整为 资源
。正如我们所知,前端页面上的操作按钮会一一映射到后端的接口上,我们只需要在 资源表
中维护相关接口的 URI
即可。
基于上述结论,我们可以设计出最核心的几张数据库表:用户表、用户角色关系表、角色表、角色资源关系表、资源表
建表语句如下所示(省略其他信息,只关注权限相关):
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 提供的权限控制的方案,但是似乎忘记了一点: 什么时候给予用户各种权限呢?或者应该表述为: 什么时候给予用户各种拥有权限的角色呢?
还记得我们之前介绍过的 UserDetails
和 UserDetailsService
接口吗? 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模型,核心主体模块有 用户
、角色
、权限
。
文章到这就结束拉,欢迎大家扫码关注小奇公众号~