分享下最近写的一个权限设计方案,目前功能比较初步,大家可以理解思路后在此基础上进行扩展,希望大家可以多多提提意见哦。感谢此方案是基于目前最流行的RBAC模型(Role-Based Access Control
),就是权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。
知道模型后大致就是有三个表和两个关联表咯,(用户,角色,权限,用户角色,角色权限)。
-- ----------------------------
-- Table structure for sys_permission 权限表
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`id` bigint(20) NOT NULL COMMENT '主键',
`permission_name` varchar(255) NOT NULL COMMENT '权限名称',
`permission_remark` varchar(255) DEFAULT NULL COMMENT '权限备注',
`url` varchar(255) DEFAULT NULL COMMENT '菜单URL',
`perms` varchar(255) DEFAULT NULL COMMENT '按钮类名方法名',
`type` int(2) NOT NULL COMMENT '菜单/按钮',
`p_id` bigint(20) DEFAULT NULL COMMENT '菜单父级',
`create_user` varchar(255) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`modify_user` varchar(255) NOT NULL COMMENT '修改人',
`modify_time` datetime NOT NULL COMMENT '修改时间',
`flag` int(2) NOT NULL COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of sys_permission
-- ----------------------------
BEGIN;
INSERT INTO `sys_permission` VALUES (1, '根据用户ID获取用户', '根据用户ID获取用户信息', '-1', 'UserController:getUser', 0, -1, 'admin', '2020-11-10 09:27:15', 'admin', '2020-11-10 09:27:10', 0);
INSERT INTO `sys_permission` VALUES (2, '用户操作', '用户操作页面', '/sys/user/index', '-1', 1, -1, 'admin', '2020-11-10 10:21:29', 'admin', '2020-11-10 10:21:33', 0);
INSERT INTO `sys_permission` VALUES (3, '权限操作', '用户权限操作页面', '/sys/user/perssmission/index', '-1', 1, 2, 'admin', '2020-11-10 10:25:10', 'admin', '2020-11-10 10:25:05', 0);
INSERT INTO `sys_permission` VALUES (4, '关单操作', '报关单操作页面', '/bill/operate/index', '-1', 1, -1, 'admin', '2020-11-10 10:27:00', 'admin', '2020-11-10 10:26:55', 0);
COMMIT;
-- ----------------------------
-- Table structure for sys_role 角色表
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_name` varchar(255) NOT NULL COMMENT '角色名称',
`role_remark` varchar(255) DEFAULT NULL COMMENT '角色描述',
`create_time` datetime NOT NULL COMMENT '创建时间',
`create_user` varchar(255) NOT NULL COMMENT '创建人',
`modify_time` datetime NOT NULL COMMENT '修改时间',
`modify_user` varchar(255) NOT NULL COMMENT '修改人',
`flag` int(2) NOT NULL COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_role` VALUES (1, '管理员', '管理员', '2020-11-10 09:24:44', 'admin', '2020-11-10 09:24:48', 'admin', 0);
COMMIT;
-- ----------------------------
-- Table structure for sys_role_permission 角色权限表
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`id` bigint(20) NOT NULL COMMENT '主键',
`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
`permission_id` bigint(20) DEFAULT NULL COMMENT '权限ID',
`create_user` varchar(255) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`modify_user` varchar(255) DEFAULT NULL COMMENT '修改人',
`modify_time` datetime DEFAULT NULL COMMENT '修改时间',
`flag` int(2) DEFAULT NULL COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
BEGIN;
INSERT INTO `sys_role_permission` VALUES (1, 1, 1, 'admin', '2020-11-10 09:28:45', 'admin', '2020-11-10 09:28:49', 0);
INSERT INTO `sys_role_permission` VALUES (2, 1, 2, 'admin', '2020-11-10 10:52:45', 'admin', '2020-11-10 10:52:55', 0);
INSERT INTO `sys_role_permission` VALUES (3, 1, 3, 'admin', '2020-11-10 10:52:45', 'admin', '2020-11-10 10:52:55', 0);
INSERT INTO `sys_role_permission` VALUES (4, 1, 4, 'admin', '2020-11-10 10:52:45', 'admin', '2020-11-10 10:52:55', 0);
COMMIT;
-- ----------------------------
-- Table structure for sys_user 用户表
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(255) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '密码',
`create_time` datetime NOT NULL COMMENT '创建人',
`create_user` varchar(255) NOT NULL COMMENT '创建时间',
`modify_time` datetime NOT NULL COMMENT '修改时间',
`modify_user` varchar(255) NOT NULL COMMENT '修改人',
`flag` int(2) NOT NULL COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
BEGIN;
INSERT INTO `sys_user` VALUES (1, 'chendd', 'chendd', '2020-11-10 09:24:01', 'admin', '2020-11-10 09:24:07', 'admin', 0);
COMMIT;
-- ----------------------------
-- Table structure for sys_user_role 用户角色表
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` bigint(20) NOT NULL COMMENT '主键',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
`create_user` varchar(255) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`modify_user` varchar(255) DEFAULT NULL COMMENT '修改人',
`modify_time` datetime DEFAULT NULL COMMENT '修改时间',
`flag` int(2) DEFAULT NULL COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_user_role` VALUES (1, 1, 1, 'admin', '2020-11-10 09:28:00', 'admin', '2020-11-10 09:28:04', 0);
COMMIT;
因为这里主要就是做下基本的权限功能所以表里边的字段可能不是很晚上,需要可以自己添加进去。
下边是项目结构。
这里只把重点你的几个文件代码做下解释,其他的就不赘述了,源码会在后边提交到Git提供下载。可以自己看看哦。
/**
* 自定义注解,作用在controller方法上,标记的方法需要进行权限的判断。
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface authorAnnotation {
}
这个注解主要是作用在controller方法上,那个方法有这个注解就需要进行配置权限在权限表,给用户授权,因为只有标记了这个注解就会进行权限的判断。
@Component
@Aspect
public class PermissionAopAdvice {
@Resource
public IUserService userService;
/**
* 标记有authorAnnotation注解的需要进去下边的aop - around环绕
*/
@Pointcut("@annotation(com.donny.erp.annotation.authorAnnotation)")
public void permissionAccess(){}
/**
* 使用环绕来对请求增强,因为类名和方法名是唯一的,所以借助这个特性来判断权限。
* 首先表里记录了该角色的所有拥有的权限的类名:方法名。然后在每次请求如果方法上注解那就判断请求的方法的类名方法名是否在表中存在
* 如果存在则表示有权限。没有则没有权限返回给前台提示没有权限。
*
* 这里说下为什么用Around而不是用Before,因为环绕是可以处理结果返回给前台。
* 前置加强只能判断有无权限并不能告知把结果返回给前台。
*
* 这里的返回值是Object,其实是请求的controller方法的返回值。所以在没有权限的时候需要包装成和controller方法的返回值一样的对象
* 这里是采用了一个全局接收异常ControllerAdvice来包装,就是抛出GlobalException异常会被GlobalExceptionHandler接收来进行处理
* 包装成ResponseData返回。以为所有请求都被统一返回成ResponseData对象。ResponseDataHandler就是处理统一返回请求。
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("permissionAccess()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Signature signature = joinPoint.getSignature();
String className = signature.getDeclaringType().getSimpleName();
String methodName = signature.getName();
System.out.println("类名:"+className+"-方法名:"+methodName);
String permission = className + ":" + methodName;
/**
* 获取用户所有权限
*/
List<String> permissions = userService.getPermissionsByUserId(1L);
boolean exist = permissions.contains(permission);
if (!exist){
throw new GlobalException("没有权限");
}
Object proceed = joinPoint.proceed();//执行原有的方法。
return proceed;
}
}
这个就是权限判断的核心文件了,其实标注了很多的注解,这里再主要说几个点。
- 切点定义成了一个注解,然后又把切点和Around绑定,也就是这个标注了这个注解会走到这个环绕AOP.
- 为什么要用环绕不用前置,因为前置只能判断有没有权限,不能告知用户没有权限。因为他不能控制返回。
- 为什么在没有权限要返回自定义异常GlobalException,因为这里return出去的必须和被增强的Controller方法返回的一致。所以这里是定义了一个该自定义异常的Advice,里边把返回包装成了和Controller方法返回的内容也就是ResponseData对象。
public class GlobalException extends RuntimeException {
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public GlobalException(String message) {
super(message);
}
public GlobalException(String message, Integer code) {
super(message);
this.code = code;
}
}
--------------------------------------------------------------------------
@ControllerAdvice
public class GlobalExceptionHandler {
//如果是GlobalException异常则会被拦截,包装成ResponseData返回
@ExceptionHandler(value = GlobalException.class)
@ResponseBody
public ResponseData jsonErrorHandler(HttpServletRequest req, GlobalException e) {
ResponseData r = new ResponseData();
r.setMessage(e.getMessage());
r.setCode(500);
r.setData(null);
r.setStatus(false);
return r;
}
}
这个就是自定义异常和捕捉到自定义异常包装后返回。
public class ResponseData {
private Boolean status = true;
private int code = 200;
private String message;
private Object data;
public static ResponseData ok(Object data) {
return new ResponseData(data);
}
public ResponseData(Object data) {
super();
this.data = data;
}
public ResponseData() {
super();
}
//沈略get/set方法
}
这个是返回的对象。
下边在看看怎么做到统一返回的。
//实现ResponseBodyAdvice接口,标注@ControllerAdvice
@ControllerAdvice
public class ResponseDataHandler implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
//true - 拦截
//flase - 不拦截
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
ResponseData result = body instanceof ResponseData ? (ResponseData) body :
new ResponseData(body);
return result;
}
}
每次返回的response就会被包装后返回到前端,所以在controller只用返回普通对象都会别统一包装成ResponseData返回。在回想下这里再aop中就做到了返回统一。
代码上的注解很重要哈,说的还是比较清晰了。
@Controller
@RequestMapping("/sys/user")
public class UserController {
@Resource
private IUserService userService;
@RequestMapping("/test")
@ResponseBody
@authorAnnotation
public User getUser(){
User byId = userService.getById(1);
return byId;
}
@RequestMapping("/test1")
@ResponseBody
@authorAnnotation
public User getUser1(){
User byId = userService.getById(1);
return byId;
}
@RequestMapping("/test2")
@ResponseBody
public List<String> getUser2(){
List<String> permissions = userService.getPermissionsByUserId(1L);
return permissions;
}
@RequestMapping("/test3")
@ResponseBody
public List<MenuVo> getUser3(){
List<MenuVo> menuVos = userService.getMenuByUserId(1L);
return menuVos;
}
}
-- 测试 有权限
http://localhost:8081/sys/user/test
{"status":true,"code":200,"message":null,"data":null}
-- 测试 无权限
http://localhost:8081/sys/user/test1
{"status":false,"code":500,"message":"没有权限","data":null}
-- 测试 没有注解不需要判断权限
http://localhost:8081/sys/user/test2
{"status":true,"code":200,"message":null,"data":["UserController:getUser"]}
项目地址:https://gitee.com/renai-cdd/author