在[Shiro-认证]中讲解了如何使用Shiro实现登陆后访问URL, 对于大部分系统来讲, 登陆只是安全的第一道屏障, 系统中的某些页面须要登陆后访问, 而有些是须要有特定权限才能够访问, 好比删除, 冻结, 查看帐号收益等敏感的操做.git
本文将带你实现基于Shiro的权限控制, Shiro中叫作受权github
系统中有A,B,C三个用户, 其中A用户是管理员, B和C是普通用户. 系统中的全部删除操做必须由管理员帐号登陆才能完成. 普通用户是没法删除数据甚至连删除按钮都看不见. 咱们说A,B,C三个用户在系统中有不一样的权限. A有删除数据的权限, B和C没有删除数据的权限. 试想一下若是没有权限设计, 全部用户均可以删除数据, 假设B是新手不当心误操做删除了数据... 后果将不堪设想.web
又例如银行的金库, 若是没有权限控制全部人均可以刷卡进入, 那岂不是要乱套. 生活中权限无处不在: 进出小区刷卡, 电梯刷卡到指定楼层, 视频网站中会员不须要看广告, 这些都是权限.数据库
假如你作了一个交友网站, 里面有查看异性的基本信息, 查看微信, 查看电话, 查看家庭住址几个功能, 普通的用户只能查看基本信息, 不能查看联系方式等. 充值100元能够查看微信, 充值200元能够查看电话, 充值500元能够查看家庭住址.apache
你必需要作权限控制, 不然用户经过其余手段(好比知道URL)就能够查看联系方式, 也就没有人给你付费了. 最初, 你可能想到这么处理权限: 用一张数据表记录每一个用户能够作什么事. 当用户查看微信时找到登陆用户的权限判断是否能够查看微信.浏览器
用户 | 基本信息 | 查看微信 | 查看电话 | 查看住址 |
---|---|---|---|---|
张三 | √ | |||
李四 | √ | √ | ||
王五 | √ | √ | √ | √ |
随着时间的增长会员愈来愈多, 有一天你新加了一个功能: 查看对方视频介绍, 只有充值500的人才能查看. 因而你须要把上表中全部用户的权限都修改一遍. 若是有几十万会员, 可能你就会累到吐血....安全
聪明的你想到了一个办法, 设置会员等级, 充值100为普通会员, 充值200元为VIP, 充值500为VIP中P. 给每个会员设置会员等级. 此时你的数据表结构以下:bash
会员等级 | 基本信息 | 查看微信 | 查看电话 | 查看住址 | 查看视频 |
---|---|---|---|---|---|
充值100元: 初级会员 | √ | ||||
充值200元: VIP | √ | √ | √ | ||
充值500元: VIP中P | √ | √ | √ | √ | √ |
用户名 | 会员等级 |
---|---|
张三 | 普通会员 |
李四 | VIP |
王五 | VIP中P |
赵六 | VIP中P |
这时, 当用户查看微信时, 根据用户找到会员等级, 在找到对应的权限. 虽然多了一步操做, 但:微信
总之, 权限只针对会员等级, 和会员并没有直接关联. 这里的会员等级就至关于系统中的角色, 基于角色的权限方案被不少系统所采用, 有了一个专有名词: RBAC-基于角色的权限访问控制.app
通俗的说就是根据用户的角色来判断是否有权限访问某个资源或URL. RBAC的模型是经典的5张表:
系统预先设计好角色, 资源, 角色资源关系. 当新建用户时只须要添加用户角色关系便可实现对该用户的权限控制. 例如: 孙七注册了用户并充值200元, 咱们能够直接设置孙七为VIP, 经过孙七的角色VIP就能够从角色资源关系中找到对应的可操做的URL.
本文中的操做是基于[Shiro-认证]之上完成的, 建议先看完Shiro认证部分. Shiro的认证是经过内置的认证过滤器(authc)完成的, 同时也提供了一些受权相关的过滤器:
port
访问的端口不是定义的端口时重定向至定义的端口,对应类为org.apache.shiro.web.filter.authz.PortFilter
filterChainDefinitionMap = [
"/**" : "port[9090]" // 若是不是经过9090端口将会重定向至9090端口访问
]
复制代码
访问http://localhost:8080/user/list
, 端口为8080
, 该请求被port过滤器拦截, 重定向至9090
端口, 即http://localhost:9090/user/list
, port过滤器适用于项目端口变动期间兼容原有用户访问或将老版本系统自动切换到新版本(8080部署老版本, 9090部署新版本)
ssl
非https访问443端口时, 重定向使用https访问443端口. 对应类为org.apache.shiro.web.filter.authz.SslFilter
filterChainDefinitionMap = [
"/**" : "ssl" // 不能够设置端口号,非https访问443端口会被重定向以https方式访问443端口
]
复制代码
访问http://localhost:456/user/list
, 因为http方式访问456
端口, 该请求被port过滤器拦截重定向至https://localhost/user/list
(80,443端口默认不显示), 适用于新增SSL证书后须要https访问, 兼容原有使用http访问的用户.
roles
用户必须具备配置的角色才能够访问. Shiro会调用Realm
中查询受权信息的方法获取用户的角色. 对应类为org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
filterChainDefinitionMap = [
"/**" : "roles['admin,guest']" // 访问用户必须同时具有admin和guest角色才能够访问
]
复制代码
如配置成roles["admin"]
表明只要是admin
角色就能够访问, 两个及以上角色表明必须同时知足.
perms
filterChainDefinitionMap = [
"/user/add" : "perms['user:add']" // 访问用户必须拥有user模块的add权限
]
复制代码
用户必须具备配置的权限才能够访问, Shiro会调用Realm中查询受权信息的方法查询用户的权限. 对应类为org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
user:add
表明user
模块的add
权限, 权限设计能够按模块划分并细化到模块的每一个功能点, 好比用户(user)模块中Admin角色有添加(add)用户权限, 删除(delete)用户权限, 数据库中可存储Admin拥有的权限为user:add
, user:delete
, 当访问/user/add
请求时, Shiro会经过Realm获取对应的权限, 若是含有user:add
便可访问该请求, 没有该权限禁止访问.
如shiro中只配置到模块级别可使用user:*
进行通配符验证. perms[user:*:add]
表明访问权限为user模块下全部子模块(*
匹配子模块)的添加(add)权限
rest
对应类为org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
filterChainDefinitionMap = [
// 访问用户必须拥有user模块的对应权限, GET请求表明read
// 已GET方式的请求必须拥有user:read权限才能够访问
"/user/*" : "rest[user]"
]
复制代码
将请求方式与增删改查操做对应, 当以POST
方式访问URL时, 过滤器认为须要对模块进行create
操做, 用户必须拥有user:create
权限, 不一样的请求方式对应不一样的权限. 具体以下表:
HTTP请求方式 | Shiro对应的操做 | 系统中须要授予用户的权限(以user模块为例) |
---|---|---|
delete | delete | user:delete |
head | read | user:read |
get | read | user:read |
put | update | user:update |
post | create | user:create |
mkcol | create | user:create |
options | read | user:read |
trace | read | user:read |
此过滤器将http请求方式和权限进行绑定, 能够算是perms过滤器的另外一种实现方式. 因为浏览器对部分HTTP请求方式支持的不友好, 此过滤器应用较少.
上述内置过滤器中能够支持RBAC的有roles
, perms
, rest
, 其中roles
只定义了角色, perms
, rest
的规则也是须要在Shiro配置文件中进行配置模块及权限. 若是系统增长功能并设置权限时还须要同步修改配置文件(修改后须要从新启动Tomcat). 有没有一种灵活的方式能够实现增长功能时不须要修改系统代码呢, 参考下面的思路:
WEB应用中全部的操做都是基于URL的, 例如: /user/add
是添加用户, /article/delete
是删除文章. 若是咱们将URL设置给角色. 当用户访问某一个URL时, 咱们只须要对比该用户拥有的权限集中是否含有该URL便可.
例: 张三的角色为部门经理, 拥有添加用户(/user/add
)和编辑用户(/user/edit
)权限, 当张三登陆系统后访问/user/add
, 经过Realm获取张三的权限后对比发现URL(/user/add
)在其权限列表中, Shiro容许访问. 当访问/user/delete
时因为URL不在其权限中, 所以Shiro拒绝访问.
全部的URL请求都使用上述方式实现, 配置文件中就不须要定义每一个URL对应的权限了. 所以新增功能时也就不须要修改系统代码了.
Shiro并无内置这种形式的过滤器, 须要咱们本身实现, 新建类继承AuthorizationFilter
类重写isAccessAllowed
方法. 后面文章会讲到isAccessAllowed
是Shiro过滤器的一个核心方法: 判断当前过滤器的验证是否成功, 若是成功则放行(访问控制器).
/**
* 自定义基于URL的受权过滤器
* 经过用户访问的URL,从数据库中查询用户是否有访问该URL的权限
*/
public class URLAuthorizationFilter extends AuthorizationFilter {
/**
* 是否容许访问资源
*/
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response,
Object mappedValue) throws Exception {
// 获取访问的URL
String requestUrl = WebUtils.toHttp(request).getRequestURI();
// 判断用户是否有权限访问该URL
// 调用isPermitted方法时Shiro会经过Realm获取用户拥有的权限集合
// 并判断URL是否在权限集合中, 若是在权限集合中返回true
return getSubject(request, response).isPermitted(requestUrl);
}
}
复制代码
自定义的过滤器须要在Shiro中进行定义, 并配置URL须要受权才能访问
// 配置自定义过滤器,名称为authz
authz(URLAuthorizationFilter) {
// 无权限页面: 用户无权限时重定向至该页面
unauthorizedUrl = "/unauthorized.jsp"
}
复制代码
// 配置URL规则
// 有请求访问时Shiro会根据此规则找到对应的过滤器处理
filterChainDefinitionMap = [
"/unauthorized.jsp" : "anon", // 未受权页不须要受权便可访问
"/logout" : "logout", // 登出使用logout过滤器
"/login": "authc", // 登陆页不配置受权
"/**": "authc, authz" // 其他全部页面须要认证和受权(顺序:先认证后受权)
]
复制代码
Shiro须要使用Realm获取用户的权限集合, 所以须要在Realm中增长一个获取权限的方法
// 自定义查询用户信息的Realm
// 受权须要继承AuthorizingRealm(只认证继承AuthenticatingRealm便可)
public class UserRealm extends AuthorizingRealm {
// 获取用户权限信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取当前登陆用户的用户名
// Shiro会将doGetAuthenticationInfo返回的用户信息保存至PrincipalCollection中
String username = ((User) principals.getPrimaryPrincipal()).getUsername();
// 模拟数据库查询, 根据用户名查询能够访问的权限URL集合
Set<String> permSet = getPermissions(username);
// 将权限URL集合设置至Shiro中,受权时会今后处获取权限URL
SimpleAuthorizationInfo authz = new SimpleAuthorizationInfo();
authz.setStringPermissions(permSet);
return authz;
}
// 模拟根据用户名在数据库中查询用户全部的权限URL
// 数据库中可根据用户找到角色,角色找到资源
private Set<String> getPermissions(String username) {
Set<String> permSet = new HashSet<String>();
// "atd681"有下列页面的访问权限
if ("atd681".equals(username)) {
permSet.add("/page/a");
permSet.add("/page/b");
}
// 其余用户有下列页面的访问权限
else {
permSet.add("/page/x");
}
return permSet;
}
// 获取用户信息的方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
// shiro-认证中的登陆逻辑
}
// 模拟根据用户名在数据库查询用户信息
private User getUser(String username) {
// shiro-认证中的模拟获取用户信息
}
}
复制代码
启动项目, 使用atd681
登陆后分别访问/page/a
和/page/b
能够正常访问. 访问/page/x
时重定向至未受权页面
上述权限控制是当用户访问URL时在服务端进行受权校验. 在页面中咱们并无根据权限控制连接或按钮是否显示, 不控制连接或按钮的显示会存在如下问题:
所以, 当用户没有某功能权限时页面中不该该显示功能对应的连接或按钮(刻意显示连接吸引用户付费等场景除外), 咱们须要在JSP中对连接或按钮进行权限判断, 没有权限时不显示对应的连接或按钮.
Shiro为咱们提供了一套在JSP中能够判断认证或受权的标签, 在/page/a的JSP中添加以下代码:
JSP头部增长Shiro标签的引用
<%@ tagliburi ="http://shiro.apache.org/tags" prefix="shiro"%>
复制代码
JSP中使用shiro:hasPermission根据用户的权限来控制是否显示连接或按钮
<body>
系统菜单:
<!--
该标签根据name值判断当前用户是否有该页面的访问权限
无权限时不显示该连接(调用subject.isPermitted方法进行验证)
-->
<shiro:hasPermission name="/page/a">
<a href="/page/a">A</a>
</shiro:hasPermission>
<shiro:hasPermission name="/page/b">
<a href="/page/b">B</a>
</shiro:hasPermission>
<shiro:hasPermission name="/page/x">
<a href="/page/x">X</a>
</shiro:hasPermission>
<br> PAGE_A, 当前登陆用户ID: ${userId}, 用户名: ${userName}
<a href="/logout">登出</a>
</body>
复制代码
<shiro:hasPermission>
中的name
属性为连接的URL, 判断用户是否有权限访问URL<shiro:hasPermission>
返回true
的时候, 标签内的HTML
才会被返回到客户端org.apache.shiro.web.tags
目录下, 有兴趣能够本身查看启动项目, 使用atd681
登陆后访问/page/a
, 因为用户atd681
有访问/page/a
和/page/b
的权限, 连接A,B被显示. 没有访问/page/x的权限, 连接X没有显示.
至此, 基于Shiro受权的示例配置完成. 有兴趣的同窗能够多建立几个用户测试一下.
atd681-shiro-authz