您的位置:首页 > 其它

宅急送 项目第十天 权限管理

2017-04-24 10:35 281 查看

1. 权限管理功能 企业实现

第一种 使用开源权限控制框架

第二种 自定义权限管理模块

1.1. 开源权限控制技术

Spring Security是一个能够为基于Spring的企业应用系统提供描述性安全访问控制解决方案的安全框架。

它提供了一组可以在Spring应用上下文中配 置的Bean,充分利用了Spring IoC(依赖注入,也称控制反转)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的 工作。

Apache shiro 开源免费的权限控制框架, Shiro 是一个用 Java 语言实现的框架,通过一个简单易用的 API 提供身份验证和授权。

使用 Shiro,您就能够为您的应用程序提供安全性而又无需从头编写所有代码。(与任何使用技术无关 , 用于任何JavaSE 和 JavaEE 项目中 )

1.2. 自定义权限控制模型

优点: 更加灵活,简化便于使用

目标: 自定义通用权限控制模型,可以用于任何的项目

1.2.1. 权限模型数据模型

问题: 数据表设计 ?

四个概念:

系统功能 (权限管理目的,就是为了对系统的功能访问进行控制 )

功能: 菜单项的功能 和 页面内部按钮功能



菜单功能



页面按钮功能

权限 ,具有访问某个功能的权限 ,权限和功能关系可以是 一对一 或者 多对多

角色 ,角色是权限的集合 ,为了方便用户授权 ,有了角色后,只需要将角色授予用户

用户 ,登陆系统的用户,具有角色,拥有系统功能权限

功能、权限、角色、用户 四者之间 都可以是多对多关系 —- 完整复杂权限模型有七张表 !

1.2.2. 今天系统的权限模型



用户表 user

功能权限表 auth_function

角色表 auth_role

角色-权限关系表 role_function

1.2.3. 自定义权限模型的权限控制原理

使用自定义注解+反射技术,基于代理 对业务方法访问 实现细粒度的权限控制

提供权限管理系统数据:



权限控制原理分析: 基于 自定义注解+ 代理+ 反射 实现方法级别权限控制



2. 设计权限管理数据表

auth_function 表 (id , name, description )

auth_role 表 (id , name , description )

user 表 添加 role_id 外键

问题: 实现动态菜单功能 ?

根据当前用户权限查询菜单项,生成菜单

需要将菜单一些数据 保存 auth_function 表

设计 Function 类
/**
* 系统功能权限
*
* @author seawind
*
*/
public class Function {
private String id; // uuid
private String name; // 功能名称
private String description; // 功能描述

// 菜单项功能
private String generateMenu; // 是否生成菜单
private int zindex; // 菜单项优先级
private String page; // 点击菜单 跳转页面

private Function parentFunction;// 父功能点
private Set<Function> childrenFunctions = new HashSet<Function>(); // 子功能点

// 一个Function 属于很多 Role
private Set<Role> roles = new HashSet<Role>();
}


配置Function.hbm.xml

<class name="cn.itcast.bos.domain.auth.Function" table="auth_function">
<id name="id">
<generator class="uuid"></generator>
</id>
<!-- 下面列要按照顺序  -->
<property name="name" unique="true"></property>
<property name="description"></property>
<property name="page"></property>
<property name="generateMenu"></property>
<property name="zindex"></property>

<!-- 表关联 -->
<many-to-one name="parentFunction" class="cn.itcast.bos.domain.auth.Function" column="pid"></many-to-one>
<set name="childrenFunctions">
<key column="pid"></key>
<one-to-many class="cn.itcast.bos.domain.auth.Function"/>
</set>

<set name="roles" table="role_function">
<key column="function_id"></key>
<many-to-many class="cn.itcast.bos.domain.auth.Role" column="role_id"></many-to-many>
</set>
</class>


设计 Role 类

/**
* 系统角色
*
* @author seawind
*
*/
public class Role {
private String id; // uuid
private String name; // 角色名称
private String description; // 角色描述

// 关联Function
private Set<Function> functions = new HashSet<Function>();

// 关联user
private Set<User> users = new HashSet<User>();
}


编写Role.hbm.xml

<class name="cn.itcast.bos.domain.auth.Role" table="auth_role">
<id name="id">
<generator class="uuid"></generator>
</id>
<property name="name" unique="true"></property>
<property name="description"></property>

<!-- 关联 -->
<set name="functions" table="role_function">
<key column="role_id"></key>
<many-to-many class="cn.itcast.bos.domain.auth.Function" column="function_id"></many-to-many>
</set>
<set name="users">
<key column="role_id"></key>
<one-to-many class="cn.itcast.bos.domain.user.User"/>
</set>
</class>


