文档原则:简洁全面、深入浅出

一、介绍

1.1 shiro初体验

二、上手

2.1 环境准备
2.2 项目搭建
2.3 使用Shiro

三、shiro结构

3.1 框架组成
3.2 shiro框架整体结构

四、shiro的主要概念及对象

五、shiro配置

4.1 通过java代码配置
4.2 通过ini配置文件配置

六、Shiro在Web应用中的使用

6.1 配置
6.2 默认的过滤器
6.3 打开和关闭过滤器
6.4 会话管理
6.5 “记住我”服务
6.6 JSP标签库

七、Spring中集成Shiro

7.1 Spring管理的独立应用
7.2 Spring管理的Web应用
7.3 激活Shiro的注解

一、介绍

  • shiro是身份认证和权限分配方面的优秀框架
  • 支持java各种程序,及各类容器
  • 内置Session功能,可以脱离servlet等web环境来进行会话管理
  • 单点登录,“Remember me”,等等功能,基本上你对于身份认证和权限分配方面的去求它都能满足
  • 支持企业级应用

1.1 shiro的源码和使用

1.1.1 获取Shiro源码(source-release):

  • 在shiro的官方下载页面上下载 shiro-root-1.4.0-source-release.zip, 可能会下载失败

  • 通过git clone从github上面获得源码包,非常慢
    git clone https://github.com/apache/shiro.git
    git checkout shiro-root-1.4.0 -b shiro-root-1.4.0

  • 直接在github上面打包下载shiro的源码包:https://github.com/apache/shiro.

1.1.2 编译执行shiro

获得到shiro的源码包后,将其解压,其中包含了一些示例程序。这些程序都是maven管理的工程,所以可以通过maven进行编译和执行。
进入/sample/quickstart文件夹,并使用maven编译执行
使用maven编译执行shiro-quickstart

可以修改/samples/quickstart/src/main/java/Quickstart.java中的代码来得到希望的结果。

二、上手

shiro非常强大,对于不同规模不同架构的系统,安全管理的实现需要很多灵活的功能配置,shiro都能一一应对。但是shiro又非常简单,在简单的命令行程序中也可以通过简单几行代码发挥其强大的身份认证和权限管理功能,本章介绍一个简单的上手示例,已经可以初窥shiro的核心能力。

2.1 环境准备

本文按照shiro官网文档测试要求,全部采用2.2.1以上版本的maven,和1.5及以上版本的java。测试方法如下:
java-maven-version-test.png

2.2 项目搭建

这里采用纯命令行的操作方式来搭建maven项目,以求叙述简洁明了。
1.创建项目文件夹

  • 创建项目根目录:/firstshiro:
    mkdir firstshiro
  • 在firstshiro下面创建源代码目录src/main/java
    mkdir src/main/java

2.导入shiro
在/firstshiro中添加pom.xml:
firstshiro/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.shiro.tutorials</groupId>
<artifactId>shiro-tutorial</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>First Apache Shiro Application</name>
<packaging>jar</packaging>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>2.0.2</version>
            <configuration>
                <source>1.5</source>
                <target>1.5</target>
                <encoding>${project.build.sourceEncoding}</encoding>
            </configuration>
        </plugin>

    <!-- 此plugin仅用来启动此测试程序,并不是shiro必须 -->
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>1.1</version>
            <executions>
                <execution>
                    <goals>
                        <goal>java</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <classpathScope>test</classpathScope>
                <mainClass>Firstshiro</mainClass>
            </configuration>
        </plugin>
    </plugins>
</build>

<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.1.0</version>
    </dependency>
    <!--Shiro使用slf4j作为日志工具,参见http://www.slf4j.org for more info. -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.6.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>
</project>

或者通过shiro的下载页面下载到各jar包,自行导入项目的classpath路径:
shiro下载页面中包含了各种版本的shiro的jar包和maven坐标。

  1. 添加包含main方法的Firstshiro类
    firstshiro/src/main/java/Firstshiro.java:

     import org.apache.shiro.SecurityUtils;
     import org.apache.shiro.authc.*;
     import org.apache.shiro.config.IniSecurityManagerFactory;
     import org.apache.shiro.mgt.SecurityManager;
     import org.apache.shiro.session.Session;
     import org.apache.shiro.subject.Subject;
     import org.apache.shiro.util.Factory;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     public class Firstshiro {
         private static final transient Logger log = LoggerFactory.getLogger(Firstshiro.class);
     
         public static void main(String[] args) {
             log.info("第一个shiro程序");
             System.exit(0);
         }
     }
    
  2. 运行demo
    在firstshiro文件夹下面执行maven运行:
    mvn compile exec:java
    mvn-run-firstshiro.png

2.3 使用Shiro

shiro的核心是SecurityManager类,一般情况下其通过一个shiro.ini配置文件进行配置。
在firstshiro/src/main下面创建resources文件夹,并在其中添加shiro.ini配置文件
mkdir src/main/resources
src/main/resources/shiro.ini

    #-----------------------------------------------
    #为用户分配角色
    #-----------------------------------------------
    [users]
    root = secret, admin
    guest = guest, guest
    presidentskroob = 12345, president
    darkhelmet = ludicrousspeed, darklord, schwartz
    lonestarr = vespa, goodguy, schwartz
    
    # ----------------------------------------------
    # 为角色分配权限
    # ----------------------------------------------
    [roles]
    admin = *
    schwartz = lightsaber:*
    goodguy = winnebago:drive:eagle5

在main方法中创建SecurityManager实例,使用shiro.ini初始化。
src/main/java/Firstshiro.java

import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.*;
    import org.apache.shiro.config.IniSecurityManagerFactory;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.subject.Subject;
    import org.apache.shiro.util.Factory;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    public class Firstshiro {
        private static final transient Logger log = LoggerFactory.getLogger("Firstshiro");
        public static void main(String[] args) {
            log.info("第一个shiro程序");
    		Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
    		SecurityManager securityManager = factory.getInstance();
    		SecurityUtils.setSecurityManager(securityManager);
        	System.exit(0);
        }
    }

IniSecurityManagerFactory通过指定的shiro.ini来获取配置信息,通过工厂方法设计模式来创建SecurityManager实例。支持三种文件修饰:"classpath:","url:","file:"来通过不同的方式来获取配置文件。
factory.getInstance()方法可以获取到SecurityManager实例,本例中获得的securityManager实例是静态的内存中全局唯一的单例对象。在其他容器中可以通过servlet或者spring bean来托管这个全局唯一的对象。

以上便实现了shiro环境。shiro根据用户来进行不同的行为,所以获取当前用户、是否允许当前用户执行某操作等是shiro的主要功能。

  • 获取当前用户
  •   Subject currentUser = SecurityUtils.getSubject();
    

这里SecurityUtils.getSubject()获取到的subject是shiro中的用户的概念,它将我们通常的用户泛化,可能表示一个人,也可能是其他的程序交互对象例如其他的程序。

  • 获取当前用户会话
  •   Session session = currentUser.getSession();
      session.setAttribute( "someKey", "aValue" );
    

session即为当前用户会话,其中保存了会话的所有信息,也可以用来保存自定义的会话状态数据。shiro的会话是一个定制的session,在web http环境的应用中,其基于HttpSession;在非web Http环境的应用中,完全使用内置的session管理器。这些对于开发者是无感的,开发者可以在各种环境中以完全相同的方式来使用shiro的session。

  • 认证当前用户
  •   if ( !currentUser.isAuthenticated() ) {
          //通过一定方式收集用户信息和凭据
          //例如: 用户名/密码, X509 certificate, OpenID等
          //此处使用最通用的用户名/密码
          UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
          //shiro内建'remember me'功能,仅需下面一句话
          token.setRememberMe(true);
          currentUser.login(token);
      }
    
  • 认证失败检查
  •   try {
          currentUser.login( token );
          //认证成功
      } catch ( UnknownAccountException uae ) {
          //未知用户
      } catch ( IncorrectCredentialsException ice ) {
          //密码错误
      } catch ( LockedAccountException lae ) {
          //账户被锁
      }
         // ... 
      } catch ( AuthenticationException ae ) {
          //其他认证异常
      }
    

AuthenticationException JavaDoc中有更多可供检查的认证异常。但是你通常提供给用户的只能是统一的提示信息,以免过于详细的程序运行信息被别有用心者利用。

  • 认证成功之后,获取登陆成功的用户信息

      log.info("当前登陆用户:"+currentUser.getPrincipal());
    
  • 测试当前用户的角色

      if ( currentUser.hasRole( "admin" ) ) {
          log.info("当前用户是admin" );
      } else {
          log.info( "当前用户不是admin" );
      }
    
  • 测试当前用户的权限

      if ( currentUser.isPermitted( "drivecar:alnoe" ) ) {
          log.info("你拥有独自驾驶汽车的权限");
      } else {
          log.info("你不能独自驾驶汽车");
      }
    
  • 登出

      //移除所有的登陆信息,删除session中的所有信息
      currentUser.logout(); 
    

通过以上的一点点实用功能,我们就有了第一个shiro完整功能的示例

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Firstshiro {
    private static final transient Logger log = LoggerFactory.getLogger("Firstshiro");
    public static void main(String[] args) {
        log.info("第一个shiro程序");
		Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
		SecurityManager securityManager = factory.getInstance();
		SecurityUtils.setSecurityManager(securityManager);
		// 获取当前登陆用户
        Subject currentUser = SecurityUtils.getSubject();
        // 获取session,并保存自定义属性
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]");
        }
        // 实用当前用户登陆,并验证其角色和权限
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true);
            try {
                currentUser.login(token);
            } catch (UnknownAccountException uae) {
                log.info("无此用户:" + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info("密码错误:" + token.getPrincipal());
            } catch (LockedAccountException lae) {
                log.info("账户被锁:" + token.getPrincipal());
            }
            // 检查认证异常
            catch (AuthenticationException ae) {
                //异常处理
            }
        }
       //显示当前登陆信息
        log.info("用户 [" + currentUser.getPrincipal() + "] 登陆成功");
        //测试角色
        if (currentUser.hasRole("admin")) {
            log.info("当前用户是admin");
        } else {
            log.info("不是admin");
        }
        //测试一个类型权限(非实例级别)
        if (currentUser.isPermitted("drive:alone")) {
            log.info("可以独自驾驶汽车");
        } else {
            log.info("不能独自驾驶汽车");
        }
        //强大的实例级别权限测试:
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("可以驾驶winnebago的eagle5");
        } else {
            log.info("无此权限");
        }
        //结束,登出
        currentUser.logout();
    	System.exit(0);
    }
}

下载以上代码firstshiro.zip

三、 shiro框架结构

3.1 核心结构

从总体上看,shiro主要由三个核心对象组成:SubjectSecurityManagerRealms,应用程序对于shiro框架的调用过程见下图:
ShiroBasicArchitecture.png

Subject 表示一个安全管理的对象,封装了认证和授权的绝大部分API接口,但是其具体认证授权的操作是通过SecurityManager中托管的其他安全对象实现的。在进行认证授权中,一版情况下,只需要调用Subject对象的接口即可完成。

SecurityManager 是shiro的核心对象,其管理了shiro的所有元件对象,负责组合和管理各个安全对象的协调。

Realms 是用户凭据和授权等数据的来源抽象,保存了认证授权的凭据和配置信息,其信息可以来自配置文件,或者JDBC,最终抽象为Realms供SecurityManager中的其他安全对象使用。

3.2 框架结构

下图是Shiro框架的架构图:
ShiroArchitecture.png

Subject(用户) (org.apache.shiro.subject.Subject)
Subject对象包含了认证授权的所有接口,是进行安全管理编程时的主要对象。

SecurityManager(安全管理器) (org.apache.shiro.mgt.SecurityManager)
如上图所示,SecurityManager包含了shiro框架的几乎所有安全管理相关的对象,包括Authentication、Authorization、Session Management、Cache Management、Realm coordination、Event propagation、“Remember Me” Services、Subject creation、Logout 等。
SecurityManager在某种意义上类似于shiro框架的容器,协调这些安全管理对象的配置和运行。
SecurityManager对象兼容javaBean规范,可以很方便的往Spring等bean容器中集成,也可以很方便的使用简单文本格式文件进行配置。

Authenticator(认证管理器) (org.apache.shiro.authc.Authenticator)
Authenticator用来执行用户的认证(登陆)操作。它会去在一个或者多个Realms中查找保存的用户账户等认证信息。Realms所管理的数据供Authenticator使用来检查登陆用户是否真的有权限登陆系统。

Authentication Strategy(认证策略)(org.apache.shiro.authc.pam.AuthenticationStrategy)
如果配置了多个Realm,认证策略会定义如何处理多个Realm之间数据冲突时的判断策略,例如如果按照一个Realm身份认证成功,另一个Realm认证失败,那么怎么决定最终结果?这时认证策略Authentication Strategy就起作用了。其定义是所有Realm都认证成功才算成功,还是第一个Realm认证成功就算成功,还是只要有一个Realm认证成功就算成功。

Authorizer(授权管理器)(org.apache.shiro.authz.Authorizer)
授权管理器Authorizer是用来定义用户权限控制的模块。它用来管理一个用户是否有某个权限。授权管理器和认证管理器一样,能够调用配置的数据来获取确定一个用户权限。

SessionManager(会话管理器)(org.apache.shiro.session.mgt.SessionManager)
会话管理器会根据shiro外部的系统环境来确定如何生成、销毁会话,它管理会话的生命周期。在Web/Servlet或者EJB容器环境,会话管理器会管理基于容器会话的会话(session),在其他环境,会话管理器会管理shiro内置的会话。shiro提供了SessionDAO供使用来进行会话数据的持久化。

SessionDAO(会话持久化对象)(org.apache.shiro.session.mgt.eis.SessionDAO)
会话持久化对象SessionDAO用来供会话管理器SessionManager使用,对会话数据进行增删改查等持久化操作。支持任意配置到shiro会话管理框架的数据源形式。

CacheManager(缓存管理器)(org.apache.shiro.cache.CacheManager)
缓存管理器专门用来管理shiro其他模块所使用的缓存实例。shiro的认证模块、授权模块、会话管理模块都支持多种形式的持久化数据源来访问和操作数据库或其他持久化存储,故shiro设计的时候就为这些功能添加了缓存功能,以提高效率。shiro支持目前主流的缓存框架,作为插件来为系统提供缓存功能,以提高整体性能和用户体验。

Cryptography(加密工具)(org.apache.shiro.crypto.*)
作为一套用来做认证和授权的安全框架,shiro提供了一套加密工具。shiro的加密机制中包含了许多易于理解和使用的密码,hash,编码等加密方式。java自带的加密工具较为繁琐难用,shiro的加密API对其进行了简化,更加简单易用且便于理解。

Realms(org.apache.shiro.realm.Realm)
Realm不太好翻译,其表示shiro中一个个保存了用户认证和授权定义信息的对象,这些对象的数据保存形式可能为配置文件,可能是数据库等等,但是最终都被抽象成shiro的Realm,供shiro的认证管理器和授权管理器来使用。

四、主要概念和对象

4.1 认证(Authentication)

ShiroFeatures_Authentication.png
Authentication用来验证用户的是否合法,是否拥有登陆系统的全力。其通过验证用户提交的Principal和Credential信息来判断用户是否能够通过认证。

  • Principal 是登陆用户用户名的抽象,shiro将用户user抽象为Principal主要是为了将其他的登陆者也涵盖进来,比如另外一个系统。

shiro会将一个principal当做一个用户,所以不同用户需要持有不同的Principal,这就要求realms中配置的principal都应该是全局唯一的。

  • Credential 是密码的一个抽象,一般就是与Principal对应的一个凭据信息,当用户提供的Principal和Credential能够与Realm中配置的信息匹配的时候,此用户即通过认证。
    一般情况下,principal就是用户名,credential就是密码。

4.1.1 用户认证操作

