精华内容
下载资源
问答
  • Java利用Mybatis进行数据权限控制

    千次阅读 2019-06-06 18:19:18
    权限控制主要分为两块,认证(Authentication)与授权(Authorization)。认证之后确认了身份正确,业务系统就会进行授权,现在业界比较流行的模型就是RBAC(Role-Based Access Control)。RBAC包含为下面四个要素:...

    权限控制主要分为两块,认证(Authentication)与授权(Authorization)。认证之后确认了身份正确,业务系统就会进行授权,现在业界比较流行的模型就是RBAC(Role-Based Access Control)。RBAC包含为下面四个要素:用户、角色、权限、资源。用户是源头,资源是目标,用户绑定至角色,资源与权限关联,最终将角色与权限关联,就形成了比较完整灵活的权限控制模型。
    资源是最终需要控制的标的物,但是我们在一个业务系统中要将哪些元素作为待控制的资源呢?我将系统中待控制的资源分为三类:

    1. URL访问资源(接口以及网页)
    2. 界面元素资源(增删改查导入导出的按钮,重要的业务数据展示与否等)
    3. 数据资源

    现在业内普遍的实现方案实际上很粗放,就是单纯的“菜单控制”,通过菜单显示与否来达到控制权限的目的。
    我仔细分析过,现在大家做的平台分为To C和To B两种:

    1. To C一般不会有太多的复杂权限控制,甚至大部分连菜单控制都不用,全部都可以访问。
    2. To B一般都不是开放的,只要做好认证关口,能够进入系统的只有内部员工。大部分企业内部的员工互联网知识有限,而且作为内部员工不敢对系统进行破坏性的尝试。

    所以针对现在的情况,考虑成本与产出,大部分设计者也不愿意在权限上进行太多的研发力量。
    菜单和界面元素一般都是由前端编码配合存储数据实现,URL访问资源的控制也有一些框架比如SpringSecurity,Shiro。
    目前我还没有找到过数据权限控制的框架或者方法,所以自己整理了一份。

    数据权限控制原理

    数据权限控制最终的效果是会要求在同一个数据请求方法中,根据不同的权限返回不同的数据集,而且无需并且不能由研发编码控制。这样大家的第一想法应该就是AOP,拦截所有的底层方法,加入过滤条件。这样的方式兼容性较强,但是复杂程度也会更高。我们这套系统中,采用的是利用Mybatis的plugin机制,在底层SQL解析时替换增加过滤条件。
    这样一套控制机制存在很明显的优缺点,首先缺点:

    1. 适用性有限,基于底层的Mybatis。
    2. 方言有限,针对了某种数据库(我们使用Mysql),而且由于需要在底层解析处理条件所以有可能造成不同的数据库不能兼容。当然Redis和NoSQL也无法限制。

    当然,假如你现在就用Mybatis,而且数据库使用的是Mysql,这方面就没有太大影响了。

    接下来说说优点:

    1. 减少了接口数量及接口复杂度。原本针对不同的角色,可能会区分不同的接口或者在接口实现时利用流程控制逻辑来区分不同的条件。有了数据权限控制,代码中只用写基本逻辑,权限过滤由底层机制自动处理。
    2. 提高了数据权限控制的灵活性。例如原本只有主管能查本部门下组织架构/订单数据,现在新增助理角色,能够查询本部门下组织架构,不能查询订单。这样的话普通的写法就需要调整逻辑控制,使用数据权限控制的话,直接修改配置就好。

    数据权限实现

    上一节就提及了实现原理,是基于Mybatis的plugins(查看官方文档)实现。

    MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
    Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    ParameterHandler (getParameterObject, setParameters)
    ResultSetHandler (handleResultSets, handleOutputParameters)
    StatementHandler (prepare, parameterize, batch, update, query)

    Mybatis的插件机制目前比较出名的实现应该就是PageHelper项目了,在做这个实现的时候也参考了PageHelper项目的实现方式。所以权限控制插件的类命名为PermissionHelper。
    机制是依托于Mybatis的plugins机制,实际SQL处理的时候基于jsqlparser这个包。
    设计中包含两个类,一个是保存角色与权限的实体类命名为PermissionRule,一个是根据实体变更底层SQL语句的主体方法类PermissionHelper。

    首先来看下PermissionRule的结构:

    public class PermissionRule {
    
        private static final Log log = LogFactory.getLog(PermissionRule.class);
        /**
         * codeName<br>
         * 适用角色列表<br>
         * 格式如: ,RoleA,RoleB,
         */
        private String roles;
        /**
         * codeValue<br>
         * 主实体,多表联合
         * 格式如: ,SystemCode,User,
         */
        private String fromEntity;
        /**
         * codeDesc<br>
         * 过滤表达式字段, <br>
         * <code>{uid}</code>会自动替换为当前用户的userId<br>
         * <code>{me}</code> main entity 主实体名称
         * <code>{me.a}</code> main entity alias 主实体别名
         * 格式如:
         * <ul>
         * <li>userId = {uid}</li>
         * <li>(userId = {uid} AND authType > 3)</li>
         * <li>((userId = {uid} AND authType) > 3 OR (dept in (select dept from depts where manager.id = {uid})))</li>
         * </ul>
         */
        private String exps;
    
        /**
         * codeShowName<br>
         * 规则说明
         */
        private String ruleComment;
    
    }
    

    看完这个结构,基本能够理解设计的思路了。数据结构中保存如下几个字段:

    • 角色列表:需要使用此规则的角色,可以多个,使用英文逗号隔开。
    • 实体列表:对应的规则应用的实体(这里指的是表结构中的表名,可能你的实体是驼峰而数据库是蛇形,所以这里要放蛇形那个),可以多个,使用英文逗号隔开。
    • 表达式:表达式就是数据权限控制的核心了。简单的说这里的表达式就是一段SQL语句,其中设置了一些可替换值,底层会用对应运行时的变量替换对应内容,从而达到增加条件的效果。
    • 规则说明:单纯的一个说明字段。

    核心流程
    系统启动时,首先从数据库加载出所有的规则。底层利用插件机制来拦截所有的查询语句,进入查询拦截方法后,首先根据当前用户的权限列表筛选出PermissionRule列表,然后循环列表中的规则,对语句中符合实体列表的表进行条件增加,最终生成处理后的SQL语句,退出拦截器,Mybatis执行处理后SQL并返回结果。

    讲完PermissionRule,再来看看PermissionHelper,首先是头:

    @Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
            @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
    public class PermissionHelper implements Interceptor {
    }
    

    头部只是标准的Mybatis拦截器写法,注解中的Signature决定了你的代码对哪些方法拦截,update实际上针对**修改(Update)、删除(Delete)生效,query是对查询(Select)**生效。

    下面给出针对Select注入查询条件限制的完整代码:

    
        private String processSelectSql(String sql, List<PermissionRule> rules, UserDefaultZimpl principal) {
            try {
                String replaceSql = null;
                Select select = (Select) CCJSqlParserUtil.parse(sql);
                PlainSelect selectBody = (PlainSelect) select.getSelectBody();
                String mainTable = null;
                if (selectBody.getFromItem() instanceof Table) {
                    mainTable = ((Table) selectBody.getFromItem()).getName().replace("`", "");
                } else if (selectBody.getFromItem() instanceof SubSelect) {
                    replaceSql = processSelectSql(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), rules, principal);
                }
                if (!ValidUtil.isEmpty(replaceSql)) {
                    sql = sql.replace(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), replaceSql);
                }
                String mainTableAlias = mainTable;
                try {
                    mainTableAlias = selectBody.getFromItem().getAlias().getName();
                } catch (Exception e) {
                    log.debug("当前sql中, " + mainTable + " 没有设置别名");
                }
    
    
                String condExpr = null;
                PermissionRule realRuls = null;
                for (PermissionRule rule :
                        rules) {
                    for (Object roleStr :
                            principal.getRoles()) {
                        if (rule.getRoles().indexOf("," + roleStr + ",") != -1) {
                            if (rule.getFromEntity().indexOf("," + mainTable + ",") != -1) {
                                // 若主表匹配规则主体,则直接使用本规则
                                realRuls = rule;
    
                                condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", mainTable).replace("{me.a}", mainTableAlias);
                                if (selectBody.getWhere() == null) {
                                    selectBody.setWhere(CCJSqlParserUtil.parseCondExpression(condExpr));
                                } else {
                                    AndExpression and = new AndExpression(selectBody.getWhere(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                    selectBody.setWhere(and);
                                }
                            }
    
                            try {
                                String joinTable = null;
                                String joinTableAlias = null;
                                for (Join j :
                                        selectBody.getJoins()) {
                                    if (rule.getFromEntity().indexOf("," + ((Table) j.getRightItem()).getName() + ",") != -1) {
                                        // 当主表不能匹配时,匹配所有join,使用符合条件的第一个表的规则。
                                        realRuls = rule;
                                        joinTable = ((Table) j.getRightItem()).getName();
                                        joinTableAlias = j.getRightItem().getAlias().getName();
    
                                        condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", joinTable).replace("{me.a}", joinTableAlias);
                                        if (j.getOnExpression() == null) {
                                            j.setOnExpression(CCJSqlParserUtil.parseCondExpression(condExpr));
                                        } else {
                                            AndExpression and = new AndExpression(j.getOnExpression(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                            j.setOnExpression(and);
                                        }
                                    }
                                }
                            } catch (Exception e) {
                                log.debug("当前sql没有join的部分!");
                            }
                        }
                    }
                }
                if (realRuls == null) return sql; // 没有合适规则直接退出。
    
                if (sql.indexOf("limit ?,?") != -1 && select.toString().indexOf("LIMIT ? OFFSET ?") != -1) {
                    sql = select.toString().replace("LIMIT ? OFFSET ?", "limit ?,?");
                } else {
                    sql = select.toString();
                }
    
            } catch (JSQLParserException e) {
                log.error("change sql error .", e);
            }
            return sql;
        }
    

    重点思路
    重点其实就在于Sql的解析和条件注入,使用开源项目JSqlParser

    • 解析出MainTable和JoinTable。from之后跟着的称为MainTable,join之后跟着的称为JoinTable。这两个就是我们PermissionRule需要匹配的表名,PermissionRule::fromEntity字段。
    • 解析出MainTable的where和JoinTable的on后面的条件。使用and连接原本的条件和待注入的条件,PermissionRule::exps字段。
    • 使用当前登录的用户信息(放在缓存中),替换条件表达式中的值。
    • 某些情况需要忽略权限,可以考虑使用ThreadLocal(单机)/Redis(集群)来控制。

    结束语

    想要达到无感知的数据权限控制,只有机制控制这么一条路。本文选择的是通过底层拦截Sql语句,并且针对对应表注入条件语句这么一种做法。应该是非常经济的做法,只是基于文本处理,不会给系统带来太大的负担,而且能够达到理想中的效果。大家也可以提出其他的见解和思路。

    展开全文
  • 接口权限控制 页面级访问权限控制 创建路由表 Mock权限列表数据 编写导航钩子 数据级操作权限控制 路由控制完整流程图 NEXT——登录及系统菜单加载 源码 本系列目录 前端权限控制的本质 在管理系统中,...

    前端权限控制的本质


    在管理系统中,感觉最让新手们搞不懂就是权限管理这部份了,而这也是后台管理系统中最基础的部分。

    一般说来,权限管理可以分为两种情况:第一种为页面级访问权限,第二种为数据级操作权限。第一种情况是非常常见的,即用户是否能够看到页面;第二种情况是用户能否对数据进行增删改查等操作。

    这两种情况在实际的前端表现中都是一致的,即用户在正常页面中看不见,看不见就不能操作,所以进行了一次视觉上的隔离。

    在这里有的人可能会说:“你这不是自欺欺人吗?视觉隔离有什么用啊,有好多方法能够绕过去的啊,比如改改代码或者直接用ajax访问”。

    是的,你说的对,这其实就是自欺欺人,不过这也正是我要说的:

    前端的权限控制实质上就是用于展示,让操作变得更加友好,真正的安全实际上是由后端控制的

    这里举个例子简单说明一下。如果用户通过其他方式绕过了前端的路由控制,访问到了该用户原本不能访问的页面,然后呢?页面中的数据需要从后端获取,页面中的操作也需要发送给后端,如果后端自身对于所有的请求接口有自己的权限控制,那么用户其实只能看到这个页面中固有的那些信息。

    这里其实还有一个大家很容易想到的问题:在做接口测试时,如果系统本身有权限设置,但是接口未做权限,那么测试直接使用API测试工具访问的话…呵呵

    所以说前端根本不用纠结于用户以各种形式绕过页面的问题,而以上这些问题在后端那里则根本不是问题。

    权限策略


    在理解了前端权限的本质后,我们说一下前端的权限策略。依照我目前的了解,大致上把权限策略分为以下两种:

    1. 前端记录所有的权限。用户登录后,后端返回用户角色,前端根据角色自行分配页面
    2. 前端仅记录页面,后端记录权限。用户登陆后,后端返回用户权限列表,前端根据该列表生成可访问页面

    第一种的优点是前端完全控制,想怎么改就怎么改;缺点是当角色越来越多时,可能会给前端路由编写上带来一定的麻烦。

    第二种的优点是前端完全基于后端数据,后期几乎没有维护成本;缺点是为了降低维护成本,必须拥有菜单配置页面及权限分配页面,否则就是噩梦。

    本篇采用第二种方式,使用第一种方式的可以去看花裤衩的开源项目的教程:
    手摸手,带你用vue撸后台 系列二(登录权限篇)

    接下来,将一点一点带你实现前端权限控制。

    接口权限控制


    上文中说到,前端权限控制中,真正能实现安全的是接口,所以先实现接口的权限控制,而且这里听上去很重要,但实际上却很简单。

    接口权限控制说白了就是对用户的校验。正常来说,在用户登录时服务器应给前台返回一个token,以后前台每次调用接口时都需要带上这个token,服务端获取到这个token后进行比对,如果通过则可以访问。

    我们通过对axios进行简单的设置,增加请求拦截器,为每个请求的Header信息中增加Token。以下为伪代码:

    const service = axios.create()
    
    // http request 拦截器
    // 每次请求都为http头增加Authorization字段,其内容为token
    service.interceptors.request.use(
        config => {
            config.headers.Authorization = `${token}`
            return config
        }
    );
    export default service

    这样简单的设置后即可实现在每次接口权限控制的前端部分。接下来则是分别讲述“页面级访问控制”和“数据级操作控制”的前端实现方式。如果对数据级操作控制不感兴趣的可以直接跳过。

    页面级访问权限控制


    虽然页面级访问控制实质上应该是控制页面是否显示,但落在实际中则有两种不同的情况:

    1. 显示系统中所有菜单,当用户访问不在自己权限范围内的页面时提示权限不足。
    2. 只显示当前用户能访问的菜单,如果用户通过URL进行强制访问,则会直接404。

    至于第一种形式,个人认为在用户体验上非常不好,但不排除有某些特殊场景会用到这种方式。而本篇的权限控制是基于第二种形式。

    依据刚才选定的方式,及上文中提到的权限策略,我们能够联想并梳理出一个大致的流程,这也是本篇中实际权限的流程:

    登录 ——> 获取该用户权限列表 ——> 根据权限列表生成能够访问的菜单 ——> 点击菜单,进入页面

    在对流程梳理完成后我们开始进行详细的编写。

    创建路由表

    在创建路由表前,我们先总结一下前端路由表构成的两种模式:

    1. 同时拥有静态路由和动态路由。
    2. 只拥有静态路由

    在第一种模式中,将系统中不需要权限的页面构成静态路由,需要权限的页面构成动态路由。当用户登录后,根据返回数据匹配动态路由表,将结果通过addRoutes方法添加到静态路由中。完整的路由中将只包含当前用户能访问的页面,用户无权访问的页面将直接跳转到404。(这也是我之前一直使用的模式)

    第二种模式则直接将所有页面都配置到静态路由中。用户正常登录,系统将返回数据记录下来作为权限数据。当页面跳转时,判断跳转的页面是否在用户的权限列表中,如果在则正常跳转,如果不在则可以跳转到任意其他页面。

    在经过不断的实践和改进后,个人认为第二种模式相对简单,只有单一的路由表,方便以后的管理和维护。同时又因无论哪种模式都不能避免所谓的“安全”问题——个别情况下跳过前端路由情况的发生,所以第二种模式无论怎么看都比第一种要好很多。

    需要注意的是,在第二种模式中,因为只有单一的静态路由,所以一定要使用vue-router的懒加载策略对路由组件进行加载行为优化,防止首次加载时直接加载全部页面组件的尴尬问题。当然,你可以对那些不需要权限的固定页面不使用懒加载策略,这些页面包括登录页、注册页等。

    关于路由懒加载的知识也可以查看官方文档。:vue-router懒加载

    路由表配置

    const staticRoute = [
        {
            path: '/',
            redirect: '/login'
        },
        {
            path: '/login',
            component: () => import(/* webpackChunkName: 'index' */ '../page/login')
        },
        // 你的其他路由
        ...
        // 当页面地址和上面任一地址不匹配,则跳转到404
        {
            path: '*',
            redirect: '/404'
        }
    ]
    
    export default staticRoute
    

    Mock权限列表数据

    在编写完路由表后,我们需要模拟一个完整的权限列表数据。这个数据在实际中是用户登录完成,后台返回的数据。这里暂定为以下格式:

    模拟返回的权限列表数据

    var data = [
        {
            path: '/home',
            name: '首页'
        },
        {
            name: '系统组件',
            child: [
                {
                    name: '介绍',
                    path: '/components'
                },
                {
                    name: '功能类',
                    child: [
                        {
                            path: '/components/permission',
                            name: '详细鉴权'
                        },
                        {
                            path: '/components/pageTable',
                            name: '表格分页'
                        }
                    ]
                }
            ]
        }
    ]    

    其中的name和path的值实际上应该在单独的菜单配置页面中填写,提交给后台,让其记录角色信息。

    这里单独说一下我司现在的做法,以更好的说明该格式的实际使用环境:

    1. 建立系统菜单。需要填写菜单的名称,和页面地址。多语言条件下填写多种名称。
    2. 创建权限组。需要填写权限组名称,并为该权限组分配页面级权限及数据级权限。
    3. 分配用户权限。将用户和某个权限组绑定。

    以上配置均在前端有对应的页面进行操作。

    编写导航钩子

    上文中我们说过,需要在页面跳转时,将路由表和返回数据进行匹配,如果存在于返回数据中则正常跳转,如果不存在则跳转到任意页面。这里我们需要实现这个逻辑。以下为简单实现,只说明原理,不涉及细节。

    // router为vue-router路由对象
    router.beforeEach((to, from, next) => {
        // ajax获取权限列表函数
        // 这里省略了一些判断条件,比如判断是否已经拥有了权限数据等
        getPermission().then(res => {
            let isPermission = false
            permissionList.forEach((v) => {
                // 判断跳转的页面是否在权限列表中
                if(v.path == to.fullPath){
                    isPermission = true
                }
            })
            // 没有权限时跳转到401页面
            if(!isPermission){
                next({path: "/error/401", replace: true})
            } else {
                next()
            }
        })
    })

    到这里我们已经完成了对页面访问的权限控制。接下来讲解数据级操作权限的实现。

    数据级操作权限控制

    数据操作权限的获取方式,我能够想到的是以下两种:

    • 用户登录后,获取权限列表时,直接在该数据中体现
    • 每次访问页面时请求后台,获取该页面的数据操作权限列表

    不管项目中具体采用哪一种方法,或者使用两种方法的结合,其实现原理都是一样的:即将返回的数据操作权限列表增加到路由表中的对应页面的自定义字段中(如permission),然后在进入页面前,根据该自定义字段来判断页面中的元素是否需要显示。

    这里的核心点是:如何将数据放置到路由表中,以及如何根据路由表中的字段在页面中进行判断。接下来则重点解决这两个问题。

    我们先简单的修改一下登录后返回的权限列表数据,为其增加permission字段

    var data = [
       ...
        {
            name: '系统组件',
            child: [
                {
                    name: '介绍',
                    path: '/components',
                    // 为介绍页面增加查看按钮权限
                    permission: ['view']
                },
                {
                    name: '功能类',
                    child: [
                        ...
                        {
                            path: '/components/pageTable',
                            name: '表格',
                            // 为表格页面增加导出、编辑权限
                            permission: ['outport', 'edit']
                        }
                    ]
                }
            ]
        }
    ]    

    将数据放置到路由表中

    为了实现“将数据放置到路由表中”这个功能,我们只需要将目光放回到上文中说的导航钩子那里。在那边,我们有一个getPermission()函数,用来获取权限。那么我们其实只要在这个函数的基础上进行修改,把返回的数据直接放到对应路由的meta字段中。

    你可以戳这里查看路由meta字段的官方文档:路由元信息

    // getPermission()
    // ajax获取权限
    axios.get("/permission").then((res) => {
        // 对返回数据扁平化,减少深层次循环
        let flatArr = flat(res)
        flatArr.forEach(function(v){
            let routeItem = router.match(v.path)
            if(routeItem){
                // 将返回的所有数据都存到路由的meta信息中
                routeItem.meta = v
            }
        })
    })

    根据路由表中的字段在页面中进行判断

    实现该功能实际上就是实现一个类似于v-if判断的插件。当传入的参数存在于路由的meta字段时则渲染该节点,不存在时则不渲染。

    这里期望的用法是这样的:

    // 如果它有outport权限则显示
    <el-button v-hasPermission="'outport'">导出</el-button>

    而实现它则需要使用vue的自定义指令。你可以戳这里查看vue自定义指令的说明:自定义指令

    const hasPermission = {
        install (Vue, options){
            Vue.directive('hasPermission', {
                bind(el, binding, vnode){
                    let permissionList = vnode.context.$route.meta.permission
                    if(permissionList && permissionList.length && !permissionList.includes(binding.value)){
                        el.parentNode.removeChild(el)
                    }
                }
            })
        }
    }
    
    export default hasPermission

    以上的方法便是对数据级权限(实质上是操作按钮)的显示控制。

    路由控制完整流程图

    至此为止,后台管理网站的权限控制流程就已经完全结束了,在最后我们总结一下完整的权限控制流程图。

    Created with Raphaël 2.1.2 开始 进入登录页面 登录成功? 是否已经请求过权限菜单? 根据数据生成系统菜单 点击菜单,进入页面前 匹配权限菜单,是否有该页面访问权限? 获取该页面详细权限,变更页面权限元素 进入页面 定制化的无权访问页面 获取权限菜单列表 获取成功? yes no yes no yes no yes no

    NEXT——登录及系统菜单加载


    在下一章中将重点讲述使用常规数据结构生成需要的菜单,以及unique-opened模式下点击跟节点不能收回多级菜单的问题

    源码


    当前源码地址:https://github.com/harsima/vue-backend
    请注意,该源码会不断更新(因为工作很忙不能保证定期更新)。源码涉及到的东西有超出本篇教程的部分,请酌情阅读。

    本系列目录


    展开全文
  • 基于Vue的addRoutes实现权限控制

    万次阅读 热门讨论 2018-05-15 15:25:57
    最近老师布置了基于Vue的权限控制,网上查阅了许多博客,例如“前端路上":这篇文章提到了Vue2.0以上使用的addRoutes增加动态路由的思路。然而我只是一个小白,尽管文章里贴出了源码,我也只能看的一知半解,...

    前言

    从今年3月到现在入门前端也已经有两个月了,这两个月在自学的过程中遇到了许多问题也解决了其中一部分,但是现在才开始意识到把学习过程的问题整理到CSDN上。最近老师布置了基于Vue的权限控制,网上查阅了许多博客,例如“前端路上":这篇文章提到了Vue2.0以上使用的addRoutes增加动态路由的思路。然而我只是一个小白,尽管文章里贴出了源码,我也只能看的一知半解,所以根据文章中提供的思路自己写了一个小测试demo。

    目录结构

    准备工作

    在路由配置文件里写上两个路由表,一个静态路由表(所有权限用户均能访问如“登录”、“404页”)和一个动态路由表,默认导出静态路由。

    const router = new Router({ //静态路由,登录、404
      mode:'history',
      routes: [
        {
          path: '/login', //登录页
          name: 'login',
          component: (resolve) => require(['../components/login.vue'], resolve)
        },
        {
          path: '/404',  //404页
          name: 'err',
          component: (resolve) => require(['../components/err.vue'], resolve)
        },
      ]
    });
    export default router //默认静态路由
    export const dynamicRouter =[ //动态路由
      {
        path: '/',
        name: 'home',
        meta: {  //添加meta标签,里面自定义了roles,用来设置权限
          roles:['admin','user']  //该路由仅admin和user权限访问
        },
        component: (resolve) => require(['../components/home.vue'], resolve)
      },
      {
        path: '/',
        name: 'home2',
        meta: {
          roles:['tourist'] //该路由仅tourist权限访问
        },
        component: (resolve) => require(['../components/home2.vue'], resolve)
      },
      {
        path: '/home21',
        name: 'home21',
        meta: {
          roles:['user']
        },
        component: (resolve) => require(['../components/home21.vue'], resolve)
      },
      {
        path: '/home11',
        name: 'home11',
        meta: {
          roles:['admin']
        },
        component: (resolve) => require(['../components/home11.vue'], resolve)
      },
    ];

    这里我们已经配置好了路由表。

    思路

    如果用户没有登录,那就将页面重定向到“/login”,实现方法:在跟组件(App.vue)的created回调里做判断

    methods:{
        judgelogin(){ //methods写一个判断是否登录的方法
          let isLogin = store.state.token; //这里我们从Vuex里取出token,因为登录请求成功后我们会将后台返回的token存储在vuex里方便使用
          if(!isLogin){ //如果没有token,那就代表用户没有登录
            return this.$router.push('/login'); //重定向到login页
          } 
        }
      },
    created() {
         this.judgelogin(); //在created里调用上面构造的方法
      }

    登录页:

    HTML:
    template>
      <div id="login">
        <form>
          <label>账号</label><input type="text" v-model="username"/><br>
          <label>密码</label><input type="password" v-model="password"/><br>
          <input type="button" value="登录" @click="Login">
        </form>
      </div>
    </template>
    JS:
    import {router,dynamicRouter} from '../router/index' //获取路由配置文件里的两个路由表
    
    export default {
      name: 'login',
      data () {
        return {
          username:'', //双向绑定获取账号
          password:'', //双向绑定获取密码
        }
      },
      methods:{
        Login(){
          sessionStorage.setItem('token',this.username); //将token和roles保存下来,因为没有后台接口,我这边就将账号当做token和roles
          sessionStorage.setItem('roles',this.username);
          let dR = new Array(); //创建一个数组用来存储符合权限的路由
          for (let i=0;i<dynamicRouter.length;i++){ //第一层循环遍历动态路由表的每一个路由
            for (let j=0;j<dynamicRouter[i].meta.roles.length;j++){ //第二次循环遍历每一个路由里的roles设置的权限并和当前登录账号的权限比较
              if (dynamicRouter[i].meta.roles[j] == this.username){ //这里因为我默认账号名就是当前用户的权限
               dR.push(dynamicRouter[i]); //符合条件的路由信息就放进数组里
             }
            }
          }
          this.$router.addRoutes(dR.concat([{ //这里调用addRoutes方法,动态添加符合条件的路由
            path: '*',
            redirect: '/404' //所有不匹配路径(*)都重定向到404,为什么写在这里而不放到静态路由表里可以查看“前端路上”的文章
          }]));
          this.$router.push('/'); //登录验证后跳转到主页"/"
        }
      },
      created(){
        sessionStorage.clear(); 
      }
    }

    到这里我们用账号“admin”登录后就可以成功跳转到主页并显示"home.vue"的内容,我们也可以修改url访问"/home11"。但是当我们访问"/home2"和"/home21"的时候没有出现我们预期的跳转到"/404"页,这里出了一个bug不知道为什么,希望用大神能够指正出来,小弟感激不尽!

    前方有坑:

    登陆后,当我们按"F5"键刷新页面后,页面内容都消失不见了(理论上应该是/404页内容)。原来addRoutes和Vuex一样都是刷新后数据会清空(这个地方我感觉是真的坑)。这是因为刷新会导致Vue重新实例化,路由也恢复到了初始路由,也就是前面的静态路由表。

    改进:

    在根组件的created回调里,我们不仅得判断用户是未登录状态还是登陆后又刷新,改进代码如下

    judgeLogin(){
          let isLogin = store.state.token;
          if(!isLogin){
            return this.$router.push('/login');
          } else {
            let dR = new Array();
            for (let i=0;i<dynamicRouter.length;i++){
              for (let j=0;j<dynamicRouter[i].meta.roles.length;j++){
                if (dynamicRouter[i].meta.roles[j] == store.state.roles){
                  dR.push(dynamicRouter[i]);
                }
              }
            }
            this.$router.addRoutes(dR);
          }
        }

    这样即使刷新,你生成的动态路由也不会失效了。

    附上:

    因为是小测试,所以Vuex里只存了两个数据:

    export default new Vuex.Store({
      state:{
        token:sessionStorage.getItem('token'), 
        roles:sessionStorage.getItem('roles'),
      },
    })

    结:

    第一次写博客还是有点激动的,不知道这个权限控制思路能不能对大家有所帮助。这样的权限控制其实分不同权限写了几套页面,然后根据权限动态添加符合权限的页面。其实我一开始的设想是一个主页的菜单栏根据不同权限的用户显示不同的导航(这个实现效果看“前端路上”大能分享的github上源码),但是技术有限还未能实现,后续实现后会贴出来的。


    展开全文
  • Vue 项目 菜单权限及按钮级权限控制

    千次阅读 2020-06-24 12:05:50
    3.全局自定义指令(directive)控制按钮权限数据的方法,登入时获取后端传来的按钮权限数组。 4.在每个按钮中调用该指令,并传入该操作的权限字段和后端保存的权限字段进行匹配,能匹配则该操作按钮可显示 具体代码...

    实现思路

    1.页面展示需要鉴权的所有按钮,需要先鉴权菜单权限的显示与隐藏。

    2.勾选每个角色或者用户所能看的权限保存在数据库。该权限数据是一个权限字段的数组。

    3.全局自定义指令(directive)控制按钮权限数据的方法,登入时获取后端传来的按钮权限数组。

    4.在每个按钮中调用该指令,并传入该操作的权限字段和后端保存的权限字段进行匹配,能匹配则该操作按钮可显示

    具体代码如下

    1. 获取登录用户所有权限
    // 获取权限菜单(存在store下modules下user.js的actions中)
     getMenuTree ({ commit, state }) {
       return new Promise((resolve, reject) => {
         getMenuTree(state.token).then(response => {
           const data = response.result
           if (!data) {
             reject('验证失败,请重新登录')
           }
           // 存菜单结构
           commit('SET_ROLES', data)
           // 重置按钮权限
           btns = []
           const btn = findAllBtn(data)
           // 存所有按钮权限
           commit('SET_BUTTONS', btn)
           resolve(data)
         }).catch(error => {
           reject(error)
         })
       })
     }
    // 递归获取按钮list
    let btns = []
    export function findAllBtn (list) {
     list.forEach(val => {
     // 与后台协商所有菜单资源(resCode)下的type是1表菜单,2为按钮
       if (val.type === '1') {
         if (val.children && val.children.length > 0) {
           findAllBtn(val.children)
         }
       } else {
         btns.push(val.reCode)
       }
     })
    }
    
    1. 对比菜单权限的方法
    在store下modules下新建一个permission.js(获取最终动态权限菜单)
    /**
    * @param  {Array} userRouter 后台返回的用户权限json
    * @param  {Array} allRouter  前端配置好的所有动态路由的集合
    * @return {Array} realRoutes 过滤后的路由
    */
    export function recursionRouter (userRouter = [], allRouter = []) {
     let realRoutes = []
     allRouter.forEach(val => {
       userRouter.forEach(item => {
         if (val.path.includes('/')) {
           if (item.resCode === val.path.split('/')[1]) {
             val.children = recursionRouter(item.children, val.children)
             realRoutes.push(val)
           }
         } else {
           if (item.resCode === val.path) {
             if (item.children && item.children.length > 0) {
               val.children = recursionRouter(item.children, val.children)
             }
             realRoutes.push(val)
           }
         }
       })
     })
     realRoutes.push({ path: '*', redirect: '/404', isShow: true })
     // console.log(222, realRoutes)
     return realRoutes
    }
    // asyncRouterMap本地配置好的所有动态路由的集合
    const actions = {
     generateRoutes ({ commit }, roles) {
       return new Promise(resolve => {
         let accessedRoutes = recursionRouter(roles, [...asyncRouterMap])
         commit('SET_ROUTES', accessedRoutes)
         resolve(accessedRoutes)
       })
     }
    }
    

    对比菜单权限需要在全局路由守卫中如下操作

    router.beforeEach(async (to, from, next) => {
     // 确定用户是否已登录
     const hasToken = getToken()
     // 判断是否有token
     if (hasToken) {
       // 运营端登录
       if (to.path === '/login') {
         // 如果已登录,则重定向到主页
         next({ path: '/' })
         NProgress.done()
       } else {
         // 确定用户是否已获得了他的权限角色
         const hasRoles = store.getters.roles && store.getters.roles.length > 0
         if (hasRoles) {
           next()
         } else {
           // 角色必须是对象数组
           const roles = await store.dispatch('user/getMenuTree')
             // 根据角色生成可访问路由映射
             const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
             // 清空静态路由
             resetRouter()
             // 动态添加可访问路由
             router.addRoutes(accessRoutes)
             // hack 方法,以确保addRoutes是完整的
             // 设置replace: true,这样导航就不会留下历史记录
             next({ ...to, replace: true })
         }
       }
     } else {
       // 在免登录白名单,直接进入
       if (whiteList.indexOf(to.path) !== -1) {
         next()
       } else {
         // 没有访问权限的其他页面被重定向到登录页面
         next(`/login?redirect=${to.path}`)
         NProgress.done()
       }
     }
    })
    
    1. 全局自定义指令
    // 需要全局注入(即在main.js引入)
    import Vue from 'vue'
    import store from '@/store'
    /**
     * 使用:v-permission="'resCode'"
     *   resCode 按钮资源(即路由path)
     * **/
    Vue.directive('permission', {
      inserted (el, vDir) {
        let btnPermission = store.getters.buttons
        if (vDir.value) {
          if (!btnPermission.includes(vDir.value)) {
            el.parentNode.removeChild(el)
          }
        }
      }
    })
    
    1. 页面中使用
    <el-button type="primary" @click="roleExport" v-permission="'ent-role-export'">导出</el-button>
    

    如图

    初始在这里插入图片描述
    取消权限后
    在这里插入图片描述
    在这里插入图片描述


    相关文章

    基于elementUI中table组件二次封装(Vue项目)


    axios二次封装,接口统一存放,满足RESTful风格


    keep-alive不能缓存多层级路由(vue-router)菜单问题解决


    基于ElementUi再次封装基础组件文档

    展开全文
  • JAVA访问权限控制

    千次阅读 2013-09-01 14:12:10
    Java提供了public, private, protected 三个访问权限修饰词,提供了以下四种访问权限控制机制: 1、包访问权限; 2、Public访问权限; 3、Private访问权限; 4、Protected访问权限;   1、包...
  • Springcloud gateway路由配置接口权限

    千次阅读 2020-03-08 23:50:17
    如下,只有openid里才能访问某接口,如微信的openid
  • 但是一些系统进程ANR,就搞得手机一时半会没法用,手机就感觉脱离了我们的控制,真的是很悲伤啊。 原因如此,那平时开发如何避免呢 1.主线程中不要任何耗时操作,将这些放到子线程去 2.开发中注意线程...
  • vue权限控制攻略(iview-admin)

    千次阅读 2018-11-23 10:20:42
    vue权限控制攻略(iview-admin) 作者:轻酌~浅醉 1,方式 在meta中添加access 只显示在当前用户组中定义的 第一个参数为当前要访问的页面的name传进去 ,第二个参数为用户的权限字段列表(登录之后通过接口获取...
  • 前端如何配合后端完成RBAC权限控制

    千次阅读 2018-12-23 13:29:12
    关联上一篇Vue 前端应用实现RBAC权限控制的一种方式,这篇写的比较懒,哈哈,还是谢谢支持的朋友。 承蒙李杨的邀请,就写了这篇文章,平时写的不多,失误之处,请大家多多包涵。 因为工作的原因要找一个管理端模板,...
  • 基于mysql 的 python学生信息管理系统(附带访问控制、数据导入导出、统计和可视化) 使用 python 自带的 tkinter 库实现基于 mysql 的简单图形化学生信息管理系统。在本地安装好 mysql 即可打开使用。功能有: 简单...
  • spring boot后台管理系统,spring security权限控制

    万次阅读 热门讨论 2018-09-14 12:20:37
     该boot-security系统,采用了jdk1.8使用了lamdba表达式,采用了spring security进行权限控制,实现了按钮级别的权限。目前数据库采用的是mysql,表格分页用的是datatables,智能排序,orm采用的是mybatis,nosql用...
  • 报表开发之批量导入导出Excel

    千次阅读 多人点赞 2016-08-05 10:16:52
    你将Excel表分发给各个员工,员工填完后,统一整理成多个Excel,你需要将这些数据,用报表的填报功能,提交录入到数据库中,这样一来可避免到服务器机房录数据的繁琐步骤,但是无论是批量导入和还是导出Excel,...
  • 数据泵 EXPDP 导出工具的使用

    万次阅读 2010-10-07 18:10:00
    --=================================--数据泵 EXPDP 导出工具的使用--================================= 对于Oracle 数据库之间的导入导出,可以使用Oracle提供的导入导出工具EXP/IMP来实现。EXP/IMP是Oracle早期...
  •  该boot-backend系统,采用了jdk1.8使用了lamdba表达式,采用了shiro进行权限控制,实现了按钮级别的权限。目前数据库采用的是mysql,表格分页用的是datatables,智能排序,orm采用的是mybatis。  完全可以作为...
  • 常见的API接口管理工具

    万次阅读 2018-11-03 17:34:35
    1、Postman ...幸运的是,Postman可以将调试过的请求保存到Collection中。形成的Collection就可以作为一份简单有效且...对QA来说,使用Postman进行接口测试和接口文档维护是同一件事情,测试即文档,维护成本也很低...
  • 接口测试 | 接口测试入门

    万次阅读 多人点赞 2018-02-24 13:08:37
    接口测试讲义 1. 接口测试的类型 主要包含三种测试: Web接口测试, 应用程序接口(API, application programming interface)测试, 数据库测试。 实际上意义就是UI界面到数据库之间,数据流...
  • Shiro权限管理框架详解

    千次阅读 2019-06-17 16:23:23
    基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。 权限管理包括用户身份...
  • db2导入导出文件

    万次阅读 2014-12-09 12:29:17
    1.导入(import)导出(export)以及导入(load)的介绍 DB2中所谓的数据移动,包括: 1. 数据的导入(Import) 2. 数据的导出(Export) 3. 数据的装入(Load) 导入和装入都是利用DB2的相关命令把某种格式的文件中...
  • Keychain数据导出

    千次阅读 2015-04-13 14:16:02
    但是,要在设备上执行keychain dumper,就需要用chmod 777设置其权限,需要root权限,而jail break之后的默认密码是:alpine。 最后可以获得好几个文件,下面是里面的2个例子。( 密码都被我用password字串替换...
  • - 微云快速开发平台 此版本堪称JAVA专业接口开发利器,集成在线接口文档 Swagger-ui,采用Jwt Token验证机制,最牛在于业务restful接口代码自动生成,更更牛的在接口可以细化权限控制(例如:不同开发商只能查...
  • 接口自动化

    千次阅读 2017-06-19 15:28:55
    一、接口测试简介 1、什么是接口测试?...2、为什么要做接口测试 a)互联网的快速发展,公司内部系统或与外部系统的关联越来越多,一个业务流程关联多个后端系统,它们的关联都是基于接口来实现,接口测试可以将
  • 1.后台restful api,组装文件...后台接口:根据projectId查询相关数据,并将相关字段封装到word模板中,最后用流输出文件。控制层代码@RequestMapping("/exportToWord") @ResponseBody public void exportToWord(Ht
  • 本指南引入了活动目录服务批量管理,您可以使用...使用这些工具,您可以导入、导出及修改诸如用户、通讯录、组、服务器、打印机及共享文件夹的对象。 使用LDIFDE工具完成批量操作。将Reskit域中Marketing组织单元(OU)
  • shiro教程(1)-基于url权限管理

    万次阅读 2017-03-30 22:31:28
    基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。 权限管理包括用户身份...
  • 所有的功能都是基于上面的模型表构建的,目前资源权限控制只做到了菜单url,等到后期有空,希望能精确到具体的字段,把功能的更加完善. 核心功能的实现 RBAC10张数据表(用户表,用户组表,角色表,权限表,菜单表,...
  • 将其嵌入到水、电、气、暖智能(卡)表、机顶盒、智能电器或其它专用设备中,可以完成数据的加密解密、双向身份认证、访问权限控制、通信线路保护、临时密钥导出、数据文件存储等多种功能。ESAM模块使用的安全芯片...
  • 如何实现AD域账户导入导出

    千次阅读 2017-11-15 22:10:00
    作为域管理员,有时我们需要批量地向AD域中添加用户帐户,这些用户帐户既有一些相同的属性,又有一些不同属性。如果在图形界面逐个添加、设置,那么需要的时间和...微软默认提供了两个批量导入导出工具,分别是CSVDE...
  • 用户·角色·权限·表

    万次阅读 2016-04-15 21:13:20
     因为过的一些系统的权限管理的功能虽然在逐步完善,但总有些不尽人意的地方,总想抽个时间来更好的思考一下权限系统的设计。  权限系统一直以来是我们应用系统不可缺少的一个部分,若每个应用系统都重新对...
  • Android权限  权限是一种限制,用于限制对部分代码或设备上数据的访问。施加限制是为了保护可能被误用以致破坏或损害用户体验的关键数据和代码。每种权限均由一个唯一的标签标识。标签通常指示受限制的操作。  ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 30,290
精华内容 12,116
关键字:

导出接口如何做权限控制