目录
1、前言
2、实现思路
3、编码实战
3.1、准备
3.2、数据库表准备
3.3、自定义注解
3.4、拦截器
3.5、接口使用
3.6、测试
3.7、结论
4、结束语
1、前言
学过Spring Security的小伙伴都知道,SpringBoot项目可以集成Spring Security做权限校验框架,然后在Controller接口上直接使用@PreAuthorize注解来校验权限,但是如果我不想引入像Security、Shiro等第三方框架,也要实现权限校验的效果,该怎么做呢?
接下来就给大家介绍一种方案:拦截器+自定义注解做基于RBAC模型的权限校验
2、实现思路
- 首先数据库需要有基于RBAC模型的表,我们这里用最简单的5张表做示例:用户表、角色表、权限表、用户角色关联表、角色权限关联表
- 用户每次登录成功后,都需要联表查询该用户拥有的权限集合字符串,加密到token串中(此处可以使用JWT),返回给前端,前端存储到浏览器中,以后每次请求后台接口时,都将此token取出,放到请求头信息中,带到后台
- 然后新建自定义注解,该注解用于接口上面,表示该接口需要校验访问者的权限
- 新建拦截器,对所有请求方法的请求进行拦截,然后判断是否有自定义注解,如果有的话,取出权限字符串,和请求头中token的权限字符串集合进行比对,如果包含其中,则说明有权限访问该方法,如果不包含其中,就说明该用户没有权限访问该方法
3、编码实战
3.1、准备
这里我准备了一个SpringBoot基础环境代码,我会在此基础上进行集成,代码我已上传,地址如下:
SpringBootBase: SpringBoot基础项目框架
基础薄弱的同学,可以下载下来,跟我下面的步骤一步一步走,就可以出来效果了,再集成到你自己的项目中
3.2、数据库表准备
5张表准备:
用户表:
CREATE TABLE `sys_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID',
`position_id` varchar(5) DEFAULT NULL COMMENT '职位ID',
`class_id` bigint(20) DEFAULT NULL COMMENT '班级ID',
`user_name` varchar(30) NOT NULL COMMENT '用户账号',
`nick_name` varchar(30) NOT NULL COMMENT '用户昵称',
`user_type` varchar(2) DEFAULT '00' COMMENT '用户类型(00系统用户)',
`email` varchar(50) DEFAULT '' COMMENT '用户邮箱',
`phonenumber` varchar(11) DEFAULT '' COMMENT '手机号码',
`sex` char(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
`avatar` varchar(300) DEFAULT '' COMMENT '头像地址',
`password` varchar(100) DEFAULT '' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
`login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP',
`login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`user_id`),
UNIQUE KEY `un_user_name` (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息表';
角色表:
CREATE TABLE `sys_role` (
`role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(30) NOT NULL COMMENT '角色名称',
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
`role_sort` int(4) DEFAULT NULL COMMENT '显示顺序',
`status` char(1) NOT NULL DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色信息表';
权限表:
CREATE TABLE `sys_menu` (
`menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` varchar(50) NOT NULL COMMENT '菜单名称',
`parent_id` bigint(20) DEFAULT '0' COMMENT '父菜单ID',
`order_num` int(4) DEFAULT '0' COMMENT '显示顺序',
`path` varchar(200) DEFAULT '' COMMENT 'menu_id全路径',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`query` varchar(255) DEFAULT NULL COMMENT '路由参数',
`is_frame` int(1) DEFAULT '1' COMMENT '是否为外链(0是 1否)',
`is_cache` int(1) DEFAULT '0' COMMENT '是否缓存(0缓存 1不缓存)',
`menu_type` char(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT '' COMMENT '备注',
PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='菜单权限表';
用户角色关联表:
CREATE TABLE `sys_user_role` (
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户和角色关联表';
角色权限关联表:
CREATE TABLE `sys_role_menu` (
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
`menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色和菜单关联表';
3表联查,传入用户ID就可以查询出该用户拥有哪些权限字符串了,SQL如下:
select * from sys_menu t1
left join sys_role_menu t2 on t1.menu_id = t2.menu_id
left join sys_user_role t3 on t2.role_id = t3.role_id
where t3.user_id = #{userId}
3.3、自定义注解
新建一个自定义注解:
package org.wujiangbo.annotation;
import java.lang.annotation.*;
/**
* 自定义注解校验权限
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermission {
/**
* 权限字符串
*/
String per() default "";
}
3.4、拦截器
新建拦截器,代码如下:
package org.wujiangbo.interceptor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.wujiangbo.annotation.CheckPermission;
import org.wujiangbo.exception.MyException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* 权限校验拦截器
*/
@Component
public class CheckPermissionInterceptor implements HandlerInterceptor {
/**
* 前置处理
* 该方法将在请求处理之前进行调用
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
if (!(handler instanceof HandlerMethod)) {
//说明拦截到的请求,不是请求方法的,那就直接放行
return true;
}
//指定到这里,说明请求的是方法
HandlerMethod handlerMethod = (HandlerMethod) handler;
//获取方法对象
Method method = handlerMethod.getMethod();
//获取方法上面的 CheckPermission 注解对象
CheckPermission methodAnnotation = method.getAnnotation(CheckPermission.class);
if (methodAnnotation != null) {
//获取权限字符串
String per = methodAnnotation.per();
if(StringUtils.isNotBlank(per)){
/**
* 说明需要校验权限,拥有该权限字符串,才能访问该方法
* 此时,需要从request头信息中拿到token,然后解密token,得到用户的权限集合,然后再做判断即可
*/
//这里我们造点测试数据,假设从token中解析出当前登录用户的权限集合如下
List<String> userPermissionList = new ArrayList<>();
userPermissionList.add("user:addUser");
userPermissionList.add("user:deleteUser");
userPermissionList.add("user:updateUser");
userPermissionList.add("user:pageList");
//开始判断
if(!userPermissionList.contains(per)){
throw new MyException("您暂无权限进行此操作");
}
}
}
//必须返回true,否则会拦截掉所有请求,不会执行controller方法中的内容了
return true;
}
}
需要将上面的拦截器类加入到web环境中,新建下面配置类:
package org.wujiangbo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.wujiangbo.interceptor.CheckPermissionInterceptor;
/**
* 统一拦截器配置类
*/
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Autowired
private CheckPermissionInterceptor checkPermissionInterceptor;
//添加自定义拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(checkPermissionInterceptor).addPathPatterns("/**");
super.addInterceptors(registry);
}
}
注意:
我这里的拦截器中,没有对token进行解析,只是模拟了一下,实际项目中,token肯定是密文,无法直接使用的,需要先解析出用户拥有的权限集合字符串
3.5、接口使用
下面我们再controller接口中就可以直接使用了,如下:
package org.wujiangbo.controller;
import lombok.extern.slf4j.Slf4j;
import org.wujiangbo.annotation.CheckPermission;
import org.wujiangbo.result.JSONResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试接口类
*/
@RestController
@Slf4j
public class TestController {
//用户新增接口
@GetMapping("/user/addUser")
@CheckPermission(per = "user:addUser") //表示访问者需要具备 user:addUser 权限才能访问该接口
public JSONResult addUser(){
return JSONResult.success("addUser success");
}
//部门新增接口
@GetMapping("/dept/addDept")
@CheckPermission(per = "dept:addDept") //表示访问者需要具备 dept:addDept 权限才能访问该接口
public JSONResult addDept(){
return JSONResult.success("addDept success");
}
}
3.6、测试
用postman工具测试,先访问新增用户接口:http://localhost:8001/user/addUser
再访问新增部门接口:http://localhost:8001/dept/addDept
3.7、结论
从测试结果可以看出,确实可以做到接口的权限校验,以后使用的话,可以直接在controller加个注解就可以了,非常的方便
4、结束语
- 这样的权限校验在实际工作中也有应用场景,希望大家都可以掌握哦
- 大家如果还有任何疑问,可以留言,我会第一时间回复的