聊一个 GitHub 上开源的 RBAC 权限管理系统,很6!
松哥原创的 Spring Boot 视频教程已经杀青,感兴趣的小伙伴戳这里-->Spring Boot+Vue+微人事视频教程
前两天写了一篇文章,和大家大概聊了聊 RBAC 权限管理系统:
RBAC 理论其实并不难,相信大家看完文章都能明白。但是如何将理论转为实践代码,这还需要一点功力。
大家可能在网上也会见到众多自称 RBAC 的权限管理系统,这些系统有的确实是 RBAC,有的虽然自称 RBAC,其实并不是。
为了帮助小伙伴掌握 RBAC 权限管理模型,松哥经过大量的摸排,找到了一些开源的 RBAC 权限管理系统,我想写几篇文章和大家分析一下这些系统的实现,搞懂了别人的实现逻辑,自己再去做就非常容易了。
今天要和大家介绍的是一个来自 GitHub 上的项目,叫做 wetech-admin。
1.项目介绍
wetech-admin 是基于 Spring Boot 2.0+Mybatis+Vue 的轻量级后台管理系统,适用于中小型项目的管理后台,支持按钮级别的权限控制,系统具有最基本的用户管理、角色管理、权限管理等通用性功能,企业或个人可直接在此基础上进行开发,扩展,添加各自的需求和业务功能!
- 项目地址:https://github.com/cjbi/wetech-admin
松哥看了下这个项目,基本上是满足 RBAC0 模型,功能上比较完整,唯一有点遗憾的是,它是用 Shiro 开发的,而不是 Spring Security。
不过这个并不影响我们学习 RBAC,RBAC 作为一种权限模型是语言无关的,无论你用什么语言都可以实现 RBAC,更不用说同一种语言的不同框架了。
只要我们把这个 RBAC 搞懂,用 Spring Security 实现 RBAC 自然也不在话下。其实 Spring Security 实现 RBAC 所需要的技术,在松哥本系列的文章中基本上都已经介绍过了,现在只要大家搞懂了模型,就能很快实现。
我们来看看这个项目的几个效果图:
可以看到,用户列表、角色列表、权限列表,三个菜单涉及到了权限管理中三张表。
用户管理
角色管理
权限管理
2.项目部署
- 通过 git 下载源码
- 创建数据库 wetech_admin,数据库编码为UTF-8
- 依次执行 src/main/resources/schema.sql 和 dsrc/main/resources/data.sql 文件,初始化数据
- 修改 application-dev.properties 文件,更新 MySQL 账号和密码
- 启动服务,账号密码:admin/123456 或者 guest/123456
这是官方给出的部署步骤,松哥再详细说两句。
首先项目下载下来后,包含两个目录:
- wetech-admin-server:服务端项目,但是由于存在一个 parent,所以不能直接打开 wetech-admin-server,从它的上级目录处打开。
- wetech-admin-ui:前端项目,需要首先执行 npm install 安装所需依赖,然后执行 npm run serve 启动前端项目。
没搞过前后端分离的小伙伴注意,需要前后端同时启动才能访问。
松哥在启动前端时遇到一个问题,就是一直报这个错误:
Cannot read property 'range' of null
检查过后,发现是 eslint 作怪,我就把 eslint 整个关闭了,然后就清净了。
因为我们这里主要是看 RBAC,前端不做过多涉及,项目跑起来就行。如果大家也遇到这个错误,可以按照松哥的办法试下。
另外需要注意,他这项目用了 Lombok,随意 idea 上需要安装一下 Lombok 插件。
3.数据库分析
这个项目数据库只有三张表:
权限表、角色表以及用户表。
他没有设计关联表,而是将关联数据做成了一个字段。
比如角色表中有一个 permission_ids 字段,表示该角色对应的权限:
用户表中有一个 role_ids 字段,表示用户对应的角色:
这样设计的好处很明显,就是查询的时候非常方便,不用做一对多查询,SQL 好写并且执行效率高。
缺点就是数据更新不太方便,不过一般来说,权限系统数据更新频率较低,大部分都是查询,所以我觉得这样设计也能接受。
我们来看下这里的权限表:
type 表示资源的类型,分为两种:菜单和按钮。
其中 1 表示菜单,2 表示按钮。如果是菜单,则需要有一个动态路由字符串,也就是 config 字段。该字段返回到前端之后,前端就会动态加载出对应的页面(这点和松哥 vhr (https://github.com/lenve/vhr)是一致的)。
角色表和用户表都比较简单,我就不再多说。
4.代码分析
普通的 CURD 代码我就不说了,和权限管理相关的代码在 tech.wetech.admin.shiro 包里边。登录在 LoginController 里边。
先来说这个登录,具体的操作是在 tech.wetech.admin.service.impl.UserServiceImpl#login 方法中:
@Override
public UserTokenDTO login(LoginDTO loginDTO) {
User user = userMapper.selectByUsername(loginDTO.getUsername());
if (user == null) {
throw new BusinessException(CommonResultStatus.LOGIN_ERROR, "用户不存在");
}
if (!passwordHelper.verifyPassword(user, loginDTO.getPassword())) {
throw new BusinessException(CommonResultStatus.LOGIN_ERROR, "密码错误");
}
UserTokenDTO userInfoDTO = new UserTokenDTO();
userInfoDTO.setUsername(user.getUsername());
userInfoDTO.setToken(JwtUtil.sign(user.getUsername(), String.valueOf(System.currentTimeMillis())));
return userInfoDTO;
}
这就是纯手工登录,登录成功后,生成一个 jwt 字符串返回给前端,以后前端每次请求,都需要携带上这个 jwt 字符串来。
tech.wetech.admin.shiro.JwtFilter 是专门处理认证请求的过滤器,所有需要认证的请求,都会经过该过滤器。这个过滤器所做的事情,就是从所有的请求头中提取出 Access-Token(就是登录成功后返回的那个 jwt 令牌),然后从令牌中解析出用户名,调用 JwtRealm 完成校验。
这是 JwtFilter:
@Slf4j
public class JwtFilter extends AccessControlFilter {
protected static final String AUTHORIZATION_HEADER = "Access-Token";
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(AUTHORIZATION_HEADER);
return authorization != null;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
if (isLoginAttempt(request, response)) {
//生成jwt token
JwtToken token = new JwtToken(req.getHeader(AUTHORIZATION_HEADER));
//委托给Realm进行验证
try {
getSubject(request, response).login(token);
return true;
} catch (Exception e) {
}
}
onLoginFail(response);
return false;
}
/**
* 登录失败时默认返回401状态码
*
* @param response
* @throws IOException
*/
private void onLoginFail(ServletResponse response) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.getWriter().write(JSONUtil.toJSONString(Result.failure(CommonResultStatus.LOGIN_ERROR)));
}
}
JwtRealm 如下:
@Slf4j
public class JwtRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = principals.toString();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(userService.queryRoles(username));
authorizationInfo.setStringPermissions(userService.queryPermissions(username));
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String credentials = (String) token.getCredentials();
String username = null;
try {
boolean verify = JwtUtil.verify(credentials);
if (!verify) {
throw new AuthenticationException("Token校验不正确");
}
username = JwtUtil.getClaim(credentials, JwtUtil.ACCOUNT);
} catch (Exception e) {
log.error("Token校验不正确:", e);
throw new AuthenticationException(e.getMessage());
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,不设置则使用默认的SimpleCredentialsMatcher
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username, //用户名
credentials, //凭证
getName() //realm name
);
return authenticationInfo;
}
}
认证逻辑松哥刚才已经说过了,就不再赘述。
授权逻辑就是根据用户名查询出用户角色,根据用户角色查询出用户权限,将角色和权限分别赋值给 SimpleAuthorizationInfo 对象即可。
最后在 ShiroConfig 中还有一些常规配置,我就不再多说。
权限的控制主要是在各个接口上添加 @RequiresPermissions 注解实现的。如 PermissionController、RoleController 以及 UserController。
5.小结
整体上来说,这个项目并没有太多难度。还是松哥一开始所说的,RBAC 只是一种权限设计模型,模型本身很好理解,涉及到的技术也都很简单。无论是 Shiro 还是 Spring Security,实现 RBAC 虽然有一些差别,但是用的都是最基本的技术点。
好啦,今天就和小伙伴们聊这么多,感兴趣的小伙伴可以看看哦~如果你有好的 RBAC 项目也可以留言告诉松哥,我抽空解析一波。
- java学习:字符串比较“==”与“equals”的差异及与c#的区别
- 纯C语言程序员写的编程新手入门基础小游戏之最炫酷推箱子
- Oracle BIEE (Business Intelligence) 11g 11.1.1.6.0 学习(2)RPD资料档案库创建
- Oracle BIEE (Business Intelligence) 11g 11.1.1.6.0 学习(3)创建一个简单的分析
- 请查收!这里有一封信鸽传给您的信
- Oracle BIEE (Business Intelligence) 11g 11.1.1.6.0 学习(4)创建多维钻取分析
- silverlight:RadMaskedTextBox设置MaskType="Numeric"及Mask="n"时的一个bug
- 微信里面最神秘的功能,你知道吗?
- 以大数据之名,变身!——In big data we trust
- 90%家长都不知道关于少儿编程的这些疑题!
- 常用SQL语句和语法汇总
- Python学习笔记1——斐波那契数列
- 视觉传感器几大技术要点详解!
- Spark之搜狗日志查询实战
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Python 进阶(一):多线程
- Python 进阶(二):多进程
- MySQL information_schema详解 EVENTS
- Python 进阶(三):邮件的发送与收取
- MySQL information_schema详解 FILES
- MySQL information_schema详解 GLOBAL_STATUS和SESSION_STATUS
- Python 进阶(四):数据库操作之 MySQL
- Python 进阶(五):数据库操作之 SQLite
- Python 进阶(六): Excel 基本操作
- 基于Rust-vmm实现Kubernetes运行时
- 云开发如何为腾讯游戏年度发布会保驾护航?
- 跨端方案的三大困境
- Python 进阶(七): Word 基本操作
- Python 进阶(八):XML 基本操作
- Python 进阶(九):JSON 基本操作