一、数据库及表关系设计
对本文感兴趣的可以参考源码,欢迎start
个人后台管理项目gitee:https://gitee.com/wslxm/spring-boot-plus2
1、权限表(本篇重点)
pid = 父级,也就是方法指定类的权限id, 构建类与方法的层级关系
CREATE TABLE `t_admin_authority` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`pid` int(11) DEFAULT NULL COMMENT '当前方法指定的当前类的权限id',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '权限名',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '权限url',
`desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '权限描叙',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=84 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
2、权限和角色中间表参考(大家可自己设计)
给所有用户分配角色,通过角色来管理权限
auth_id = 权限id
role_id = 角色id
这样就可以通过获取用户关联的角色,在通过角色查询到用户的全部URL权限了
CREATE TABLE `t_admin_role_auth` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`auth_id` int(11) DEFAULT NULL COMMENT 'url权限id',
`role_id` int(11) DEFAULT NULL COMMENT '角色id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
3、前台权限管理页参考(大家可自己构建,不做说明)
通过用户的角色指定接口权限
类为父级,方法指定父Id 构建层级关系,此次方便管理
4、角色与用户管理参考,一对多关系(大家可自己构建,不做说明)
二、实现url 权限拦截
1、自定义注解
使用参考:
类上: @LdyAuthority(value = {“user”, “用户管理”})
方法上: @LdyAuthority(value = {“user:findAll”, “查询”})
/**
* TODO url 权限
*
* @date 2019/11/25 0025 7:45
* @return
*/
@Target(value = { ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface LdyAuthority {
/**
* 1,权限名,2。权限描叙
*/
String [] value();
}
参考示例1
参考示例2
2、基于Aop 实现URL 权限管理
1、拦截所有有权限注解的接口
2、Url 权限配置到数据库
3、通过 用户 --> 角色 --> 权限
aop 通知(前置通知、环绕通知都可以)
拦截范围配置参考,所有controller 接口
@Around("execution(* com.ws.ldy.*.controller.*.*(..))")
@Around("execution(* com.ws.ldy.adminconsole.controller.*.*(..))")
@Around("execution(* com.ws.ldy..*.*(..))")
@Around("execution(* com.ws.ldy.*.controller.*.*(..))")
public Object doAroundAdvice(ProceedingJoinPoint jp) throws Throwable {
// 允许所有跨域请求访问接口
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
// 获取request
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 允许所有跨域
response.setHeader("Access-Control-Allow-Origin", "*");
// 获取请求参数
Object[] args = jp.getArgs();
// URL 权限管理逻辑
}
3、URL 权限管理逻辑代码示例
逻辑代码参考:需根据当前业务定制化(在aop方法中调用即可)
List list = 所有接口的权限集合,从数据库获取(测试可以先直接定义)
// 1、判断用户是否登录,未登陆前不验证授权
UserAdmin user = (UserAdmin) request.getSession().getAttribute("user");
if (user == null) {
//用户未登录直接放行
return ResponseData.success(0);
}
// 2、获取方法, 在获取权限注解,jp=aop通知拦截获取的请求信息
Signature signature = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
// 获取请求方法
Method targetMethod = methodSignature.getMethod();
// 获取接口上的LdyAuthority注解
LdyAuthority annotation = targetMethod.getAnnotation(LdyAuthority.class);
// 3、无权限注解直接放行
if (annotation == null) {
return ResponseData.success(0);
}
// 4、查询角色当前权限并把角色权限url 权限放入map容器(此处应添加缓存)
List<AuthorityAdmin> list = authorityAdminDao.findUserIdRoleAuthority(user.getId());
Map<String, AuthorityAdmin> map = new HashMap<>();
list.forEach(item -> map.put(item.getName(), item));
// 5、获取接口权限名称,判断是否有权限
String authName = annotation.value()[0];
if (map.containsKey(authName.trim())) {
return ResponseData.success(0);
} else {
//无权限
return ResponseData.error("403", "没有访问权限");
}
4、ResponseData 返回工具类可参考
package com.ws.ldy.admincore.controller.vo;
import lombok.Data;
import java.io.Serializable;
/**
* TODO 返回的数据格式(layui 必须,及其他通用返回数据格式)
*
* @author 王松
* @WX-QQ 1720696548
* @date 2019/11/14 14:55
*/
@Data
public class ResponseData implements Serializable {
private static final long serialVersionUID = -5666504070515657048L;
private int count; // 分页查询总数据数
private String code; // 状态码
private Object data; // 数据
private String msg; // 提示信息
public ResponseData(Object data,int count, String code, String msg) {
this.count = count;
this.code = code;
this.data = data;
this.msg = msg;
}
/**
* 成功
* @param data
* @return
*/
public static ResponseData success( Object data) {
return new ResponseData( data, 0,"0", "ok");
}
/**
* 成功带分页
* @param data
* @param count
* @return
*/
public static ResponseData success( Object data,int count) {
return new ResponseData( data, count,"0", "ok");
}
/**
* 统一500 异常处理
* @return
*/
public static ResponseData error() {
return new ResponseData(null,0, "500", "is no error");
}
/**
* 自定义失败
* @param code
* @param msg
* @return
*/
public static ResponseData error(String code,String msg) {
return new ResponseData(null,0, code, msg);
}
}
三、一键生成URL 权限列表
1、反射工具类(获取包下的所有类)
package com.ws.ldy.admincore.utils;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* 反射工具类,根据包路径获取包下的所有类
* @author wangsong
* @date 2019/10/26
*/
@SuppressWarnings("all")
public class ClassUtil {
/**
* 从包package中获取所有的Class
*
* @param packageName
* @return
*/
public static List<Class<?>> getClasses(String packageName) {
List<Class<?>> classes = new ArrayList<Class<?>>(); // 第一个class类的集合
boolean recursive = true; // 是否循环迭代
String packageDirName = packageName.replace('.', '/'); // 获取包的名字 并进行替换
// 定义一个枚举的集合 并进行循环来处理这个目录下的things
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
// 循环迭代下去
while (dirs.hasMoreElements()) {
// 获取下一个元素
URL url = dirs.nextElement();
// 得到协议的名称
String protocol = url.getProtocol();
// 如果是以文件的形式保存在服务器上
if ("file".equals(protocol)) {
// 获取包的物理路径
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
// 以文件的方式扫描整个包下的文件 并添加到集合中
findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes);
} else if ("jar".equals(protocol)) {
// 如果是jar包文件
// 定义一个JarFile
JarFile jar;
try {
// 获取jar
jar = ((JarURLConnection) url.openConnection()).getJarFile();
// 从此jar包 得到一个枚举类
Enumeration<JarEntry> entries = jar.entries();
// 同样的进行循环迭代
while (entries.hasMoreElements()) {
// 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
JarEntry entry = entries.nextElement();
String name = entry.getName();
// 如果是以/开头的
if (name.charAt(0) == '/') {
// 获取后面的字符串
name = name.substring(1);
}
// 如果前半部分和定义的包名相同
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf('/');
// 如果以"/"结尾 是一个包
if (idx != -1) {
// 获取包名 把"/"替换成"."
packageName = name.substring(0, idx).replace('/', '.');
}
// 如果可以迭代下去 并且是一个包
if ((idx != -1) || recursive) {
// 如果是一个.class文件 而且不是目录
if (name.endsWith(".class") && !entry.isDirectory()) {
// 去掉后面的".class" 获取真正的类名
String className = name.substring(packageName.length() + 1, name.length() - 6);
try {
// 添加到classes
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
/**
* 以文件的形式来获取包下的所有Class
* @param packageName
* @param filePath
* @param recursive
* @param classes
*/
private static void findAndAddClassesInPackageByFile(String packageName, String packagePath,
final boolean recursive, List<Class<?>> classes) {
// TODO Auto-generated method stub
// 获取此包的目录 建立一个File
File dir = new File(packagePath);
// 如果不存在或者 也不是目录就直接返回
if (!dir.exists() || !dir.isDirectory()) {
return;
}
// 如果存在 就获取包下的所有文件 包括目录
File[] dirfiles = dir.listFiles(new FileFilter() {
// 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
public boolean accept(File file) {
return (recursive && file.isDirectory()) || (file.getName().endsWith(".class"));
}
});
// 循环所有文件
for (File file : dirfiles) {
// 如果是目录 则继续扫描
if (file.isDirectory()) {
findAndAddClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive,
classes);
} else {
// 如果是java类文件 去掉后面的.class 只留下类名
String className = file.getName().substring(0, file.getName().length() - 6);
try {
// 添加到集合中去
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
/**
* 取得某一类所在包的所有类名 不含迭代
* @param classLocation
* @param packageName
* @return
*/
public static String[] getPackageAllClassName(String classLocation, String packageName) {
// 将packageName分解
String[] packagePathSplit = packageName.split("[.]");
String realClassLocation = classLocation;
int packageLength = packagePathSplit.length;
for (int i = 0; i < packageLength; i++) {
realClassLocation = realClassLocation + File.separator + packagePathSplit[i];
}
File packeageDir = new File(realClassLocation);
if (packeageDir.isDirectory()) {
String[] allClassName = packeageDir.list();
return allClassName;
}
return null;
}
/**
* 取得某个接口下所有实现这个接口的类
* @param c
* @return
*/
public static List<Class> getAllClassByInterface(Class c) {
List<Class> returnClassList = null;
if (c.isInterface()) {
// 获取当前的包名
String packageName = c.getPackage().getName();
// 获取当前包下以及子包下所以的类
List<Class<?>> allClass = getClasses(packageName);
if (allClass != null) {
returnClassList = new ArrayList<Class>();
for (Class classes : allClass) {
// 判断是否是同一个接口
if (c.isAssignableFrom(classes)) {
// 本身不加入进去
if (!c.equals(classes)) {
returnClassList.add(classes);
}
}
}
}
}
return returnClassList;
}
}
2、生成 url 注解方法
JPA方法说明:
authorityAdminDao.save(authority); //添加一条或update 一条数据
authorityAdminDao.saveAll(athorityList); //批量添加或修改
package com.ws.ldy.adminconsole.service.impl;
import com.ws.ldy.adminconsole.dao.AuthorityAdminDao;
import com.ws.ldy.adminconsole.entity.AuthorityAdmin;
import com.ws.ldy.adminconsole.service.AuthorityAdminService;
import com.ws.ldy.admincore.annotation.LdyAuthority;
import com.ws.ldy.admincore.service.impl.BaseServiceApiImpl;
import com.ws.ldy.admincore.utils.ClassUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class AuthorityAdminServiceImpl extends BaseServiceApiImpl<AuthorityAdmin, Integer> implements AuthorityAdminService {
/**
* url权限注解扫包范围(最好指定controller层)
*/
private final static String PACKAGE_NAME = "com.ws.ldy";
/**
* 权限dao
*/
@Autowired
private AuthorityAdminDao authorityAdminDao;
/**
* TODO 生成 url 注解
*
* @return void
* @date 2019/11/25 0025 9:02
*/
@Override
public void putClass() {
// 扫描包,获得包下的所有类(使用反射工具类)
List<Class<?>> classByPackageName = ClassUtil.getClasses(PACKAGE_NAME);
// 当前当前数据库已经存在的url权限列表
List<AuthorityAdmin> list = authorityAdminDao.findAll();
Map<String, AuthorityAdmin> map = new HashMap();
list.forEach(item -> map.put(item.getName(), item));
// 需保存的权限聚合
List<AuthorityAdmin> athorityList = new ArrayList<>();
AuthorityAdmin authority = null;
RequestMapping reqClass = null;
for (Class<?> classInfo : classByPackageName) {
// 判断该类上属否存在 @LdyAuthority 注解
LdyAuthority ldyClass = classInfo.getDeclaredAnnotation(LdyAuthority.class);
if (ldyClass != null) {
System.out.println("类--》" + ldyClass.value()[0] + "-->" + ldyClass.value()[1]);
//存在修改,不存在新添加
if (map.containsKey(ldyClass.value()[0])) {
//权限名
authority = map.get(ldyClass.value()[0]);
} else {
authority = new AuthorityAdmin();
}
//本类所有方法
authority.setPid(0);
authority.setName(ldyClass.value()[0]);
authority.setDesc(ldyClass.value()[1]);
reqClass = classInfo.getDeclaredAnnotation(RequestMapping.class);
authority.setUrl(reqClass.value()[0]);
// 添加类级别权限,返回添加信息
AuthorityAdmin save = authorityAdminDao.save(authority);
// 添加方法级权限至athorityList
this.putMethods(classInfo, athorityList, map, save);
}
}
//添加所有方法级权限
authorityAdminDao.saveAll(athorityList);
}
/**
* TODO 添加指定类的所有接口权限到athorityList
*
* @param classInfo
* @param authority 方法类的权限数据
* @param athorityList 所有方法的权限集
* @param map 当前数据库存在权限
* @return void
* @date 2019/11/25 0025 9:02
*/
private void putMethods(Class<?> classInfo, List<AuthorityAdmin> athorityList, Map<String, AuthorityAdmin> map, AuthorityAdmin authority) {
Method[] methods = classInfo.getDeclaredMethods();
AuthorityAdmin auth = null;
//循环添加方法级权限
for (Method method : methods) {
LdyAuthority ldyMethod = method.getAnnotation(LdyAuthority.class);
if (ldyMethod == null) {
continue;
}
RequestMapping reqMethod = method.getDeclaredAnnotation(RequestMapping.class);
String url = reqMethod.value()[0];
String updUrl = reqMethod.value()[0];
//过滤url有{}的参数,如 /save/{type} --> /save
if (url.lastIndexOf("}") != -1) {
int index = url.lastIndexOf("/");
updUrl = url.substring(0, index);
}
//存在修改,不存在新添加
if (map.containsKey(ldyMethod.value()[0])) {
//权限名
auth = map.get(ldyMethod.value()[0]);
} else {
auth = new AuthorityAdmin();
}
auth.setPid(authority.getId()); // 类权限id(父级id)
auth.setName(ldyMethod.value()[0]); // 权限名称
auth.setDesc(ldyMethod.value()[1]); // 权限描叙
auth.setUrl(authority.getUrl() + updUrl); // 接口url
athorityList.add(auth);
System.out.println("方法--》" + ldyMethod.value()[0] + "-->" + ldyMethod.value()[1]);
}
}
}
3、controller 层接口展示
/**
* TODO 权限列表数据刷新,根据权限注解动态生成权限列表,无权限注解默认然后用户有权限访问
*
* @return java.lang.String
* @date 2019/11/25 0025 8:08
*/
@ResponseBody
@RequestMapping("/putAuthority")
public String putAuthority() {
//获得到所有类
// List<Class<?>> classByPackageName = ClassUtil.getClasses(PACKAGE_NAME);
//保存
authorityAdminServiceImpl.putClass();
return "success";
}
4、前端展示
存在会修改且id不变,不存在添加(所以只能生成,无法删除),可以在权限列表添加删除URL权限按钮,后台添加删除接口
5、URL权限删除( 手动添加,修改权限)
感谢观看 !!!