Shiro-受权(RBAC)

0. 前言

[Shiro-认证]中讲解了如何使用Shiro实现登陆后访问URL, 对于大部分系统来讲, 登陆只是安全的第一道屏障, 系统中的某些页面须要登陆后访问, 而有些是须要有特定权限才能够访问, 好比删除, 冻结, 查看帐号收益等敏感的操做.git

本文将带你实现基于Shiro的权限控制, Shiro中叫作受权github

1. 什么是权限

系统中有A,B,C三个用户, 其中A用户是管理员, B和C是普通用户. 系统中的全部删除操做必须由管理员帐号登陆才能完成. 普通用户是没法删除数据甚至连删除按钮都看不见. 咱们说A,B,C三个用户在系统中有不一样的权限. A有删除数据的权限, B和C没有删除数据的权限. 试想一下若是没有权限设计, 全部用户均可以删除数据, 假设B是新手不当心误操做删除了数据... 后果将不堪设想.web

又例如银行的金库, 若是没有权限控制全部人均可以刷卡进入, 那岂不是要乱套. 生活中权限无处不在: 进出小区刷卡, 电梯刷卡到指定楼层, 视频网站中会员不须要看广告, 这些都是权限.数据库

2. 权限设计方案

假如你作了一个交友网站, 里面有查看异性的基本信息, 查看微信, 查看电话, 查看家庭住址几个功能, 普通的用户只能查看基本信息, 不能查看联系方式等. 充值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张表:

  • 用户: 记录系统的用户信息, 登陆时使用. 例: 张三, 李四...
  • 角色: 记录系统中存在的角色. 例: 普通会员, VIP...
  • 资源: 记录系统中的有哪些能够作的事. 在WEB中对应的就是URL, 例: 查看资料URL, 查看微信URL, 查看电话....
  • 用户角色关系: 记录用户所属的角色, 例: 张三是普通会员, 李四是VIP...
  • 角色资源关系: 记录了某个角色能够作什么事, 例: VIP能够查看资料, 查看微信...

系统预先设计好角色, 资源, 角色资源关系. 当新建用户时只须要添加用户角色关系便可实现对该用户的权限控制. 例如: 孙七注册了用户并充值200元, 咱们能够直接设置孙七为VIP, 经过孙七的角色VIP就能够从角色资源关系中找到对应的可操做的URL.

3. Shiro中实现RBAC

3.0 内置过滤器

本文中的操做是基于[Shiro-认证]之上完成的, 建议先看完Shiro认证部分. Shiro的认证是经过内置的认证过滤器(authc)完成的, 同时也提供了一些受权相关的过滤器:

3.0.1 端口过滤器: 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部署新版本)

3.0.2 SSL过滤器: 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访问的用户.

3.0.3 角色过滤器: roles

用户必须具备配置的角色才能够访问. Shiro会调用Realm中查询受权信息的方法获取用户的角色. 对应类为org.apache.shiro.web.filter.authz.RolesAuthorizationFilter

filterChainDefinitionMap = [
    "/**" : "roles['admin,guest']" // 访问用户必须同时具有admin和guest角色才能够访问
]
复制代码

如配置成roles["admin"]表明只要是admin角色就能够访问, 两个及以上角色表明必须同时知足.

3.0.4 权限过滤器: 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)权限

3.0.5 REST风格权限过滤器: 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请求方式支持的不友好, 此过滤器应用较少.

3.1 自定义权限过滤器

上述内置过滤器中能够支持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);

    }

}
复制代码

3.2 配置权限过滤器

自定义的过滤器须要在Shiro中进行定义, 并配置URL须要受权才能访问

// 配置自定义过滤器,名称为authz
authz(URLAuthorizationFilter) {
    // 无权限页面: 用户无权限时重定向至该页面
    unauthorizedUrl = "/unauthorized.jsp"
}
复制代码
// 配置URL规则
// 有请求访问时Shiro会根据此规则找到对应的过滤器处理
filterChainDefinitionMap = [
    "/unauthorized.jsp" : "anon", // 未受权页不须要受权便可访问
    "/logout" : "logout", // 登出使用logout过滤器
    "/login": "authc", // 登陆页不配置受权
    "/**": "authc, authz" // 其他全部页面须要认证和受权(顺序:先认证后受权)
]
复制代码
  • 受权和认证的顺序, 先认证后受权, 若是用户未登陆, 没法获取用户所拥有的权限信息(受权时发现未认证会跳转登陆页).
  • 登陆页不须要受权: authc不会处理登陆页, 若是配置到authz中, 会出现死循环(authz认为未登陆重定向到登陆页)

3.3 Realm实现获取权限

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-认证中的模拟获取用户信息
    }

}
复制代码
  • 获取权限信息的Realm必须继承AuthorizingRealm实现doGetAuthorizationInfo方法
  • 获取权限时根据关系(用户>角色>资源)找到用户所拥有的资源(可访问的URL)
  • 须要将获取的权限集合(Set)设置到SimpleAuthorizationInfo类中并返回至Shiro
  • 本例中用户atd681能够访问a,b页面, 不可访问x页面

4. 测试

启动项目, 使用atd681登陆后分别访问/page/a/page/b

能够正常访问. 访问/page/x时重定向至未受权页面

5. 视图层控制权限

上述权限控制是当用户访问URL时在服务端进行受权校验. 在页面中咱们并无根据权限控制连接或按钮是否显示, 不控制连接或按钮的显示会存在如下问题:

  • 用户无权限时点击连接或按钮后没法访问, 用户体验较差.
  • 暴露了系统该功能的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没有显示.

6. 示例代码

至此, 基于Shiro受权的示例配置完成. 有兴趣的同窗能够多建立几个用户测试一下.

相关文章
相关标签/搜索