你的第一个Apache Shiro应用
如果你是第一次接触Apache Shiro,这个简短的教程将向你展示如何创建并初始化一个非常简单的Apache Shiro应用。在这个过程中我们将讨论Shiro的核心概念,以此来帮助你熟悉Shiro的设计和API。
如果你并不想编写这个教程接下来的代码,你可以通过下面两种方式得到一个简单例子:
- Apache Shiro的Git仓库:https://github.com/apache/shiro/tree/master/samples/quickstart
- Apache Shiro源代码目录下的
samples/quickstart
目录,下载页面
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()
我们可以得到当前执行的得Subject
。Subject是一个安全术语,意思是当前执行用户的一个特定安全视图(原文使用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);
}
}