和 Spring Security 一样,Shiro 也属于权限安全框架。和 Spring Security 相比,Shiro 更简单,学习曲线更低。关于 Shiro 的一系列特征及优点,很多文章已有列举,这里不再逐一赘述。这里记下学习 Spring 4.x + Shiro 1.2 的过程,可能有水平不够的地方,敬请指正。
一点概念
所有操作其实离不开理论、基础概念。虽然有点啰嗦、晦涩,但出于真正掌握的目的,仍是要强调其价值的。Shiro 为 Java 程序提供了认证(Authentication)、授权(Authorization)、加密(Encryption)和会话(Session)等等诸多功能。这里所提的话题若展开来说一个个那都是宏大的命题,因此本文将会蜻蜓点水般点出概念。
- 所谓“认证”,就是搞清楚“我是谁”的过程:在认证过程中,用户需要提交实体信息(Principals)和凭据信息(Credentials)以检验用户是否合法。最常见的“实体/凭证”组合便是“用户名/密码”组合;
- “授权”就是搞清楚我是谁之后,确定我能够做什么的问题:一般情况下用户通过了身份验证可以登录到某系统,但是没有特定的权限,或者根本未经过授权,不准做任何事情(虽然登录了)。有时一种可能是用户虽然具有了某种程度的授权,却并未经过身份验证(典型如“游客”)。
- 加密就是不是明文报文的意思,得给人家看不出和破解不出那是啥的意思。
- 会话与 HTTP 服务器的“会话”有点贴近却不尽相同。
归根到底,最后结果是我到底能不能做某样事情,可以对该命题作出 true 或 false 的结果。若展开来讲里面又分几个层次,首先的是“用户”,用户有用户名和密码,显然那是自然而然要存在的事物,没有用户便没有余下的操作。用户于 Shiro 框架中所对应的概念是 Subject;然后我们把“能不能做事情”的操作分为权限 Permission 和角色 Role 两大抽象概念。Permission 可以理解为对一个资源的操作,典型的如 CRUD 操作,可以是多个的。但是这里务必强调,我们用户不能直接和权限 Permission 打交道,而是必须经过 Role。角色 Role 实质是“包着”权限的,等于是权限的集合。——为什么要“如此费劲”呢?其中之要义比较难一时半刻说清楚。随着理解的深入我们会渐渐明白其用心的。这里我们要清楚,用户信息与角色 Role 之间构成了多对多关系,表示同一个用户可以拥有多个 Role,一个 Role 可以被多个用户所拥有,而 Role 又与 Permission 之间构成多对多关系,如下面类图所示。
大概是这几种逻辑过程了,我们要好好懂得 Shiro 具体是怎么做的,以及学会运用它。
调用 & 配置 Shiro
程序第一步的仍然是使用 Servlet 的过滤器,相当于“入口”。不过不是直接指定 Shiro 的类,而是通过 Spring MVC 的代理过滤器和 Spring IOC 两者合力加载 Shiro。这里发挥了 Spring 依赖注射的威力,使得配置 Shiro 变得简单(无须很多教程所使用的 ini 文件)。我们先看看 web.xml 的配置。
<!-- 通过过滤代理类与 Spring 集成 --> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- // -->
明显,我们定义了该 web 项目所有的 url 路径均受 Shiro 过问并加以控制,于是定义了 <url-pattern>/*</url-pattern> 全部路径。
其中注意尽量把这个过滤器放在其他过滤器之前,保证安全检验为“第一道板斧”;另外过滤器的名字(该例是 shiroFilter) 要与 Spring 里面配置的 bean 名字一致,方能正确调用。其中 init-param 声明的参数有何作用呢?原来是说明生命周期由 ServletContainer 管理(true 情况下如此,如果是 false 则是由 SpringApplicationContext 管理)。
上述是结合 Spring 的情形,如果没有 Spring 而是原生 Servlet 开发,那是这样的:
<listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener> ... <filter> <filter-name>ShiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>ShiroFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping
程序第二步是 MVC 的配置文件。既然上述提到 filter 的名字与 MVC 里面配置的一致,那么 Shiro 的配置在哪里呢?详见下面的 springMVC-servlet.xml。
ShiroFilterFactoryBean 是 Shiro 与 Spring 进行对接的工厂类,Spring 会在容器中查找名字为 shiroFilter(filter-name)的 bean 并将所有 Filter 的操作委托给它。Web 应用中 Shiro 控制的 Web 请求都必须经过 Shiro 主过滤器的拦截。关于过滤器的深入理解,可以参见这文章《ShiroFilterFactoryBean 源码及拦截原理深入分析》。
接着的工作就是如上图所示,一步步查找依赖的 bean。紧接着是 SecurityManager,为 Shiro 的核心类(典型的 Facade 模式),Shiro 通过 SecurityManager 来管理内部组件实例,处理了大部分认证授权会话的关键工作。这里我们是 Web 环境,使用了默认的 WebSecurityManager。Shrio 支持 Servlet 的 session 和其自身的 session,后者用于脱离 Web 的环境。WebSecurityManager 默认使用 Servlet 的 session。我们可通过 sessionMode 属性来指定使用 Shiro 原生 Session,即 <property name="sessionMode" value="native" />。
SecurityManager 中出现了一个必填的属性: Realm,它到底是什么呢?前面提到“我是谁”的一个问题,置于 Shiro 语境中就是 Realm 负责要解决的问题。也就是说,Shiro 获取所需要的用户信息,从 Realm 获取。用户信息包括用户账号名称、密码这一类信息。Realm 又从哪里获取这些信息呢?就是数据源——当然此处的数据源是个抽象的、广泛的概念。具体数据源可以是 JDBC(一般实际编码中就是 UserServcie 类提供)、LDAP 甚至 Shiro 默认的 ini 也可以。总之,我们可以说 Realm 是专用于安全框架的 DAO(Data Access Object)。Realm 在Shiro 具体对应的类是 AuthorizingRealm,另外还有现成的子类供我们使用:JdbcRealm、InitRealm、PropertiesRealm 等。如果不满足我们可以继承 AuthorizingRealm,并重写认证授权方法。
值得一提的是,配置多个 Realm 是可以的。若有多个 Realm,可用 'realms' 属性代替。如下例子所示。
<bean id="jdbcRealm" class="org.apache.shiro.realm.jdbc.JdbcRealm"> <property name="credentialsMatcher" ref="credentialsMatcher"></property> <property name="authenticationQuery" value="select password from user where username = ?"></property> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list> <ref bean="jdbcRealm" /> </list> </property> </bean>
图中的最后一步,我们定义了 shiroDbRealm 的 bean。这就是继承 AuthorizingRealm 的自定义 bean,由此我们可以看到 Shiro 是怎么认证和授权的工作的。
认证和授权
假设有一 url 正在受 Shiro 保护,用户访问的时候,Shiro 首先会对其身份进行识别,如果该身份通过验证,则接着进行权限的校验,否则跳到登录页面。这个过程就是代码中 AuthenticatingRealm.doGetAuthenticationInfo() 的逻辑。然后的权限校验(也称作 授权校验),需要的用户权限信息包括 Role 或 Permission,可以是其中任何一种或同时两者,具体取决于受保护资源的配置。如果用户权限信息未包含 Shiro 需要的 Role 或 Permission,则授权不通过。只有授权通过,才可以访问受保护 URL 对应的资源,否则跳转到“未经授权页面”。这个过程就是代码中 AuthenticatingRealm.doGetAuthorizationInfo() 的逻辑。值得注意的是 Authentication 和 Authorization 虽然字面贴近,但千万不要傻傻分不清,它们存在着微妙的不同。
下面用代码来说明上述过程。首先接收到请求的,仍然是控制器。
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @Controller("loginAction") @RequestMapping("/login") public class LoginAction { @RequestMapping("") //登录 public ModelAndView execute(HttpServletRequest request, HttpServletResponse response,String username,String password) { UsernamePasswordToken token = new UsernamePasswordToken(username,password); //记录该令牌 token.setRememberMe(false); //subject权限对象 Subject subject = SecurityUtils.getSubject(); try { subject.login(token); } catch (UnknownAccountException ex) {//用户名没有找到 ex.printStackTrace(); } catch (IncorrectCredentialsException ex) {//用户名密码不匹配 ex.printStackTrace(); }catch (AuthenticationException e) {//其他的登录错误 e.printStackTrace(); } //验证是否成功登录的方法 if (subject.isAuthenticated()) { return new ModelAndView("/main/index.jsp"); } return new ModelAndView("/login/login.jsp"); } //退出 @RequestMapping("/logout") public void logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); } }
控制器代码中用到了 UsernamePasswordToken。这里增加一点 Shiro 的令牌概念。在 Shiro 术语中,令牌 Token 指的是一个键,可用它登录到一个系统。最基本和常用的令牌是 UsernamePasswordToken,表示指定用户的用户名和密码。UsernamePasswordToken 类实现了 AuthenticationToken 接口,它提供了一种获得凭证和用户的主体(帐户身份)的方式。UsernamePasswordToken 适用于大多数应用程序,并且您还可以在需要的时候扩展 AuthenticationToken 接口来将您自己获得凭证的方式包括进来。例如验证码的应用就需要扩展这个 UsernamePasswordToken。
控制器中没有进行身份判断,该工作交到ShiroDbRealm 类完成。自定义的 Realm 如下代码,实现了 doGetAuthenticationInfo 和 doGetAuthorizationInfo,比较简单。
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthenticatingRealm; import org.apache.shiro.subject.PrincipalCollection; public class ShiroDbRealm extends AuthenticatingRealm { /** * * 认证回调函数,登录时调用. * 授权方法,在配有缓存的情况下,只加载一次。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String userName = token.getUsername(); if (user != null) { return new SimpleAuthenticationInfo(userName, token.getPassword(), getName()); } else { return null; } } /** * * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用. * */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String loginName = (String) principals.fromRealm(getName()).iterator().next(); Object user = ""; if (user != null) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addStringPermission("common-user"); return info; } else { return null; } } }
上述 doGetAuthenticationInfo(AuthenticationToken authcToken) 也有 UsernamePasswordToken。一般 MVC 的做法是在从 LoginController 里面 currentUser.login(token) 设置令牌,传到这里变成 authcToken,实际两个 token 的引用都是一样的。
这里为了简单起见,没有复杂的业务判断,实际过程还是需要一些控制的,例如 user 是否 null 等等。Shiro 为我们提供了丰富的异常准备。
- DisabledAccountException (禁用的帐号)
- LockedAccountException (锁定的帐号)
- UnknownAccountException(错误的帐号)
- ExcessiveAttemptsException(登录失败次数过多)
- IncorrectCredentialsException (错误的凭证)
- ExpiredCredentialsException (过期的凭证)
- ……
若身份验证成功的话,会直接跳转到之前的访问地址或是 successfulUrl 去。相关 url 在 MVC 配置文件中定义。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/common/security/login" /> <property name="successUrl" value="/common/security/welcome" /> <property name="unauthorizedUrl" value="/common/security/unauthorized" /> …… </bean>
doGetAuthorizationInfo(PrincipalCollection principals) 代码中用到了 Principal。Principal 是安全领域术语,即用户 Subject 之标识,一般情况下是唯一标识,比如用户名。doGetAuthorizationInfo 具体作用就是获取用户权限信息,也就是“授权”就是搞清楚我是谁之后,确定我能够做什么的问题。
URL过滤器的规则
一个 Web 程序下面的 URL 的权限肯定不会都相同的,因此我们需要配置 Shiro,声明不同 url 对应的权限。我们仍旧回看 springMVC-servlet.xml 配置文件。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/common/security/login" /> <property name="successUrl" value="/common/security/welcome" /> <property name="unauthorizedUrl" value="/common/security/unauthorized" /> <property name="filterChainDefinitions"> <value> /resources/** = anon /manageUsers = perms[user:manage] /** = authc </value> </property> </bean>
其中 filterChainDefinitions 配置了 url 对应的过滤器。Filter Chain 定义说明:URL目录是基于 HttpServletRequest.getContextPath() 此目录设置,也就是 web 网站的根目录;URL 可使用通配符,** 代表任意子目录;Shiro 验证 URL 时,URL 匹配成功便不再继续匹配查找。所以要注意配置文件中的 URL 顺序,尤其在使用通配符时;一个 URL 可以配置多个 Filter,使用逗号分隔,当全部 Filter 验证通过时方能通过 。
Filter Name | Class |
anon 匿名 | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc 表单 | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port | org.apache.shiro.web.filter.authz.PortFilter |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl | org.apache.shiro.web.filter.authz.SslFilter |
user | org.apache.shiro.web.filter.authc.UserFilter |
一些例子如下。
anon:例子 /admins/**=anon 没有参数,表示可以匿名使用。
authc:例如 /admins/user/**=authc 表示需要认证(登录)才能使用,没有参数。
authcBasic:例如 /admins/user/**=authcBasic没 有参数表示 httpBasic 认证。
roles:例子 /admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如 admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于 hasAllRoles() 方法。
perms:例子 /admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于 isPermitedAll() 方法。
rest:例子 /admins/user/**=rest[user],根据请求的方法,相当于 /admins/user/**=perms[user:method] ,其中 method 为post,get,delete 等。
port:例子 /admins/user/**=port[8081],当请求的 url 的端口不是 8081 是跳转到 schemal://serverName:8081?queryString,其中 schmal 是协议 http 或 https 等,serverName 是你访问的host,8081是url配置里port的端口,queryString 是你访问的 url 里的?后面的参数。
ssl:例子/admins/user/**=ssl没有参数,表示安全的 url 请求,协议为 https
user:例如 /admins/user/**=user 没有参数表示必须存在用户,当登入操作时不做检查
注:这些过滤器中 anon,authcBasic,auchc,user 是认证过滤器,perms,roles,ssl,rest,port 是授权过滤器。
小结
本文的例子不是一个完整实用的例子,旨在围绕 Shiro 各个知识点来阐述一下。接着我将会写关于 Shiro 更“接地气”的应用。