使用shiro进行安全管理的应用系统的用户认证应该分为以下三步:

  1. 收集用户提交的用户名和密码

    //用户名密码组成的一个token对象:UsernamePasswordToken
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //使用shiro内置的"Remember Me"功能:
    token.setRememberMe(true);
    shiro不关心用户名和密码从何而来,可以是通过html的form表单提交,也可以是通过http的header提交,也可以是命令行参数等等。shiro通过AuthenticationToken接口将自己的功能内聚于认证的逻辑,与认证信息收集解耦。
    UsernamePasswordToken 是Token接口的一个实现,表示使用用户名和密码作为用户认证凭证的token。使用其setRemeberMed方法即可使用Shiro的RememberMe功能,使用户在一段时间之后再访问时自动认证。

  2. 提交用户名和密码进行认证
    当获得到用户认证凭据,并且封装成为一个AuthenticationToken接口的对象之后,即可进行认证:

    //获取当前用户
    Subject currentUser = SecurityUtils.getSubject();
    //调用当前用户subject对象的login方法,并将封装了认证凭据的AuthenticationToken对象传递进去,即执行认证。
    currentUser.login(token);

  3. 认证成功或者失败后的处理
    如果Subject对象的login方法没有抛出异常正常返回,则表示认证成功。这时SecurityUtils.getSubject()方法返回是通过认证的Subject对象,Subject对象的isAuthenticated()会返回true。
    如果Subject对象的login方法没有正常返回,有异常抛出则表示认证过程出现问题,认证失败。shiro内建了丰富的AuthenticationException认证异常,在login方法外面加上trycatch块即可捕捉认证失败所抛出的异常信息:

    try {
    currentUser.login(token);
    } catch ( UnknownAccountException uae ) { ...
    } catch ( IncorrectCredentialsException ice ) { ...
    } catch ( LockedAccountException lae ) { ...
    } catch ( ExcessiveAttemptsException eae ) { ...
    } ...捕获其他异常 ...
    } catch ( AuthenticationException ae ) {
    //未知错误?
    }
    //正常返回,登陆成功...

4.1.2 “remembered” 和 “authenticated”(记住的和认证的)

前面收集认证信息的示例代码中,展示了在登陆认证时,如何使用shiro的“remember me”功能。需要指出的是shiro中的通过记住我功能通过认证的用户,与通过登陆通过认证的用户并不完全相同。

  • 记住的:一个被记住的用户和普通的匿名用户不同,其通过subject.getPrincipals()能够获得到用户名。但是这个用户信息是从之前的会话中保留过来的,并不是当前会话认证的。通过调用subject.isRemembered()方法,即可查看当前用户是从之前的会话记住的。
  • 认证的:通过认证的用户,指在当前会话中通过subject.login*方法,通过了认证。通过调用subject.isAuthenticated()方法即可知道当前用户是否是“通过认证”的。

"remembered" 与 "authenticated"的互斥性
当前登陆用户,通过调用subject.isRemembered()方法和subject.isAuthenticated()方法即可知道其是否是记住的和认证的。通过以上对于两者的描述可知,如果当前用户是“remembered”记住的,那么就肯定不是“authenticated”认证的;反之如果是“authenticated”认证的,就肯定不是“remembered”记住的。

为什么要区分“remembered” 和“authenticated”?

shiro比较严谨的控制用户的登陆认证状态,来保证系统安全,同时更加灵活和强大的进行认证管理。因此将“authenticated”状态设置为严格需要在当前会话提供有效凭据的用户,“remembered”的用户也可以进行普通的操作,但是对于一些敏感操作,可以通过“authenticated”来要求用户。
“remembered”的用户,仅仅是从之前会话中保存了部分用户信息,来告诉系统当前用户的一些基本信息,提高用户体验。但是它的认证凭据均未在本次会话中有效提交,因此不应该允许其进行敏感操作。
因此尽管系统通过“remember me”功能,可以允许用户在不登录状态下查看某些信息,形成特定的用户视图等,但是不应该允许“remembered”的用户进行实质性的操作,当需要进行这些操作时,还是应该要求用户进行登录认证,将“remembered”状态升级为“authenticated”。
例如:可以使用isRemembered()来检查是否为当前用户呈现基本信息,使用isAuthenticated()来为当前用户检查是否能够进行数据操作。

"remembered"和"authenticated"的应用示例

对于Amazon.com,你登录成功,并添加几本书到购物车。但是你未登出系统即紧急离开去开会了。会议结束回来后就直接下班回家了。
第二天上班,你又想起来昨天的书还没有完成付款,因此你又打开了Amazon.com。这次Amazon.com记住了你是谁,并且在页面上显示你的用户名,并且根据你平时的浏览喜好列出了一些购书建议。对于Amazon来说,subject.isRemembered()为true。
但是当你想更新你的信用卡信息来完成付款的时候,Amazon.com不能确认你的确拥有当前账户的操作权力,需要你补充凭据重新认证以确认你的确拥有此账户的操作权。这样当你进行敏感操作(更新信用卡信息)前,Amazon会强制你进行登录认证来确保你拥有修改的权力。登录认证成功后,isAuthenticated()即会返回true。
shiro内建了“remember me”功能,并且能够方便的检查当前用户是否是remembered,来帮助程序来管理用户认证状态。

4.1.3 登出

登录认证所对应的就是登出。当用户完成所有的操作后,可以调用subject.logout()方法来丢弃所有的身份信息,包括浏览器中的RememberMe cookie也会被删除:

currentUser.logout(); //丢弃所有的身份信息,并使当前的会话变为未认证的状态

Web应用注意事项
Web应用的remembered标识信息一般通过cookies来保存,而cookies只能在一个response体被提交前才能被删除,所以建议在调用过subject.logout()之后,采用重定向(redirect)的方法跳转到另外一个页面。这样能够保证相关的cookies能够被删除。

4.1.4 SecurityManager中的认证执行机制

[用户认证]介绍了采用shiro作为安全管理的系统,登陆认证的一般步骤。这里介绍进行登录认证时,shiro内部的处理机制。
下图中仅保留了shiro中与认证相关的安全组件,以及标记了处理认证操作时个组件的执行次序:
ShiroAuthenticationSequence.png

步骤1
应用程序的代码调用subject.login方法 ,并且将用户名密码构造为AuthenticationToken传入认证凭据作为参数
步骤2
Subject用户实例,通过代理调用实际执行认证的SecurityManager.login(token)方法。
步骤3
作为容器的SecurityManager,将token有通过内部代理的Authenticator实例的authenticator.authenticate(token)方法。通常这里的authenticator是一个ModularRealmAuthenticator实例,支持多个Realm的环境。ModularRealmAuthenticator提供了一个PAM风格的范例,将每个Realm实例都当做一个模块。
步骤4
如果环境中配置了多个Realm实例,ModularRealmAuthenticator实例会使用为其配置的AuthenticationStrategy初始化一个Multi-Realm认证。在Realms被调用来做认证之前,期间和之后,都会调用AuthenticationStrategy来获取Realm的结果。后面会介绍AuthenticationStrategy。

单Realm应用
如果仅配置了一个Realm,会直接调用此Realm,单Realm应用不需要AuthenticationStrategy。
步骤5
ModularRealmAuthenticator会去查看所有的Realm是否支持提交的凭据类型(AuthenticationToken)。如果支持,那么会调用Realm的getAuthenticationInfo方法,并传入token。Realm的getAuthenticationInfo方法表示一次认证。后面会介绍Realm的认证时怎么执行的。

认证管理器 Authenticator

如前所诉,当用户通过subject.login(token)提交一个认证请求时,SecurityManager会使用其代理的ModularRealmAuthenticator实例来执行认证操作。ModularRealmAuthenticator支持单Realm应用和多Realm应用。
在一个单Realm应用中,ModularRealmAuthenticator会直接调用唯一的一个Realm来进行认证。如果应用中有多个Realm,ModularRealmAuthenticator会使用AuthenticationStrategy实例来协调多Realm的认证。
还可以为SecurityManager配置一个自定义的Authenticator实例,如下:

[main]
...
authenticator = com.foo.bar.CustomAuthenticator
securityManager.authenticator = $authenticator

但是实际使用中,默认的ModularRealmAuthenticator实例可以满足几乎所有的需求,一般不需要自定义Authenticator。

认证策略 AuthenticationStrategy

对于配置有多个Realm的应用,ModularRealmAuthenticator需要一个shiro内部的AuthenticationStrategy组件来定义如何协调多个Realm的认证结果,怎么通过多个Realm的认证结果来决定最终的认证结果。
例如,如果有一个Realm认证成功,其他的都失败,那么最终的认证结果是成功还是失败?是否需要所有的Realm都认证成功才认为最终的结果成功?是否只要有一个Realm认证成功即认为最终的认证结果为成功?AuthenticationStrategy中会指定遇到以上情形时,shiro应该如何做能够满足应用的需求。
一个AuthenticationStrategy是一个无状态的组件,在认证过程中会被调用4次,这4次调用中需要的状态,都通过参数传入方法。以下是4次调用:
1.在所有Realm调用前
2.在每一个Realm的getAuthenticationInfo方法调用前
3.在每一个Realm的getAuthenticationInfo方法调用后
4.在所有的Realm调用后
AuthenticationStrategy负责收集每个realm的认证记过,并集成到一个AuthenticationInfo上面。这个AuthenticationInfo就是最终被认证器返回的认证结果。

用户标识“视图”
如果应用中配置了多个Realm,并从多个数据源中获取认证数据,AuthenticationStrategy负责最终的整合用户标识到一个统一的视图中供应用程序使用。
shiro有三个AuthenticationStrategy实现:
|AuthenticationStrategy实现类|描述|
|---|---|
|AtLeastOneSuccessfulStrategy|有等于或者多余一个Realm认证成功,则认为最终的认证结果为成功;所有的realm都失败,则认为认证失败|
|FirstSuccessfulStrategy|将第一个认证成功的Realm的认证结果作为最终结果,其他的Realm会被忽略;所有的realm都认证失败,则认为认证失败|
|AllSuccessStrategy|所有的Realm都认证成功,则认为成功;有一个及以上Realm认证失败,则认为失败|
SecurityManager中代理的ModularRealmAuthenticator实例默认采用AtLeastOneSuccessfulStrategy作为AuthenticationStrategy实现。也可以在shiro.ini中配置采用其他的AuthenticationStrategy:

[main]
...
authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $authcStrategy
...

定制AuthenticationStrategy
如果shiro提供的三个AuthenticationStrategy实现都满足不了需求,可以使用org.apache.shiro.authc.pam.AbstractAuthenticationStrategy作为基础来定制自己的AuthenticationStrategy。

Realm认证次序

对于配置了多个Realm的应用程序,ModularRealmAuthenticator会按照迭代的顺序来调用Realm进行认证。
当执行一个认证的时候,ModularRealmAuthenticator会迭代realm集合,并且调用每一个支持提交的AuthentionToken的Realm的getAuthenticationInfo方法。

1> 隐式的认证次序
默认的ModularRealmAuthenticator会按照shiro.ini中配置的Realm的顺序来调用其getAuthenticationInfo方法。

blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm

采用了以上的Realm配置,SecurityManager会有三哥Realm,在每一次的认证操作中,blahRealm,fooRealm和barRealm会按照配置的顺序被调用。
不需要显式的将这三个realm赋值给securityManager的realm属性,shiro会自动将shiro.ini中的realm实例装配进SecurityManager对象的realm属性。

但是这一句配置并不是必须的,没必要显示的为SecurityManager对象指定realm,只要是配置文件中出现的realm实例都会自动被装入SecurityManager对象的realm属性中。

2> 显式的认证次序
也可以在shiro.ini中,显式的将配置的realm实例装入SecurityManager的realm属性中:

securityManager.realms = $blahRealm, $fooRealm, $barRealm

ModularRealmAuthenticator会按照这里显式的集合顺序来调用Realm进行认证。

如果显式的为SecurityManager的realm属性指定了realm实例,那么隐式的realm装配机制将不会生效,只有显式赋值的realm才会被装入SecurityManager对象中,其他的realms都会被忽略。

4.1.5 Realm中的认证执行机制

用户通过login方法请求执行认证,会被SecurityManager通过代理的ModularRealmAuthenticator实例按照AuthenticationStrategy的认证策略在多个或者一个Realm中调用getAuthenticationInfo方法获得认证结果并汇总成为最终的认证结果。那么其实真正的认证行为都是在Realm的getAuthenticationInfo方法中执行的。在Realm部分的Realm 认证部分会详细介绍。

4.2 授权(Authorization)

授权管理也就是权限控制,用来控制某个用户对于特定资源的访问权限。不同的用户可以访问不同的页面,就是通过配置不同的权限,通过授权管理实现的。

4.2.1 授权管理所涉及的元素

shiro中有三个核心的授权管理元素经常使用到:permissions,roles和users。通过用户、角色和许可三个元素的配合使用可以实现丰富的授权策略。

Permission许可

permission许可用来表示在一个在应用程序中可以进行的基本操作,是shiro中进行权限控制的最小单元。permission可以配置出允许一个当前登录用户所能访问的某类型资源,和在此类型资源上面可以进行的操作,还可以指定对于此类型资源的某个实例来进行操作。但是permission许可并不包含任何关于用户分配权限的信息,仅用来描述一个个系统的基本操作。对于大多数类型资源的操作都是增删改查,部分资源需要除此之外的其他操作。

以下是permission许可所表示的行为示例:

  • 打开一个文件
  • 查看'/user/list'页面
  • 打印文档
  • 删除‘robert’用户
    不同系统依据其用户、角色等的数据组织方式的不同,有不同的权限分配方式。
    例如,可以为某个角色分配多个许可,再将这个角色分配到某个用户上,那么这个用户就拥有了为此角色所分配的所有许可。如果多个用户属于一个用户组,那么可以为此用户组分配某个角色,那么这个用户组下面的所有用户都拥有了此角色的所有许可。
    还可以采取其他更加灵活的用户-角色-许可组织和关联方式,来适配应用程序,实现需要的授权管理模型。

1> 许可的粒度
一般情况下,许可所描述的操作一般为对于某类型资源的增删改查。一些情况下,许可也可以描述更细粒度的行为,例如:删除用户名为‘robert’的用户。shiro可以实现你需要的任何粒度控制。

2> 使用通配符来编写许可表达式
shiro支持使用通配符来编写permission,这样可以使permission更加灵活,且易读。

使用通配符编写简单许可
要编写一个打印机使用的许可,表示一些人能够往指定打印机发送打印任务,其他人可以查看当前打印队列中的任务。最简单的实现方式是未一部分用户分配"queryPrinter"权限。之后可以通过以下语句查看某个登录的用户是否有"queryPrinter"的权限:
subject.isPermitted("queryPrinter");
shiro会自动将字符串"queryPrinter"组装成为WildcardPermission,subject通过调用SecurityManager中管理的Authorization对象来查询当前登录用户是否拥有此许可。等同于以下语句:
subject.isPermitter(new WildcardPermission("queryPrinter"));
以上验证只有为当前登录用户分配了“queryPrinter”许可才能验证成功。如果为一个用户拥有通配符*的许可,那么此用户拥有此应用程序的所有许可,也可以通过验证。
通配符还可以应用在复杂许可上面,来表示更丰富的意义。

使用通配符编写复杂许可
许可可以包含多个部分,以 资源类型:操作:实例 的结构来表示更细粒度的操作。例如上例中的“queryPrinter”许可,可以写作:“printer:query”。其中的冒号用来分割许可中的多个部分。第一部分的“printer”表示操作对象为“printer”,第二部分的“query”表示操作为“query”(查询)。类似的对于打印机的管理许可可以写作:“printer:manage”
shiro并没有限制许可能够包含多少个部分,可以根据自己应用程序来设计许可的结构。
常用的复杂许可是实例级别的权限控制。使用第一部分表示资源类型,第二部分表示操作,第三部分表示实例。例如:
printer:query:lp7200,printer:print:epsoncolor,第一个表示查询ID为lp7200的打印机,第二个表示使用ID为epsoncolor的打印机打印。可以使用如下代码来检查许可:

if ( SecurityUtils.getSubject().isPermitted("printer:query:lp7200")) {
    // 返回lp7200打印机上面当前的任务
}

三部分实例级别许可是比较常用的且强大的复杂许可的应用方式。但是必须为每个打印机分配不同的实例ID,当拥有新的实例ID的打印机添加进来后,就需要更新许可,此时可以使用通配符*来表示某类型资源的某操作,例如:printer:print:*, 甚至可以使用通配符*来表示某类型资源的所有实例的所操作 printer:*:*,或者某类型资源的某个实例的所有操作printer:*:lp7200,或者某类型资源的某个实例的若干操作:pinter:query,print:lp7200, 通过灵活使用通配符'*'和分隔符',' ,可以实现很灵活的许可定义。

多个值
许可的每个部分可以包含多个值,这样可以将一类型资源的多种操作写到一个许可上面,上面的两个许可
printer:query
printer:manage
可以简写为 pinter:query,manage\

所有值
如果需要定义一个表示某类型资源的所有操作的许可,可以使用通配符表示所有操作。例如:printer:query,print,manage 可以简写为printer:*
这时所有subject.isPermitted("printer:XXX")都会返回true。
类似的,也可以将所有类型资源的查看操作写成一个许可,例如:*:view 表示 所有类型资源的查看操作许可。\

缺省值
如果一个许可只定义了多个部分中的几个部分,那么缺少的部分自动补为*,匹配此部分的所有值,例如:printer:print等价于printer:print:*,printer等价于printer:*:*,但是printer:lp7200不等价于printer:*:lp7200。

