Apache Shiro 教程

相关内容

开始使用

面向Shiro新用户的一些资源、指南和教程
阅读更多 >>

十分钟Shiro教程

用十分钟的时间亲自动手体验下Shiro。
阅读更多 >>

Web 应用教程

一步一步教你使用Shiro保护web应用。
阅读更多 >>

你的第一个Apache Shiro程序

如果你刚开始接触Apache Shiro,那么这篇教程会向你展示如何搭建一个最基本最简单的Apache Shiro程序,与此同时我们会一边介绍Shiro的核心概念一边帮你熟悉Shiro的设计与API。

如果不想跟着教程编写所有的文件,可以从下面的位置获取一份类似的样例工程以作参考:

搭建

该示例中我们会创建一个简单的命令行程序,运行然后立马退出,这样可以让你体验下Shiro的API。

任何应用
Apache Shiro在设计之初就是为了支持所有的程序——从最小的命令行到最庞大的web集群。即便我们在这里只是创建的一个简单的应用,但Shiro的使用方法却无关乎程序的创建或部署方式。

本教程需要用到Java 1.5或是更高的版本。我们将使用 Apache Maven作为构建工具,当然你也可以使用Apache AntIvy

请确保Maven的版本为2.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”,将下面的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>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>

Tutorial类

因为要运行一个命令行程序,所以需要创建一个带public static void main(String[] args)方法的类。

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

先不要管上面import的声明语句,这个之后再谈。现在可以得到一个命令行shell程序,它打印完“My First Apache Shiro Application”接着就退出了。

测试运行

在上述工程所在的根目录命令行提示符中输入以下:

mvn compile exec:java

接着你会看到这个程序运行然后退出并打印类似如下的信息(注意加粗的部分):

Run the Application

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。随着教程的进行,你可以在每次新增某些代码时运行mvn compile exec:java命令来查看变更后的结果。

启用Shiro

在程序中使用Shiro需要知道的第一件事就是,在Shiro中几乎所有的东西都会跟一个叫SecurityManager的核心组件相关联。对于熟悉Java 安全编程的码农们要注意,这个SecurityManager是Shiro中的概念——它跟java.lang.SecurityManager可不是一回事。

虽然之后我们会于架构这一章中再探讨Shiro的设计,但是现在至少应该了解到在使用Shiro的程序中,SecurityManager处于核心的位置,而且一个程序只能有一个 SecurityManager。所以,我们现在需要做的就是创建一个SecurityManager的实例。

配置

虽然可以直接在代码里实例化SecurityManager类,但是Shiro中SecurityManager的所有实现类都有很多的配置选项和内部组件,这么做会很痛苦,所以还是使用更加灵活的文本配置方式简单一些。

Shiro默认使用基于INI文本格式的配置。相对于冗杂的XML文件,INI更便于阅读和使用,依赖更少。稍后你会发现INI还可以用来配置简单的对象图。

配置格式
Shiro中所有的SecurityManager实现和相关支持组件都是兼容JavaBeans规范的。所以Shiro可以通过各种格式进行配置比如XML (Spring, JBoss, Guice等), YAML, JSON, Groovy等等。INI只是Shiro的默认的通用配置方式。
shiro.ini

因此这里将使用INI文件来配置SecurityManager。首先创建目录结构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文件,现在可以在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的输出日志,如果程序运行正常没有错误,那么就是OK的)!

说明一下上面几行代码:

  1. 这里使用IniSecurityManagerFactory类来提取(解析)位于classpath根路径下的shiro.ini文件,这个类也反应了Shiro对工厂方法模式的支持,classpath:是一个资源指示符,用来告知Shiro载入ini文件的位置(其他前缀如url:file:也是支持的)。
  2. factory.getInstance()会解析INI文件并返回一个配置好的SecurityManager实例。
  3. 在该示例中,我们把SecurityManager设为静态的单例,可以被整个JVM访问到,但是要注意,如果在一个JVM中有多个使用shiro的程序的话这么做是不可以的。简单的情况还好,对于复杂的应用环境应该将SecurityManager放到应用独占的内存中(比如web应用的ServletContext或是 Spring, Guice及JBoss DI的容器实例中)。

使用Shiro

既然SecurityManager已经配置好,现在可以做我们真正关心的事情——执行安全操作。

当保护我们的程序时,最关注的问题可能就是“当前的用户是谁?”或“用户是否允许执行某项操作?”。在设计/编写基于用户故事的代码/界面而你又想拥有基于每个用户展现(或被保护)的功能时这些问题就很常见了。所以很常见的思路就是根据当前的用户来保证应用的安全。而“当前用户”这个概念在Shiro的API中是用Subject来表示的。

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

Subject currentUser = SecurityUtils.getSubject();

使用 SecurityUtils.getSubject()可以得到当前执行(操作的)SubjectSubject是一个安全术语,大概意思就是安全领域视角中的“当前执行操作的用户”。没有使用“User”的原因是这个词通常会联想到人,而在安全领域,术语“Subject”可能是指人,或者第三方进程,定时任务,后台账户等等。所以Subject大概就是“当前正与软件交互的事物”。多数情况下,你可以认为Subject就是Shiro中的“User”。

getSubject()调用在独立程序中返回的Subject是基于程序特定位置的用户数据,而在服务器环境下返回的Subject是基于与当前进程或当前请求关联的用户数据。.

现在有了Subject,可以做些什么呢?

如果想让某些数据在用户的当前会话中都可使用,可以如下:

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

这个Session是Shiro特有的,它提供了HttpSessions大部分的以及其他一些功能,而最大的不同是,Session是不需要HTTP 环境的。

如果部署到web环境,Session默认使用HttpSession。但若是在非web环境,就比如本教程,Shiro会自动使用它自有的企业会话管理功能。也就是说,不管什么样的环境下,你只需要使用一样的API就可以了!这简直就是打开了新世界的大门,从此不必再纠缠于HttpSession或EJB的Session Beans,而且任何的客户端都可以共享会话数据了。

现在可以得到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");

    // 使用“记住我”就只需这一行(无需配置,自带支持)
    token.setRememberMe(true);

    currentUser.login(token);
}

就是这样,简直不能更简单了!

不过要是登录失败了呢?你可以通过捕捉各种异常来知晓原因并做出相应的回应:

try {
    currentUser.login( token );
    // 如果没有问题,那就对了,搞定!
} catch ( UnknownAccountException uae ) {
    // 未知用户的帐号,告诉他们错误信息?
} catch ( IncorrectCredentialsException ice ) {
    // 密码不匹配,重试?
} catch ( LockedAccountException lae ) {
    // 该帐号被锁定了,无法登录。给他们显示一条信息?
} 
    ... more types exceptions to check if you want ...
} catch ( AuthenticationException ae ) {
    //unexpected condition - error?
}

还有很多类型的异常可以检查,或者也可以抛出自己定义的异常。详情还是参考文档AuthenticationException JavaDoc

提醒一下
安全领域的最佳实践是给用户提供通用的登录错误信息,毕竟你也不想帮助黑客们根据不同的提示攻破系统。

好吧,现在有一个登录的用户了,还可以做点什么呢?

看看登录用户的身份吧:

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

最终版本的Tutorial类

通过上面的示例,下面就是我们最终的Tutorial类文件了,你可以随意的修改:

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: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环境以及一些主要的概念,比如SubjectSecurityManager等。

不过这只是一个简单应用。你可能会问:“如果要使用更加复杂而不是INI文件配置的用户数据源该怎么做呢?”

解决这个问题需要对Shiro的架构和配置机制有更深入的理解,我们下一章Architecture再探讨。