在User类 添加Role 关联

private Role role;
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}


编写 User.hbm.xml 为 User关联Role

<!-- 关联角色 -->
<many-to-one name="role" class="cn.itcast.bos.domain.auth.Role" column="role_id"></many-to-one>


3. 功能权限管理

3.1. 导入初始化数据

将auth_function.sql 初始化数据导入 auth_function表

初始化数据参照 menu.json 菜单文件制作 !

3.2. 功能权限添加

点击功能权限管理— 添加权限 — /WEB-INF/pages/admin/function_add.jsp

在添加权限页面中,获取父功能点列表,从列表中选择父功能点

<input name="parentFunction.id" class="easyui-combobox"
data-options="valueField:'id',textField:'info',
url:''${pageContext.request.contextPath}/function_ajaxlist.action"/>


编写 服务器代码 FunctionAction 查询所有列表显示

public class FunctionAction extends BaseAction implements ModelDriven<Function> {
}
public interface FunctionService {
}
public class FunctionServiceImpl extends BaseService implements FunctionService {
}


将 DAO 注入 BaseService

将Service 注入BaseAction

<result name="ajaxlistSUCCESS" type="json">
<param name="root">functions</param>
<param name="includeProperties">
\[\d+\]\.id,
\[\d+\]\.info
</param>
</result>


在Function 添加 getInfo public String getInfo() {

return name + “(” + id + “)”;

}单提交

// 点击保存
$('#save').click(function(){
// 先判断form 是否通过校验,如果通过提交表单
if($("#functionForm").form('validate')){
$('#functionForm').submit();
}else{
$.messager.alert('警告','表单存在非法数据项!','warning');
}
});


提交添加表单,在FunctionAction 添加save方法 ,保存权限信息

@Override
public void saveFunction(Function function) {
// 防止 "" 的id外键关联
if (function.getParentFunction() != null &&
function.getParentFunction().getId() != null &&
function.getParentFunction().getId().trim().length() == 0) {
function.setParentFunction(null);
}
funtionDAO.save(function);
}


3.3. 权限列表查询

为function.jsp的datagrid 添加url ,使用不分页的列表查询,服务器返回list

url : '${pageContext.request.contextPath}/function_list.action'


在服务器FunctionAction 添加 list 方法

<!-- 权限所有数据查询 -->
<result name="listSUCCESS" type="json">
<param name="root">functions</param>
<param name="includeProperties">
\[\d+\]\.id,
\[\d+\]\.name,
\[\d+\]\.description,
\[\d+\]\.page,
\[\d+\]\.generateMenu,
\[\d+\]\.zindex
</param>
</result>


4. 角色管理(关联权限)

4.1. 添加角色(为角色授权)

4.1.1. 授权树的编写

/WEB-INF/pages/admin/role_add.jsp 添加角色页面

制作ztree 勾选树,主要配置

var setting = {
check : {
enable : true
}
}


第一步: 在显示树位置
<ul class=”ztree” id=”functionTree” >


第二步: setting

第三步: zNodes 节点数据

第四步: 初始化树
$.fn.zTree.init($("#functionTree"), setting, zNodes);


显示授权树,根据auth_function 表查询,返回json数据

url : '${pageContext.request.contextPath}/function_treedata.action'


修改setting

var setting = {
data : {
key : {
title : "t"
},
simpleData : {
enable : true,
pIdKey: "parentId",
}
},
check : {
enable : true,
}
};


服务器返回 ztree节点数据,应该包含 id、name、parentId !

在FunctionAction 添加treedata的方法

// 查询树,进行排序
DetachedCriteria detachedCriteria = DetachedCriteria.forClass(Function.class);
// 按照zindex 升序
detachedCriteria.addOrder(Order.asc("zindex"));


配置返回结果:

<!-- 授权树 -->
<result name="treedataSUCCESS" type="json">
<param name="root">functions</param>
<param name="includeProperties">
\[\d+\]\.id,
\[\d+\]\.name,
\[\d+\]\.parentId
</param>
</result>


在Function 实体类 添加getParentId 方法

public String getParentId() {
if (parentFunction == null) {
// 这是根节点
return "0";
} else {
return parentFunction.getId();
}
}


4.1.2. 提交添加角色表单

