你的第一个Apache Shiro应用

     如果你是刚开始接触Apache Shiro,这个简短的教程将会展示怎么构建一个被Apache Shiro保护的简单应用。我们将讨论Shiro的核心概念来帮助你熟悉Shiro的设计和它的API。
     如果你不想编辑教程中涉及到的文件。你可以从下列两个应用程序获取到一个差不多一样的应用,作为参考:

     构建

     在这个简单的例子中,我们将会创建一个简单的命令行应用,它能跑起来并且很快结束。它能让你对Shiro的API有个感性的认识。

     
  Apache Shiro从一开始就被设计成支持所有的应用-从最简单的命令行应用到最庞大的web集群应用。尽管在本教程中我们只创建了一个简单的应用,但是无论在那种应用以及该应用部署在哪,用法都是一样的。


     本教程要求Java1.5或更高版本。我们也采用了Apache Maven作为构建工具,当然这不是必须的。你可以获取Shiro的jar包,以任何你喜欢的方式包含进你的应用,比如可能用Apache Ant和Ivy。

     本教程中,请确保使用Maven2.2.1或更高版本。你可以通过在命令行窗口执行mvn --version命令,得到类似以下信息:


Testing Maven Installation


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,并且保存下面的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>2.0.2</version>
                <configuration>
                    <source>1.5</source>
                    <target>1.5</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.1.0</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.6.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

教程中的类

我们将运行一个命令行程序,所以我们需要创建一个带有main方法的Java类。
和pom.xml文件同一个目录下,创建一个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);
    }
}

不用管那些还没用到的import语句,我们很快就会用到它们。现在我们得到了一个典型的命令行应用‘shell’,这个程序的所有功能就是打印“My First Apache Shiro Application”这条文本然后退出。

测试运行

试试教程中的应用,在教程工程的根目录下(例如 shiro-tutorial)打开命令窗口,执行如下命令:


mvn compile exec:java

然后你就会看到我们教程中的小应用运行起来并且退出。你将看到类似下面的输出:


1 [Tutorial.main()] INFO Tutorial - My First Apache Shiro Application

我们已经验证了这个应用能运行成功,接下来在这个应用中提供Apache Shiro的功能。随着教程的深入,你可以在每次加入代码后,都执行一下“mvn compile exec:java”,看看结果有什么变化。

加入Shiro功能

首先要明白的事情是,在一个加入了Shiro功能的应用中,所有Shiro的功能都关联到一个叫做SecurityManager的核心组件。对于熟悉Java security概念的人得解释一句,这和java.lang.SecurityManager不一样。

我们将在架构章节介绍Shiro的详细设计,现在我们只要知道SecurityManager是Shiro环境的应用的核心,并且每个Shiro环境的应用必须有一个SecurityManager。所以在本教程应用中,我们要做的第一件事就是构建SecurityManager实例。

配置

虽然我们可以直接初始化一个SecurityManager类,但是SecurityManager有很多配置项和内部组件,让我们很难用java源码直接初始化——而利用一个可伸缩的文本格式配置文件去初始化它会容易很多。

为此,Shiro提供了一个默认通用的基于 INI文本配置的解决方案。现在大家都厌倦了使用笨重的xml文件,相对而言,INI易读易用而且依赖少。稍后你会看到一个简单易懂的对象图导航(object graph navigation),INI能有效地配置像SecurityManager一样的简单对象图(object graph )。


很多配置项

Shiro的SecurityManager组件及其支持组件都是JavaBeans兼容的,这就使Shiro能够被任何具体的配置格式配置,比如xml, YAML, JSON, Groovy Builder markup等等。INI只是Shiro的通用配置格式,防止其他配置格式在某种环境下不可用。

shiro.ini

我们在这个简单的教程应用中将用INI文件配置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等等。

引用这个配置

现在我们有了一个INI文件定义,我们可以在教程应用的类中创建一个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的默认日志级别是debug或者更低,所以如果没有error,你将看不到任何日志信息,如果没有信息就说明运行是正确的)。

上面添加的代码做了如下的事情:

