Apache Shiro 教程

您的第一个 Apache Shiro 应用程序

如果您不熟悉 Apache Shiro,此简短教程将向您展示如何设置受 Apache Shiro 保护的初始且非常简单的应用程序。我们将一路讨论 Shiro 的核心概念,以帮助您熟悉 Shiro 的设计和 API。

如果您不希望在遵循本教程的过程中实际编辑文件,则可以获取几乎相同的示例应用程序并随时参考。选择一个位置:

Setup

在这个简单的示例中,我们将创建一个非常简单的命令行应用程序,该应用程序将运行并快速退出,以便您可以轻松了解 Shiro 的 API。

Any Application

Apache Shiro 从一开始就设计为支持任何应用程序-从最小的命令行应用程序到最大的群集 Web 应用程序。即使我们正在为本教程创建一个简单的应用程序,也要知道,无论您的应用程序如何创建或部署在哪里,都将使用相同的使用模式。

本教程需要 Java 1.6 或更高版本。我们还将使用 Apache Maven作为构建工具,但是使用 Apache Shiro 当然不需要。您可以获取 Shiro 的.jars 并将其以任何您喜欢的方式合并到您的应用程序中,例如使用 Apache AntIvy

对于本教程,请确保您使用的是 Maven 2.2.1 或更高版本。您应该能够在命令提示符下键入mvn --version并看到类似以下内容的内容:

测试 Maven 安装

hazlewood:~/shiro-tutorial$ mvn --version
Apache Maven 2.2.1 (r801777; 2009-08-06 12:16:01-0700)
Java version: 1.6.0_24
Java home: /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
Default locale: en_US, platform encoding: MacRoman
OS name: "mac os x" version: "10.6.7" arch: "x86_64" Family: "mac"

现在,在文件系统上创建一个新目录,例如 shiro-tutorial ,并将以下 Maven pom.xml 文件保存在该目录中:

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>3.8.0</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>

        <!-- This plugin is only to test run our little application.  It is not
             needed in most Shiro-enabled applications: -->
            <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>Tutorial</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.1</version>
        </dependency>
        <!-- Shiro uses SLF4J for logging.  We'll use the 'simple' binding
             in this example app.  See http://www.slf4j.org for more info. -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.21</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.21</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

辅导班

我们将运行一个简单的命令行应用程序,因此我们需要使用public static void main(String[] args)方法创建一个 Java 类。

在包含pom.xml文件的同一目录中,创建* src/main/java子目录。在src/main/java中,创建一个Tutorial.java文件,其内容如下:

src/main/java/Tutorial.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 Tutorial {

    private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);

    public static void main(String[] args) {
        log.info("My First Apache Shiro Application");
        System.exit(0);
    }
}

暂时不用担心导入语句-我们很快就会处理它们。但是到目前为止,我们已经有了一个典型的命令行程序“ shell”。该程序所要做的就是打印文本“ My First Apache Shiro Application”并退出。

Test Run

要尝试我们的 Tutorial 应用程序,请在您的 Tutorial 项目的根目录(例如shiro-tutorial)的命令提示符下执行以下命令,然后键入以下内容:

mvn compile exec:java

您将看到我们的小教程“应用程序”运行并退出。您应该看到类似于以下内容(请注意粗体字,指示我们的输出):

运行应用程序

lhazlewood:~/projects/shiro-tutorial$ mvn compile exec:java ... a bunch of Maven output ... 1 [Tutorial.main()] INFO Tutorial - My First Apache Shiro Application lhazlewood:~/projects/shiro-tutorial\$

我们已经验证了该应用程序已成功运行-现在让我们启用 Apache Shiro。在 continue 本教程时,每次添加更多代码以查看更改结果后,您都可以运行mvn compile exec:java

Enable Shiro

在应用程序中启用 Shiro 时要了解的第一件事是,Shiro 中的几乎所有内容都与称为SecurityManager的中央/核心组件有关。对于熟悉 Java 安全性的人来说,这是 Shiro 对 SecurityManager 的概念-它与java.lang.SecurityManager是“不”相同。

虽然我们将在Architecture章节中详细介绍 Shiro 的设计,但现在足以知道 Shiro SecurityManager是应用程序的 Shiro 环境的核心,每个应用程序必须存在一个SecurityManager。因此,我们在 Tutorial 应用程序中必须做的第一件事是设置SecurityManager实例。

Configuration

虽然我们可以直接实例化SecurityManager类,但是 Shiro 的SecurityManager实现具有足够的配置选项和内部组件,这使得在 Java 源代码中很难做到这一点-使用灵活的基于文本的配置格式来配置SecurityManager会容易得多。

