在本教程中,我們會寫一個簡單的、僅僅輸出一些內容命令行程序,從而對Shiro有一個大體的感覺。
一、准備工作
本教程需要Java1.5+,並且我們用Maven生成項目,當然Maven不是必須的,你也可以通過導入Shiro jar包的方式、或使用Ant、Ivy,喜歡哪種就用哪種。
開始之前,確定你的Maven版本為2.2.1+(如果你用的是Maven的話),用mvn --version確定Maven的版本。
現在,我們將正式開始。首先新建一個文件夾,比如說shiro-tutorial,然后將下面的Maven pom.xml文件放到該文件夾下。
pom.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 5 6 <modelVersion>4.0.0</modelVersion> 7 <groupId>org.apache.shiro.tutorials</groupId> 8 <artifactId>shiro-tutorial</artifactId> 9 <version>1.0.0-SNAPSHOT</version> 10 <name>First Apache Shiro Application</name> 11 <packaging>jar</packaging> 12 13 <properties> 14 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 15 </properties> 16 17 <build> 18 <plugins> 19 <plugin> 20 <groupId>org.apache.maven.plugins</groupId> 21 <artifactId>maven-compiler-plugin</artifactId> 22 <version>2.0.2</version> 23 <configuration> 24 <source>1.5</source> 25 <target>1.5</target> 26 <encoding>${project.build.sourceEncoding}</encoding> 27 </configuration> 28 </plugin> 29 30 <!-- This plugin is only to test run our little application. It is not 31 needed in most Shiro-enabled applications: --> 32 <plugin> 33 <groupId>org.codehaus.mojo</groupId> 34 <artifactId>exec-maven-plugin</artifactId> 35 <version>1.1</version> 36 <executions> 37 <execution> 38 <goals> 39 <goal>java</goal> 40 </goals> 41 </execution> 42 </executions> 43 <configuration> 44 <classpathScope>test</classpathScope> 45 <mainClass>Tutorial</mainClass> 46 </configuration> 47 </plugin> 48 </plugins> 49 </build> 50 51 <dependencies> 52 <dependency> 53 <groupId>org.apache.shiro</groupId> 54 <artifactId>shiro-core</artifactId> 55 <version>1.1.0</version> 56 </dependency> 57 <!-- Shiro uses SLF4J for logging. We'll use the 'simple' binding 58 in this example app. See http://www.slf4j.org for more info. --> 59 <dependency> 60 <groupId>org.slf4j</groupId> 61 <artifactId>slf4j-simple</artifactId> 62 <version>1.6.1</version> 63 <scope>test</scope> 64 </dependency> 65 </dependencies> 66 67 </project>
二、The Tutorial class
由於我們的目的是創建一個命令行程序,因此我們需要先新建一個具有public static void main(String[] args)的Java類。
在和pom.xml所處的同一目錄下,創建src/main/java子文件夾。在src/main/java文件夾下創建Tutorial.java文件,文件內容如下:
src/main/java/Tutorial.java
1 import org.apache.shiro.SecurityUtils; 2 import org.apache.shiro.authc.*; 3 import org.apache.shiro.config.IniSecurityManagerFactory; 4 import org.apache.shiro.mgt.SecurityManager; 5 import org.apache.shiro.session.Session; 6 import org.apache.shiro.subject.Subject; 7 import org.apache.shiro.util.Factory; 8 import org.slf4j.Logger; 9 import org.slf4j.LoggerFactory; 10 11 public class Tutorial { 12 13 private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class); 14 15 public static void main(String[] args) { 16 log.info("My First Apache Shiro Application"); 17 System.exit(0); 18 } 19 }
在往下進行之前,先測試一下是否可以運行。
三、測試運行
首先進入項目根目錄(本教程為shiro-tutorial目錄,即pom.xml所在的目錄),打開控制台,輸入命令:
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\$
現在我們已經驗證了程序可以運行,接下來讓我們使用Shiro。每次改變程序后,都可以運行mvn compile exec:java運行程序。
四、使用Shiro
在Shiro中有一個非常重要的組件--SecurityManager,Shiro的所有功能幾乎都與這個組件相關。這樣Java security類似,但是和java.lang.SecurityManager是不一樣的。
我們會在后續教程中對SecurityManager做詳細介紹。但是現在我們就要明確一點:SecurityManager是Shrio的核心組件,並且任何一個應用都要有一個SecurityManager才可以使用Shiro的其他功能。所有,在本教程中必須要做的事就是實例化一個SecurityManager。
五、配置
即使我們可以直接實例化一個SecurityManager,然而SecurityManager還是有比較多的配置和內部組件的,直接用java代碼配置這些內容比較麻煩。通過配置文件進行配置會比較簡單。
Shiro提供了一個默認的‘common denominator’,這是一個簡單的文本配置文件(INI格式)。與XML格式相比,INI格式更加易讀、易用並且幾乎不需要任何依賴。INI格式可以輕松的配置SecurityManager。
事實上,由於Shiro完全兼容了JavaBeans,所以Shiro可以用XML、YAML、JSON、Groovy等很多格式進行配置。
下面我們用INI文件配置本教程的SecurityManager。首先,在pom.xml所在文件夾下創建src/main/resources文件夾,然后在src/main/resources文件夾下新建一個名為shiro.ini的文件,該文件內容如下:
src/main/resources/shiro.ini
1 # ============================================================================= 2 # Tutorial INI configuration 3 # 4 # Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :) 5 # ============================================================================= 6 7 # ----------------------------------------------------------------------------- 8 # Users and their (optional) assigned roles 9 # username = password, role1, role2, ..., roleN 10 # ----------------------------------------------------------------------------- 11 [users] 12 root = secret, admin 13 guest = guest, guest 14 presidentskroob = 12345, president 15 darkhelmet = ludicrousspeed, darklord, schwartz 16 lonestarr = vespa, goodguy, schwartz 17 18 # ----------------------------------------------------------------------------- 19 # Roles with assigned permissions 20 # roleName = perm1, perm2, ..., permN 21 # ----------------------------------------------------------------------------- 22 [roles] 23 admin = * 24 schwartz = lightsaber:* 25 goodguy = winnebago:drive:eagle5
如你所見,這個文件設置了一些基本的用戶帳戶,這對本教程已經足夠了。在后續章節中,你將會學習如何使用更加復雜的用戶數據源,如數據庫、LDAP、ActiveDirectory等。
六、引用配置文件
現在我們已經有了一個INI文件,這樣我們就可以創建一個SecurityManager對象了。將Tutorial.java文件中的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的IniSecurityManagerFactory類讀取shiro.ini文件。從這個類名我們可以看出Shiro使用率設計模式中的工廠模式。代碼中的classpath前綴告訴shiro去哪里加載ini文件(支持的前綴還有url:,file:)
- factory.getInstance()函數解析ini文件並返回一個Securitymanager對象,該對象含有配置信息。
- 在本例中,我們將SecurityManager設置為一個靜態的,實現了單例模式,可以通過java虛擬機獲得。然而如果要在多個應用中使用shiro,這樣做就不行了。從而在比較復雜的大型應用中,我們通常將SecurityManager放在一塊應用內存中,如web應用中的ServletContec或Spring、Guice或JBoss DI容器。
七、使用Shiro
現在SecurityManager已經設置好了,我們終於可以做一些真正地與安全相關的操作了、
當我們考慮應用的安全性時,通常會遇到的問題是“當前用戶是誰?”,“當前用戶可以做什么?”所以,應用的安全性工作主要建立在當前用戶之上。在shiro API中用Subject這個含義更廣的概念代替當前用戶這個概念。
幾乎在任何環境中,你都可以通過下述代碼獲得當前正在執行程序的用戶。
Subject currentUser = SecurityUtils.getSubject();
使用SecurityUtils.getSubject方法,我們可以獲得當前正在執行程序的Subject。我們並不稱之為前正在執行程序的用戶,因為用戶通常是指人,而Subject可以指人、進程、計划任務、守護進程等。准確的說,Subject指的是“當前和軟件交互的事物”。在多數場景中,你可以將Subject粗暴地認為是用戶。
在一個獨立的程序中,getSubject()函數基於應用內存中的用戶數據返回一個Subject,在一個服務器環境中(如web應用),Subject通常是基於與當前進程有關的用戶數據或是來到的請求。
既然我們已經有了Subject,我們可以拿它來做什么?
你可以獲取當前Session並存儲一些東西。
Session session = currentUser.getSession();
session.setAttribute( "someKey", "aValue" );
這里的Session是基於Shiro的,它的功能與HttpSession類似,但是有一個巨大的不同:它不需要HTTP環境!
如果在web應用中部署shiro,則Session默認就是基於HttpSession的。但是在非web應用中,比如本教程,Shiro會自動用它的Enterprise Session Managerment。這樣你就可以使用一樣的API而不用管部署環境是什么了。
現在我們已經獲得了Subject和它的Session,那么怎么用這些東西去檢測Subject是否具有某權限、某許可呢?
我們只能對當前用戶檢測這些東西。我們的Subject對象就是當前用戶,但是Subject是誰?不知道,它是匿名的,除非它至少登錄過一次,否則我們無從得知Subject是誰。所以,我們讓Subject登錄:
1 if ( !currentUser.isAuthenticated() ) { 2 //collect user principals and credentials in a gui specific manner 3 //such as username/password html form, X509 certificate, OpenID, etc. 4 //We'll use the username/password example here since it is the most common. 5 UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); 6 7 //this is all you have to do to support 'remember me' (no config - built in!): 8 token.setRememberMe(true); 9 10 currentUser.login(token); 11 }
現在,Subject已經登錄了。
如果登錄失敗,我們可以捕獲異常並且做相應處理。
1 try { 2 currentUser.login( token ); 3 //if no exception, that's it, we're done! 4 } catch ( UnknownAccountException uae ) { 5 //username wasn't in the system, show them an error message? 6 } catch ( IncorrectCredentialsException ice ) { 7 //password didn't match, try again? 8 } catch ( LockedAccountException lae ) { 9 //account for that username is locked - can't login. Show them a message? 10 } 11 ... more types exceptions to check if you want ... 12 } catch ( AuthenticationException ae ) { 13 //unexpected condition - error? 14 }
這里有很多不同類型的異常,你也可以自定義自己的異常。
到這一步,我們已經有了一個登錄過的用戶,我們可以來做些什么呢?
我們來看看它是誰:
//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."); }
我們也可以進行一些insstance-level(實例級別)的許可檢測。即檢測用戶是否被允許訪問某些實例。
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 Class文件的最終內容
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,並且了解Subject和SecurityManager這兩個基本概念。
但是這只是一個非常非常簡單的應用。你可能會問“如果我不想要INI而想用更加復雜的數據源該怎么做?”
為了解答這個問題,我們需要更加深入地了解一下shiro的結構,我門將在后面的章節中學習。