// 点击保存
$('#save').click(function(){
if($("#roleForm").form('validate')){
// 获得勾选ztree节点
var treeObj = $.fn.zTree.getZTreeObj("functionTree");
var nodes = treeObj.getCheckedNodes(true);
// 将多个勾选id 转换为字符串,用, 分隔
var ids = [];
for(var i=0;i<nodes.length; i++){
ids.push(nodes[i].id);// 将id 加入数组
}
// 放入form 隐藏域
$('#functionIds').val(ids.join(","));

$("#roleForm").submit();
}else{
$.messager.alert('警告','表单存在非法数据项','warning');
}
});


编写服务器 RoleAction

public class RoleAction extends BaseAction implements ModelDriven<Role> {}
public interface RoleService {
}
public class RoleServiceImpl extends BaseService implements RoleService {
}


将DAO 注入 BaseService

将Service 注入BaseAction

添加角色业务代码

@Override
public void saveRole(Role role, String functionIds) {
// 将role信息保存角色表
roleDAO.save(role); // 持久态
// 建立 role 和 function联系,向role_function 中间表插入数据
if (functionIds != null) {
String[] ids = functionIds.split(",");
for (String id : ids) {
Function function = funtionDAO.findById(id); // 功能权限
role.getFunctions().add(function); // 多对多关联,向中间表插入数据
}
}
}


配置跳转

<!-- 角色管理 -->
<action name="role_*" class="roleAction" method="{1}">
<result name="saveSUCCESS">/WEB-INF/pages/admin/role.jsp</result>
</action>


4.2. 角色列表查询

/WEB-INF/pages/admin/role.jsp 修改 datagrid 的url

url : '${pageContext.request.contextPath}/role_list.action',


在服务器 RoleAction 添加 list 方法

<!-- 角色列表-->
<result name="listSUCCESS" type="json">
<param name="root">roles</param>
<param name="includeProperties">
\[\d+\]\.id,
\[\d+\]\.name,
\[\d+\]\.description
</param>
</result>


5. 用户管理(关联角色)

已经在第二天, 创建 UserAction 完成修改密码

5.1. 添加用户

/**
* 用户 User entity. @author MyEclipse Persistence Tools
*/


public class User implements java.io.Serializable {
// Fields
private String id; // 编号 修改生成策略 uuid
private String username; // 用户名
private String password; // 密码
private Double salary; // 工资
private Date birthday; // 生日
private String gender; // 性别
private String station; // 单位
private String telephone; // 电话
private String remark; // 电话
}


修改 /WEB-INF/pages/admin/userinfo.jsp

<tr>
<td>为用户授予角色</td>
<td colspan="3">
<input name="role.id" class="easyui-combobox"       data-options="valueField:'id',textField:'name',url:'${pageContext.request.contextPath }/role_list.action'" />
</td>
</tr>


直接使用在RoleAction 的 list 方法

点击 保存按钮提交form表单

$('#save').click(function(){
// 先校验form
if($('#userForm').form('validate')){
$('#userForm').submit();
}else{
$.messager.alert('警告','表单存在非法数据项','warning');
}
});


在 UserAction 添加 save 方法

public void saveUser(User user) {
// 防止 Role的id 为空串
if (user.getRole() != null && user.getRole().getId() != null && user.getRole().getId().trim().length() == 0) {
user.setRole(null);
}
// 对密码 进行 md5 加密
user.setPassword(MD5Utils.md5(user.getPassword()));
userDAO.save(user);
}


配置结果跳转

<!-- 添加用户 -->
<result name="saveSUCCESS">
/WEB-INF/pages/admin/userlist.jsp
</result>


如果添加的中文账户,在登陆页面无法登陆 !



无法输入中文

5.2. 用户列表查询

/WEB-INF/pages/admin/userlist.jsp

修改 datagrid 的 url

url : "${pageContext.request.contextPath}/user_list.action",


在服务器 UserAction 提供 list 查询方法

<!-- 查询所有用户列表 -->
<result name="listSUCCESS" type="json">
<param name="root">users</param>
<param name="includeProperties">
\[\d+\]\.id,
\[\d+\]\.username,
\[\d+\]\.telephone,
\[\d+\]\.gender,
\[\d+\]\.station,
\[\d+\]\.salary,
\[\d+\]\.birthday,
\[\d+\]\.role\.name
</param>
</result>


修改 userlist.jsp 定义 datagrid 的 columns

添加 一列 显示角色

{
field : 'role',
title :'角色',
width : 400 ,
align : 'left',
rowspan : 2,
formatter : function(value,rowData,rowIndex){
if(value == null){
return "";
}else{
return value.name;
}
}
}


5.3. 为用户授予(修改)角色

1、 为用户列表 每行数据,添加右键效果

原始js 事件 : oncontextmenu 事件 (鼠标右键事件)

使用 datagrid 数据行 右键事件