3> 检查许可
在定义许可的时候可以使用通配符*来匹配所有当前部分的值,来简化许可表达式。但是在进行许可验证的时候,应该尽可能的使用完整的许可语句来验证。例如,如果你有一个文件需要打印,这时检查当前登录用户是否拥有打印机的打印权限,会用以下语句来检查:

if ( SecurityUtils.getSubject().isPermitted("printer:print:lp7200") ) {
    //使用lp7200打印机来打印文件 }
}

使用printer:print:lp7200这个完整的许可表达式,精确的描述了打印文件需要的许可。使用如下的检查语句也能一定程度实现功能:

if ( SecurityUtils.getSubject().isPermitted("printer:print") ) {
    //打印文件 
}

但是这么写,shiro会去检查当前登录用户是否拥有printer:print:*的许可,那么如果当前登录用户仅仅拥有printer:print:lp7200的权限的话,就不能够验证成功。所以在进行许可验证的时候尽可能的使用完整的且尽可能详细的许可表达式,以免扩大许可需求造成验证失败。

4> 检查许可时使用Permission接口计算包含关系
shiro在进行许可检查的时候验证的是包含关系,而不是相等。例如,如果一个用户被分配了user:*的权限,那么他就拥有了user:view权限,因为user:view包含于user:*(显然,user:view不等于user:*),user:view所定义的许可是user:*的一个子集。
为了实现这样的包含关系,shiro会将所有的许可表达式转化为实现了org.apache.shiro.authz.Permission接口的实现类的对象。所有使用通配符*所表示的许可表达式,shiro都将其转化成了org.apache.shiro.authz.WildcardPermission类的实现对象。以下是一些许可表达式包含关系的示例:user:*包含了user:delete, user:*:12345包含了user:update:12345,printer包含了printer:print

5> 性能优化
为了实现许可检查时的包含逻辑,shiro会隐式的将许可表达式转化为Permission接口实现类的对象。当使用上面示例这些许可表达式时,shiro会将其转换为WildcardPermission对象来隐式的计算包含关系。
对于shiro默认的Realm实现时,针对每一次许可检查,都会检查分配到用户的所有许可是否有包含被检查的许可表达式。一旦检查到某个许可包含了被检查的许可表达式,则终止检查来提高性能,但是这并不一定是最优的结果。
通常如果使用properCacheManager将users、roles、permissions缓存在内存中时,检查许可的速度回非常的快。按照这个逻辑,为某个用户所分配的许可越多,对其执行的许可检查所需要的时间会相应的增加。
如果一个Realm实现需要更加高效的方式来检查许可并且执行包含逻辑的运算,特别是基于应用程序的数据模型,这种情况下应该自行实现Realm的isPermitted方法。shiro默认的Realm和WildcardPermission支持80%-90%的用户情景,但是对于用户有巨大数量的许可的情况,需要自己来做相应的实现来做许可检查。

Roles角色

角色是一个代表特定行为或者职责的命名实体。这些行为代表了此应用程序的功能。角色会被分配给用户,这样用户就拥有了角色所代表的行为或职责。当然一个用户可以有多个角色,并相应拥有多个角色所代表的行为和职责。
shiro支持两种类型的角色概念:

1> 隐式角色
隐式角色指的是直接使用角色名来判断用户是否可以执行某些操作,而不是将角色关联到一些许可来控制权限。这种方式编码简单,但是不利于扩展和维护。
应用程序在判断用户是否可以执行某操作时,会去判断当前用户是否拥有某个角色,而不是判断当前用户的角色是否拥有某项许可。

2> 显式角色
显示角色是为角色指定可以执行的操作(许可),这种角色理念使程序更加易读和易于扩展和维护。
应用程序在判断用户是否可以执行某操作时,可以通过用户的角色来查找和检查相应分配的许可,来确定用户权限。

基于资源的权限控制
请阅读The New RBAC: Resource-Based Access Control来了解使用显式角色,以及为角色分配许可这中权限管理方式的优点。

Users用户

用户是访问或者使用应用程序功能的实体,可以是人,也可以是第三方程序等,shiro将其抽象为subject表示用户。
用户可以执行应用程序中通过角色和许可所分配的云讯的操作。应用程序中的数据模型(用户角色许可的组织)定义了用户可以执行的操作。
例如,应用程序的数据模型可能是直接为用户分配许可,来进行权限控制;也可能是将许可分配给角色,然后再为用户分配角色来进行权限控制;也许是将多个用户分入一个用户组,为用户组分配角色,再给角色分配许可来进行权限控制....
你的数据模型决定了授权管理的执行方式。shiro会将应用程序的数据模型抽象转化为一个Realm实现来接入授权管理中。

Realm与应用程序的数据源(RDBMS,LDAP)连接,来获取到应用程序的数据模型中所定义的授权信息,将之告诉给shiro来判断当前用户是否拥有某个角色或者许可。应用程序中的数据模型可以完全按照需求来设计,不受shiro影响。

4.2.2 检查用户权限

shiro中有三种方式来检查用户权限:

  • 编程式:可以使用java代码的if-else语句来进行授权检查
  • JDK注解:可以在java方法上面使用jdk的authorization注解
  • JSP/GSP标签:可以使用JSP或者GSP标签检查用户角色和许可的来控制输出的页面。
编程式检查权限

在代码中使用Subject对象来进行权限检查是最简单的方法。

1> 基于角色的权限检查
基于角色的权限检查是比较老的,比较简单的权限管理方式。

角色检查
可以简单的通过Subject对象的hasRole()方法来判断Subject对象是否拥有某个角色,进而是否可以执行某个操作

Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("administrator")) {
    //显示admin按钮 
} else {
    //不显示admin按钮,或者将之变为不可用状态 
}

基于角色的权限检查还有其他几个方法可以使用:

Subject方法 描述
hasRole(String roleName) 如果当前用户拥有roleName角色,则返回true,否则返回false
hasroles(List roleNames) 返回一个boolean数组,对应于roleNames中的roleName当前Subject对象是否拥有,当需要检查用户对多个角色的拥有情况时,可以使用此方法,例如在定制一个复杂的页面时。
hasAllRoles(Collection roleNames) 如果当前用户拥有roleNames中的所有时,返回true,否则返回false

角色断言
可以使用断言+抛出异常的方式代替if-else语句来进行判断用户权限。如果用户拥有指定角色,则正常执行,否则抛出AuthorizationException异常。

Subject currentUser = SecurityUtils.getSubject();
//断言当前用户拥有角色bankTeller,如果拥有则正常执行,没有的话抛出异常 
currentUser.checkRole("bankTeller");
openBankAccount();

使用基于断言的checkRole方法可以使程序更加简洁,且AuthorizationException不需要自己定义,还有其他几个基于断言的权限检查方法:

Subject方法 描述
checkRole(String roleName) 如果Subject对象拥有roleName角色,则顺利执行,否则抛出AuthorizationException异常
checkRoles(Collection roleNames) 如果Subject对象拥有roleNames所有角色,则顺利执行,否则抛出AuthorizationException异常
checkRoles(String... roleNames) 和以上方法一致,但是支持java5的var-args风格参数

2> 基于许可的权限检查
以上基于角色的权限检查较为简单,但是没有显示的定义角色的权限,这样应用程序的扩展性和维护性都比较低。使用基于许可的权限检查,将操作抽象为许可分配给角色,再将角色分配给用户,以实现权限管理。基于许可的权限检查可以使权限的分配信息独立于应用程序,且可以方便的添加和修改权限,提高应用程序的扩展性和可维护性。

许可检查
如果应用程序的数据模型中已经通过许可表达式定义了许可及其相关联的角色和用户信息,则可使用Subject对象的isPermitted()方法来检查用户是否许可执行某操作。

基于Permission许可对象的许可检查
Subject的isPermitted()允许传入实现了Permission接口的对象作为参数。例如以下场景:办公室有个唯一编号为laserjet4400n的打印机,应用程序需要在允许用户点击“打印”按钮之前检查打印机中是否有别的打印任务正在执行或者排队,如下例:

Permission printPermission = new PrinterPermission("laserjet4400n", "print");
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isPermitted(printPermission)) {
    //显示“打印”按钮
} else {
    //不显示或者使其失效
}

此例中定义了一个针对一个实例的某个操作的许可对象,来供当前用户检查是否拥有。
通常基于Permissino对象的许可检查通常在以下情形用到:

  1. 需要编译期检查类型安全
  2. 需要确保许可被正确表示和使用
  3. 需要显式的控制Permission对象如何执行包含运算逻辑
  4. 需要精确的管理资源
    基于Permission对象的许可检查,通常还会用到Subject对象的以下方法:
    |Subject方法|描述|
    |---|---|
    |isPermitted(Permission P)|如果当前用户允许执行P所定义的操作,则返回true,否则返回false|
    |isPermitted(List perms)|返回一串针对perms中的各个许可判断的结果,用于有大量许可需要判断的场合|
    |isPermittedAll(Collection perms)|如果Subject对象被许可perms中的所有操作,则返回true,否则返回false|

基于Permission许可表达式的许可检查
大多数情况下可以直接使用许可表达式来进行许可检查,省掉了定义Permission许可对象的步骤。
如以下代码 即可完成上述例子中的功能:

Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isPermitted("printer:print:laserjet4400n")) {
   //显示“打印”按钮
} else {
    //不显示或者使其失效
}

以上代码中的printer:print:laserjet4400n使用了最好用的三部分实例级别许可表达式结构,这里随便看起来使用了字符格式的许可表达式,但是实际上shiro会隐式的将其转化为WildcardPermission对象。详见Permission许可

基于许可表达式的许可检查还有以下方法可以使用:

Subject方法 描述
isPermitted(String perm) 如果当前用户拥有perm许可表达式所表示的许可,则返回true,否则返回false
isPermitted(String... perms) 返回一个boolean数组,表示perms中每个许可表达式当前用户的拥有情况。
isPermittedAll(String... perms) 如果当前用户拥有perms许可表达式所表示的所有许可,则返回true,否则返回false

许可断言
为了代码整洁,使用异常的方式来代替if-else语句控制代码,可以使用基于断言的许可检查方式。例如下面代码:
使用许可对象来进行许可检查:

Subject currentUser = SecurityUtils.getSubject();
//确保当前用户拥有open许可
Permission p = new AccountPermission("open");
currentUser.checkPermission(p);
openBankAccount();

使用许可表达式来进行许可检查:

Subject currentUser = SecurityUtils.getSubject();
//确保当前用户拥有open许可
currentUser.checkPermission("account:open");
openBankAccount();

以上代码中的checkPermission()方法,当拥有参数中所表示的许可时及正常返回,否则抛出AuthorizationException异常。
还有以下方法可以进行许可断言:

Subject方法 描述
chekcPermission(Permission p) 如果当前用户拥有p许可,则正常返回,否则抛出AuthorizationException异常
checkPermission(String perm) 如果当前用户拥有perm许可表达式所表示的许可,则正常返回,否则抛出AuthorizationException异常
checkPermission(Collection perms) 如果当前用户拥有perms中所表示的所有许可,则正常返回,否则抛出AuthorizationException异常
checkPermission(String... perms) 同上,只是参数是用String类型的许可表达式表示的许可
使用注解进行权限控制

除了使用Subject对象的方法来进行权限检查,在java5及以后的jdk版本中,还可以在方法上面使用注解来进行权限控制。

配置
使用注解,需要首先激活应用程序所使用的bean容器的AOP功能,shiro支持AspectJ、Spring、Guice等的AOP。这里仅保留其对于Spring的AOP的配置方式,其他参见官网文档

1> RequiresAuthentication注解
RequiresAuthentication注解要求被注解的类/实例/方法使用或调用前,执行检查当前用户是否通过认证。
例如:

@RequiresAuthentication
public void updateAccount(Account userAccount) {
    //此方法仅能被通过认证,登录成功的用户调用
}

如果使用Subject对象的API来写上面的功能的话,如下:

public void updateAccount(Account userAccount) {
    if (!SecurityUtils.getSubject().isAuthenticated()) {
        throw new AuthorizationException(...);
    }
    //确定用户认证成功后需要执行的操作
}

2> RequiresGuest注解
RequiresGuest注解需要当前用户是一个“guest”,即当前用户没有认证,且没有之前会话记住的认证状态,这样其所注解的类/实例/方法才能使用或者调用。
例如:

@RequiresGuest
public void signUp(User newUser) {
    //本方法不能被认证的用户调用
}

使用Subject对象的API写上面的功能的话,如下:

public void signUp(User newUser) {
    Subject currentUser = SecurityUtils.getSubject();
    PrincipalCollection principals = currentUser.getPrincipals();
    if (principals != null && !principals.isEmpty()) {
        //known identity - not a guest:
        throw new AuthorizationException(...);
    }
    //当前用户是一个guest的话,需要执行的代码
}

3> RequiresPermission注解
RequiresPermission注解要求当前用户拥有注解中的许可,才能执行被注解的方法。
例如:

@RequiresPermissions("account:create")
public void createAccount(Account account) {
    //本方法只有用户拥有创建账户(account:create)的许可时才能被调用
}

使用Subject的API编写的话,如下:

public void createAccount(Account account) {
    Subject currentUser = SecurityUtils.getSubject();
    if (!subject.isPermitted("account:create")) {
        throw new AuthorizationException(...);
    }
    //用户拥有要求的许可的话执行的代码
}

4> RequiresRoles注解
RequiresRoles注解要求当前用户必须有注解中的所有角色,才能执行被注解的方法。如果当前用户没有注解中的角色,那么被注解的方法不会被执行,切回抛出AuthorizationException异常。
例如:

@RequiresRoles("administrator")
public void deleteUser(User user) {
    //本方法只有拥有administrator角色的用户才能调用
}

用Subject对象的API来编写此功能如下:

public void deleteUser(User user) {
    Subject currentUser = SecurityUtils.getSubject();
    if (!subject.hasRole("administrator")) {
        throw new AuthorizationException(...);
    }
    //当前用户拥有administrator角色时执行的代码
}

5> RequiresUser注解
RequiresUser注解要求当前用户必须是应用程序的用户(其与RequiresAuthentication注解类似,但是不仅认可当前会话中通过认证的当前用户,也认可之前会话中通过记住我功能保存的用户),才能使用或调用被注解的类/实例/方法。
例如 :

@RequiresUser
public void updateAccount(Account account) {
    //当前用户必须是某个用户才能调用 
}

使用Subject API编写以上功能如下:

public void updateAccount(Account account) {
    Subject currentUser = SecurityUtils.getSubject();
    PrincipalCollection principals = currentUser.getPrincipals();
    if (principals == null || principals.isEmpty()) {
        //no identity - they're anonymous, not allowed:
        throw new AuthorizationException(...);
    }
    //当前用户是指定用户时执行的代码
}
使用JSP/GSP标签进行权限控制

shiro提供了一个jsp标签库来实现基于当前用户状态控制页面输出的功能。具体参见JSP标签库

4.2.2 授权次序及机制

如上内容已经介绍了如何基于当前用户来进行检查角色或检查许可等授权管理功能。这里介绍当调用了授权管理相关方法后,shiro内部都执行了什么。
重新查看shiro架构章节所展示的结构图,但是只保留与授权Authorization功能相关的组件,途中的数字代表执行的步骤:
ShiroAuthorizationSequence.png

第一步:应用程序或者框架调用Subject对象的hasRol(),checkRole(),isPermitted()或者checkPermission()等方法,并传入表示许可或者角色的参数。
第二步: Subject实例,其实是一个代理类,调用了SecurityManager的hasRole(),checkRole(),isPermitted()或者checkPermission()等相应方法。(SecurityManager实现了org.apache.shiro.authz.Authorizer接口)
第三步: SecurityManager在shiro框架中就像一把伞,使用其内部代理的org.apache.shiro.authz.Authorizer实例,并调用其相应的hasRole(),checkRole(),isPermitted(),或者checkPermission()方法。authorizer实例默认是一个ModularRealmAuthorizer实例,支持在任意的授权中协调一个或多个Realm实例来进行授权操作者。
第四步: 所配置的每一个Realm都会被检查是否实现了相同的Authorizer接口,如果是的,那么就会调用这个Realm本身的相应方法hasRole(),checkRole(),isPermitted()或者checkPermission()。

ModularRealmAuthorizer(模块化Realm授权器)的运行机制

shiro的SecurityManager默认使用一个ModularRealmAuthorizer实施来进行授权操作。ModularRealmAuthorizer支持单Realm,也支持多Realm。
对于任意的授权操作,ModularRealmAuthorizer会迭代SecurityManager中配置的Realm集合,按照迭代的顺序与Realm进行交互,交互逻辑如下:

