控制层、功能权限、菜单权限、按钮权限、Shiro
# 引言
平台基于 Shiro 框架实现功能权限管理,Shiro 是 Apache 的一个开源框架,实现用户认证、用户授权、菜单授权、按钮授权、最小化颗粒度授权等。
只要有用户参与一般都要有权限管理,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
# 用户身份认证
用户去访问系统,系统需要验证用户身份的合法性,证明您是否是合法的用户。
最常用的用户身份验证的方法:
- 用户名、密码方式:通过登录界面进入系统
- 用户名、安全密钥方式:通过SSO接口或OAuth2接口进入系统
- 基于硬件或证书验证方法:通过特定的硬件或证书进入系统
也就是说,系统验证用户身份合法,用户方可访问系统的资源。
主要包括:
- 主体对象(Subject):理解为用户,可能是程序,都要去访问系统的资源,系统需要对subject进行身份认证,获取方法:UserUtils.getSubject()
- 身份信息(Principal):通常是唯一的,一个主体还有多个身份信息,但是都有一个主身份信息(Primary principal),获取方法:UserUtils.getLoginInfo()
# 用户权限授权
是对用户身份认证的细化。可简单理解为访问控制,在用户身份认证通过后,系统对用户访问菜单或按钮进行控制。也就是说,该用户有身份进入系统了,但他不一定能访问系统里的所有菜单或按钮,而他只能访问管理员给他分配的权限菜单或按钮。
主要包括:
- Permission(权限标识、权限字符串):针对系统访问资源的权限标识,如:用户添加、用户修改、用户删除,判断方法:UserUtils.getSubject().isPermitted(permissions);
- Role (角色):可以理解为权限组,也就是说角色下可以访问和点击哪些菜单、访问哪些权限标识。
权限标识或权限字符串校验规则:
- 权限字符串:指定权限串必须和菜单中的权限标识匹配才可访问
- 权限字符串命名规范为:
模块:功能:操作
,例如:sys:user:edit
- 使用冒号分隔,对授权资源进行分类,如
sys:user:edit
代表系统模块:用户功能:编辑操作
- 设定的功能指定的
权限字符串
与当前用户的权限字符串
进行匹配,若匹配成功说明当前用户有该功能权限 - 还可以使用简单的通配符,如
sys:user:*
,建议省略为sys:user
(分离前端不能使用星号写法) - 举例1
sys:user
将于sys:user
或sys:user:
开头的所有权限字符串匹配成功 - 举例2
sys
将于sys
或sys:
开头的所有权限字符串匹配成功
# 权限管理模型
JeeSite 中的权限管理关键数据模型如下:
- 用户:登录账号、密码、用户类型
- 角色:角色名称、归属用户类型
- 菜单:菜单名称、菜单URL、权限标识
- 用户角色关系:用户编码、角色编码
- 角色菜单关系:角色编码、菜单编码
关系图如下:
【用户】 <---多对多---> 【角色】 <---多对多---> 【菜单/权限】
# 权限配置步骤
- 进入菜单管理:新增菜单和权限。其中权限类型的菜单,也可理解为系统资源,不会显示到用户菜单树中。
- 菜单中的 “权限标识” 即 Permission 是控制权限的关键字段,系统根据该权限标识 与 Controller 内指定的 @RequiresPermissions 注解进行对应,匹配成功后才可以访问该 URL 地址,此例是注解式验证。另外还有编程式、视图页面、URI控制方式,详见下文。
- 菜单配置完成后,进入角色管理:新增并给角色的功能权限,勾选对应的菜单和权限,建立角色菜单的关系。
- 角色配置完成后,进入机构管理及用户管理,创建机构,创建用户,选择相应角色,建立用户和角色关系。
- 这样整个权限体系建立完成,用户登录系统后,就可以通过用户关联的角色,获取菜单/权限了。
- 若提示“403 - 您的操作权限不足”,说明对应的 URL 当前用户没有权限访问,检查 Controller 的映射方法上的 @RequiresPermissions 注解,是否在 “菜单管理” 里的 “权限字符串” 中配置了,并且当前用户对应的角色是否拥有该权限菜单。
# 其它概念理解
# 管理员类型
- 超级管理员:主要为开发者使用的最高级别管理员,主要用于开发和调试,有些修改会直接影响系统的正常运行。
- 系统管理员:主要为客户方使用的管理员,用于一些基础数据配置,如机构、用户、权限、用户字典等,默认账号为admin。
- 二级管理员,是由系统管理员指定的,可以分担系统管理员的工作,可以管理用户、分配菜单权限和操作权限一种特殊角色,但它仅具备系统管理员指定范围的管理数据。
# 菜单权重
菜单权重是指什么样的用户或管理员可以操作或访问什么级别的菜单,对菜单的权重级别进行划分,比如:比较重要敏感的菜单,只有管理员才可以拥有。如下:
- 超级管理员可以访问二级管理员、系统管理员、超级管理员权重的菜单,但不允许访问默认权限(业务菜单);
- 系统管理员可以访问超级管理员指定给他的系统管理员及以下权限的部分菜单;
- 二级管理员可以访问超级或系统管理员指定给他的二级管理员及以下的部分菜单;
- 普通用户只能访问管理员指定给他的默认权重的菜单。
菜单权重的设置,会影响角色授权菜单的时候,列出的菜单和权限列表:
- 如果当前用户管理身份为二级管理员,则列出的是二级管理员菜单权重以下的菜单;
- 如果当前用户管理员身份是系统管理,则列出的是系统管理员菜单权重以下的菜单;
- 如果当前用户管理员身份是超级管理员,则列出的是超级管理员菜单权重下的菜单。
此举是为了更好的提高授权安全,不能越级授权,权限互相牵制等。
# 支持四种授权方式
- 编程式:通过 if/else 代码块来完成。
- 注解式:通过在执行的方法上放置相应的注解来完成,没有权限则抛出相应异常。
- 视图页面: 在视图页面(Beetl/JSP)通过相应的标签完成。
- 基于URI拦截:根据URI匹配,决定访问权限。
# 编程式
场景:编辑和审核共用一个资源,可以在资源内判断是否有审核权限,然后再去执行对应操作
Subject subject = UserUtils.getSubject();
subject.isAuthenticated(); // 是否身份验证授权通过
subject.isPermitted(permission); // 验证权限字符串
subject.isPermittedAll(permissions); // 验证权限字符串全部通过
subject.hasRole(roleIdentifier); // 验证是否有角色权限
2
3
4
5
例如:
if (subject.isPermitted("sys:user:edit")){
System.out.pirntln("当前用户有编辑用户权限");
}
2
3
# 注解式
场景:对 URL 资源、按钮 等操作进行权限过滤,如果用户跳过了界面验证,直接去访问 URL 地址也需要经过授权验证的。
在Controller的方法上指定如下注解:
- @RequiresPermissions (value={“sys:user:view”, “sys:user:edit”}, logical= Logical.OR):表示当前 Subject 需要权限 user:view 或 user:edit。
- @RequiresRoles(value={“admin”, “user”}, logical=Logical.AND):表示当前 Subject 需要角色 admin 和 user
- @RequiresAuthentication:表示当前Subject已经通过login进行了身份验证;
- @RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的。
- @RequiresGuest:表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
例如:
@RequiresPermissions(value="sys:user:edit")
@PostMapping(value = "save")
@ResponseBody
public String save(@Validated User user, HttpServletRequest request) {
}
2
3
4
5
# 视图页
场景:在视图上通过权限字符串,控制某个按钮或某个信息是否显示,提高用户体验。
判断是否有该权限:
- 单个权限验证:${hasPermi('sys:user:edit')}
- 多个AND关系:${hasPermi('sys:user:view,sys:user:edit', 'and')}
- 多个OR关系:${hasPermi('sys:user:view,sys:user:edit', 'or')}
例如:
<% if(hasPermi('sys:user:edit')){ %>
<a href="${ctx}/sys/user/form" class="btn btn-default btnTool"
title="新增用户"><i class="fa fa-plus"></i> 新增</a>
<% } %>
2
3
4
判断是否有某角色:
- 单个权限验证:${hasRole('ceo')}
- 多个AND关系:${hasRole('ceo,dept', 'and')}
- 多个OR关系:${hasRole('ceo,dept', 'or')}
例如:
<% if(hasRole('ceo')){ %>
<span>ceo</span>
<% } %>
2
3
# 基于URI拦截
场景:当不方便利用编程式、注解式等方式过滤权限时,可以使用 URI 控制权限,或全局控制某个 URI 地址的权限
application.yml(4.0.x:jeesite.yml):
shiro:
# URI 权限过滤器定义
filterChainDefinitions: |
/ReportServer/** = user
${adminPath}/** = user
2
3
4
5
格式:
URI地址及通配符 = 过滤器名称(支持多个,用英文逗号分隔并加双引号)
以上权限过滤器定义配置,是依照由上到下的第一次匹配优先原则,优先匹配成功的URI优先受用,支持通配符。
URI通配符:
? :匹配一个字符。
* :匹配零个或多个字符串。
** :匹配路径中的零个或多个路径。
2
3
认证过滤器名称:
- anon: 例如
/a/file/**=anon
没有参数,表示不需要登录,就可以访问资源 - user: 例如
/a/sys/user/**=user
没有参数,表示必须用户登录后才能访问资源,当登入操作时不做检查 - authc: 例如
/a/login=authc
没有参数,表示需要登录认证才能访问资源,一般用于登录接口
授权过滤器名称:
- perms:例如
/a/sys/user/**=perms[user:add:*]
,参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/a/sys/user/**=perms["user:add:*,user:modify:*"]
,当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。 - roles:例如
/a/sys/user/**=roles[admin]
,参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如/a/sys/user/**=roles["admin,guest"]
,每个参数通过才算通过,相当于hasAllRoles()方法。
**注意事项:**增加新的过滤器的时候,不要删掉默认的过滤器,顺序从上到下依次匹配,先符合条件的优先使用。如果默认的过滤器被删除,会话超时后,将不会跳转到登录页,则直接返回403页面。
# 异常处理
全局异常处理视图文件:
- 找不到页面处理:error/404.html (opens new window)
- 资源权限验证处理:error/403.html (opens new window)
- 后台表单验证处理:error/400.html (opens new window)
- 异常信息处理:error/500.html (opens new window)
自定义异常类:
建议利用 Spring 提供的 @ControllerAdvice 全局异常处理器注解
// 定义实现自己的异常类
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
}
// 异常类捕获定义
@ControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(CustomException.class)
public String loginException(CustomException e) {
return ServletUtils.renderResult(Global.FALSE, e.getMessage());
}
}
// 测试异常抛出
@Controller
public class TestCustomExController {
@GetMapping("/testCustomEx")
public String testCustomEx() {
// 异常抛出到 -> 异常类捕获定义
throw new CustomException("异常信息");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25