阻止默认右键菜单显示 :

e.preventDefault(); // 阻止默认事件


2、 自定义右键菜单

使用 easyui 提供 menu 控件 制作菜单

<!-- 自定义菜单 -->
<div id="mm" class="easyui-menu" style="width:120px;">
<div onclick="alert('点击我了!');">菜单一</div>
<div>菜单二</div>
</div>


点击右键时,在鼠标位置弹出菜单

// 弹出自定义菜单
$('#mm').menu('show', {
left: e.pageX,
top: e.pageY
});


复制 “授予角色.txt”window代码 进行授权

业务代码

@Override
public void grantRole(User user) {
User exist = userDAO.findById(user.getId());
exist.setRole(user.getRole()); // 关联角色 自动更新
}


6. 使用代理进行权限拦截

基于 自定义注解 + 反射 + 代理 实现方法级别细粒度权限控制

URL粗粒度权限控制, 比如admin登陆,访问 /admin/* 开始页面

6.1. 自定义注解

/**
* 自定义注解
*
* @author seawind
*
*/
@Retention(RetentionPolicy.RUNTIME)
// 运行时使用
@Target(value = { ElementType.METHOD })
// 修改方法
@Inherited
// 使用注解应用具有继承性
public @interface Privilege {
String value(); // 这个属性代表访问业务方法 需要权限
}


在需要进行权限控制业务方法上,使用该注解

// 业务方法 --- 添加用户
@Privilege("添加用户")
public String save() {
// 调用业务层 保存用户
userService.saveUser(user);
return "saveSUCCESS";
}


6.2. 为目标业务方法建立代理

为目标创建对象创建代理 : JDK动态代理、Cglib动态代理、 静态代理

对于Struts2 Action 而言,Interceptor 就是代理

public class PrivilegeInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
return null;
}
}


6.3. 在代理中通过反射解析注解信息,控制权限

public class PrivilegeInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
// 假设 用户已经登陆
// 1、 判断目标Action业务方法上,是否具有Privilege 注解
Class c = invocation.getAction().getClass(); // 目标Action的Class对象
String methodName = invocation.getProxy().getMethod(); // 目标业务方法名称
Method method = c.getDeclaredMethod(methodName);

// 判断是否具有注解
if (method.isAnnotationPresent(Privilege.class)) {
// 有注解 ,需要权限
// 2、 获得注解中需要权限
Privilege privilege = method.getAnnotation(Privilege.class);
String needPrivilege = privilege.value();

// 3、判断当前用户是否具有该权限
User user = (User) ServletActionContext.getRequest().getSession().getAttribute("user");
if (PrivilegeUtils.checkHasPrivilege(user, needPrivilege)) {
// 有权限
return invocation.invoke();
} else {
// 没有权限
return "noprivilege";
}
} else {
// 无注解,不需要权限
return invocation.invoke();
}
}
}


抽取 PrivilegeUtils 工具类

/**
* 判断 用户是否具有 访问权限
*
* @param user
* @param needPrivilege
* @return
*/
public static boolean checkHasPrivilege(User user, String needPrivilege) {
// admin 直接放行
if (user.getUsername().equals("admin")) {
return true;
}

// 正常判断流程
Role role = user.getRole();
if (role == null) {
// 当前登录用户没角色,没权限
return false;
} else {
// 有角色
Set<Function> functions = role.getFunctions();
for (Function function : functions) {
if (function.getName().equals(needPrivilege)) {
// 满足权限
return true;
}
}
return false;
}
}


在 struts.xml 配置 权限拦截器

<!-- 注册拦截器  -->
<interceptors>
<interceptor name="login" class="cn.itcast.bos.web.interceptor.LoginInterceptor"></interceptor>
<interceptor name="privilege" class="cn.itcast.bos.web.interceptor.PrivilegeInterceptor"></interceptor>
<!-- 定义新的拦截器栈  -->
<interceptor-stack name="loginStack">
<interceptor-ref name="defaultStack"></interceptor-ref>
<interceptor-ref name="login"></interceptor-ref>
</interceptor-stack>
<interceptor-stack name="privilegeStack">
<interceptor-ref name="defaultStack"></interceptor-ref>
<interceptor-ref name="login"></interceptor-ref>
<interceptor-ref name="privilege"></interceptor-ref>
</interceptor-stack>
</interceptors>

<!-- 设置默认拦截器栈 -->
<default-interceptor-ref name="privilegeStack"></default-interceptor-ref>


定义全局结果集 noprivilege 页面

<!-- 配置全局结果集 -->
<global-results>
<result name="login">/login.jsp</result>
<result name="noprivilege">/noprivilege.jsp</result>
</global-results>