1.如果遍历到的当前Realm对象实现了Authorizer接口,就调用这个Realm实现的Authorizer相关方法API(hasRole*,checkRole*,isPermitted*,或者checkPermission*等)。

如果调用的Authorizer接口API抛出了异常,将会结束本次遍历,终止本次授权检查,还未遍历的Realm将会被忽略。

如果使用hasRole和isPermitted等返回boolean类型结果的方法,则会直接将此方法的返回结果返回,并结束遍历。这种策略可以提升性能。并且默认用户是没有相关权限,只有显式的得到用户拥有权限的检查结果后,才会通过用户的授权检查,这是最安全的授权策略。

2.如果遍历到的当前Realm对象没有实现Authorizer接口,则会直接忽略此Realm。

配置PermissionResolver(许可解析器)

1> 配置全局的PermissionResolver
当调用Authorizer接口的实现方法(isPermitted*,checkPermission*等)时,如果传入的参数是字符串类型的许可表达式,那么shiro将会自动使用PermissinResolver将许可表达式解析为Permission许可对象,再调用相应的重写方法进行检查,这样shiro可以计算权限的包含逻辑,而非简单的字符串相等逻辑。
大部分的Realm实现都会使用PermissionResolver来将字符串型的许可表达式转化为Permission对象,以进行包含逻辑的计算。
所有的Realm实现默认使用WildcardPermissionResolver来解析许可表达式。
也可以自定义PermissionResolver来解析自定义的许可表达式,将自定义的PerimssionResolver在shiro.ini中设置为全局的PermissionResolver后,所有的Realm实现即会调用此自定义的PermissionResolver来进行许可表达式的解析。
如下示例了如何在shiro.ini中配置全局的许可表达式解析器:

globalPermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.permissionResolver = $globalPermissionResolver
...

PermissionResolverAware
如果要使用全局的自定义的许可表达式解析器(PermissionResolver),要使用此全局许可表达式解析器的Realm还要实现PermissionResolverAware接口。

2> 为某个Realm指定PermissionResolver
如果不想在代码中实现PermissionResolverAware接口,或者不需要所有的Realm都使用指定的许可表达式解析器,还可以为某个Realm实现指定许可表达式解析器。如下:

permissionResolver = com.foo.bar.authz.MyPermissionResolver
realm = com.foo.bar.realm.MyCustomRealm
realm.permissionResolver = $permissionResolver
...
配置RolePermissionResolver

RolePermissinoResolver也是用来检查当前用户是否拥有某项许可,与PermissionResolver不同的是,其传入的不是许可表达式,而是一个角色名,RolePermissionResolver会将此角色名解析成一个Permission对象,包含了此角色所有的许可。
RolePermissionResolver主要用于比较老的系统,比较早的系统中可能没有许可Permission这个数据概念,仅用角色来做权限控制,shiro可以使用系统原有的角色名,配合附加的角色许可分配信息,将角色显式的转换为许可信息。
将角色转换为许可的操作因程序而不同,shiro默认的Realm实现并没有使用RolePermissionResolver。
如果需要设置RolePermissionResolver来做角色到许可的转换,则可以设置全局的RolePermissionResolver,如下为shiro.ini的配置方法:

globalRolePermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver
...

RolePermissionResolverAware
使用全局的RolePermissionResolver的话,需要将角色名转换为许可信息的Realm还需要实现RolePermissionResolverAware接口。这样此Realm才能使用此功能。

也可以为某个Realm单独的配置RolePermissionResolver,配置方法如下:

rolePermissionResolver = com.foo.bar.authz.MyRolePermissionResolver
realm = com.foo.bar.realm.MyCustomRealm
realm.rolePermissionResolver = $rolePermissionResolver
...
自定义授权管理器Authorizer

默认的授权管理器有着一些局限性,如果SecurityManager中配置了多个Realm,默认的ModularRealmAuthorizer的简单的基于迭代顺序的遍历Realm,并且找到会自动结束后面的查找的逻辑不能满足需求,那么可以自定义一个授权管理器,然后将其配置到SecurityManager中来。
shiro.ini的配置方法如下:

[main]
...
authorizer = com.foo.bar.authz.CustomAuthorizer
securityManager.authorizer = $authorizer

4.3 Realms

Realm是shiro中安全管理的重要组件,它能够连接到保存有用户、角色和许可等信息的数据源,将各式各样的数据结构组织成Realm,供shiro使用。它是应用程序和shiro之间的桥梁,能够帮助shiro读取到应用程序的相关数据,并进行相关的操作。
一般情况下一个用户数据源对应于一个Realm,数据源可以是RDBMS、LDAP文件夹、文件系统或其他的数据源。

Realm可以简单的理解为一个数据持久层对象,只是它只读取和安全相关的数据,并将数据组织成为Realm这种shiro可以使用的数据格式。
因为大部分的数据源通常即保存了认证相关的数据(用户凭据例如密码),同时保存了授权相关的数据(例如角色和许可),因此每个Realm都可以执行认证和授权两个操作。

4.3.1 Realms的配置

在shiro.ini文件的main部分配置realm

显式配置Realms
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm
//显式的将realms赋值给securityManager的realms属性
securityManager.realms = $fooRealm, $barRealm, $bazRealm

RealmSecurityManager在执行认证和授权时,其ModularRealmAuthenticator和ModularRealmAuthorizer实例都会按照显示赋值的集合中的realm顺序进行迭代调用各realm进行认证和授权。

隐式配置Realms
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm
//不显示的赋值的话,shiro会自动的将shiro.ini中配置的所有realm自动装入securityManager的realms属性中
//securityManager.realms = $fooRealm, $barRealm, $bazRealm

RealmSecurityManager在执行认证和授权时,其ModularRealmAuthenticator和ModularRealmAuthorizer实例会按照各realm在shiro.ini中出现的次序来进行遍历和调用各realm进行认证和授权。

4.3.2 Realms执行认证

理解了SecurityManager中执行认证的机制之后,接下来需要了解下更加细节的Realm中执行认证的机制。

支持的AuthenticationToken

SecurityManager中的认证执行机制所述,在一个Realm开始一个认证操作之前,会调用它的supports方法,来查看此Realm是否支持提交的AuthenticationToken。如果返回支持,则调用此Realm的getAuthenticationInfo(token)方法。
调用该Realm的supports方法时,会检查其是否支持当前提交的AuthenticationToken,和能够处理此token。例如一个用来处理biometric数据的Realm是无法处理UsernamePasswordToken的,在调用它的supports方法是也会返回false。

处理支持的AuthenticationToken

如果通过supports方法已经知道一个Realm支持提交的AuthenticationToken,则会调用它的getAuthenticationInfo(token)方法。此方法即执行了查询数据源,获取信息,并执行认证信息验证的操作:
1.从提交的token中获取用户标识
2.基于用户名,在数据源的账户数据中查询相应的账户信息
3.验证token中的凭据是否与数据源中保存的密码匹配
4.如果匹配,则返回一个AuthenticationInfo对象,其中保存了shiro可以使用的账户数据
5.如果不匹配,则抛出一个AuthenticationException
以上为Realm的getAuthenticationInfo方法执行时的主要操作,另外还有其他的操作,例如记录日志,更新数据记录,或者其他与认证相关的数据操作。

节省时间
直接的实现Realm接口会比较麻烦也容易出错。多数人会继承AuthorizingRealm抽象类,这个抽象类中已经实现了一些通用的认证和授权方法来提高编程效率。

凭据匹配

在Realm的getAuthenticationInfo方法进行认证时,会将数据源中获取到的凭证与提交的token中的凭证比对,有以下两种情况。

在shiro安全框架中,SecurityManager管理了所有的安全组件,其中代理了Authenticator的实现类(ModularRealmAuthenticator实例),Authenticator又会依照AuthenticationStrategy策略来调用SecurityManager中保存的Realms来进行认证操作。
Realm在执行认证时,会对token中提交的凭据同数据源中查询到的凭据进行匹配,为了使匹配这个动作是可插拔可定制的,AuthenticationRealm抽象类及其实现类都包含了CredentialsMatcher的对象来执行匹配运算。
在从数据源中获取了用户账户信息之后,获取到的用户数据和提交的AuthenticationToken都会被交给CredentialsMatcher来比较是否一致。
Shiro中有几个CredentialsMatcher可供使用,例如SimpleCredentialsMatcher和HashedCredentialsMatcher实现类,通过以下方式也可以自定义CredentialsMatcher实现:

Realm myRealm = new com.company.shiro.realm.MyRealm();
CredentialsMatcher customMatcher = new com.company.shiro.realm.CustomCredentialsMatcher();
myRealm.setCredentialsMatcher(customMatcher);

也可以在shiro.ini中配置自定义的CredentialsMatcher:

[main]
...
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
myRealm = com.company.shiro.realm.MyRealm
myRealm.credentialsMatcher = $customMatcher
...

1> 简单匹配
默认情况下,Realm会使用SimpleCredentialsMatcher来执行凭据的匹配,直接将用户提交的token中的密码与Realm从数据源读取到的密码进行简单的相等计算,其支持一般的字节格式,例如字符串、字符数组、字节数组、文件及文件流等,具体参见SimpleCredentialsMatcher的javaDoc。

2> 哈希匹配
直接将用户账户密码存储在数据源中是极不安全的,因此一般安全起见,会将用户密码进行哈希运算后再存入数据源中。
这样能够避免将密码明文存储在数据库中,所有的系统都应该采用此种方式来保存密码。
为此Shiro提供了HashedCredentialsMatcher来进行密码的匹配计算。HashedCredentialsMatcher支持加盐哈希,也支持多次哈希,具体参见其JavaDoc。

使用相应的HashedCredentialsMatcher来进行hash运算
Shiro提供了多个HashedCredentialsMatcher子类来供使用,但是使用时必须将其在shiro.ini中配置为相应的realm,并且其hash算法应该与你在往数据源中保存hash计算过的密码时使用的hash算法一致(加盐、多次)。
例如,对于一个使用用户名/密码作为验证凭据的系统,使用SHA-265算法进行单向哈希运算,并将hash计算后的值进行保存:

import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
...

//这里采用一个随机数作为盐,安全性回比采用用户名作为盐或者不使用盐要高的多。
//
//一般情况下应用程序应该采用一个随机数生成器的引用,来提高随机数的分布随机性。
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
Object salt = rng.nextBytes();

//使用随机数作为盐,多次哈希,将明文的密码进行加密,最后使用base64对结果进行编码
String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64();

User user = new User(username, hashedPasswordBase64);
//保存上面作为盐的随机数,供HashedCredentialsMatcher后面在进行登录认证操作时使用。
user.setPasswordSalt(salt);
userDAO.create(user);

因为使用了SHA-256来进行哈希加密,就需要在shiro.ini中告诉shiro使用相应的HashedCredentialsMatcher来进行匹配计算。这里采用随机数作为盐,并进行1024此哈希计算(详情参见JavaDoc),配置方式如下:

[main]
...
credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
# 使用base64进行编码,而不是hex
credentialsMatcher.storedCredentialsHexEncoded = false
credentialsMatcher.hashIterations = 1024
# 下面这条配置仅在Shiro1.0中需要,shiro1.1及之后的版本都不需要
credentialsMatcher.hashSalted = true
...
myRealm = com.company.....
myRealm.credentialsMatcher = $credentialsMatcher
...

SaltedAuthenticationInfo
如果一个realm配置了使用HashedCredentialsMatcher作为密码匹配器,那么这个Realm必须返回一个SaltedAuthenticationInfo(而不是AuthenticationInfo),这样才能HashedCredentialsMatcher才能够引用到创建账户信息时所使用的盐,及通过user.setPasswordSalt(salt)所保存的盐。

关闭认证

默认,Realm都可以调用为其配置的数据源来进行认证和授权操作,但是如果只需要某个Realm进行授权,而不需要认证功能,那么进行设置,让这个Realm的supports方法持续返回false,使SecurityManager不使用此Realm进行认证操作。
在一个系统中,至少需要一个能够使用认证功能的Realm。

4.3.3 Realms执行授权

SecurityManager会将对于许可和角色检查的请求代理给授权管理器(Authorizer),默认是一个ModularRealmAuthorizer。

基于角色的授权

当在Subject对象上面调用hasRoles或者checkRoles方法时:\

  1. Subject对象会将请求委托给SecurityManager,来检查当前用户是否拥有指定的角色。\
  2. SecurityManager再委托授权管理器\
  3. 授权管理器Authorizer再调用所有Realm检查,直到某个Realm找到了当前用户拥有指定角色。如果所有的Realm都没有找到指定角色,则返回false。
  4. 检查到有相应角色的Realm返回的AuthorizationInfo对象的getRoles*方法能够获得当前用户的所有角色
  5. 如果返回的角色列表中有指定的角色,则给当前用户授权。
基于许可的授权

当调用了Subject对象的isPermitted()或者checkPermission()方法时:

  1. Subject对象会委托SecurityManager来进行授权或者拒绝授权
  2. SecurityManager会委托给Authorizer授权管理器来进行操作
  3. Authorizer授权管理器调用所有的可以进行授权检查的Realm进行许可检查,直到检查到相应许可
  4. 如果所有的Realm都没有查询到指定许可,则拒绝授权给当前用户
  5. 授权操作的Realm执行以下步骤来检查当前用户是否获得授权
    a. 调用AuthorizationInfo对象的getObjectPermissions和getStringPermissions方法来获得当前用户所拥有的所有许可
    b. 如果注册了RolePermissionResolver,会通过调用RolePermissionResolver.resolvePermissionsInRole()来基于分配给当前用户的所有角色来获取相应的许可
    c. 将提交的许可,与a和b所获取到的许可做包含运算,计算a和b获得到的许可是否包含所提交的许可。

4.4 Session管理

Apache Shiro为小至命令行程序、智能手机应用,大至大型集群企业级web应用系统,提供了一套完整的企业级的会话解决方案。
有了shiro,不仅可以在web应用或者EJB无状态会话Bean中使用会话,shiro提供了更加简单易用且易于管理的会话功能,不再受其他容器框架的限制。
并且shiro自带的会话管理器与web容器及EJB容器的会话兼容,可以使用shiro的会话管理器代替容器的会话管理器以下是shiro的会话管理器的功能:

  • 基于POJO/J2SE,对于IOC容器比较友好:shiro框架所有对象都是基于接口,使用POJO实现类。这样可以简便的使用多种格式来配置。
  • 支持自定义会话的存储方式:shiro的会话数据可以存储在文件系统、内存、网络分布式缓存、RDBMS等等数据源中
  • 独立于容器的集群:shiro的会话功能可以使用任意网络缓存工具配置成为集群,例如Ehcache + Terracotta, Coherence, GigaSpaces等。这意味着你只需要为shiro配置集群,不需要再在容器中为shiro配置集群。
  • 多客户端访问:shiro的会话功能支持多种不同类型的客户端共享。
  • 事件监听:可以监听会话生命周期各个阶段的事件,自定义相应操作。
  • 主机IP地址记忆:shiro的会话功能能够记录会话发起的IP地址。这使得你能够定位会话开始人,并进行相应操作。
  • 不活跃/过期 支持:当会话有一段时间不活动之后,会话会自动过期,但是可以使用touch()方法来使一个会话一直保持存活。
  • 与Web应用的Session兼容:shiro的web部分完全实现和支持Servlet2.5规范的Session(HttpSession接口和所有的API)。这意味着你可以直接在已有的web应用中使用Shiro的会话,不需要改动已有代码。
  • 可以用作单点登录(SSO): 因为shiro的会话对象都基于POJO,可以很方便的进行持久化,而且可以跨应用共享。这称之为“穷人的单点登录poor man's SSO”。利用它应用间共享状态是认证状态也会共享的特性,可以使用shiro来实现登录功能。

4.4.1 使用会话

可以通过以下方式在代码中获取到shiro的会话对象:

Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute( "someKey", someValue);

其中的currentUser.getSession()方法是currentUser.getSession(true)的简写。shiro的这个方法参照了HttpServletRequest API中的HttpServletRequest.getSession(boolean create)方法。

  • 如果当前用户currentUser已经拥有了一个会话,那么传入的布尔类型的参数会被忽略掉,并且将此会话立即返回。
  • 如果当前用户还没有相应会话,那么如果传入参数为true,就会新建一个会还然后返回。
  • 如果传入参数为false,就会返回null。

shiro的会话相关API不仅对于基于web的应用是如此,对于所有的应用程序的都是一致的。

subject.getSession(false)用在当一个会话并没必要创建的场合,可以节省资源,提高效率。
一旦你获得了用户的会话对象,就可以做很多事情,比如设置和获取属性,设置会话的超时时间等。详细参照Session的JavaDoc

4.4.2 SessionManager会话管理器