为此,Shiro 通过基于文本的INI配置提供了默认的“共母”解决方案。如今,人们已经厌倦了使用庞大的 XML 文件,并且 INI 易于阅读,易于使用并且几乎不需要依赖。稍后您还将看到,通过简单地了解对象图导航,可以有效地使用 INI 来配置简单的对象图,例如 SecurityManager。

Many Configuration Options

Shiro 的SecurityManager实现和所有支持的组件都与 JavaBeans 兼容。这使 Shiro 可以使用几乎任何配置格式进行配置,例如 XML(Spring,JBoss,Guice 等),YAML,JSON,Groovy Builder 标记等。 INI 只是 Shiro 的“通用分母”格式,允许在没有其他选项的情况下在任何环境中进行配置。

shiro.ini

因此,我们将使用 INI 文件为该简单应用程序配置 Shiro SecurityManager。首先,从pom.xml所在的目录创建一个 src/main/resources 目录。然后在新目录中创建一个shiro.ini文件,其内容如下:

src/main/resources/shiro.ini

# =============================================================================
# Tutorial INI configuration
#
# Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
# =============================================================================

# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

如您所见,此配置基本上设置了一小组静态用户帐户,足以满足我们的第一个应用程序的需要。在后面的章节中,您将看到我们如何使用关系数据库,LDAP 和 ActiveDirectory 等更复杂的 User 数据源。

引用配置

现在已经定义了一个 INI 文件,我们可以在 Tutorial 应用程序类中创建SecurityManager实例。更改main方法以反映以下更新:

public static void main(String[] args) {

    log.info("My First Apache Shiro Application");

    //1.
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");

    //2.
    SecurityManager securityManager = factory.getInstance();

    //3.
    SecurityUtils.setSecurityManager(securityManager);

    System.exit(0);
}

接下来,仅添加三行代码即可在示例应用程序中启用 Shiro!那有多容易?

随意运行mvn compile exec:java并查看一切仍可成功运行(由于 Shiro 的默认调试日志记录或更低版本,您将看不到任何 Shiro 日志消息-如果它启动并运行无错误,则说明一切仍然正常)。

以下是上述添加的功能:

  • 我们使用 Shiro 的IniSecurityManagerFactory实现来摄取位于 Classpath 根目录的shiro.ini文件。此实现反映了 Shiro 对工厂方法设计模式的支持。 classpath:前缀是一个资源指示符,用于告诉 shiro 从何处加载 ini 文件(也支持其他前缀,如url:file:)。

  • 调用factory.getInstance()方法,该方法解析 INI 文件并返回反映配置的SecurityManager实例。

  • 在这个简单的示例中,我们将SecurityManager设置为* static *(内存)单例,可通过 JVM 访问。但是请注意,如果您在单个 JVM 中拥有多个启用 Shiro 的应用程序,则这是不希望的。对于这个简单的示例,可以,但是更复杂的应用程序环境通常会将SecurityManager放置在特定于应用程序的内存中(例如,在 Web 应用程序的ServletContext或 Spring,Guice 或 JBoss DI 容器实例中)。

Using Shiro

现在我们的 SecurityManager 已经设置好并且可以使用了,现在我们可以开始做我们 true 关心的事情了-执行安全操作。

在保护我们的应用程序安全时,我们可能问自己最相关的问题是“当前用户是谁?”或“是否允许当前用户执行 X”?在编写代码或设计用户界面时,通常会问这些问题:应用程序通常是基于用户案例构建的,并且您希望基于每个用户来表示(并保护)功能。因此,我们考虑应用程序安全性的最自然方法是基于当前用户。 Shiro 的 API 的Subject概念从根本上代表了“当前用户”的概念。

在几乎所有环境中,都可以通过以下调用获取当前执行的用户:

Subject currentUser = SecurityUtils.getSubject();

使用SecurityUtilsgetSubject(),我们可以获得当前正在执行的Subject。 * Subject *是一个安全术语,基本上表示“当前正在执行的用户的特定于安全性的视图”。它不称为“用户”,因为“用户”一词通常与人类相关联。在安全性世界中,“主题”一词可以表示一个人,但也可以指第三方进程,cron 作业,守护程序帐户或任何类似内容。它仅表示“当前正在与软件交互的事物”。但是对于大多数意图和目的,您可以将Subject视为 Shiro 的“用户”概念。

独立应用程序中的getSubject()调用可能会基于特定于应用程序位置的用户数据返回Subject,而在服务器环境(例如 Web 应用程序)中,它会基于与当前线程或传入请求相关联的用户数据获取Subject

现在您有了Subject,该怎么办?

如果要在用户与应用程序的当前会话期间使事情可用,则可以获取他们的会话:

Session session = currentUser.getSession();
session.setAttribute( "someKey", "aValue" );

