Shiro 学习应用

简介: 和 Spring Security 一样,Shiro 也属于权限安全框架。和 Spring Security 相比,Shiro 更简单,学习曲线更低。关于 Shiro 的一系列特征及优点,很多文章已有列举,这里不再逐一赘述。

和 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 更“接地气”的应用。

目录
相关文章
|
9月前
|
缓存 数据库 数据安全/隐私保护
【Shiro】第三章 Shiro入门(四)
【Shiro】第三章 Shiro入门(四)
55 0
|
9月前
|
存储 算法 数据安全/隐私保护
【Shiro】第三章 Shiro入门(三)
【Shiro】第三章 Shiro入门(三)
60 1
|
9月前
|
存储 算法 程序员
【Shiro】第三章 Shiro入门(二)
【Shiro】第三章 Shiro入门(二)
56 1
|
9月前
|
安全 数据安全/隐私保护
【Shiro】第三章 Shiro入门(一)
【Shiro】第三章 Shiro入门
53 1
|
9月前
|
存储 缓存 安全
Shiro学习之Shiro简介
Shiro学习之Shiro简介
71 0
|
9月前
|
Java 数据库 数据安全/隐私保护
Shiro学习之Shiro基本使用(2)
Shiro学习之Shiro基本使用(2)
39 0
|
9月前
|
Java API 数据库
Shiro学习之Shiro基本使用(1)
Shiro学习之Shiro基本使用(1)
63 0
|
缓存 安全 Java
Shiro框架学习
Shiro框架学习
157 0
Shiro框架学习
|
SQL 安全 Java
Shiro - 基础篇(下)
Shiro - 基础篇(下)
97 0
Shiro - 基础篇(下)
|
缓存 Java 数据库
Shiro - 基础篇(上)
Shiro - 基础篇(上)
115 0
Shiro - 基础篇(上)