SessionManager管理所有用户的所有会话,管理会话的创建、删除、停用和验证等。SessionManager是shiro的顶级安全对象,托管在SecurityManager中。
默认的SecurityManager会使用DefaultSessionManager,其提供了所有的企业级的会话管理功能,比如会话验证,无效会话清理等。

在Web应用程序中,SecurityManager会使用另外一个SessionManager的实现,详请参考Web部分的适用于web应用的会话管理。

SessionManager也可以在配置文件中进行获取和配置中:

[main]
...
sessionManager = com.foo.my.SessionManagerImplementation
securityManager.sessionManager = $sessionManager

但是完全自定义一个SessionManager是一个比较复杂的工作,shiro默认使用的SessionManager是高度可定制和可配置的,可以满足绝大部分需求。

会话超时

shiro的默认的SessionManager实现会使用30分钟作为会话过期阈值。如果一个会话保持idle(未被使用,其lastAccessedTime未被更新)持续超过30分钟,此会话就会被认为过期,会不允许再被使用。
可以设置SessionManager的全局会话超时阈值属性globalSessiontimeout,此值会成为所有会话的默认超时时间。例如,如果你希望超时时间从30分钟改为1个小时,参照如下配置:

[main]
...
# 3,600,000 milliseconds = 1 hour
securityManager.sessionManager.globalSessionTimeout = 3600000

为单个session指定超时时间
除了直接设置SecurityManager的globalSessionTimeout属性,也可以对每一个session单独设置过期时间。

会话监听器

shiro更是支持session事件监听。可以通过实现SessionListener或者集成SessionListenerAdapter来实现自己的会话监听器,并对会话进行相应的处理。
SessionManager的sessionListeners属性是一个集合类型,因此可以为其设置多个监听器实例,配置方法如下:

[main]
...
aSessionListener = com.foo.my.SessionListener
anotherSessionListener = com.foo.my.OtherSessionListener
securityManager.sessionManager.sessionListeners = $aSessionListener, $anotherSessionListener, etc.

sessionListener监听的是所有会话的事件,而不是某一个会话。

会话持久化

每当一个会话被创建或者更新,那么它的数据就需要被持久化到一个存储位置,那么应用程序访问它就会有一个很小的延迟。类似的当一个会话失效,那么它就需要从存储中被删除已保证会话数据不会无限制的增长而是系统资源耗尽。SessionManager会将增删改查操作委托给一个内部的组件-采用了DAO设计模式的SessionDAO,
可以通过实现SessionDAO接口来与任意的数据存储进行通信。这意味着会话数据可以被保存在内存中、文件系统中、或者一个RDBMs中,或者NoSQL中,以及任意其他的文职。你可以完全控制持久化方式。
可以在shiro.ini中配置SessionDAO实现,配置方式如下:

[main]
...
sessionDAO = com.foo.my.SessionDAO
securityManager.sessionManager.sessionDAO = $sessionDAO

Shiro内置了一些非常好用的SessionDAO实现可以直接使用。

Web应用的持久化
以上配置SecurityManager.sessionManager.sessionDAO=$sessionDAO,为SessionManager指定持久化对象的方式不能直接使用在Web应用中,因为基于Web的应用中shiro借用了Web容器的会话管理器,而不是自带的会话管理器。而Web容器的会话管理器是不支持会话持久化对象的。如果需要在Web应用中使用会话持久化功能,以实现自定义会话存储或者实现会话集群,需要首先配置一个shiro自带的web会话管理器,配置方式如下:

[main]
...
sessionManager =org.apache.shiro.web.session.mgt.DefaultWebSessionManager
securityManager.sessionManager = $sessionManager

配置一个SessionDAO
Shiro默认的SessionManager会将会话数据存储在内存当中,但是这个对于大部分的应用都是不满足需求的。大多数的应用都需要使用EHCache来做持久化或者使用自定义的SessionDAO实现来做持久化。对于Web应用来说使用了基于servlet容器会话的SessionManager。

1> 使用EHCache SessionDAO
非Web容器应用下,使用shiro默认的SessionManager会直接将会话数据存储在内存中,这种情况下当会话数据过多会造成系统资源耗尽,数据丢失等问题。Web容器下shiro会使用基于Web容器的会话管理器,不会有此问题。但是对于这些情况,如果没有其他会话持久化方式的情况下,建议打开shiro的EHCache缓存功能,以提高效率,保证数据安全。配置了EHCache后,当内存不满足需求是会将数据存储到磁盘。

强烈建议打开EHCache
EHCache不仅会帮助你避免会话数据的异常丢失,还能够帮助加快认证和授权等操作的效率。具体参见缓存

跨容器共享会话
使用EHCache+TerraCotta的配置可以实现Web应用的跨容器共享会话功能,组件多容器共存的web应用集群,不用再担心Tomcat,JBoss,Jetty,WebSphere或者WebLogic等会话的不同造成无法集群。

激活EHCache非常简单。首先,在classpath中确定有shiro-ehcache-.jar文件。在获取shiro部分可以获得下载方式,也可以使用maven来获取依赖。
添加shiro-ehcache依赖后,即可配置shiro的ehcache支持。这样不仅会话功能能够使用缓存功能,shiro的其他功能也可以借助ehcache的缓存功能提高性能,以下为配置方法:

[main]
sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
securityManager.sessionManager.sessionDAO = $sessionDAO
cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
securityManager.cacheManager = $cacheManager

以上配置文件的最后一行,securityManager.cacheManager=$cacheManager,为Shiro配置了CacheManager。这时SessionDAO就能自动的使用这个cacheManager(EnterpriseCacheSessionDAO实现了CacheManagerAware)。
然后,当SessionManager请求EnterpriseCacheSessionDAO持久化一个会话时,他就会调用EHCache来保存会话数据。

web应用中会话的持久化
在Web应用中使用EHCache等会话持久化的话,需要按照Web应用的持久化配置为其指定SessionManager。

EHCache会话缓存配置
默认情况下,EhCacheManager使用一个为shiro定义的ehcache.xml文件来设置会话缓存范围以及为保证会话正确存储和获取的设置。
也可以自定义缓存设置,配置自己的ehcach.xml或者EHCache net.sf.ehcache.CacheManager实例,你需要配置擦车范围来使会话被正确处理。
查看默认的ehcache.xml文件,可以看到下面的shiro-activeSessionCache缓存配置:

<cache name="shiro-activeSessionCache"
   maxElementsInMemory="10000"
   overflowToDisk="true"
   eternal="true"
   timeToLiveSeconds="0"
   timeToIdleSeconds="0"
   diskPersistent="true"
   diskExpiryThreadIntervalSeconds="600"/>

自定义ehcache.xml文件的话,需要在其中定义shiro需要的一些设置项,最少需要包含下面两个属性:

  • overflowToDisk="true" - 这保证当内存耗尽之后,会话数据能够被存放如硬盘中。
  • eternal="true" - 使缓存入口(Session实例)永不过期或者不被缓存自动删除。shiro会按照一个计划的进程(详见会话确认和计划)来进行会话状态检查。如果eternal被设置为false,缓存会自己将会话数据自行删除而不通知shiro,这会导致严重问题。

EHCache会话缓存名称
默认情况下,EnterpriseCacheSessionDAO会以名称"shiro-activeSessionCache"来寻找和请求CacheManager。这个会话缓存名称可以在ehcache.xml中配置。
如果使用了自定义的EHCache会话缓存名称,需要在shiro.ini中为EnterpriseCacheSessionDAO的相应属性设置此名称,配置如下:

...
sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
sessionDAO.activeSessionsCacheName = myname
...

对于EHCache的配置文件ehcache.xml,最少要保证ehcache.xml中的name与shiro.ini中的SessionDAO的acitveSessionsCacheName一致,并且最少要在ehcache.xml中配置overflowToDisk和eternal两个设置项。

会话确认和计划

对于会话必须有有效性检查机制,以删除无效(过期或者停止的)的会话,避免系统资源被耗尽。处于性能考虑,shiro仅仅会在使用subject.getSession()获取session时会对session进行检查,这意味着已经过期或者停止的无效session有可能不能被及时的清除。
例如,当一个用户在浏览器中浏览一个web应用,web应用通过保存在用户浏览器的session来记录登陆状态及购物车等信息。如果用户没有执行登出操作,直接关闭了浏览器,那么web应用并不知道这个session已经被用户关闭,会一直存在于web应用中。
为此,需要有一个定期清理的机制。SessionManager的实现支持SessionValidationScheduler的概念,能够定期的执行会话有效性检查来清理系统垃圾。

1> 默认的SessionValidationScheduler
适用于所有环境的SessionValidationScheduler实例是ExecutorServiceSessionValidationScheduler,它使用了JDK中的ScheduledExecutorService来控制定期检查的周期。默认的,ExecutorServiceSessionValidationScheduler会每隔一个小时执行一次会话检查。可以通过为shiro指定一个新的ExecutorServiceSessionValidationScheduler实例并设置需要的周期间隔来指定shiro进行会话有效性的周期。
如下配置了一个ExecutorServiceSessionValidationScheduler,并指定了检查间隔:

[main]
...
sessionValidationScheduler = org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler
# Default is 3,600,000 millis = 1 hour:
sessionValidationScheduler.interval = 3600000
securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler

2> 定制SessionValidationScheduler
也可以自定义一个SessionValidationScheduler实例,shiro.ini配置方法如下:

[main]
...
sessionValidationScheduler = com.foo.my.SessionValidationScheduler
securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler

3> 关闭SessionValidation
如果使用了shiro以外的方式来进行了会话检查,例如使用企业级的缓存并设置了缓存生存时间,或者设置了cron任务来自动清理session存储的数据源。这时就可以关闭shiro自带的会话检查计划功能。
如下配置关闭了shiro的会话检查计划功能

[main]
...
securityManager.sessionManager.sessionValidationSchedulerEnabled = false

如果关闭了shiro的会话检查计划功能,那么就必须另外的自行定期进行会话检查,以保证系统资源不被异常耗尽。

4> 无效会话的删除
每当shiro检查到一个无效的会话,会尝试通过SessionDAO.delete(session)方法来删除此无效会话。以此来避免系统资源无效占用。
有些情况下,也不希望shiro自动删除无效的会话。例如如果一个应用程序的SessionDAO所连接的数据源同时也供其他地方进行查询,那么也许会希望保留这些无效的会话供分析查看,而不是直接删除。
这时,可以完全的关闭会话删除,在shiro.ini中的配置方法如下:

[main]
...
securityManager.sessionManager.deleteInvalidSessions = false

但是,一旦关闭了会话删除功能,那么必须自己来管理会话数据的存储,避免存储空间被占满。
即使关闭了会话删除功能,也应该保留会话检查功能:通过shiro自带的检查机制或者自定义的检查机制,来更新各个session的状态(失效时间,最后访问时间等)。

关闭会话删除,和关闭会话检查计划效果是不同的
关闭会话删除功能,shiro自带的会话检查功能或者自己的会话检查功能依然需要定期检查会话并更新会话状态。关闭shiro自带的会话检查计划功能,则应该自己再实现独立的或外置的会话检查计划功能来定期的清理会话存储。

4.4.3 会话集群

shiro的会话功能支持集群用户会话,不用担心容器不同造成会话集群问题。如果你使用shiro自带的会话管理器并配置一个会话集群,可以支持Jetty或者Tomcat、JBoss或者Geronimo等所有的Web容器。会话集群功能的实现完全与容器的种类和配置无关。
shiro的会话集群是如何工作的?
由于shiro的POJO 和 多层设计结构,使用shiro的会话集群功能相当于在会话持久层进行集群化。即,当你配置了一个会话集群化的shiro,仅仅是SessionDAO会与一个集群数据源交互,SessionManager并不知道任何与集群相关的信息。
分布式缓存
可以使用Ehcache+TerraCotta,GigaSpaces Oracle Coherence 和 Memcached(及其他缓存框架)来实现 分布式数据持久层的功能。但是使用shiro的会话集群,最简单的方式是使用一个分布式的缓存。
只要使用分布式的持久层,就可以实现shiro的会话集群管理,因此shiro的集群功能实现非常灵活。

要使用分布式/企业级的缓存来实现shiro的会话集群功能,下面两点必须满足:

  • 分布式缓存必须有足够的存储来保存所有的会话数据
  • 如果空间不足够大,就必须配置缓存的持久化当缓存空间不足时保存到硬盘
    以上两点如果不能够满足其一,就有可能造成会话数据丢失,而造成严重的问题。
EnterpriseCacheSessionDAO 企业级别的会话缓存DAO

shiro已经提供了SessionDAO来将会话数据持久化到一个企业级/分布式的缓存系统中。EnterpriseCacheSessionDAO需要一个Shiro Cache或者CacheManger,使他能够使用缓存机制。
如下是配置爱EnterpriseCacheSessionDAO的方式:

#这个CacheImplementation会使用你想要的分布式缓存产品的API
activeSessionsCache = my.org.apache.shiro.cache.CacheImplementation
sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
sessionDAO.activeSessionsCache = $activeSessionsCache
securityManager.sessionManager.sessionDAO = $sessionDAO

shiro支持直接往SessionDAO中注入一个缓存实例,但是通常给shiro配置一个通用的CacheManager,提供缓存供shiro使用,包括认证和授权相关数据的缓存。这种情况下,不是直接配置一个缓存实例,而是应该告诉EnterpriseCacheSessionDAO,在CacheManager中应该使用的缓存的名称。
配置方式如下:

# This implementation would use your caching product's APIs:
cacheManager = my.org.apache.shiro.cache.CacheManagerImplementation

# Now configure the EnterpriseCacheSessionDAO and tell it what
# cache in the CacheManager should be used to store active sessions:
sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
# This is the default value.  Change it if your CacheManager configured a different name:
sessionDAO.activeSessionsCacheName = shiro-activeSessionsCache
# Now have the native SessionManager use that DAO:
securityManager.sessionManager.sessionDAO = $sessionDAO

# Configure the above CacheManager on Shiro's SecurityManager
# to use it for all of Shiro's caching needs:
securityManager.cacheManager = $cacheManager
Ehcache+Terracotta

这种组合需要用到商业软件,此处不详述。
官方文档:http://shiro.apache.org/session-management.html#SessionManagement-ehcachesessiondao#ehcache-terracotta

Zookeeper

Zookeeper是最热门的集群管理软件,且开源免费,社区活跃。可以实现shiro的分布式集群功能。

4.4.4 会话和用户状态

有状态程序(有会话)

默认的,shiro的SecurityManager实例将用户的会话来保存用户的标识和认证状态(subject.isAuthenticated(),subject.isRemembered())。
有状态的会话有如下优势:

  • 同一用户多次request间的状态保存。shiro更是支持通过一个sessionId创建出用户Subject对象。
    requestSubject = new Subject.Builder().sessionId(sessionId).buildSubject();
  • 支持应用程序使用“remember me”功能
无状态应用程序(无会话)

但是如果一个应用程序的访问量非常巨大,会话的管理任务会非常的沉重,且一旦会话管理系统发生故障,将会导致非常严重的问题。即使shiro支持了分布式的会话持久化,其对于应用程序的集群部署依然不是很友好。
因此无状态的(无会话)的应用程序,更加利于大型分布式集群系统的部署。
无状态应用程序中,每一次request都需要携带用户标识和凭据信息,让应用程序及shiro来进行认证,确定用户身份和权限。
无状态应用程序会增加每一次request的处理负担,但是会极大的降低服务端的资源管理复杂性及提高分布式集群系统的容错能力,即使部分系统出现故障,不会造成整体的宕机。
在shiro中可以很简单的设置为无状态(无会话)模式:

[main]
...
securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
...

如此配置后,shiro将不会再保存任何Subject对象的状态到session中,用户也不能再跨request在session中传递数据。每次的request都要携带认真信息来告诉shiro访问者的身份。

混合模式

shiro甚至支持部分Subject有状态(有会话),部分Subject无状态(无会话)。以下情景也许用得到这种混合模式:

  • 人类的用户(访问来自于一个浏览器)可以使用session
  • 非人类用户(访问来自于第三方程序调用API接口)不使用session,因为此类的交互可能是间歇性的或者不稳定的,保存session会造成资源长期占用
  • 某一类型的用户可以使用session,其他类型的用户不能使用。

混合模式需要借助于SessionStorageEvaluator来实现。
1> SessionStorageEvaluator
可以实现org.apache.shiro.mgt.SessionStorageEvaluator接口来告诉shiro什么样的Subject需要支持session,什么样的不需要支持。
SessionoStorageEvaluator只有一个方法:

public interface SessionStorageEvaluator {
    public boolean isSessionStorageEnabled(Subject subject);
}

