第一个Apache Shiro应用

如果你刚刚接触Apache Shiro,这个简短的教程将会给你展示如何快速的建立起一个被Apache Shiro保护的初始应用程序。本文中,我们将一直讨论Shiro的核心要点,以便帮助你尽快熟悉Shiro的设计理念与API。

生成应用

在这个简单的例子中,我们将会创建一个非常简单的命令行应用来运行和快速退出,这样你可以快速了解Shiro的API。

Apache Shiro从设计出来的那天起,就是为了能够支持任何应用程序,从最简单的命令行应用到大型的分布式web应用程序。尽管我们在这里只是创建了一个简单的应用程序,但是也同样适用于任何一个你所知的应用。

这个教程最低需要java1.6及以上的环境和Apache Maven依赖管理工具,当然你也可以获取Shiro的jar包来直接在你的应用程序中使用。
同样请确保你使用的Maven版本在2.2.1及以上,你可以在命令提示符下键入命令 **mvn --version**来查看当前版本。

root:~/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"

接下来,我们使用最原始的方式创建一个应用程序(当然读者可以直接使用IDEA来创建),先生成一个目录,比如 shiro-tutorial ,并且在目录下生成一个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>

接下来创建一个有main方法的java类,同样在当前目录下生成子文件目录 src/main/java 和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);
    }
}

测试启动

在当前目录下(shiro-tutorial)输入一下命令提示符(当然你也可以在idea中直接指向main方法):
mvn compile exec:java
你将会看到应用的启动与结束,命令句界面会展示如下内容:

root:~/projects/shiro-tutorial$ mvn compile exec:java

... a bunch of Maven output ...

1 [Tutorial.main()] INFO Tutorial - My First Apache Shiro Application
root:~/projects/shiro-tutorial\$

添加Shiro

在应用程序中启用Shiro时需要注意的是,Shiro中的几乎所有内容都与 SecurityManager 的中心/核心组件相关 - 它与java.lang.SecurityManager不同。
我们将在下个章节来详细了解Shiro的设计理念,本章节知道 SecurityManager 是应用程序中使用的Shiro环境的核心并且每个应用程序必须存在一个 SecurityManager 就足够了。因此,我们在Tutorial应用程序中必须做的第一件事是设置 SecurityManager 实例。
虽然我们可以直接实例化SecurityManager类,但是Shiro的SecurityManager实现有着太多的配置选项和内部组件,这使得我们如果想要采用硬编码的方式来实现会非常困难, 相反,使用灵活的基于文本的配置方式来配置 SecurityManager 会更容易得多。 人们现在已经厌倦了使用庞大的XML文件,INI易于阅读,易于使用,并且需要很少的依赖性。

所以我们将在这个应用程序中使用INI文件来配置Shiro的 SecurityManager 。首先在 pom.xml 同级下创建一个 src/main/resources 目录 ,在其目录下创建一个shiro.ini文件并输入如下内容:

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

# -----------------------------------------------------------------------------
# 用户名及对应的角色
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# 角色名及对应的权限
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

以上内容所示,这个配置设置了一组静态的基础用户账号,足以满足我们的第一个应用程序。在后面的章节中,我们会使用更复杂的用户数据源,如关系数据库,LDAP和ActiveDirectory等。
现在我们已经定义了INI文件,接下来我们在应用程序中创建一个 src/main/resources 实例,在main方法中作如下改动:

public static void main(String[] args) {

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

    //1.创建一个INI安全管理工厂的实例
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");

    //2.获取SecurityManager 实例
    SecurityManager securityManager = factory.getInstance();

    //3.在上下文语义环境中添加SecurityManager 实例
    SecurityUtils.setSecurityManager(securityManager);

    System.exit(0);
}

