API 权限管理 之基于Aop 实现用户的 URL权限管理

一、数据库及表关系设计

对本文感兴趣的可以参考源码,欢迎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权限删除( 手动添加,修改权限)

在这里插入图片描述
感谢观看 !!!

发布了186 篇原创文章 · 获赞 23 · 访问量 6万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 书香水墨 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览