Session是 Shiro 特定的实例,它提供了常规 HttpSession 所使用的大部分功能,但具有一些额外的好处,并且有一个“很大的”区别:它不需要 HTTP 环境!

如果部署在 Web 应用程序中,则默认情况下Session将基于HttpSession。但是,在非 Web 环境中,例如此简单的教程应用程序,Shiro 会默认自动使用其企业会话 Management。这意味着无论部署环境如何,您都可以在任何层的应用程序中使用相同的 API!这为应用程序打开了一个全新的世界,因为不需要强制要求会话的任何应用程序使用HttpSession或 EJB 状态会话 Bean。而且,任何 Client 端技术现在都可以共享会话数据。

因此,现在您可以获得Subject和他们的Session。 “true”有用的东西(例如检查是否被允许做事,例如检查角色和权限)呢?

好吧,我们只能对已知用户进行检查。上面的Subject实例代表当前用户,但是是当前用户?好吧,他们是匿名的-也就是说,直到他们至少登录一次。因此,让我们这样做:

if ( !currentUser.isAuthenticated() ) {
    //collect user principals and credentials in a gui specific manner
    //such as username/password html form, X509 certificate, OpenID, etc.
    //We'll use the username/password example here since it is the most common.
    UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");

    //this is all you have to do to support 'remember me' (no config - built in!):
    token.setRememberMe(true);

    currentUser.login(token);
}

而已!再简单不过了。

但是,如果他们的登录尝试失败了怎么办?您可以捕获各种特定的异常,这些异常可以准确地告诉您发生了什么,并允许您进行相应的处理和响应:

try {
    currentUser.login( token );
    //if no exception, that's it, we're done!
} catch ( UnknownAccountException uae ) {
    //username wasn't in the system, show them an error message?
} catch ( IncorrectCredentialsException ice ) {
    //password didn't match, try again?
} catch ( LockedAccountException lae ) {
    //account for that username is locked - can't login.  Show them a message?
}
    ... more types exceptions to check if you want ...
} catch ( AuthenticationException ae ) {
    //unexpected condition - error?
}

您可以检查许多不同类型的异常,也可以针对自定义条件抛出自己的异常,而 Shiro 可能无法解决。有关详情,请参见AuthenticationException JavaDoc

Handy Hint

安全最佳实践是将通用的登录失败消息发送给用户,因为您不想帮助攻击者尝试闯入您的系统。

好的,到目前为止,我们已经有一个登录用户。我们还能做什么?

假设他们是谁:

//print their identifying principal (in this case, a username): 
log.info( "User [" + currentUser.getPrincipal() + "] logged in successfully." );

我们还可以测试一下它们是否具有特定作用:

if ( currentUser.hasRole( "schwartz" ) ) {
    log.info("May the Schwartz be with you!" );
} else {
    log.info( "Hello, mere mortal." );
}

我们还可以查看他们是否有权对某种类型的实体采取行动:

if ( currentUser.isPermitted( "lightsaber:wield" ) ) {
    log.info("You may use a lightsaber ring.  Use it wisely.");
} else {
    log.info("Sorry, lightsaber rings are for schwartz masters only.");
}

此外,我们可以执行极其强大的* instance-level *权限检查-能够查看用户是否具有访问类型的特定实例的能力:

if ( currentUser.isPermitted( "winnebago:drive:eagle5" ) ) {
    log.info("You are permitted to 'drive' the 'winnebago' with license plate (id) 'eagle5'.  " +
                "Here are the keys - have fun!");
} else {
    log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}

小菜一碟吧?

最后,当用户使用完应用程序后,他们可以注销:

currentUser.logout(); //removes all identifying information and invalidates their session too.

最终辅导课

添加上述代码示例后,这是我们最终的 Tutorial 类文件。随意编辑和使用它,并根据需要更改安全性检查(和 INI 配置):

Final src/main/java/Tutorial.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 Tutorial {

    private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);

    public static void main(String[] args) {
        log.info("My First Apache Shiro Application");

        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);

        // get the currently executing user:
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        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 + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true);
            try {
                currentUser.login(token);
            } catch (UnknownAccountException uae) {
                log.info("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        //test a typed permission (not instance-level)
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        //a (very powerful) Instance Level permission:
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        //all done - log out!
        currentUser.logout();

        System.exit(0);
    }
}

Summary

希望本入门教程可以帮助您了解如何在基本应用程序中设置 Shiro 以及 Shiro 的主要设计概念SubjectSecurityManager

但这是一个相当简单的应用程序。您可能会问自己:“如果我不想使用 INI 用户帐户,而是想连接到更复杂的用户数据源,该怎么办?”

要回答该问题,需要对 Shiro 的体系结构和支持的配置机制有更深入的了解。接下来我们将讨论 Shiro 的Architecture