其详细信息可以查看SessionStorageEvaluator JavaDoc
可以实现此接口来告诉shiro你的判断。

检查Subject
可以在SessionStorageEvaluator接口的isSessionStorageEnabled(subject)实现方法中,使用Subject对象的所有接口来获取此用户信息,进而做出你的决定。
例如在一个web应用程序中,如果需要基于当前的request来做出一些判断,可以获取到真正的request或者response对象。

...
public boolean isSessionStorageEnabled(Subject subject) {
    boolean enabled = false;
    if (WebUtils.isWeb(Subject)) {
        HttpServletRequest request = WebUtils.getHttpRequest(subject);
        //set 'enabled' based on the current request.
    } else {
        //not a web request - maybe a RMI or daemon invocation?
        //set 'enabled' another way...
    }
    return enabled;
}

2> 配置
可以按照如下方式,将SessionStorageEvaluator接口的实现类配置进shiro:

[main]
...
sessionStorageEvaluator = com.mycompany.shiro.subject.mgt.MySessionStorageEvaluator
securityManager.subjectDAO.sessionStorageEvaluator = $sessionStorageEvaluator
...
Web应用程序

通常,web应用程序只需要简单的有会话或者无会话,而不关心是哪个用户在执行请求。通常这种方式对于支持REST和Message/RMI结构的应用有较好的效果。例如,来自于浏览器的请求需要支持会话,而使用REST或者SOAP的API调用不需要支持会话(一般REST和SOAP调用本身会包含有认证信息)。
要支持这种基于请求uri的混合模式,必须添加一个noSessionCreation过滤器。这个filter可以阻止request来的时候自动创建session。在shiro.ini的urls部分,你可以在所有filter之前定义这个filter来保证session不被使用。

