第一个Apache Shiro程序
如果你是初次接触Apache Shiro,该文章将指导你创建一个初级的非常简单的使用Apache Shiro进行安全认证的程序,同时我们将讨论Shiro的核心理念以帮助你熟悉Shiro的设计方式和API。
如果你确实不想按照该示例一步一步地编写代码,你可以从下面地址下载一个基本上完全相同的程序作为参考,选择下载位置:
在Apache Shiro的版本库中: https://svn.apache.org/repos/asf/shiro/trunk/samples/quickstart/
在Apache Shiro的源码发布的samples/quickstart目录中,源码发布在 Download(http://shiro.apache.org/download.html)页面。
Setup
在这个简单示例中,我们将建立一个非常简单的命令行程序,你可以从中感受一下Shiro的API。
注意:任何程序
Apache Shrio从设计之初就是为了支持所有程序--从最小的命令行程序到大型的集群的web程序,虽然我们在这个向导中只使用了一个简单的程序,但要知道无论你的程序如何创建发布到何处,这种方式都适用。
该示例需要Java1.5及更高版本,同时用Apache Maven作为建造工具,但这不是Apache Shiro所必须的。你可以获得Shiro的jar并以你喜欢的任何方式将其加入你的程序中,例如你可以使用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 并将下面的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>
z <scope>test</scope>
</dependency>
</dependencies>
</project>
示例类文件
我们准备运行的是一个命令行程序,所以我们需要创建一个带有public static void main(String[] args)函数的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
你可以看到我们的这个小程序运行并且退出,你将看到类似于下面这样的输出住处(注意加粗字体,标示着我们的输出)
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 security的人请注意:这和java.lang.SecurityManager不是一回事。
我们将在Architecture章节详细描述shiro的设计,但现在有必要知道Shrio SecurityManager是程序中Shiro的核心,每一个程序都必定会存在一个SecurityManager,所以,在我们这个示例程序中必须做的第一件事情是建立一个SecurityManager实例。
配置
虽然我们可以直接对SecurityManager实例化,但在Java代码中对Shiro的SecurityManager所须的选项和内部组件进行配置会让人感觉有点小痛苦--而将这些SecurityManager配置用一个灵活的配置文件实现就会简单地多。
为此,Shiro默认提供了一个基本的INI配置文件的解决方案,人们已经对庞大的XML文件有些厌倦了,而一个INI文件易读易用,而且所依赖的组件很少,稍后你就会通过一个简单易懂的示例明白INI在对简单对象进行配置的时候是非常有效率的,比如SecurityManager。
多种配置选择
Shiro的SecurityManager的实现和其所依赖的组件都是JavaBean,所以可以用多种形式对Shiro进行配置,比如XML(Spring, JBoss, Guice, 等等),YAML, JSON, Groovy Builder markup,及其它,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和活动目录等。
引用配置文件
现在我们已经定义了一个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的日志住处--只要运行时没有错误提示,你就可以知道已经成功了)。
上面所加入的代码做了下面的事情:
1. 使用Shiro的IniSecurityManagerFactory加载了我们的shiro.ini文件,该文件存在于classpath根目录里。这个执行动作反映出shiro支持Factory Method Design Pattern。classpath:资源的指示前缀,告诉shiro从哪里加载ini文件(其它前缀,如url:和file:也被支持)。
2.factory.getInstance()方法被调用,该方法分析INI文件并根据配置文件返回一个SecurityManager实例。
3.在这个简单示例中,我们将SecurityManager设置成了static (memory) singleton,可以通过JVM访问,注意如果你在一个JVM中加载多个使用shiro的程序时不要这样做,在这个简单示例中,这是可以的,但在其它成熟的应用环境中,通常会将SecurityManager放在程序指定的memory(如在web中的ServletContexct或者Spring、Guice、 JBoss DI 容器实例)中。
使用Shiro
现在我们的SecurityManager已经准备好了,我们可以开始进行我们真正关心的事情--执行安全操作了。
为了保护我们的程序安全,我们或许问自己最多的问题就是“谁是当前的用户?”或者“当前用户是否允许做某件事?”通常我们会在写代码或者设计用户接口的时候问这些问题:程序通常建立在用户基础上,程序功能展示(和安全)也基于每一个用户。所以,通常我们考虑我们程序安全的方法也建立在当前用户的基础上,Shiro的API提供了'the current user'概念,即它的Subject。
在几乎所有的环境中,你可以通过如下语句得到当前用户的信息:
Subject currentUser = SecurityUtils.getSubject();
使用SecurityUtils.getSubject(),我们可以获取当前执行的Subject,Subject是一个安全术语意思是“当前运行用户的指定安全视图(a security-specific view of the currently executing user)”,这里并不称之为“User”因为“User”这个词通常和一个人相关,但在安全认证中,“Subject”可以认为是一个人,也可以认为是第三方进程、时钟守护任务、守护进程帐户或者其它。它可简单描述为“当前和软件进行交互的事件”,在大多数情况下,你可以认为它是一个“人(User)”。
在一个独立的程序中调用getSubject()会在程序指定位置返回一个基于用户数据的Subject,在服务器环境(如web程序)中,它将获取一个和当前线程或请求相关的基于用户数据的Subject。
现在你得到了Subject,你可以利用它做什么呢?
如果你针对该用户希望一些事情在程序当前会话期内可行,你可以获取他们的session:
Session session = currentUser.getSession();
session.setAttribute( "someKey", "aValue" );
Session是shiro指定的一个实例,提供基本上所有HttpSession的功能,但具备额外的好处和不同:它不需要一个HTTP环境!
如果发布到一个web程序中,默认情况下Session将会使用HttpSession作为基础,但是,在一个非web程序中,比如该简单示例程序中,Shiro将自动默认使用它的Enterprise Session Management,这意味着你可以在任何程序中使用相同的API,而根本不需要考虑发布环境!这打开了一个全新的世界,从此任何需要session的程序不再需要强制使用HttpSession或者EJB Stateful Session,并且,终端可以共享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);
}
就是这样,不能再简单了。
但如果登录失败了呢,你可以捕获所有异常然后按你期望的方式去处理:
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.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配置):
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,并理解Shiro的设计理念,Subject和SecurityManager。
但这个程序太简单了,你可能会问自己,“如果我不想使用INI用户帐号,而希望连接更为复杂的用户数据源呢?”
解决这些问题需要更深入地了解shiro的架构和配置机制,我们将在下一节Architecture中介绍。