怎样构建一个灵活的权限系统

本贴最后更新于 3007 天前,其中的信息可能已经事过景迁

背景

我所在的公司是一家培训机构,公司给了我一个学生管理系统的项目

思绪

以上是背景,然后公司给我一个要求是权限系统必须要足够的灵活,可单独为任何的角色添加、删除可修改的东西,还可以任意添加、删除角色。

好吧,我服,然后开始对权限系统做思考,以下是思路(前方人格分裂?#雾):

这样的权限系统很简单嘛,我把每个角色可访问的路由,使用 json 格式存入数据库不就得了。
(你傻啊?存路由?从哪儿取这些路由啊?)。
恩,有道理,我 把网站所有的路由全部存在数据库,然后使用 id 做唯一标示,然后把角色允许访问的这些 id 使用 Json 格式存入数据库。
(不允许的链接地址,你虽然会拒绝访问,但是你的按钮还在啊喂!比如【学生管理】这个菜单项,你点进去会拒绝访问,但是你的这个菜单项还在菜单里啊)。
恩...好像有道理,那我使用不同的模板,不就行啦。

(我无力吐槽..说好的灵活呢...添加角色的时候你不就只有一脸懵逼?)

那我提前判断哪些显示哪些不能显示呗?

(那不是每次访问的时候,还要把所有的路由遍历一次?)

额。。。好吧。我服我服..不简单啊喂!

清空一下思路,从新思考一下,于是...想法成型了

成型

我们仔细的观察大部分的网站,基本上链接呈现这样一个结构,访问首页-> 点击导航-> 到各个页面-> 再到页面下单独模块-> 模块内的功能。它就像一样开支散叶。我们访问的每一个路由到的页面其实就是顺着树干(注意是必须顺着树干)到下一个分叉口(我们称之为结点),结点中存储着这里的信息,我们就可以取出其中的信息,这棵树就是整体的网站结构,然后我们的权限系统呢?就像是从这棵树中去枝,把不能去的地方剃掉,留下能去的地方,然后我们就可以畅通无阻啦。
于是我决定使用这种结构去做网站权限的设计。它有什么好处呢,我总结了一下:

1、直观,网站结构、权限结构一目了然。

2、效率高,树的遍历有一个特点,很符合我们的要求,那就是要访问子节点,必须访问他的父亲,所以我们只要限制父亲不能去了,子节点根本不需要访问,直接抛弃。

3、规范性,这是一个隐式的好处,它将我们的所有的链接功能,都做了很好的管理,不再杂乱,而我们每次去添加一个路由,都需要添加在树中,很规范,功能的设计更加的模块化了(怎么模块化?后面再说)

说了这么多,准备上代码啦~

(不慌啊喂,树到底是啥概念,完全不懂啊?)
请自行百度~~
(喂喂)

树的存储方式,一般有两种,好吧,我的一般写法是这两种,如果有其他写法,请务必告诉我,谢谢。

第一种是在节点中存储子节点组成的链表,大概是这样:

public class Node{ /*节点信息*/ ... /*子节点链表*/ List<Node> children; }

第二种是使用二叉链表,其实也就是在节点中只存储两个子节点,分别为 leftChild,nextSibling。分别就是自己的第一个孩子以及右边的兄弟
大概是这样:

public class Node{ /*节点信息*/ ... /*子节点链表*/ Node leftChild; Node nextSibling; }

我是使用的第二种方式,以下是我的代码(小朋友不要轻易模仿)

TreeNode.java 接口

/** * Created by wuhongxu on 2016/9/4 0004. */ public interface TreeNode { public void setChildCount(java.lang.Integer childCount); public java.lang.Integer getChildCount(); public void setDegree(java.lang.Integer degree); public java.lang.Integer getDegree(); public TreeNode getParent(); public void setParent(TreeNode treeNode); public void setLeftChild(TreeNode treeNode); public TreeNode getLeftChild(); public void setNextSibling(TreeNode treeNode); public TreeNode getNextSibling(); }

Mapping.java

import com.hudongwx.studentsys.common.BaseMapping; import com.hudongwx.studentsys.util.TreeNode; /** * create by wuhongxu. */@SuppressWarnings("serial") public class Mapping extends BaseMapping<Mapping> implements TreeNode { public static final Mapping dao = new Mapping(); private Mapping parent,leftChild,nextSibling; public Mapping(){ init(); } public Mapping(String icon,String title,String url){ setIcon(icon); setTitle(title); setUrl(url); init(); } public Mapping(String icon,String title,String url,String message){ this(icon,title,url); setMessage(message); } public void init(){ /*getParent(); getLeftChild(); getNextSibling();*/ setChildCount(0); setParentId(0); setLeftChildId(0); setNextSiblingId(0); } @Override public Mapping getParent() { return parent; } @Override public void setParent(TreeNode treeNode) { parent = (Mapping) treeNode; } @Override public void setLeftChild(TreeNode treeNode) { leftChild = (Mapping)treeNode; } @Override public Mapping getLeftChild() { return leftChild; } @Override public void setNextSibling(TreeNode treeNode) { nextSibling = (Mapping) treeNode; } @Override public Mapping getNextSibling() { return nextSibling; } public boolean equals(Mapping mapping) { return mapping.getId().equals(getId()) && mapping.getTitle().equals(getTitle()) && mapping.getUrl().equals(getUrl()); } }

ArrayTree.java

此处更新于 2016.12.30

package com.hudongwx.crowdSourcing.util; import java.util.*; /** * 二叉链存储方式实现的树. * Date: 2016/12/30 0026 * Time: 14:47 * * @author wuhongxu. * @version 1.0.0 */public class ArrayTreeextends TreeNode> { private List tree; //暂时缓存上次添加的父节点,方便下次添加到同一父节点下时直接添加不需要传入父节点参数 private T parent; //暂定层数最多一百层 private int[] index = new int[Common.MAX_DEGREE]; //这里得到的是复制,不能操作树本身 public List getList() { return new ArrayList<>(tree); } public ArrayTree setTree(List tree){ this.tree = tree; return this; } public void addGoodNode(T node){ if(tree == null) tree = new ArrayList<>(); tree.add(node); } public void addGoodNode(int index,T node){ if(tree == null) tree = new ArrayList<>(); tree.add(index,node); } public ArrayTree() { tree = new ArrayList(); } //通过此接口实现额外对node的操作 public interface onOverListener { void onOver(T node, T parent, T nextSibling); } public interface onCheckListener { boolean onCheck(T now); } public List initTree(T root) { clearTree(); root.setChildCount(0); root.setDegree(0); tree.add(root); return tree; } public void destroyTree() { tree = null; } public void clearTree() { if (!tree.isEmpty()) tree.clear(); } public boolean isEmpty() { return null != tree && tree.isEmpty(); } public T root() { if (!tree.isEmpty()) { T t = tree.get(0); while (t.getParent() != null) { t = (T) t.getParent(); } return t; } return null; } public T value(T node) { if (!tree.isEmpty() && tree.contains(node)) return node; return null; } public void assign(T node, T newNode) { if (!tree.isEmpty() && tree.contains(node)) node = newNode; } public T getParent(T node) { if (!tree.isEmpty() && tree.contains(node)) return (T) node.getParent(); return null; } public T getLeftChild(T node) { if (tree.isEmpty() && tree.contains(node)) return (T) node.getLeftChild(); return null; } public T getNextSibling(T node) { if (tree.isEmpty() && tree.contains(node)) return (T) node.getNextSibling(); return null; } //为了通用,没有为node的做多余操作,所以使用接口来让调用者实现 public boolean insertChild(T node, T parent, T leftSibling, onOverListener opn) { if (null == parent) return false; this.parent = parent; if (tree.contains(node) || !tree.contains(parent)) return false; if (leftSibling != null && !tree.contains(leftSibling)) { return false; } if(parent.getDegree() >= Common.MAX_DEGREE) return false; parent.setChildCount(parent.getChildCount() + 1); node.setDegree(parent.getDegree() + 1); node.setParent(parent); if (null != leftSibling) { T tmp = (T) leftSibling.getNextSibling(); leftSibling.setNextSibling(node); node.setNextSibling(tmp); } else { TreeNode leftChild = parent.getLeftChild(); parent.setLeftChild(node); node.setNextSibling(leftChild); } if (null != opn) opn.onOver(node, parent, leftSibling); //添加位置为无论如何添加所有同层的在一起 /*index[node.getDegree()] += index[parent.getDegree()];*/ tree.add(++index[node.getDegree()],node); //TODO 思考更快速的位置标记,暂时没想到 for(int i = node.getDegree() + 1; i < Common.MAX_DEGREE; i++) index[i]++; return true; } public boolean insertChild(T node, T parent, int pos, onOverListener ovl) { if (null == parent) return false; T nowChild = (T) parent.getLeftChild(); T left = null; for (int i = 1; i < pos && nowChild != null; i++) { left = nowChild; nowChild = (T) nowChild.getNextSibling(); } return insertChild(node, parent, left, ovl); } public boolean insertChild(T node, T parent, onOverListener ovl) { return insertChild(node, parent, Integer.MAX_VALUE, ovl); } public boolean insertChild(T node, T parent){ return insertChild(node,parent,null); } public boolean insertChild(T node){ return insertChild(node,parent,null); } public boolean insertChild(T node,onOverListener ovl){ return insertChild(node,parent,ovl); } public boolean deleteChild(T node) { return tree.remove(node); } public void checkTree(T parent, onCheckListener ocl) { if (!tree.contains(parent) || null == ocl) return; Queue queue = new LinkedList<>(); queue.add(parent); while (!queue.isEmpty()) { T now = queue.poll(); //如果tree不包含此节点,将不对其进行操作遍历(因为其实操作的对象是同一个,所以可能不会包含在链表中) if(!tree.contains(now)) continue; T first = null; if(now.getParent() != null) first = (T) now.getParent().getLeftChild(); //如果返回值为false,将放弃子节点遍历,但是会继续遍历本层 if (!ocl.onCheck(now)) continue; T c = (T) now.getLeftChild(); while (c != null) { queue.offer(c); c = (T) c.getNextSibling(); } } } //bfs完成遍历 public void checkTree(onCheckListener ocl) { checkTree(root(),ocl); } public void checkTreePreorder(onCheckListener ocl){ checkTreePreorder(root(),ocl); } //深搜,顺序还是为从前到后,比正常深搜多一个倒栈操作 public void checkTreePreorder(T parent,onCheckListener ocl){ if (!tree.contains(parent) || null == ocl) return; Stack stack = new Stack<>(); Stack bufferStack = new Stack(); stack.push(parent); while (!stack.isEmpty()) { T now = stack.pop(); //如果tree不包含此节点,将不对其进行操作遍历(因为其实操作的对象是同一个,所以可能不会包含在链表中) if(!tree.contains(now)) continue; T first = null; if(now.getParent() != null) first = (T) now.getParent().getLeftChild(); //如果返回值为false,将放弃子节点遍历 if (!ocl.onCheck(now)) continue; T c = (T) now.getLeftChild(); while (c != null) { //加入缓存栈 bufferStack.push(c); c = (T) c.getNextSibling(); } while(!bufferStack.isEmpty()){ //倒序入栈 stack.push(bufferStack.pop()); } } } }
使用示例

此处更新于 2016.12.30 ,读取 xml 权限信息,并持久化到数据库(为了方便= =)

public static void generatorXML(ArrayTree<Mapping> tree) throws IOException { Document document = DocumentHelper.createDocument(); final Map<Mapping,Element> map = new HashMap<>(); tree.checkTree(now->{ if(now.getParent() == null){ Element rt = DocumentHelper.createElement("mapping"); document.setRootElement(rt); Element element = addAttributeToElement(rt, now); map.put(now,element); return true; } Element e = map.get(now.getParent()).addElement("mapping"); map.put(now,addAttributeToElement(e,now)); return true; }); //输出到控制台 XMLWriter xmlWriter = new XMLWriter(); xmlWriter.write(document); // 输出到文件 // 格式 OutputFormat format = new OutputFormat(" ", true);// 设置缩进为4个空格,并且另起一行为true XMLWriter xmlWriter2 = new XMLWriter( new FileOutputStream("src\\main\\resources\\permission.xml"), format); xmlWriter2.write(document); // 另一种输出方式,记得要调用flush()方法,否则输出的文件中显示空白 /* XMLWriter xmlWriter3 = new XMLWriter(new FileWriter("student2.xml"), format); xmlWriter3.write(document2); xmlWriter3.flush();*/ // close()方法也可以 } private static Element addAttributeToElement(Element e, Mapping mapping){ e.addAttribute(Common.LABEL_ID,""+mapping.getId()); e.addAttribute(Mapping.LABEL_ICON,mapping.getIcon()); e.addAttribute(Mapping.LABEL_URL,mapping.getUrl()); e.addAttribute(Mapping.LABEL_TITLE,mapping.getTitle()); e.addAttribute(Mapping.LABEL_FUNCTION,""+mapping.getFunction()); return e.addElement("mappings"); } public static void readXML(final ArrayTree<Mapping> tree,final String path) throws Exception { SAXReader saxReader = new SAXReader(); File file = new File(path); if(!file.exists()) throw new Exception(path+"文件不存在"); Document document = saxReader.read(file); //bfs实现建树 Queue<Element> queue = new LinkedList<>(); queue.offer(document.getRootElement()); while (!queue.isEmpty()){ Element now = queue.poll(); if(now.attributeCount() < 4) throw new Exception(now.getPath()+"节点的元素个数不足"); Mapping mapping = new Mapping(); List<Attribute> attributes = now.attributes(); for(Attribute attribute : attributes){ mapping.set(attribute.getName(),attribute.getValue()); } if(now.isRootElement()){ tree.initTree(mapping); }else{ List<Mapping> list = tree.getList(); for(Mapping m : list){ Element parent = now.getParent().getParent(); String s = parent.attributeValue(Common.LABEL_ID); if(Objects.equals(m.get(Common.LABEL_ID), s)){ tree.insertChild(mapping,m); break; } } } List<Element> list = now.element("mappings").elements(); list.forEach(queue::offer); } //建树完成 注意:因类型关系,此树不能重复使用,必须重新读树 tree.checkTree(now->{ log.info(now.toString()); return true; }); tree.checkTree(now -> { if(now.getParent() != null){ now.set("parentId",now.getParent().get(Common.LABEL_ID)); /*now.setParentId(now.getParent().getId());*/ } if(now.getLeftChild() != null) now.set("leftChildId",now.getLeftChild().get(Common.LABEL_ID)); /* now.setLeftChildId(now.getLeftChild().getId());*/ if(now.getNextSibling() != null) now.set("nextSiblingId",now.getNextSibling().get(Common.LABEL_ID)); /*now.setNextSiblingId(now.getNextSibling().getId());*/ return now.save(); }); CacheKit.put(Common.CACHE_60TIME_LABEL,"mappingTree",null); }

xml 文件实例

[b96984f14cd146c496e40871d973f594-permission.xml](https://b3logfile.com/file/2016/12/b96984f14cd146c496e40871d973f594-permission.xml)

BaseController 完成基础权限地图处理操作

//基础渲染方法 public void index() { fillHeaderAndFooter(); if (!fillContent()) { renderError(403); return; } render("/index.ftl"); } /** * @return 返回一级菜单的mapping */ public abstract Mapping init(); public void fillHeader() { //三个地址:servePath用于得到去掉参数的网址、holdPath为带参数网址 String uri = getRequest().getRequestURI(); String url = String.valueOf(getRequest().getRequestURL()); String staticPath = getAttr(Common.LABEL_STATIC_SERVE_PATH); //将不需要的参数忽略掉 String para = StrPlusKit.ignoreQueryString(getRequest().getQueryString(), "_pjax", "list_p", "chart_p", "p"); if (!StrPlusKit.isEmpty(para)) para = "?" + para; String actionKey = getAttr(Common.LABEL_ACTION_KEY); String servePath = staticPath + actionKey; if (para != null) url += para; setAttr(Common.LABEL_SERVE_PATH, servePath); setAttr(Common.LABEL_HOLD_PATH, url); setAttr(Common.LABEL_STATIC_RESOURCE_VERSION, new Date().getTime()); User currentUser = getCurrentUser(this); setAttr(Common.LABEL_IS_LOGIN, currentUser == null); setAttr(Common.LABEL_LOGIN_ROLE, currentUser != null ? currentUser.getUserRole() : ""); setAttr(Common.LABEL_USER, currentUser); Prop langProp = LangConfig.getLangProp(); setAttr(Common.LABEL_LOGIN_NAME_ERROR, langProp.get(Common.LABEL_LOGIN_NAME_ERROR)); setAttr(Common.LABEL_INVALID_PASSWORD, langProp.get(Common.LABEL_INVALID_PASSWORD)); } public void fillFooter() { /*Prop langProp = LangConfig.getLangProp(); Enumeration elements = langProp.getProperties().elements(); while(elements.hasMoreElements()) { Object o = elements.nextElement(); //System.out.println(o); }*/} //页面测试 protected void fillTest() { List<Mapping> mappings = mappingService.getTree().getList(); mappings.remove(0); setAttr(Common.LABEL_SIDES, mappings); } protected boolean fillContentParent() { User user = getCurrentUser(getRequest()); if (user == null) { forwardAction("/user/showLogin"); return false; } if (mapping == null) { renderError(403); return false; } ArrayTree<Mapping> roleTree = roleService.getRoleTree(roleService.getRoleByName(user.getUserRole())); List<Mapping> sides = new ArrayList<>(); List<Mapping> childSides = new ArrayList<>(); List<Mapping> views = new ArrayList<>(); final int[] size = new int[Common.MAX_DEGREE]; roleTree.checkTreePreorder(now -> { if (roleTree.getParent(now) == null) return true; //子菜单计数,只支持二级菜单。。。子菜单下继续遍历子视图 if (now.getFunction() > Mapping.FUNCTION_MENUITEM) { size[sides.indexOf(now.getParent())]++; childSides.add(now); //子菜单为当前点击菜单才会继续向子视图遍历 return now == mapping; } //一级菜单 if (now.getFunction() == Mapping.FUNCTION_MENUITEM) { sides.add(now); return true; } //视图遍历,遍历到一级视图停止遍历,并添加到视图链表,以便后续功能或子视图的遍历处理 if (now.getParent() == mapping && now.getFunction() == Mapping.FUNCTION_VIEW) { views.add(now); return false; } //如果同层不同访问,则其他同层节点子节点放弃遍历 /*if(mappingService.getBaseMenu(now) == mapping) return true; return mappingService.getBaseMenu(now) == mapping;*/ return false; }); setAttr(Common.LABEL_SIDES, sides); setAttr(Common.LABEL_SIDES_SIZE, size); setAttr(Common.LABEL_SIDES_CHILD, childSides); setAttr(Common.LABEL_VIEWS, views); setAttr(Common.LABEL_NOW_VISIT, mapping); //base处理通用的,其他处理继续下放 setAttr(Common.LABEL_ROLE_TREE, roleTree); setAttr(Common.LABEL_ROOT_MAPPING, roleTree.root()); return true; } //如果没有子视图模块,则可以使用通用的操作遍历 protected boolean fillContentChild() { User user = getCurrentUser(getRequest()); if (user == null) { forwardAction("/user/showLogin"); return false; } if (mapping == null) { renderError(403); return false; } ArrayTree<Mapping> roleTree = roleService.getRoleTree(roleService.getRoleByName(user.getUserRole())); List<Mapping> sides = getAttr(Common.LABEL_VIEWS); Map<String, List<Mapping>> map = new HashMap<>(); for (Mapping side : sides) { List<Mapping> operators = new ArrayList<>(); roleTree.checkTree(side, now -> { if (now.getFunction() == Mapping.FUNCTION_OPERATE) { operators.add(now); return false; } return true; }); map.put("operators" + side.getId(), operators); } setAttr("map", map); return true; } protected boolean fillContent() { return fillContentParent() && fillContentChild(); } public void fillHeaderAndFooter() { fillHeader(); fillFooter(); }

然后 controller 几乎可以直接使用 super.index(),完成大部分权限地图处理操作(同时也生成了菜单、视图,后来的开发人员也能够很简单的使用,直接专注于功能,而要达到这些,只需要在 xml 文件中简单添加菜单
视图信息就行了)
classController 示例

public class ClassController extends BaseController { public ClassService classService; public StudentService studentService; public UserService userService; public RegionService regionService; public void index() { //注意此方法~ super.index(); List<Mapping> views = new ArrayList<>(); Mapping mapping = mappingService.getMappingByUrl("/classManager/classList.ftl"); views.add(mapping); setAttr(Common.LABEL_VIEWS, views); Integer p = getParaToInt("p", 1); Page<Class> allClass = classService.getAllClass(p); setAttr("classes", allClass); List<Region> regionList = regionService.getAllRegions(); setAttr("regionList", regionList); } /** * @return 返回一级菜单的mapping */ @Override public Mapping init() { return mappingService.getMappingByUrl("/classManager"); } @Before(POST.class) public void addClass() { Class model = getModel(Class.class); boolean flag; if (model.getId() == null) flag = model.save(); else { flag = model.update(); } if (!flag) { RenderKit.renderError(this, "保存班级失败"); return; } RenderKit.renderSuccess(this, "保存班级成功"); } public void getClassStudents() { Integer classId = getParaToInt("classId"); List<Student> studentByClassId = studentService.getStudentByClassId(classId, Student.STATUS_STUDYING); } @Before(POST.class) public void deleteClass() { Integer id = getParaToInt(0); if (id == null) { RenderKit.renderError(this, "该班级不存在或已被删除"); return; } Class aClass = classService.getClassById(id); if (aClass == null) { RenderKit.renderError(this, "该班级不存在或已被删除"); return; } if (aClass.delete()) { List<Student> studentList = studentService.getAllStudentByClassId(aClass.getId()); if (studentList != null) { for (Student student : studentList) { User stuAccount = userService.getUserByStuPhone(student); studentService._deleteStudentById(student.getId()); userService._deleteUser(stuAccount); } } RenderKit.renderSuccess(this, "班级以及学生信息删除成功!"); return; } RenderKit.renderError(this, "删除不成功!"); } public void letGraduate() { String jsonStuIdList = getPara("classStuIdList"); int clsId = getParaToInt("clsId"); System.out.println(jsonStuIdList); JSONArray jsonStuIdArray = JSON.parseArray(jsonStuIdList); boolean allComplete = true; for (Object o : jsonStuIdArray) { if (o == null) continue; int id = Integer.valueOf(o.toString()); Student student = studentService.getUnEmpStudentById(id); if (student != null) { student.setStatus(Student.STATUS_GRADUATION); student.setEmploymentStatus(Student.EMPLOYMENTSTATUS_UN_EMPLOYED); student.setRemark("毕业啦!"); boolean b = studentService._updateStudentById(student); if (!b) { allComplete = false; } } } Class aClass = classService.getClassById(clsId); boolean b = true; boolean isG = false; if (aClass != null) { if (aClass.getStatus() == Class.CLASS_STATUS_GRADUATED) { isG = true; } aClass.setStatus(Class.CLASS_STATUS_GRADUATED); aClass.setRemark("毕业班!"); b = classService._updateClass(aClass); } if (allComplete && b) { if (isG) { RenderKit.renderError(this, "该班级已经毕业,不建议反复提交!"); } else { RenderKit.renderSuccess(this, "操作成功!"); } } else { RenderKit.renderError(this, "操作存在异常!"); } } }

而在前台(这里使用的 freemarker),就拥有一个遍历的机会了,这样把每个模块也就分开了,最主要还是,不能显示的导航也去掉了,在单独的模块里面可以继续做类似的遍历。

填充侧边导航

<div class="tip-container"> <ul class="nav" id="main-menu"> <#list sides as side> <li> <a href="${side.url}"><i class="${side.icon}"></i>${side.title}</a> </li> </#list> </ul> </div>

填充内容

<div id="page-wrapper"> <div id="page-inner"> <#list content as c> <#include c.url> </#list> </div> </div>

以上使用的 JFinal 框架,大同小异,我二次开发过 sym,简单说明一下,这里没有用 latke 那种先取模板再在 model 中设置参数(因为 JFinal 在这方面没有专门的使用 json),而是直接在 request 里面设置参数,然后渲染模板(这是基类,渲染在子类里,这里没有)。

子类重写渲染示例

/** * 重写index方法,渲染为自己的首页 */ @Override public void index() { super.index(); //对roleTree继续做处理 ... //----处理结束---- render("index.ftl"); }

结构.png

鉴于有人看不懂代码,那么明确的给出一个树结构图。
但是画的丑,不要介意~

强调一下,这不是二叉树...是用的二叉链表存储树结构只是一种存储方式而已

前端界面截图

管理员视图

5dedc067d7594bfdb7e05677ff687f73.png

3773c3a74e5746b18def8be0ef7d570d.png

普通学生视图

45c0b4db6f6f46708c30dbd4fda3277f.png

2b3ad34bbeec4fd3b764b8f87c89dc65.png

教师视图

87df3f4b09824fc4a3ec35955c66cb37.png

a23b3c65a6b84e21ac664e6878492da1.png

  • 权限管理
    8 引用 • 24 回帖
  • 数据结构
    87 引用 • 115 回帖 • 4 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3194 引用 • 8214 回帖
  • 创造

    你创造的作品可能会帮助到很多人,如果是开源项目的话就更赞了!

    183 引用 • 1010 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • yangyujiao

    我就是那种知道树有有根有叶的,就是不会用的人。。。
    反正也从来木有用过。
    我们最近在做单点登录,从网上直接找的 demo 改。用的 spring-boot jpa shiro。 不过目前就是简单的实现了授权获取 token,然后简单的用户表,权限表,角色表等的 操作。
    我记得我们之前公司做的权限就是 DB 中直接存 url。是完整的 url。 不分父子的。

    你这样的,如果过来一个 http://localhost:8080/father/child 那需要把 father 截出来然后做验证吗?

    1 回复
  • wuhongxu
    作者

    不需要,只要 key,也就是 child,因为在找是否能够访问时,已经在 tree 的遍历里面,访问到了 father 节点,通过 child,就已经知道了父节点了

    1 回复
  • yangyujiao

    那这跟我们以前存储 http://localhost:8080/father,http://localhost:8080/father/child 判断全 url 是不是有权限 有区别吗? 而且这个如果要添加一个 child 还要去找找 father,感觉更麻烦。

    而且万一 father 没有权限,不小心给了一个有权限的 child,不是出错了。

    2 回复
  • wuhongxu
    作者

    哦,我说了的哈
    不允许的链接地址,你虽然会拒绝访问,但是你的按钮还在,比如【学生管理】这个菜单项,你点进去会拒绝访问,但是你的这个菜单项还在菜单里啊。

    额。。怪我没写完,这是一个整体的用法,其实已经相当于一整个网站的构建了,容我再加点东西,应该就更容易知道它的好处了,稍等一会儿

  • wuhongxu
    作者

    而在前台(这里使用的 freemarker),就拥有一个遍历的机会了,这样把每个模块也就分开了,最主要还是,不能显示的导航也去掉了,在单独的模块里面可以继续做类似的遍历。

    填充侧边导航

    <#list sides as side> ${side.title}

    填充内容

    <#list content as c> <#include c.url>

    这是新添加的,至于你说的麻烦,其实并没有多多少,带来的好处有很多,能够把整个界面更好的呈现出来,可以说做到了“所见即所得”,不能访问的东西根本不会显示在你的面前,还有更高的代码重用性,甚至不仅仅只是权限处理了,整个网站的结构化、模块化。

    1 回复
  • yangyujiao

    我只是说出我的疑问,高大上的东西。我基本看不懂

    1 回复
  • wuhongxu
    作者

    别想多了,我也只是菜鸟~,你尽管说疑问,我可以给你一一回答,我现在也不知道这种结构的可取性~,有问题随便问,正好也来验证可行性啊

  • zonghua

    给个页面最好啦

    1 回复
  • wuhongxu
    作者

    给了一个结构图哈~页面因为没完成,等我完成了,会放出页面的

  • wfifi

    可以参考下 Thinkphp 的 rbac 权限控制,控制粒度也是比较细的

    1 回复
  • wuhongxu
    作者

    有没有原理链接,给一个?我这个的话,控制粒度也很细了,我没有参考过别人怎么做的权限系统,我这个做完之后,总监给过我一个权限源码,但是因为忙,只是粗略看了一下也是用的权限树

    1 回复
  • wfifi

    可以在官方网站搜索 rbac
    http://www.thinkphp.cn/tag/rbac.html

  • kevin0101 via macOS

    在 NodeJS 平台下见过的颗粒度最细的权限系统:CabloyJS

    可以参考这篇文章的说明:角色基本概念

请输入回帖内容 ...