以下shiro.ini的配置告诉shiro,当访问/rest/**这个链接是,调用noSessionCreation过滤器,不创建session。

[urls]
...
/rest/** = noSessionCreation, authcBasic, ...

非/rest/**的url并不会调用noSessionCreation过滤器,依然会创建会话。经过noSessinCreation过滤器后的请求中,在用户Subject对象上面调用以下方法会抛出DisabledSessionException:

  • httpServletRequest.getSession()
  • httpServletRequest.getSession(true)
  • subject.getSession()
  • subject.getSession(true)
    但是如下两个方法不受影响:
  • httpServletRequest.getSession(false)
  • subject.getSession(false)

4.5 加密工具

TODO

五、shiro的配置

为了支持从最简单的命令行程序,到大型的企业系统,再到企业集群应用。shiro提供了丰富的配置入口来达到各种应用的安全需求。
shiro的SecurityManager及其托管了安全管理对象,都兼容javaBean规范,所以可以采用大多数的配置方式,包括xml、json、YAML及java等等。

5.1 通过java代码配置

SecurityManager中组装了几乎所有的shiro安全管理对象,他们都兼容javaBean规范,故可以通过getter 和setter来获取和设置所有的内容实例,因此shiro可以直接采用java编程的形式来配置。

如下三行代码实现了一个简单的shiro环境:

Realm realm = //后面Realm章节会介绍如何获取或者创建Realm实例
SecurityManager securityManager = new DefaultSecurityManager(realm);
//由于SecurityUtils是一个静态类,这里将securityManager对象设置到了这个全局静态类中。
SecurityUtils.setSecurityManager(securityManager);

如下代码,实现了一个sessionDao的实现和配置:

...
DefaultSecurityManager securityManager = new DefaultSecurityManager(realm);
SessionDAO sessionDAO = new CustomSessionDAO();
((DefaultSessionManager)securityManager.getSessionManager()).setSessionDAO(sessionDAO);
...

shiro大量支持如上的配置方式,优点是快捷简单,节省系统资源,对于嵌入式系统或者移动端程序非常友好,可以有效提高运行效率。但是将配置直接放到代码中,也有很多劣势:

  • 每次修改配置都需要重新编译代码
  • 通过getter获取到的对象实例,需要大量的强制转换,提高了耦合性
  • SecurityUtils.setSecurityManager(securityManager)将securityManager实例设置为SecurityUtils静态类的属性,那么整个jvm内存中securityManager都是公用的。

所以,如果不考虑系统性能的情况下,建议采用文本配置文件的配置方式,shiro推荐采用ini文件来保存配置信息。

5.2 通过配置文件配置

5.2.1 ini配置文件介绍

shiro内置了ini(org.apachi.shiro.config.Ini)类用来加载ini配置文件。ini格式的配置文件与java的Properties配置文件类似,其中使用键值对来表示配置信息,不同的是ini配置文件允许使用section语法,来讲一个配置文件分为若干部分,互不影响。

5.2.2 ini配置文件的加载

以下两段代码演示了加载ini配置文件:
直接加载ini配置文件,可以使用前缀classpath:,url:,file:来指定加载位置。

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.config.IniSecurityManagerFactory;
...
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

通过ini对象实例加载ini配置文件:

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.config.Ini;
import org.apache.shiro.config.IniSecurityManagerFactory;
...
Ini ini = new Ini();
//可以对ini实例进行一些操作,再交给SecurityManager的工厂类来初始化。
...
Factory<SecurityManager> factory = new IniSecurityManagerFactory(ini);
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

5.2.3 几个主要Section介绍

以下代码中包含了shiro的几个section:main,users,roles,urls

# =======================
# Shiro INI configuration
# =======================
[main]
# Objects and their properties are defined here,
# Such as the securityManager, Realms and anything
# else needed to build the SecurityManager
[users]
# The 'users' section is for simple deployments
# when you only need a small number of statically-defined
# set of User accounts.
[roles]
# The 'roles' section is for simple deployments
# when you only need a small number of statically-defined
# roles.
[urls]
# The 'urls' section is used for url-based security
# in web applications.  We'll discuss this section in the
# Web documentation
[main]

main section用来配置SecurityManager及其依赖,例如Realm。得益于shiro大量的遵守javaBean规范,即使只有键值对数据格式的ini文件也能完成,SecurityManager这种复杂程度的对象的配置。官方称此为“穷人的”依赖注入,因为它不像spring等的xml配置文件那么复杂。好吧,再来一局,良好的设计,造就了shiro的简单而强大。

  • 定义一个对象

    [main]
    myRealm = com.company.shiro.realm.MyRealm
    ...

  • 为属性赋值

基本数据类型的赋值方式:

...
myRealm.connectionTimeout = 30000
#等同于在java中执行:myRealm.setConnectionTimeout(30000);
myRealm.username = jsmith
#等同于在java中执行:myRealm.setUsername("jsmith");
...

使用$符号来进行引用对象实例的赋值:

...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
myRealm.credentialsMatcher = $sha256Matcher
...

可以使用.来进入某个对象的较深层级来进行赋值:

...
securityManager.sessionManager.globalSessionTimeout = 1800000
#等价于java代码:securityManager.getSessionManager().setGlobalSessionTimeout(1800000);
...

字符数据类型的值,需要先转化为base64编码,或者Hex16进制编码进行再进行赋值,推荐使用base64编码:

# 'cipherKey'属性是byte array类型,默认情况下shiro要求将字符数组进行base64编码后再使用
securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==
...
# 也可以使用Hex16进制编码,但是前面需要加上 0x前缀,以告诉shiro是16进制编码
securityManager.rememberMeManager.cipherKey = 0x3707344A4093822299F31D008

集合类型数据采用,来表示:

#Set 或者 List
sessionListener1 = com.company.my.SessionListenerImplementation
...
sessionListener2 = com.company.my.other.SessionListenerImplementation
...
securityManager.sessionManager.sessionListeners = $sessionListener1, $sessionListener2

#Map
object1 = com.company.some.Class
object2 = com.company.another.Class
...
anObject = some.class.with.a.Map.property
anObject.mapProperty = key1:$object1, key2:$object2

#Map 类型不仅可以使用String作为key值,也可以使用对象来作为key值
anObject.map = $objectKey1:$objectValue1, $objectKey2:$objectValue2
...

ini配置文件赋值顺序问题,shiro会将ini文件翻译为java代码来顺次执行,因此如果对一个引用重复赋值,那么新的值会覆盖老的值,并且老的值会被销毁和GC

...
myRealm = com.company.security.MyRealm
...
myRealm = com.company.security.DatabaseRealm
...

shiro不需要显式的配置securityManager对象,默认已经被实例化好了,可以在配置文件中直接对其配置。也可以自行实现自定义的SecurityManager对象,但是由于securityManager的高可配置性,没有必要自行实现。

[users]

在较小用户数量较少,且不需要再程序中动态修改的程序中,可以直接在shiro.ini配置文件的users部分来定义用户信息,如下:

[users]
admin = secret
lonestarr = vespa, goodguy, schwartz
darkhelmet = ludicrousspeed, badguy, schwartz

shiro官方文档提到,如果配置文件中没有配置[users]和[roles]两部分的话,shiro会自动实例化一个 org.apache.shiro.realm.text.IniRealm对象,这个iniRealm可以[main]部分直接使用。

在以上代码中,每一行内容的定义如下:
username = password, roleName1, roleName2, …, roleNameN
用户名=密码(必须有), 角色名1(可选),...
为了避免将密码显式的写入shiro.ini配置文件中,可以使用shiro提供的Hash工具对密码进行加密,常用的方式有MD5加密和SHA1加密。将加密后的密码写在这里后,还要在[main]部分告诉shiro自动实例化的iniRealm对象应该怎么来解码密码

[main]
...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
iniRealm.credentialsMatcher = $sha256Matcher
...
[users]
# user1 = sha256-hashed-hex-encoded password, role1, role2, ...
user1 = 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b, role1, role2, ...

shiro使用上面配置文件中的sha256Matcher来进行解密,默认sha256Matcher会使用hex来进行编码,若要进行加密细节方面的配置,可以对sha256Matcher的属性进行设置,如下会告诉shiro所使用的sha256Matcher使用base64编码:

[main]
...
# true = hex, false = base64:
sha256Matcher.storedCredentialsHexEncoded = false

也可以根据org.apache.shiro.authc.credential.Sha256CredentialsMatcher的java doc中的介绍,对其进行更多的额设置,例如设置加盐、设置hash次数等。

[roles]

小规模的程序中,可以使用[roles]部分来为[users]部分的角色定义权限。如果程序或系统比较复杂,数据量较大,为每个角色定义访问权限的数据应该放在数据库中。以下是一个roles部分的示例配置:

[roles]
# 'admin' 角色的权限为*,*通配符这里表示所有权限
admin = *
# 'driver'角色拥有汽车相关的所有权限
driver = car:*

roles部分的字段定义为:

rolename = permissionDefinition1, permissionDefinition2, …, permissionDefinitionN

六、Shiro在Web应用中的使用

6.1 配置方式

使用一个Servlet ContextListener即可在一个web应用中启动shiro,并配置shiro.ini配置文件的位置。除了第五章配置所介绍的配置,要在web应用中使用shiro,还有一些专用的配置需要设置。

如果web应用使用了spring容器,参考Spring中集成Shiro

6.1.1 配置web.xml,在web应用启动时启动Servlet ContextListener

shiro 1.2及之后版本的启动方式

对于shiro1.2及之后的版本。将以下xml添加进web.xml中即可启动shiro:

<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>

shiro启动时会自动到以下位置查找shiro.ini配置文件:

  • /WEB-INF/shiro.ini
  • classpath的根目录下面

以上配置下的shiro,会有以下的动作:

  • EnvironmentLoaderListener初始化了一个shiro的 WebEnvironment实例(其中有shiro需要的所有安全对象,包括SecurityManager),其可以在ServletContext中访问。如果需要调用这个WebEnvironment实例,可以使用WebUtils.getRequiredWebEnvironment(servletContext)获取到。
  • ShiroFilter会使用这个WebEnvironment为所有通过过滤器的request执行需要的安全操作。
  • filter-mapping使所有的请求都能被shiroFilter过滤,对于大部分的web应用都应该使用shiroFilter过滤所有的请求。

ShiroFilter的filter-mapping应该放在其他filter-mapping之前,保证ShiroFilter能够首先过滤所有的请求。

1> 自定义WebEnvironment类

默认情况下EnvironmentLoaderListener会基于shiro.ini创建一个IniWebEnvironment实例。也可以使用web.xml中ServletContext的context-param来自定义一个WebEnvironment实例。

<context-param>
    <param-name>shiroEnvironmentClass</param-name>
    <param-value>com.foo.bar.shiro.MyWebEnvironment</param-value>
</context-param>

可以通过实现IniWebEnvironment来自定义WebEnvironment实例,主要用来读取不同格式的配置文件。一般情况下不需要自定义WebEnvironment。

2>自定义配置位置
EnvironmentLoaderListener默认会从按顺序以下位置来读取shiro.ini

  1. /WEB-INF/shiro.ini
  2. classpath:shiro.ini

可以在web.xml中通过配置context-param来指定配置文件路径:

<context-param>
    <param-name>shiroConfigLocations</param-name>
    <param-value>YOUR_RESOURCE_LOCATION_HERE</param-value>
</context-param>

默认情况下,路径通过ServletContext.getResource()方法来解析。但是也可以将shiro.ini放置在文件系统中、classpath或者URL路径中,通过一个shiro的ResourceUtils类支持的资源前缀指明是哪种路径解析方式。shiro支持以下几种路径前缀:

shiro 1.1及之前版本

6.1.2 Web INI 配置

在Web应用中直接使用shiro,还需要shiro.ini的[urls]部分指明对于请求url的不同处理方式:

# [main], [users] and [roles] above here
...
[urls]
...

[urls]部分用来定义一些临时的过滤器链来匹配web应用的URL路径。这样可以相较于web.xml更加灵活和简洁的配置过滤器链。

urls

[urls]部分的配置格式如下:

_URL_Ant_Path_Expression_ = _Path_Specific_Filter_Chain_

例如:

...
[urls]
/index.html = anon
/user/create = anon
/user/** = authc
/admin/** = authc, roles[administrator]
/rest/** = authc, rest
/remoting/rpc/** = authc, perms["remote:invoke"]

1> 等号左侧表示资源路径模式

等号左侧是ant风格的路径表达式,用来表示web应用中的资源相对路径。
例如:

/account/** = ssl, authc

这行配置表示,任意请求到/account及以下资源的请求,都会触发ssl和authc过滤器链。
这里所有的路径都是到达web应用根目录的相对路径。意味着如果资源域名改变,或者端口号改变,此路径依然可以正常工作。这个路径拼接上HttpServletRequest.getContextpath()就是完整的绝对路径。

需要注意顺序问题
请求会按照[urls]部分所配置的路径,按顺序依次查找,一旦有一个路径定义匹配当前请求路径,即使用此路径定义后面的过滤器链,之后的所有路径都会被忽略掉。
例如按照如下配置:
/account/** = ssl, authc
/account/signup = anon
如果一个request请求的资源为/account/signup/index.html,希望走第二条配置,来使用anon过滤器。但是由于第一条就已经匹配成功,那么第二条就不会再进行匹配,因此就与希望的匹配方式不同了。
因此在定义过滤器链时,一定要基于第一次匹配原则来定义。

2> 等号右侧表示过滤器链
等号右侧是一个或者多个过滤器,表示如果请求路径匹配上左侧的路径模式后,需要让此请求通过这些过滤器。过滤器链定义格式如下:

filter1[optional_config1], filter2[optional_config2], ..., filterN[optional_configN]
  • filterN 表示在[main]部分定义的过滤器
  • 中括号中的optional_configN表示针对此过滤器的路径配置,如果不需要特殊指定路径,可以省略[optional_configN]

如果一行配置中,等号右侧有多个过滤器组成过滤器链,匹配的请求会依次通过这些过滤器进行处理,因此过滤器链的顺序要指定的。

shiro自带的filter都可以配置[optional_configN],如果自定义的filter也想使用[optional_configN],需要实现org.apache.shiro.web.filter.PathMatchingFilter。
可用的过滤器
[urls]部分所使用的过滤器,都是在[main]部分定义好的,例如:

[main]
...
myFilter = com.company.web.some.FilterImplementation
myFilter.property1 = value1
...

[urls]
...
/some/path/** = myFilter

6.2 默认的过滤器

shiro自带了一些过滤器,不需要[main]部分声明,即可直接使用,当然也可以显式的在[main]部分定义出来,指定成自定义的filter或者进行属性赋值。例如:

[main]
...
# Notice how we didn't define the class for the FormAuthenticationFilter ('authc') - it is instantiated and available already:
authc.loginUrl = /login.jsp
...

[urls]
...
# make sure the end-user is authenticated.  If not, redirect to the 'authc.loginUrl' above,
# and after successful authentication, redirect them back to the original account page they
# were trying to view:
/account/** = authc
...

默认的filter由DefaultFilter枚举自动定义,DefaultFilter枚举的属性名称都是可用的filter名称,具体如下:

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnoymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.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

6.3 打开和关闭过滤器

如web.xml和shiro.ini中的filter定义方式,添加一个filter即打开它,移除一个filter即关闭它。
shiro1.2后不需要修改filter chain的配置即可激活/关闭filter。可以在配置属性中触发一个filter的打开状态,或者基于一个request来触发filter chain的打开状态。
所有的shiro自带的filter都实现了抽象类OnceperRequesstFilter,来实现这样的功能。自定义的filter也可以实现此抽象类来实现这样的功能。

后续shiro-224可能会支持不实现此抽象类即可拥有打开/关闭filter的功能。

6.3.1 整体打开/关闭

OncePerRequestFilter及其子类支持对于所有request或者指定request打开/关闭。
对于所有的请求,关闭一个filter,可以通过将其enabled属性设置为true或者false实现。默认的enabled值为true。
shiro.ini的配置方式示例如下:

[main]
...
# configure Shiro's default 'ssl' filter to be disabled while testing:
ssl.enabled = false

[urls]
...
/some/path = ssl, authc
/another/path = ssl, roles[admin]
...

此示例演示了如何在开发环境下关闭ssl过滤器。当部署到生产环境之后,只需要简单的将ssl.enabled改为true即可激活通过此过滤器的ssl验证。而不需要更改所有相关的[urls]配置。

6.3.2 基于request的打开/关闭

OncePerRequestFilter实际上通过它的isEnabled(request,response)方法来判断本filter是打开或者是关闭的。
此方法默认的返回属性enabled的值。如果需要基于路径来决定本filter打开或关闭,可以重写OncePerRequesFlter的isEnable(request,response)方法来执行自定义的检查。

6.3.3 基于path的打开/关闭

Shiro的PathMatchingFilter(OncePerRequestFilter的一个子类)可以根据路径来打开和关闭过滤器。
如果希望实现基于路径的过滤器打开和关闭功能,可以通过以上的重写OncePerRequestFilter的isEnable方法来实现,也可以通过重写PathMatchingFilter的isEnabled(request,response,path,pathConfig)来实现。

6.4 会话管理

6.4.1 servlet容器会话

Web环境下,shiro默认是使用ServletContainerSessionManager作为SecurityManager的SessionManager属性的值。ServletContainerSessionManager简单的代理了容器的所有session管理功能。
使用这个默认的ServletContainerSessionManager的好处是可以直接使用容器session的所有配置(timeout,其他的容器集群机制等)都能正常使用。
劣势是只能使用容器的session的功能。例如如果希望在jetty和tomcat容器间创建session集群,那么容器本身的session很难创建跨容器的集群,可以使用shiro自带的session manager来创建于容器无关的session集群。另一个示例是如果测试环境下使用的是jetty,生产环境下是tomcat,那么关于session的设置就要在环境改变时相应修改,使用shiro自带的Session Manager就不需要再环境切换时修改。

Servlet容器会话超时

可以在web.xml中配置默认的容器会话的超时时间,例如:

<session-config>
  <!-- web.xml expects the session timeout in minutes: -->
  <session-timeout>30</session-timeout>
</session-config>

6.4.2 shiro自带的Session

shiro自带的Session Manager有着诸多优势如Session管理所述。例如实现跨容器集群,或者跨容器迁移等。
shiro自带的DefaultWebSessionManager不仅具有本身的特性,而且实现了Servlet规范的相关部分,因此web/http相关的代码依然可以正常使用。使用shiro自带的DefaultWebSessionManager对于Web应用来说是透明的。

DefaultWebSessionManager

要在web应用中使用shiro自带的session管理,需要通过以下配置实现:

[main]
...
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
# configure properties (like session timeout) here if desired
# Use the configured native session manager:
securityManager.sessionManager = $sessionManager

同时可以在配置文件中设置session的超时时间,需要的话可以参照Session设置集群配置。

1> shiro自带Session的超时设置
可以按照Session管理:Session超时设置session的超时时间。

2> Session的Cookie
DefaultWebSessionManager支持两种针对web应用的配置属性:

  • sessionIdCookieEnabled (a boolean)
  • sessionIdCookie, a Cookie instance.

Cookie as a template
sessionIdCookie属性本质上市一个模板-以可以配置Cookie实例的属性,此模板可以使用一个合适的session ID 来创建实际的HTTP Cookie header。

会话cookie的配置
DefaultWebSessionManager的sessionIdCookie默认是一个SimpleCoolie。它的JavaBean风格支持你为http cookie设置所有想要的属性。
例如,可以设置如下Cookie域:

[main]
...
securityManager.sessionManager.sessionIdCookie.domain = foo.com 

详情可参考SimpleCoolie JavaDoc
Cookie默认名称遵循Servlet规范为JSESSIONID。shiro的cookie支持HttpOnly标记,安全起见,默认HttpOnly为true。

Shiro的DefaultWebSessionManager支持HttpOnly标记早于servlet规范,servlet规范在2.6及之后版本中才支持HttpOnly标记。

关闭会话cookie
可以通过配置sessionIdCookieEnabled 属性来关闭cookie,配置如下:

[main]
...
securityManager.sessionManager.sessionIdCookieEnabled = false

6.5 “记住我”服务

如果AuthenticationToken实现了org.apache.shiro.authc.RememberMeAuthenticationToken,shiro会提供“rememberMe”服务。RememberMeAuthenticationToken中包含了一个方法:

boolean isRememberMe();

如果AuthenticationToken的isRememberMe()方法返回true,shiro会跨会话的记住AuthenticationToken所代表用户的信息。

UsernamePasswordToken和RememberMe
最常用的AuthenticationToken是UsernamePasswordToekn,其实现了RememberMeAuthenticationToken接口,因此在登陆的时候支持"RememberMe"功能。

6.5.1 编程实现“remember me”功能

在代码中,仅需要调用实现了RememberMeAuthenticationToken的AuthenticationToken的实现的setRememberMe(true)方法,即可对token对应的用户激活RememberMe状态。

UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
SecurityUtils.getSubject().login(token);
...

6.5.2 基于表单的登陆

Web应用中,authc过滤器默认是FormAuthenticationFilter。这支持直接从提交的身份认证信息中读取“rememeberMe”布尔值。默认的,它会检查提交表单中名称为“rememberrMe”的参数。如下是一个shiro.ini的配置示例:

[main]
authc.loginUrl = /login.jsp
[urls]
# your login form page here:
login.jsp = authc

Html中的form示例:

<form ...>
   Username: <input type="text" name="username"/> <br/>
   Password: <input type="password" name="password"/>
    ...
   <input type="checkbox" name="rememberMe" value="true"/>Remember Me?
   ...
</form>

默认的,FormAuthenticationFilter会检查request参数中名称为“username”“password”和“remberMe”的参数。如果表单你中的名称不是这些,那么需要为FormAuthenticationFilter自定义表单字段名称,配置方式如下:

[main]
...
authc.loginUrl = /whatever.jsp
authc.usernameParam = somethingOtherThanUsername
authc.passwordParam = somethingOtherThanPassword
authc.rememberMeParam = somethingOtherThanRememberMe
...

6.5.3 Cookie配置

可以设置SecurityManager的rememberMeManager中cookie的属性来配置cookie:

[main]
...
securityManager.rememberMeManager.cookie.name = foo
securityManager.rememberMeManager.cookie.maxAge = blah
...

详情参阅CookieRememberMeManagerSimpleCookie的JavaDoc。

6.5.4 自定义RememberMeManager

可以通过以下配置,来自定义RememberMeManager:

[main]
...
rememberMeManager = com.my.impl.RememberMeManager
securityManager.rememberMeManager = $rememberMeManager

6.6 JSP标签库

Apache Shiro提供了Subject-aware JSP标签库,允许你基于当前用户状态来控制jsp jstl的输出。当编写基于用户状态展现不同内容的jsp时,这些标签非常有用。

6.6.1 标签库配置

shiro的TLD(Tag Library Descriptr)文件在shiro-web.jar的META-INF/shiro.tld文件中。要使用这些标签,添加下面一行到jsp页面的顶部:

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

这里使用shiro作为shiro标签的前缀,以区别命名空间,可以自定义自己想要的前缀。
如下介绍各个标签的使用。

6.6.2 guest标签

只有当前用户为guest的,被guest标签所包括的代码才会显示。Subject对象中没有任何标识的都会被认为是guest,或者没有经过认证且没有被rememberMe记住的都是guest。

<shiro:guest>
    Hi there!  Please <a href="login.jsp">Login</a> or <a href="signup.jsp">Signup</a> today!
</shiro:guest>

6.6.3 user标签

只有当前用户被认为是“user”,其包括的代码才会被显示。被认证或被“rememberMe”记住的用户都是“user”。此标签与“authenticated”标签并不相同,“authenticated”标签只认通过认证的用户,而不认“rememberMe”记住的用户。

<shiro:user>
    Welcome back John!  Not John? Click <a href="login.jsp">here<a> to login.
</shiro:user>

user标签与guest标签所表示的意义互斥。

6.6.4 authenticated标签

只有当前用户通过认证,被authenticated标签包括的jsp代码才会被放开和显示。authenticated管制的范围比user标签更加严格,其只会放开通过认证的用户,user不仅放开通过认证的用户,还放开“RememberMe”记住的用户。用来保护更加敏感的代码。
以下是authenticated标签的使用示例:

<shiro:authenticated>
    <a href="updateAccount.jsp">Update your contact information</a>.
</shiro:authenticated>

authenticated标签与notAuthenticated标签逻辑相反。

6.6.5 notAuthenticated标签

notAuthenticated标签只会放开当前用户未通过认证的内容。
以下是notAuthenticated标签的使用示例:

<shiro:notAuthenticated>
    Please <a href="login.jsp">login</a> in order to update your credit card information.
</shiro:notAuthenticated>

notAuthenticated标签与authenticated标签逻辑相反。

6.6.6 principal标签

principal标签会输出用户的用户名,或者其他表示用户标识的信息。
如果不带属性的使用principal标签,会调用principal的toString()方法输出字符串格式的用户名。例如:

Hello, <shiro:principal/>, how are you today?

以上jsp代码与以下jsp代码一致:

Hello, <%= SecurityUtils.getSubject().getPrincipal().toString() %>, how are you today?
有类型的用户名Typed principal

subject.getPrincipal()方法会输出默认的用户标识信息,但是用户的Principal可能是一个集合,里面包含了多个可以标识用户的信息。那么使用带类型属性的principal标签就可以按照类型来获取用户标识。
例如,要输出一个用户的ID(不要用户名),假设用户ID被保存在principal集合中:

User ID: <principal type="java.lang.Integer"/>

以上jsp代码与以下jsp代码一致:

User ID: <%= SecurityUtils.getSubject().getPrincipals().oneByType(Integer.class).toString() %>
Principal属性Principal property

当principal集合中保存的用户标识信息是一个复杂引用类型(而不是简单类型),你想获得此标识的引用。可以使用property属性来指定标识名(标识对象中必须有相应名称的属性,及相应的set get方法,兼容JavaBeans规范)。
例如(假设首选用户标识是一个用户对象):

Hello, <shiro:principal property="firstName"/>, how are you today?

以上jsp代码与以下jsp代码一致:

Hello, <%= SecurityUtils.getSubject().getPrincipal().getFirstName().toString() %>, how are you today?

principal标签的property属性也可以和type属性一块儿使用:

Hello, <shiro:principal type="com.foo.User" property="firstName"/>, how are you today?

与以下jsp代码一致:

Hello, <%= SecurityUtils.getSubject().getPrincipals().oneByType(com.foo.User.class).getFirstName().toString() %>, how are you today?

6.6.7 hasRole标签

hasRole标签会在当前用户被分配至hasRole标签指定的角色时,展示被包括的内容。
例如:

<shiro:hasRole name="administrator">
    <a href="admin.jsp">Administer the system</a>
</shiro:hasRole>

hasRole标签和lacksRole标签逻辑相反。

6.6.8 lacksRole标签

和hasRole标签相反,当前用户没有lacksRole标签指定的角色时,展示其包括的内容。例如:

<shiro:lacksRole name="administrator">
    Sorry, you are not allowed to administer the system.
</shiro:lacksRole>

6.6.9 hasAnyRole标签

hasAnyRole标签在当前用户拥有指定的多个角色中的任意一个或多个时,就会展示其所包括的内容。
例如:

<shiro:hasAnyRoles name="developer, project manager, administrator">
    You are either a developer, project manager, or administrator.
</shiro:hasAnyRoles>

hasAnyRole标签没有逻辑相反的对应标签。

6.6.10 hasPermission标签

hasPermission标签在当前用户所对应的角色被分配了指定的许可时,展示其所包括的内容。
例如:

<shiro:hasPermission name="user:create">
    <a href="createUser.jsp">Create a new User</a>
</shiro:hasPermission>

hasPermission标签与lacksPermission标签逻辑相反。

6.6.11 lacksPermission标签

与hasPermission标签相反,lacksPermission标签在当前用户所对应的角色没有所指定的许可时,展示其所包括的内容。
例如:

<shiro:lacksPermission name="user:delete">
    Sorry, you are not allowed to delete user accounts.
</shiro:lacksPermission>

lacksPermission标签与hasPermission标签逻辑相反。

七、Spring中集成Shiro

经过前面的一系列对shiro的介绍,其实这里来到了可能使用最为多的场景。shiro在管理b/s架构的web应用上面试一把绝对的好手,那么现在b/s架构的web应用大部分都使用spring作为bean容器。
shiro兼容javaBean规范,因此很容易放到spring容器中进行管理。关键点在于在spring容器中保留一份唯一的SecurityManager单例,静态的单例或者普通单例都可以。

7.1 Spring管理的独立应用

如下是一个简单示例,介绍了如何在Spring管理的独立(非 web)应用中使用单例SecurityManager:

<!-- 自定义的能够连接包含了安全相关信息的数据源的Realm对象: -->
<bean id="myRealm" class="...">
    ...
</bean>

<bean id="securityManager" class="org.apache.shiro.mgt.DefaultSecurityManager">
    <!-- 这里只配置了一个realm,如果有多个realm,使用realms属性进行配置 -->
    <property name="realm" ref="myRealm"/>
</bean>

<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<!-- 这里将SecurityManager配置为了一个静态的单例,在web应用中不能使用静态单例 -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
    <property name="arguments" ref="securityManager"/>
</bean>

7.2 Spring管理的Web应用

Shiro拥有对于spring web应用一流的支持水平,或者说其特别适合做spring web应用的安全管理框架。所有被Shiro过滤的请求,都会通过一个强大的shiro的主过滤器,其可以根据URL路径表达式来确定请求都需要通过哪些过滤器。
Shiro1.0的spring web应用配置,需要在web.xml中配置所有的过滤器及相应的配置属性,在spring XML中定义和配置SecurityManager。这样做有如下缺点:

  1. 无法在一个位置完成所有的shiro相关配置
  2. 影响了spring一些高级配置功能的使用,例如PropertyPlaceHolderConfigurer和抽象Bean来形成通用配置等。

目前在Shiro1.0及之后版本中,所有的shiro配置都在Spring XML中完成,这样就可以使用spring强大的配置功能。
如下是一个spring web中使用shiro的示例:

web.xml

除了Spring web.xml需要的元件(ContextLoaderListener, Log4jConfigListener等),定义如下的shiro相关的filter和filter-mapping:

<!-- Shiro的主过滤器-->
<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-mapping的第一个,保证所有的请求都会被shiroFilter拦截处理-->
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
applicationContext.xml

在applicationContext.xml中需要配置打开了web功能的SecurityManager实例和web.xml中的shiroFilter所能引用到的实例。

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <!-- override these for application-specific URLs if you like:
    <property name="loginUrl" value="/login.jsp"/>
    <property name="successUrl" value="/home.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/> -->
    <!-- The 'filters' property is not necessary since any declared javax.servlet.Filter bean  -->
    <!-- defined will be automatically acquired and available via its beanName in chain        -->
    <!-- definitions, but you can perform instance overrides or name aliases here if you like: -->
    <!-- <property name="filters">
        <util:map>
            <entry key="anAlias" value-ref="someFilter"/>
        </util:map>
    </property> -->
    <property name="filterChainDefinitions">
        <value>
            # some example chain definitions:
            /admin/** = authc, roles[admin]
            /docs/** = authc, perms[document:read]
            /** = authc
            # more URL-to-FilterChain definitions here
        </value>
    </property>
</bean>

<!-- Define any javax.servlet.Filter beans you want anywhere in this application context.   -->
<!-- They will automatically be acquired by the 'shiroFilter' bean above and made available -->
<!-- to the 'filterChainDefinitions' property.  Or you can manually/explicitly add them     -->
<!-- to the shiroFilter's 'filters' Map if desired. See its JavaDoc for more details.       -->
<bean id="someFilter" class="..."/>
<bean id="anotherFilter" class="..."> ... </bean>
...

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- Single realm app.  If you have multiple realms, use the 'realms' property instead. -->
    <property name="realm" ref="myRealm"/>
    <!-- By default the servlet container sessions will be used.  Uncomment this line
         to use shiro's native sessions (see the JavaDoc for more): -->
    <!-- <property name="sessionMode" value="native"/> -->
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<!-- Define the Shiro Realm implementation you want to use to connect to your back-end -->
<!-- security datasource: -->
<bean id="myRealm" class="...">
    ...
</bean>

7.3 激活Shiro的注解

不论是独立的spring应用,还是web spring应用,都可以使用shiro的注解功能来执行安全检查。(例如:@RequiresRoles,@RequeresPermissions等,这需要使用到shiro的spring aop功能,来扫描类或者方法上面的注解)。
下面是激活shiro知足街的方法,仅需要将如下的been定义添加到applicationContext.xml中:

<!-- Enable Shiro Annotations for Spring-configured beans.  Only run after -->
<!-- the lifecycleBeanProcessor has run: -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean>

7.4 为spring的远程调用加密

shiro支持为spring的远程调用做安全管理。为实现对spring的远程调用的安全管理,需要分别在远程调用的服务端和客户端对shiro进行配置,由于spring的RPC调用并不是很热门的实践方式,此处仅保留原文文档。

7.4.1 服务端配置

When a remote method invocation comes in to a Shiro-enabled server, the Subject associated with that RPC call must be bound to the receiving thread for access during the thread’s execution. This is done by defining Shiro’s SecureRemoteInvocationExecutor bean in applicationContext.xml:

<!-- Secure Spring remoting:  Ensure any Spring Remoting method invocations -->
<!-- can be associated with a Subject for security checks. -->
<bean id="secureRemoteInvocationExecutor" class="org.apache.shiro.spring.remoting.SecureRemoteInvocationExecutor">
    <property name="securityManager" ref="securityManager"/>
</bean>

Once you have defined this bean, you must plug it in to whatever remoting Exporter you are using to export/expose your services. Exporter implementations are defined according to the remoting mechanism/protocol in use. See Spring’s Remoting chapter on defining Exporter beans.

For example, if using HTTP-based remoting (notice the property reference to the secureRemoteInvocationExecutor bean):

<bean name="/someService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
    <property name="service" ref="someService"/>
    <property name="serviceInterface" value="com.pkg.service.SomeService"/>
    <property name="remoteInvocationExecutor" ref="secureRemoteInvocationExecutor"/>
</bean>

7.4.2 客户端配置

When a remote call is being executed, the Subject identifying information must be attached to the remoting payload to let the server know who is making the call. If the client is a Spring-based client, that association is done via Shiro’s SecureRemoteInvocationFactory:

<bean id="secureRemoteInvocationFactory" class="org.apache.shiro.spring.remoting.SecureRemoteInvocationFactory"/>

Then after you’ve defined this bean, you need to plug it in to the protocol-specific Spring remoting ProxyFactoryBean you’re using.

For example, if you were using HTTP-based remoting (notice the property reference to the secureRemoteInvocationFactory bean defined above):

<bean id="someService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
    <property name="serviceUrl" value="http://host:port/remoting/someService"/>
    <property name="serviceInterface" value="com.pkg.service.SomeService"/>
    <property name="remoteInvocationFactory" ref="secureRemoteInvocationFactory"/>
</bean>

八、其他功能

8.1 缓存

Shiro的开发团队考虑到大多数应用系统都非常关心的性能问题,将缓存功能作为首要功能添加进了shiro,来保证安全管理功能能够快速执行。
虽然缓存是shiro的一个基础理念,但是具体缓存的实现却并不是shiro这个安全管理框架所应该关心和实现的。shiro的缓存部分主要是一些抽象的API,用来管理或者使用其他的缓存框架(例如:Hazelcast,Ehcache,OSCache,Terracotta,Coherence,GigaSpaces,JBossCache等),这样shiro的用户可以选择自己合适的缓存框架。

8.1.1 缓存API

Shiro有三个重要的缓存API:

  1. CacheManager: 用来管理所有的缓存,可以返回缓存实例。
  2. Cache:维护缓存数据,比如键值对数据。
  3. CacheManagerAware:实现了此接口的安全组件,可以自动获得CacheManager实例。

一个CacheManager返回一个Cache实例,多个shiro的安全组件都使用了这个Cache实例做数据缓存。
如果为SecurityManager设置了CacheManager,它会按顺序为所有实现了CacheManagerAware接口的Realm设置CacheManager。
Shiro自带的安全组件,例如SecurityManager和AuthenticationRealm、AuthorizingRealm都实现了CacheManagerAware接口。
如下是在shiro.ini中配置CacheManager的方式:

securityManager.realms = $myRealm1, $myRealm2, ..., $myRealmN
...
cacheManager = my.implementation.of.CacheManager
...
securityManager.cacheManager = $cacheManager
# at this point, the securityManager and all CacheManagerAware
# realms have been set with the cacheManager instance

8.1.2 CacheManager实现

shiro自带了若干个CacheManger的实现。

MemoryConstrainedCacheManager

MemoryConstrainedCacheManager用于单JVM环境(非集群/分布式环境),如果你的应用包含了多个JVM(例如,集群wbe应用),并且希望可以在多个JVM之间共享缓存,那么应该使用一个分布式的缓存实现,而不是MemoryConstrainedCacheManager。
MemoryConstrainedCacheManager管理MapCache实例,一个MapCache实例对应一个有名字的cache。每个MapCache实例都对应一个shiro的SoftHashMap,这个SoftHashMap可以根据应用程序运行时的内容限制/需求(通过JDK SoftReference实例)来自动调整自身大小。
以上特性,使得MemoryConstrainedCacheManager应用于单JVM应用时是内存安全的。
但是MemoryConstrainedCacheManager缺少一些高级的缓存功能,比如设置缓存数据的生存时间、过期时间等。要使用这些高级缓存功能,可以使用下面的缓存管理器。
如下是在shiro.ini中配置MemoryConstrainedCacheManager的方式:

MemoryConstrainedCacheManager shiro.ini configuration example
...
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
...
securityManager.cacheManager = $cacheManager
HazelcastCacheManager

TBD

EhCacheManager

TBD

8.1.3 授权缓存失效Authorization Cache Invalidation

AuthorizingRealm有一个clearCachedAuthorizationInfo方法,用来移除某个账户的授权缓存信息。这个方法通常在某个账户的授权信息更改后调用,来保证下一次授权检查使用的是新的数据。

8.2 shiro功能的单元测试

8.2.1 关于测试

Subject章节所述,Subject类似于一个安全视图,表示当前用户,并且Subject实例总是会绑定在一个线程上面来保证线程在任何时候任何环节知道是哪个用户在执行当前的代码逻辑。
要访问当前用户,必须保证三件事:

  1. 必须有一个Subject实例
  2. Subject实例必须绑定到当前线程
  3. 线程执行结束后(或者线程抛出异常),Subject必须解绑来保证线程“干净”,防止线程被还回线程池后还携带有用户信息。

Shiro在结构上有相应组件来为一个运行的程序自动执行这些绑定/解绑逻辑。例如在一个web应用中,shiro的主filter:shiroFilter在过滤到一个请求时会执行这些逻辑。但是由于测试环境的不同,或者框架的不同,我们可能需要根据所使用的测试环境自行实现绑定/解绑的逻辑。

8.2.2 设置测试

我们知道在创建了一个Subject实例后,它必须被绑定到一个线程上面。在线程执行结束后,必须将Subject实例从线程上面解绑来保持线程的干净。
幸运的是,现代的测试框架例如JUnit和TestNG都原生的支持“setup”和“teardown”的理念。可以直接利用这项功能在一个“完整”的应用中模拟shiro的绑定/解绑逻辑。我们已经创建了一个基本的抽象类可以用于实现自己的测试样例(可以根据自己的需要进行修改),可以用于单元测试和集成测试,这里使用JUint作为测试框架,TestNG类似。

AbstractShiroTest
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.UnavailableSecurityManagerException;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.LifecycleUtils;
import org.apache.shiro.util.ThreadState;
import org.junit.AfterClass;

/**
 * Abstract test case enabling Shiro in test environments.
 */