1.我们用Shiro的 IniSecurityManagerFactory 获取解析classpath根目录下的shiro.ini文件。它使用了工厂方法模式。classpath:前缀是资源指示器,告诉Shiro从哪里下载ini文件(其他前缀,如url:和file: Shiro也支持)
2.调用了factory.getInstance()的时候,解析ini文件并且返回一个反射了这个配置文件的SecurityManager实例。
3.在这个简单例子中,我们把SecurityManager设置成静态单例,通过JVM直接访问。如果在一个JVM中含有多个加了Shiro功能的应用,这种做法就不可取了。在这个简单的例子中,这样做没问题,但是在更多的复杂应用环境中,我们通常把SecurityManager实例放在具体应用的memory中(比如放在web应用的ServletContext或者一个Spring、Guice或者JBoss DI容器实例中)。

使用Shiro

现在我们的SecurityManager已经构建完成并且可以使用了,现在可以用它做我们真正关心的事情——执行安全操作。

当保护我们的应用,也许我们问的最相关的问题就是“当前的用户是谁?”或“当前的用户允许做X事吗?”。我们编写或设计用户接口来回答这些问题的方式是有共性的:应用通常是建立在用户行为基础之上的,你希望基于每个用户来实现功能(包括安全功能)。所以,在应用中添加安全功能的最自然的方式就是基于当前用户。Shiro的API用用它的“主体(Subject)”概念代表当前用户。

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

Subject currentUser = SecurityUtils.getSubject();

使用 SecurityUtils.getSubject(), 我们可以获取当前执行  Subject. 主体是一个安全术语,它的基本意思是“一个当前执行用户的安全特性视图” 。它不被称为“用户”,是因为“用户”通常用来表示人类。在安全世界里,“主体”这个术语可以表示是一个人,但也可以是一个第三方进程、定时job、守护进程账户或者任何其他相似的东西。它简单地表示“当前和这个软件交互的东西”。但是在大多数情况下,你可以把“主体”概念看成“用户”。

在一个独立应用的getSubject()调用,可能返回包含用户数据的主体,用户数据保存在应用特定的地方。在服务器环境(例如Web应用程序)中,它获取基于与当前线程或传入请求相关联的用户数据的主体(Subject)。

现在你有了一个主体,你能用它做什么呢?

如果你想在应用的用户当前会话期间做一些事情,你可以通过下面的代码获取用户的会话:


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

Session是Shiro特有的东西,提供了大部分你经常用到的HttpSessions的功能,但是有一些额外的好功能,还有一个最大的不同点:它不需要HTTP环境。

如果部署在给web应用中,默认的Session建立在HttpSession上。但是,在一个非web环境,像这个简单的教程应用,Shiro将默认使用它的企业级Session管理。这意味着你可以在应用中使用同一套API——在任何层级,也不管是在什么部署环境下。这开辟了一个应用的新世界,因为任何应用使用Session都不再强制要求用HttpSession或者EJB的有状态Session Beans。并且,任何客户端技术现在都可以共享Session数据。

现在你可以获取主体(Subject)和它的Session。那接下来怎么做真正有用的东西呢?比如通过检查角色和权限,判断某用户是否可以做某事?

然而,我们只能对已知的用户做这些检查。上面代码的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);
}

就这样!不能再简单了!

但是如果他们登录尝试失败了会怎样?你可以catch所有种类的异常,并分别处理它们:


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  获取更多信息。

温馨提示

安全的最佳实践是给一个通用的登录失败消息给用户,因为你不想被攻击者利用错误消息攻破你的系统

OK,现在我们有了一个已登录用户。我们还能做其他什么呢?

让我们说出他们是谁:

//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:weild" ) ) {
    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(); //removes all identifying information and invalidates their session too.

最终的教程类

当上面所有的示例代码都加进来之后,就成了我们最终的教程类文件。你可以随意编辑它,改变安全检查(甚至改变INI配置),只要你喜欢:

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:weild")) {
            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);
    }
}

总结

希望这个入门教程能帮助你理解怎么在一个基本的应用中通过Shiro的最主要的设计理念(主体和SecurityManager)来构建Shiro。

但是,这只是一个相当简单的应用,你应该自问:如果我不想用INI用户账户,而是想连接一个更复杂的用户数据源,我该怎么办?

要回答这个问题,需要更深入地了解Shiro的架构及其配套的配置机制。我们接下来将介绍Shiro的架构( Architecture)。