你的第一個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);
}
}