Apache Shiro入门指南

你的第一个Apache Shiro应用

如果你是第一次接触Apache Shiro,这个简短的教程将向你展示如何创建并初始化一个非常简单的Apache Shiro应用。在这个过程中我们将讨论Shiro的核心概念,以此来帮助你熟悉Shiro的设计和API。

如果你并不想编写这个教程接下来的代码,你可以通过下面两种方式得到一个简单例子:

Setup

在这个简单的例子中,我们将创建一个非常简单的命令行应用,它将运行并很快的退出,让你领略下Shiro的API。

任何应用
Apache Shiro从设计的第一天起就支持任何应用,从最小的命令行应用到最大的web集群应用。尽管在这个教程中我们创建了一个简单的app,但这些方式也在其他地方也同样适用。

这个教程需要Java 1.5以上,我们也将会使用Apache Maven作为我们的构建工具,当然这并不是使用Apache Shiro所必须的。你也可以使用任何你喜欢的方式获得Shiro的jar包并将它们合并到你的应用中,例如使用Apache Ant和Ivy。

在这个教程中,请确保你使用的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>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>

class

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

在包含pom.xml文件的目录中创建一个src/main/java的子目录,在src/main/java目录中创建Tutorial.java文件并输入下面内容:

src/main/java/Tutoral.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部分,后面我们会讲到。现在,我们得到了一个典型的命令行应用,这个应用将会在控制台输出“My First Apache Shiro Application”然后退出。

Test Run

打开一个命令行窗口,切换到你的tutorial项目的根目录下(例如:shiro-tutorial)并执行下面代码:
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这个命令来查看我们每次添加代码后的结果。

Enable Shiro

在应用中使用Shiro,我们首先要明白的是在Shiro中的所有组件都和一个核心组件相关,这个组件就是SecurityManager。对于那些熟悉Java安全的人来说,这个是Shiro概念里的SecurityManager。它和java.lang.SecurityManager不是同一回事

我们将会在Shiro架构章节详细讲述Shiro的设计细节,现在我们只要知道Shiro SecurityManager是所有使用Shiro的应用的核心,并且每个应用都需要一个SecurityManager就已经足够了。因此,第一件事就是我们必须在我们的应用中获取一个SecurityManager实例。

配置

虽然我们可以直接实例化一个SecurityManager类,但是Shiro的SecurityManager有很多的配置选项和内部组件,使得用Java源代码来配置非常痛苦。更容易和灵活的方式是通过基于文本的配置。

为实现这个目标,Shiro提供了一个默认“common denominator”来实现基于文本 INI 的配置。人们已经厌倦了使用笨重的XML文件,在加上INI文件容易阅读,易于使用,并且依赖较少。你将看到一个简单的对象图,可以有效的使用INI配置简单对象图,就像SecurityManager

配置选项
Shiro的SecurityManager实现和所有支持的组件都和JavaBeans兼容。事实上任何格式的配置文件都可以配置Shiro的SecurityManager,例如XML(Spring,JBoss,Guice等)、YAML、JSON、Groovy Builder markup等其他格式。INI是Shiro“common denominator”的格式,允许在任何环境中使用以防止其他的格式不可用。

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、等等。

配置引用

现在我们定义好了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或更低,因此你不会看到任何Shiro的日志信息。如果运行没有出现任何错误信息,那么就说明一起都正常)。

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

  • 我们使用Shiro的IniSecurityManagerFactory来实现对classpath根目录下的shiro.ini文件的提取。这是通过工厂模式来实现的。classpath:前缀是一个资源指示器,它告诉Shiro到什么地方去找ini文件(还有一些其他前缀,比如url:file:都支持的很好)
  • 调用factory.getInstance()方法,将解析INI文件并返回一个``SecuriManager`实例。
  • 在这个例子中,我们将SecurityManager设置为单例,在JVM范围内访问。

使用Shiro

现在我们的SecurityManager已经设置好并且可以运行了,现在我们可以添加一些安全相关的操作了。
在思考应用安全性的时候,我们可能最关心的事情是“当前用户是谁?”或者“当前用户是否允许做X?”,我们在写代码或者设计用户接口的时候通常会问这些问题。通常构建应用程序是基于用户故事,和你基于用户需求想要的功能。因此,思考应用安全问题最自然的方式便是基于当前用户。在Shiro API里面使用Subject这个概念来代表当前用户。

几乎在所有环境下,你可以使用通过下面的代码获取当前用户:
Subject currentUser = SecurityUtils.getSubject();
使用SecurityUtils.getSubject()我们可以得到当前执行的得SubjectSubject是一个安全术语,意思是当前执行用户的一个特定安全视图(原文使用security-specific view)。我们不称之为“用户”是因为“用户”这个词通常代表一个人类。在安全的世界里,“Subject”可以代表一个真实的人,也可以代表一个第三方处理、cron任务、守护进程或其他类似的东西,简单来说就是和当前软件系统交互的主体。在大多数情况下你可以认为Subject就是Shiro的“用户”概念。

在独立应用中调用getSubject()可以从指定位置的用户数据返回一个Subject,在服务器环境下(例如 web app),将基于当前线程或收到的请求返回一个Subject

现在你已经有了一个Subject,通过它可以做什么呢?

如果你想让用户在当前会话中使用该应用程序,你可以得到他们的session:

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

Session是一个特定的Shiro实例,它不仅代表最常用的普通HttpSesstion,而且还带有一些额外的东西,其中最大的区别是:Shiro Session并不需要HTTP环境!

如果在web应用中使用,默认情况下Session就是HttpSession。但是在非web环境下,例如在我们教程中创建的这个应用里,Shiro将自动默认使用企业级会话管理。也就是说在你的应用中你可以使用同一套API而不用关心你应用的发布环境。这就为那些需要使用会话却不想强制使用HttpSession或则EJB会话的应用打开了一个新的世界。而且,客户端还可以分享会话数据。

现在你可以获得一个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);
}

就这样了吗?并不是这样简单,如果他们登录失败了怎么办?你可以捕获各种特定的异常,这些异常可以准确的告诉你发生了什么,并允许你做出相应的处理和反应。例如:

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

你还可以检查其他不同的异常类型,或则抛出你自定义的异常类型。更多异常类型请查看文档

现在我们已经有了一个登陆用户,我们还可以做些什么呢?
我们来打印出他们是谁:

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

Final src/main/java/Turorial

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);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容