Apache Shiro 示例
第一個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中介紹。