和 Spring Security 一样,Shiro 也属于权限安全框架。和 Spring Security 相比,Shiro 更简单,学习曲线更低。关于 Shiro 的一系列特征及优点,很多文章已有列举,这里不再逐一赘述。这里记下学习 Spring 4.x + Shiro 1.2 的过程,可能有水平不够的地方,敬请指正。
所有操作其实离不开理论、基础概念。虽然有点啰嗦、晦涩,但出于真正掌握的目的,仍是要强调其价值的。Shiro 为 Java 程序提供了认证(Authentication)、授权(Authorization)、加密(Encryption)和会话(Session)等等诸多功能。这里所提的话题若展开来说一个个那都是宏大的命题,因此本文将会蜻蜓点水般点出概念。
归根到底,最后结果是我到底能不能做某样事情,可以对该命题作出 true 或 false 的结果。若展开来讲里面又分几个层次,首先的是“用户”,用户有用户名和密码,显然那是自然而然要存在的事物,没有用户便没有余下的操作。用户于 Shiro 框架中所对应的概念是 Subject;然后我们把“能不能做事情”的操作分为权限 Permission 和角色 Role 两大抽象概念。Permission 可以理解为对一个资源的操作,典型的如 CRUD 操作,可以是多个的。但是这里务必强调,我们用户不能直接和权限 Permission 打交道,而是必须经过 Role。角色 Role 实质是“包着”权限的,等于是权限的集合。——为什么要“如此费劲”呢?其中之要义比较难一时半刻说清楚。随着理解的深入我们会渐渐明白其用心的。这里我们要清楚,用户信息与角色 Role 之间构成了多对多关系,表示同一个用户可以拥有多个 Role,一个 Role 可以被多个用户所拥有,而 Role 又与 Permission 之间构成多对多关系,如下面类图所示。
大概是这几种逻辑过程了,我们要好好懂得 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 为我们提供了丰富的异常准备。
若身份验证成功的话,会直接跳转到之前的访问地址或是 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 具体作用就是获取用户权限信息,也就是“授权”就是搞清楚我是谁之后,确定我能够做什么的问题。
一个 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 更“接地气”的应用。