问题: 控制权限 出现 No Session 异常?

原因,参考图四

解决: 在登陆业务方法中 UserServiceImpl 的 login 方法

public User login(User user) {
List<User> list = userDAO.findByNamedQuery("User.login", user.getUsername(), MD5Utils.md5(user.getPassword()));
User loginUser = list.isEmpty() ? null : list.get(0);
if (loginUser.getRole() != null) {
// 手动对用户管理 权限信息初始化
Hibernate.initialize(loginUser.getRole().getFunctions());
}
return loginUser;
}


7. 在应用权限管理后,控制页面显示

7.1. 动态菜单

根据当前用户具有权限,来显示菜单

修改 /WEB-INF/pages/common/index.jsp

在动态返回 json 菜单数据时, ztree使用 SimpleData ,存在id 和 pId , 如果使用pId 返回会有问题, 配置ztree setting ,将返回pId 定制为 parentId

var setting = {
data : {
key : {
title : "t" // 鼠标在停留在菜单上提示
},
simpleData : { // 简单数据
enable : true,
pIdKey: "parentId",
}
},
callback : {
onClick : onClick
}
};


将页面 加载menu.json 的url 改为动态地址

url : '${pageContext.request.contextPath}/function_menu.action',


在FunctionAction 提供 menu 方法,返回当前用户具有权限对应菜单

public String menu() {
// 复杂查询 使用 QBC
DetachedCriteria detachedCriteria = DetachedCriteria.forClass(Function.class);

// 查询当前用户具有权限菜单
User user = (User) ServletActionContext.getRequest().getSession().getAttribute("user");
if (!user.getUsername().equals("admin")) {// 如果用户是admin 需要显示所有功能
// 多表关联,每关联一个表,创建一个别名
detachedCriteria.createAlias("roles", "r");
detachedCriteria.createAlias("r.users", "u");
detachedCriteria.add(Restrictions.eq("u.id", user.getId()));
}
// 查询generateMenu 为 1 的功能
detachedCriteria.add(Restrictions.eq("generateMenu", "1"));
// 按照zindex 排序
detachedCriteria.addOrder(Order.asc("zindex"));

// 调用业务层
List<Function> functions = functionService.findTreeData(detachedCriteria);
// 压入值栈
ActionContext.getContext().put("functions", functions);

return "menuSUCCESS";
}


配置 struts-auth.xml 返回结果

<!-- 动态菜单 -->
<result name="menuSUCCESS" type="json">
<param name="root">functions</param>
<param name="includeProperties">
\[\d+\]\.id,
\[\d+\]\.name,
\[\d+\]\.parentId,
\[\d+\]\.click,
\[\d+\]\.page
</param>
</result>


没有click属性 ,在Function添加 getClick 方法

public boolean getClick() {
if (page == null || page.trim().length() == 0) {
// 没有page属性
return false;
} else {
return true;
}
}


7.2. 页面内部动态功能按钮

实现细粒度显示控制 ,控制到页面内部每一个按钮的显示

使用 自定义标签来实现



步骤一: 自定义标签类

使用简单标签库

/**
* 自定义标签类
*
* @author seawind
*
*/
public class PrivilegeTag extends SimpleTagSupport {

// 接收属性
private String value; // 显示内容需要的权限

public void setValue(String value) {
this.value = value;
}

@Override
public void doTag() throws JspException, IOException {
User user = (User) ServletActionContext.getRequest().getSession().getAttribute("user");
if (PrivilegeUtils.checkHasPrivilege(user, value)) {
// 具有权限, 显示标签体内容
this.getJspBody().invoke(null); // 等价
// this.getJspBody().invoke(this.getJspContext().getOut());
}
}
}


步骤二: 配置标签描述文件 tld



修改头信息 , 没有提示

<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"> 
<tag>
<name>privilege</name>
<tag-class>cn.itcast.bos.web.tag.PrivilegeTag</tag-class>
<body-content>scriptless</body-content>
<!-- 定义属性 -->
<attribute>
<name>value</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue> <!-- 接收EL -->
</attribute>
</tag>


步骤三: 在jsp导入标签库

<%@taglib  uri="http://www.itcast.cn/tag" prefix="itcast"%>
<itcast:privilege value="收派标准">
{
id : 'button-add',
text : '增加',
iconCls : 'icon-add',
handler : doAdd
},
</itcast:privilege>


其它

课前资料



课后资料



权限数据模型分析



自定义权限模型原理分析



控制权限发生noSession



需要功能动态生成菜单分析



课程视频内容



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息