public abstract class AbstractShiroTest {

    private static ThreadState subjectThreadState;

    public AbstractShiroTest() {
    }

    /**
     * Allows subclasses to set the currently executing {@link Subject} instance.
     *
     * @param subject the Subject instance
     */
    protected void setSubject(Subject subject) {
        clearSubject();
        subjectThreadState = createThreadState(subject);
        subjectThreadState.bind();
    }

    protected Subject getSubject() {
        return SecurityUtils.getSubject();
    }

    protected ThreadState createThreadState(Subject subject) {
        return new SubjectThreadState(subject);
    }

    /**
     * Clears Shiro's thread state, ensuring the thread remains clean for future test execution.
     */
    protected void clearSubject() {
        doClearSubject();
    }

    private static void doClearSubject() {
        if (subjectThreadState != null) {
            subjectThreadState.clear();
            subjectThreadState = null;
        }
    }

    protected static void setSecurityManager(SecurityManager securityManager) {
        SecurityUtils.setSecurityManager(securityManager);
    }

    protected static SecurityManager getSecurityManager() {
        return SecurityUtils.getSecurityManager();
    }

    @AfterClass
    public static void tearDownShiro() {
        doClearSubject();
        try {
            SecurityManager securityManager = getSecurityManager();
            LifecycleUtils.destroy(securityManager);
        } catch (UnavailableSecurityManagerException e) {
            //we don't care about this when cleaning up the test environment
            //(for example, maybe the subclass is a unit test and it didn't
            // need a SecurityManager instance because it was using only
            // mock Subject instances)
        }
        setSecurityManager(null);
    }
}

测试和框架
AbstractShiroTest类的代码使用了Shiro的ThreadState概念和一个静态的SecurityManager。这些技术在测试和框架中非常有用,但是很少用于应用程序的代码中。大多数需要保证线程状态一致的shiro用户会使用Shiro的自动管理机制,也就是"Subject.associateWith"和"Subject.execute"方法。这些方法在Subject 线程联合部分有所叙述。

8.2.3 单元测试

单元测试主要用于在一个有限的范围内测试你你的代码。测试shiro主要测试使用shiro的API来调用的其他代码,对于shiro本身的实现一般工作都不会有问题。
测试shiro的实现是否与应用程序一起工作正常,属于集成测试的内容。

ExampleShiroUnitTest

由于单元测试更加适用于测试自己实现的逻辑(与其他的功能逻辑无关),模拟依赖的API来进行测试是一个好主意。你可以模拟Subject接口让它返回你希望的值,来供测试使用。我们可以使用现代的模拟框架例如EasyMock和Mockito来做模拟。
要注意的一点是,shiro测试的关键是将模拟的Subject实例或者真实的Subject实例绑定到测试执行的线程。因此我们要将模拟的Subject实例绑定到线程上面,使其他的功能按预期执行。

以下是一个使用EasyMock编写的单元测试代码,Mockito类似:

import org.apache.shiro.subject.Subject;
import org.junit.After;
import org.junit.Test;

import static org.easymock.EasyMock.*;

/**
 * Simple example test class showing how one may perform unit tests for 
 * code that requires Shiro APIs.
 */
public class ExampleShiroUnitTest extends AbstractShiroTest {

    @Test
    public void testSimple() {

        //1.  Create a mock authenticated Subject instance for the test to run:
        Subject subjectUnderTest = createNiceMock(Subject.class);
        expect(subjectUnderTest.isAuthenticated()).andReturn(true);

        //2. Bind the subject to the current thread:
        setSubject(subjectUnderTest);

        //perform test logic here.  Any call to
        //SecurityUtils.getSubject() directly (or nested in the
        //call stack) will work properly.
    }

    @After
    public void tearDownSubject() {
        //3. Unbind the subject from the current thread:
        clearSubject();
    }

}

如上面代码所示,并没有设置一个SecurityManager实例或者配置一个Realm等安全组件。这里仅是创建了一个mock用户实例,并通过setSubject方法绑定到线程上,这能够保证测试代码中的所有调用SecurityUtils.getSubject()能够正常工作。
另外,setSubject方法的实现可以将你的mock Subject绑定到线程,它能够一直保留到通过setSubject设置另外一个Subject实例或者显式的通过clearSubject()方法将Subject从线程清除。
subject绑定到线程(或者清除后用于另一个新的实例)的时间长短,取决于测试的需求。

tearDownSubject()

以上示例中的tearDownSubject()方法使用了一个@Junit4注解来确保Subject在测试方法执行后能够从线程上面清除掉。这需要你通过setSubject为每一次测试设置一个新的Subject实例。
但这并不是严格需要的。例如,你可以使用@Before-annotated注解,仅仅在每次测试开始时,通过setSubject绑定一个新的Subject实例。这样的话,还需要在tearDownSubject()方法上面使用@After注解,来使功能对称,保持测试线程干净。
还可以使用@Before和@After注解来自己控制setup/teardown逻辑。AbstractShiroTest父类会在所有的测试执行完后从线程中解绑所有Subject,因为父类的tearDownShiro()方法上面有@AfterClass注解。

8.3 shiro功能的集成测试

单元测试之后,就是集成测试。集成测试时测试各实例通过API的调用。例如测试实例A,实例A调用了实例B,实例B是否按照预期执行。
可以简单的对shiro进行集成测试。shiro的SecurityManager实例和其所包含的安全组件(例如Realms和SessionManager等)都是轻量级的POJO,只使用少量的内容。这意味着你可以为每一个测试类创建和拆除一个SecurityManager实例。当集成测试运行时,会模拟应用程序正常运行时的状态,使用‘真正的’SecurityManager或Subject实例。

ExampleShiroIntegrationTest

此集成测试的示例代码看上去和上面的单元测试一样,但是第三步处理有些微不同:
这里有一个‘0’步,创建了一个‘真正的’SecurityManager实例。
第‘1’步使用Subject.Builder构造了一个‘真正的’Subject'实例,并将其绑定到线程上。
线程绑定和解绑(第2步和第3步)功能和单元测试一样。

import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;

public class ExampleShiroIntegrationTest extends AbstractShiroTest {

    @BeforeClass
    public static void beforeClass() {
        //0.  Build and set the SecurityManager used to build Subject instances used in your tests
        //    This typically only needs to be done once per class if your shiro.ini doesn't change,
        //    otherwise, you'll need to do this logic in each test that is different
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:test.shiro.ini");
        setSecurityManager(factory.getInstance());
    }

    @Test
    public void testSimple() {
        //1.  Build the Subject instance for the test to run:
        Subject subjectUnderTest = new Subject.Builder(getSecurityManager()).buildSubject();

        //2. Bind the subject to the current thread:
        setSubject(subjectUnderTest);

        //perform test logic here.  Any call to
        //SecurityUtils.getSubject() directly (or nested in the
        //call stack) will work properly.
    }

    @AfterClass
    public void tearDownSubject() {
        //3. Unbind the subject from the current thread:
        clearSubject();
    }
}

可以通过setSecurityManager方法来为测试的remainder设置SecurityManager实例和访问。测试方法可以随后通过getSecurityManager()方法使用Subject.Builder使用SecurityManager示例。

Also note that the SecurityManager instance is set up once in a @BeforeClass setup method - a fairly common practice for most test classes. But if you wanted to, you could create a new SecurityManager instance and set it via setSecurityManager at any time from any test method - for example, you might reference two different .ini files to build a new SecurityManager depending on your test requirements.

Finally, just as with the Unit Test example, the AbstractShiroTest super class will clean up all Shiro artifacts (any remaining SecurityManager and Subject instance) via its @AfterClass tearDownShiro() method to ensure the thread is ‘clean’ for the next test class to run.

shiro 命令行Hash工具
可以在shiro的发布包中,或者通过以下maven命令来获得到shiro-tools-hasher-version-cli.jar这个jar包。

# 如下命令会把shiro-tools-hasher工具下载到如下路径:
# ~/.m2/repository/org/apache/shiro/tools/shiro-tools-hasher/X.X.X/shiro-tools-hasher-X.X.X-cli.jar
$ mvn dependency:get -DgroupId=org.apache.shiro.tools -DartifactId=shiro-tools-hasher -Dclassifier=cli -Dversion=X.X.X

在命令行中执行以下命令,可以在命令行打印所有可以使用的加密方式(MD5,SHA1等)。详细使用可参考Command Line Hasher