权限管理中,角色授权与认证属于权限模块中的关键模块,角色授权即是将角色能够操作的菜单资源分配给指定角色的行为,角色认证即是当用户扮演指定角色登录系统后系统对于用户操作的资源进行权限校验的操作,意思这里说明白了,那么在代码中应该具体怎么实现呢?
## 角色授权与认证的方式
- 前端页面展示控制
- 后端权限访问控制
## 案例实操
### 角色授权
#### 树形数据展示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BgVO6ukv-1600737050094)(https://imgkr.cn-bj.ufileos.com/2c68bc08-2a92-43bf-b742-da0a717b1c03.png)]
完成角色记录基本 crud 功能之后,接下来实现角色授权功能,这里实现角色授权首先完成待授权资源显示功能。对于资源的显示,这里使用开源的 tree 插件 [ztree](http://www.treejs.cn/v3/main.php#_zTreeInfo "ztree")。
##### 资源数据查询后端实现
前端 ztree 显示的资源数据格式参考<a href='http://www.treejs.cn/v3/demo.php#_102'>这里</a>。
- ModuleMapper.xml
```xml
<select id="queryAllModules" resultType="com.xxxx.crm.dto.TreeDto">
select
id,
IFNULL(parent_id,0) as pId,
module_name AS name
from t_module
where is_valid=1
</select>
```
- ModuleService.java
```java
public List<TreeDto> queryAllModules(){
return moduleMapper.queryAllModules();
}
```
- ModuleController.java
```java
@RequestMapping("queryAllModules")
@ResponseBody
public List<TreeDto> queryAllModules(){
return moduleService.queryAllModules();
}
```
##### 资源数据 ztree 显示
- role.js 添加授权点击事件
```js
//头工具栏事件
table.on('toolbar(roles)', function(obj){
var checkStatus = table.checkStatus(obj.config.id);
switch(obj.event){
case "add":
openAddOrUpdateRoleDialog();
break;
case "grant":
openAddGrantDailog(checkStatus.data);
break;
};
});
function openAddGrantDailog(datas){
if(datas.length==0){
layer.msg("请选择待授权角色记录!", {icon: 5});
return;
}
if(datas.length>1){
layer.msg("暂不支持批量角色授权!", {icon: 5});
return;
}
var url = ctx+"/role/toAddGrantPage?roleId="+datas[0].id;
var title="角色管理-角色授权";
layui.layer.open({
title : title,
type : 2,
area:["600px","280px"],
maxmin:true,
content : url
});
}
```
- RoleController.java 添加视图转发方法
```java
@RequestMapping("toAddGrantPage")
public String toAddGrantPage(Integer roleId,Model model){
model.addAttribute("roleId",roleId);
return "role/grant";
}
```
- 准备显示资源数据模板
views/role 目录下添加 grant.ftl 模板文件
```html
<html>
<head>
<link rel="stylesheet" href="${ctx}/static/js/zTree_v3-3.5.32/css/zTreeStyle/zTreeStyle.css" type="text/css">
<script type="text/javascript" src="${ctx}/static/lib/jquery-3.4.1/jquery-3.4.1.min.js"></script>
<script type="text/javascript" src="${ctx}/static/js/zTree_v3-3.5.32/js/jquery.ztree.core.js"></script>
<script type="text/javascript" src="${ctx}/static/js/zTree_v3-3.5.32/js/jquery.ztree.excheck.js"></script>
</head>
<body>
<div id="test1" class="ztree"></div>
<input id="roleId" value="${roleId!}" type="hidden">
<script type="text/javascript">
var ctx="${ctx}";
</script>
<script type="text/javascript" src="${ctx}/static/js/role/grant.js"></script>
</body>
</html>
```
- 添加 grant.js
```js
var zTreeObj;
$(function () {
loadModuleInfo();
});
function loadModuleInfo() {
$.ajax({
type:"post",
url:ctx+"/module/queryAllModules"
dataType:"json",
success:function (data) {
// zTree 的参数配置,深入使用请参考 API 文档(setting 配置详解)
var setting = {
data: {
simpleData: {
enable: true
}
},
view:{
showLine: false
// showIcon: false
},
check: {
enable: true,
chkboxType: { "Y": "ps", "N": "ps" }
}
};
var zNodes =data;
zTreeObj=$.fn.zTree.init($("#test1"), setting, zNodes);
}
})
}
```
#### 角色授权
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5JmYMFS0-1600737050100)(https://imgkr.cn-bj.ufileos.com/f8996734-3edf-4901-85b8-39aa640f25b3.png)]
##### 权限记录添加后端实现
- RoleService.java
```java
public void addGrant(Integer[] mids, Integer roleId) {
/**
* 核心表-t_permission t_role(校验角色存在)
* 如果角色存在原始权限 删除角色原始权限
* 然后添加角色新的权限 批量添加权限记录到t_permission
*/
Role temp =selectByPrimaryKey(roleId);
AssertUtil.isTrue(null==roleId||null==temp,"待授权的角色不存在!");
int count = permissionMapper.countPermissionByRoleId(roleId);
if(count>0){
AssertUtil.isTrue(permissionMapper.deletePermissionsByRoleId(roleId)<count,"权限分配失败!");
}
if(null !=mids && mids.length>0){
List<Permission> permissions=new ArrayList<Permission>();
for (Integer mid : mids) {
Permission permission=new Permission();
permission.setCreateDate(new Date());
permission.setUpdateDate(new Date());
permission.setModuleId(mid);
permission.setRoleId(roleId);
permission.setAclValue(moduleMapper.selectByPrimaryKey(mid).getOptValue());
permissions.add(permission);
}
permissionMapper.insertBatch(permissions);
}
}
```
- RoleController.java
```java
@RequestMapping("addGrant")
@ResponseBody
public ResultInfo addGrant(Integer[] mids,Integer roleId){
roleService.addGrant(mids,roleId);
return success("权限添加成功");
}
```
##### 权限记录添加前端核心 js
修改 grant.js 文件 添加 ztree 复选框点击回调 onCheck 事件。
```js
var zTreeObj;
$(function () {
loadModuleInfo();
});
function loadModuleInfo() {
$.ajax({
type:"post",
url:ctx+"/module/queryAllModules",
dataType:"json",
success:function (data) {
// zTree 的参数配置,深入使用请参考 API 文档(setting 配置详解)
var setting = {
data: {
simpleData: {
enable: true
}
},
view:{
showLine: false
// showIcon: false
},
check: {
enable: true,
chkboxType: { "Y": "ps", "N": "ps" }
},
callback: {
onCheck: zTreeOnCheck
}
};
var zNodes =data;
zTreeObj=$.fn.zTree.init($("#test1"), setting, zNodes);
}
})
}
function zTreeOnCheck(event, treeId, treeNode) {
var nodes= zTreeObj.getCheckedNodes(true);
var roleId=$("#roleId").val();
var mids="mids=";
for(var i=0;i<nodes.length;i++){
if(i<nodes.length-1){
mids=mids+nodes[i].id+"&mids=";
}else{
mids=mids+nodes[i].id;
}
}
$.ajax({
type:"post",
url:ctx+"/role/addGrant",
data:mids+"&roleId="+roleId,
dataType:"json",
success:function (data) {
console.log(data);
}
})
}
```
##### 角色已添加权限记录回显
这里要实现已添加的角色记录权限再次查看或授权时显示原始权限的功能,ztree 复选框是否选择属性配置参考<a href='http://www.treejs.cn/v3/demo.php#_201'>这里</a>。
###### 资源查询后端方法实现
- ModuleService.java
```java
public List<TreeDto> queryAllModules02(Integer roleId) {
List<TreeDto> treeDtos=moduleMapper.queryAllModules();
// 根据角色id 查询角色拥有的菜单id List<Integer>
List<Integer> roleHasMids=permissionMapper.queryRoleHasAllModuleIdsByRoleId(roleId);
if(null !=roleHasMids && roleHasMids.size()>0){
treeDtos.forEach(treeDto -> {
if(roleHasMids.contains(treeDto.getId())){
// 说明当前角色 分配了该菜单
treeDto.setChecked(true);
}
});
}
return treeDtos;
}
```
- 角色拥有权限 sql 查询
```xml
<select id="queryRoleHasAllModuleIdsByRoleId" parameterType="int" resultType="java.lang.Integer">
select module_id from t_permission where role_id=#{roleId}
</select>
```
- ModuleController.java
```java
@RequestMapping("queryAllModules")
@ResponseBody
public List<TreeDto> queryAllModules(Integer roleId){
return moduleService.queryAllModules02(roleId);
}
```
###### 权限回显前端 js
这里修改 grant.js 查询资源时传入当前选择角色 id
```js
function loadModuleInfo() {
$.ajax({
type:"post",
url:ctx+"/module/queryAllModules",
data:{
roleId:$("#roleId").val()
},
dataType:"json",
success:function (data) {
// zTree 的参数配置,深入使用请参考 API 文档(setting 配置详解)
var setting = {
data: {
simpleData: {
enable: true
}
},
view:{
showLine: false
// showIcon: false
},
check: {
enable: true,
chkboxType: { "Y": "ps", "N": "ps" }
},
callback: {
onCheck: zTreeOnCheck
}
};
var zNodes =data;
zTreeObj=$.fn.zTree.init($("#test1"), setting, zNodes);
}
})
}
```
### 角色认证
当完成角色权限添加功能后,下一步就是对角色操作的资源进行认证操作,这里对于认证包含两块:
1. 菜单级别显示控制
2. 后端方法访问控制
#### 菜单级别访问控制实现
系统根据登录用户扮演的不同角色来对登录用户操作的菜单进行动态控制显示操作,这里显示的控制使用 [freemarker](http://freemarker.foofun.cn/ "freemarker") 指令+内建函数实现,指令与内建函数操作参考<a href='http://freemarker.foofun.cn/ref_builtins_sequence.html#ref_builtin_seq_contains'>这里</a>。
##### 登录用户角色拥有权限查询实现
- IndexController.java
```java
/**
* 后端管理主页面
* @return
*/
@RequestMapping("main")
public String main(HttpServletRequest request){
Integer userId = LoginUserUtil.releaseUserIdFromCookie(request);
request.setAttribute("user",userService.selectByPrimaryKey(userId));
List<String> permissions=permissionService.queryUserHasRolesHasPermissions(userId);
request.getSession().setAttribute("permissions",permissions);
return "main";
}
```
- PermissionService.java
```java
@Service
public class PermissionService extends BaseService<Permission,Integer> {
@Autowired
private PermissionMapper permissionMapper;
public List<String> queryUserHasRolesHasPermissions(Integer userId) {
return permissionMapper.queryUserHasRolesHasPermissions(userId);
}
}
```
- PermissionMapper.java & PermissionMapper.xml
```java
public interface PermissionMapper extends BaseMapper<Permission,Integer> {
List<String> queryUserHasRolesHasPermissions(Integer userId);
}
```
```xml
<select id="queryUserHasRolesHasPermissions" parameterType="int" resultType="java.lang.String">
select distinct p.acl_value
from t_user_role ur left join t_permission p on ur.role_id = p.role_id
where ur.user_id=#{userId}
</select>
```
##### 系统主页面菜单显示指令控制
这里仅显示部分菜单控制。
```html
<#if permissions?seq_contains("60")>
<li class="layui-nav-item">
<a href="javascript:;" class="layui-menu-tips"><i class="fa fa-gears"></i><span class="layui-left-nav"> 系统设置</span> <span class="layui-nav-more"></span></a>
<dl class="layui-nav-child">
<#if permissions?seq_contains("6010")>
<dd>
<a href="javascript:;" class="layui-menu-tips" data-type="tabAdd" data-tab-mpi="m-p-i-11" data-tab="user/index" target="_self"><i class="fa fa-user"></i><span class="layui-left-nav"> 用户管理</span></a>
</dd>
</#if>
<#if permissions?seq_contains("6020")>
<dd class="">
<a href="javascript:;" class="layui-menu-tips" data-type="tabAdd" data-tab-mpi="m-p-i-12" data-tab="role/index" target="_self"><i class="fa fa-tachometer"></i><span class="layui-left-nav"> 角色管理</span></a>
</dd>
</#if>
<#if permissions?seq_contains("6030")>
<dd class="">
<a href="javascript:;" class="layui-menu-tips" data-type="tabAdd" data-tab-mpi="m-p-i-13" data-tab="module/index" target="_self"><i class="fa fa-tachometer"></i><span class="layui-left-nav"> 菜单管理</span></a>
</dd>
</#if>
</dl>
</li>
</#if>
```
#### 后端方法级别访问控制
实现了菜单级别显示控制,但最终客户端有可能会通过浏览器来输入资源地址从而越过 ui 界面来访问后端资源,所以接下来加入控制方法级别资源的访问控制操作,这里使用 aop+自定义注解实现
##### 自定义注解@RequirePermission
```java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequirePermission {
String code() default "";
}
```
##### 方法级别使用注解
```java
@RequestMapping("list")
@ResponseBody
@RequirePermission(code = "101001")
public Map<String,Object> querySaleChancesByParams(Integer flag,HttpServletRequest request,SaleChanceQuery saleChanceQuery){
if(null !=flag &&flag==1){
// 查询分配给当前登录用户 营销记录
saleChanceQuery.setAggsinMan(LoginUserUtil.releaseUserIdFromCookie(request));
}
return saleChanceService.queryByParamsForTable(saleChanceQuery);
}
```
##### 定义aop切面类拦截指定注解标注的方法
```java
@Component
@Aspect
public class PermissionProxy {
@Autowired
private HttpSession session;
@Around(value = "@annotation(com.xxx.sys.annotaions.RequirePermission)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
List<String> permissions = (List<String>) session.getAttribute("permissions");
if(null == permissions || permissions.size()==0){
throw new NoPermissionException();
}
Object result =null;
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
RequirePermission requirePermission = methodSignature.getMethod().getDeclaredAnnotation(RequirePermission.class);
if(!(permissions.contains(requirePermission.code()))){
throw new NoPermissionException();
}
result= pjp.proceed();
return result;
}
}
```
## 扩展~自定义注解实例
从 JDK5 开始,Java 增加对元数据的支持,也就是注解,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。下面我们来看看如何自定义注解。需要视频配套资料或其他资料+我们小姐姐V lezijie007(加好友暗号 -98 ,不备注不加)
**创建自定义注解类**
```java
package com.lebyte.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/* @Target,@Retention,@Inherited,@Documented
* 这四个是对注解进行注解的元注解,负责自定义的注解的属性
*/
@Target({ElementType.TYPE,ElementType.METHOD}) //表示注解的作用对象,ElementType.TYPE表示类,ElementType.METHOD表示方法
@Retention(RetentionPolicy.RUNTIME) //表示注解的保留机制,RetentionPolicy.RUNTIME表示运行时注解
@Inherited //表示该注解可继承
@Documented //表示该注解可生成文档
public @interface Design {
String author(); //注解成员,如果注解只有一个成员,则成员名必须为value(),成员类型只能为原始类型
int data() default 0; //注解成员,默认值为0
}
```
**使用注解**
```java
package com.lebyte;
import com.lebyte.annotations.Design;
@Design(author="lebyte",data=100) //使用自定义注解,有默认值的成员可以不用赋值,其余成员都要赋值
public class Person {
@Design(author="lebyte",data=90)
public void live(){
}
}
```
**解析注解**
```java
package com.lebyte;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import com.lebyte.annotations.Design;
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class c=Class.forName("com.lebyte.Person"); //使用类加载器加载类
//1、找到类上的注解
if(c.isAnnotationPresent(Design.class)){ //判断类是否被指定注解注解
Design d=(Design) c.getAnnotation(Design.class); //拿到类上的指定注解实例
System.out.println(d.data());
}
//2、找到方法上的注解
Method[] ms=c.getMethods();
for(Method m:ms){
if(m.isAnnotationPresent(Design.class)){ //判断方法是否被指定注解注解
Design d=m.getAnnotation(Design.class); //拿到类上的指定注解实例
System.out.println(d.data());
}
}
//3、另外一种方法
for(Method m:ms){
Annotation[] as=m.getAnnotations(); //拿到类上的注解集合
for(Annotation a:as){
if(a instanceof Design){ //判断指定注解
Design d=(Design) a;
System.out.println(d.data());
}
}
}
}
}
```