再次执行 mvn compile exec:java 命令(或执行main方法),能够看到一切执行顺利。
下面具体介绍一下我们的修改:

  1. 我们使用Shiro的 IniSecurityManagerFactory 来实现获取root跟目录下的shiro.ini文件配置。该实现体现了Shiro对设计模式中工厂模式的支持。 classpath: 前缀指明了资源的位置(其他前缀,例如 url:file: 同样支持)
  2. factory.getInstance() 方法是用来解析INI文件并且返回一个对应配置的 SecurityManager 实例。
  3. 在这个简单的示例中,我们将 SecurityManager 设置为可通过JVM访问的静态(内存)单例。需要注意的是,虽然再上面这个例子中可以使用,但是如果您在单个JVM中拥有多个使用Shiro的应用程序将无法进行此操作。更复杂的应用程序环境通常会将 SecurityManager 放在特定于应用程序的内存中(例如在Web应用程序的 ServletContext 或Spring,Guice或JBoss DI容器实例中)。

使用Shiro

现在我们的 SecurityManager 已经准备就绪,我们可以去做真正想做的事 - 执行安全操作。 在保护我们的应用程序时,我们关心的最重要的问题往往是“谁是当前用户?”或“当前用户是否允许执行X”?在我们编写代码或设计用户界面时,通常会问这些问题。应用程序通常是基于用户构建的,因此,我们在应用程序中考虑安全性的最自然方式是基于当前用户。 Shiro的API从根本上代表了“当前用户”的 Subject 概念。
在几乎所有环境中,您都可以通过以下调用获取当前正在执行的用户:

Subject currentUser = SecurityUtils.getSubject();

使用SecurityUtils.getSubject()方法,我们可以获取当前正在执行的 Subject 。 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默认会自动使用企业会话管理。这意味着无论部署环境如何,您都可以在应用程序中使用相同的API!这等于打开了一个全新的应用程序世界,因为任何需要会话的应用程序都不需要被强制使用HttpSession或EJB的有状态会话Bean。而且,现在任何客户端都可以共享会话数据。

所以现在你可以获得一个 SubjectSession 。所以现在可以真正处理那些需要被处理的事情了吗?比如检查是否允许他们做某些事情,比如检查角色和权限? 很抱歉,我们现在还是只能对已知用户进行检查。上面的我们的 Subject 实例代表当前用户,但谁是当前用户?直到他们至少登录一次我们才能知道。所以,让我们这样做:

if ( !currentUser.isAuthenticated() ) {
    //以某种特定的方式收集用户主体和凭据
    //比如网页的form表单 username/password, X509 certificate, OpenID 等等.
    //我们在这里将使用最通用的方法 username/password.
    UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");

    //这就是添加“记住我”功能
    token.setRememberMe(true);

    currentUser.login(token);
}

大功告成,怎么样,是不是简单的不可思议?

但如果用户登录失败了怎么办?我们需要一个异常处理机制来捕获登录异常:

try {
    currentUser.login( token );
    //如果没有异常,代表请求正常走完!
} catch ( UnknownAccountException uae ) {
    //用户名不存在?
} catch ( IncorrectCredentialsException ice ) {
    //密码验证失败,请重试?
} catch ( LockedAccountException lae ) {
    //用户状态被锁?
}
    ... 其他更多的异常 ...
} catch ( AuthenticationException ae ) {
    //意外错误?
}
安全性最佳做法是向用户提供通用的登录失败消息(比如用户名或密码失败),因为您不希望错误提示消息会被帮助攻击者侵入您的系统。

现在我们拿到了登录成功的当前用户,我们才可以真正去做之前想做的事了。

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

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

        // 用Session做一些事情(不需要web或EJB容器!!!)
        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("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 (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //打印用户主体(在这种情况下,用户名):
        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.");
        }

        //测试级别化的权限:
        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();

        System.exit(0);
    }
}

摘要

希望本教程能够帮助您了解如何在应用程序中设置Shiro以及Shiro的主要设计概念,即 SubjectSecurityManager 。 但这是一个相当简单的应用程序。您可能会问自己,“如果我不想使用INI用户帐户而是想连接到更复杂的用户数据源,该怎么办?” 要回答这个问题,需要更深入地了解Shiro的架构和配置机制。下篇文章将介绍Shiro的 Architecture

上一篇:Java策略文件 – 拒绝对代码库的权限


下一篇:QGIS学习一