Shiro主要功能有認證,受權,加密,會話管理,與Web集成,緩存等. java
新建一個簡單的Maven項目,咱們只是使用Junit和shiro-core包.POM最後是以下代碼: sql
<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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.credo</groupId> <artifactId>shiro-study</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.9</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> </dependencies> </project>
package org.credo.test; import junit.framework.Assert; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.junit.Test; public class TestHelloShiro { public static final String DEFAULT_INI_RESOURCE_PATH = "classpath:shiro.ini"; @Test public void TestShiroFirst() { // 使用ini文件方式實例化shiro IniSecurityManagerFactory. IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory(DEFAULT_INI_RESOURCE_PATH<span></span>); // 獲得SecurityManager實例 並綁定給SecurityUtils SecurityManager securityManager = securityManagerFactory.getInstance(); SecurityUtils.setSecurityManager(securityManager); //獲得Subject Subject shiroSubject = SecurityUtils.getSubject(); //建立用戶名/密碼身份驗證Token(即用戶身份/憑證) UsernamePasswordToken normalToken = new UsernamePasswordToken("credo", "123"); try { //登陸,進行身份驗證 shiroSubject.login(normalToken); } catch (Exception e) { //登陸失敗,打印出錯誤信息,可自定義 System.out.println(e.getMessage()); } //斷言登陸成功 Assert.assertEquals(true, shiroSubject.isAuthenticated()); //登出 shiroSubject.logout(); } }
shiro.ini文件經過[users]指定了兩個user:credo/12三、zhaoqian/123,: 數據庫
1
2
3
|
[users]
credo=123
zhaoqian=123
|
從外部觀察shiro,shiro的結構就是 外部代碼--->Subject---->SecurityManager---->Realm apache
知識點: 設計模式
今後咱們就能夠理解shiro的處理流程. api
咱們能夠更進一步理解,Realm的數據是怎麼來的?固然是咱們本身定義的,也就是說,咱們須要本身定義權限,角色,受權方面的數據資源(數據庫存儲或shiro.ini文件存儲). 緩存
shiro的Factory<SecurityManager>是一個工廠模式的應用.咱們追溯源碼能夠看到其內部的實現. 安全
Factory最底層接口:org.apache.shiro.util.Factory.class app
package org.apache.shiro.util; //應用工廠設計模式的泛型接口 public interface Factory<T> { //返回一個實例 T getInstance(); }
Factory接口聲明的getInstance()方法,由其直接子類AbstractFactory實現。
以後AbstractFactory在實現的getInstance()方法中調用了一個新聲明的抽象方法,這個方法也是由其直接子類實現的。
這樣,從Factory開始,每一個子類都實現父類聲明的抽象方法,同時又聲明一個新的抽象方法並在實現父類的方法中調用。 框架
經過源碼追溯,咱們能夠發現有2個類是實現了Factory接口:
接着是org.apache.shiro.config.IniFactorySupport,抽象類public abstract class IniFactorySupport<T> extends AbstractFactory<T>
最終是package org.apache.shiro.config包下的IniSecurityManagerFactory.
IniSecurityManagerFactory類主要是用工廠模式建立基於Ini配置SecurityManager實例.
IniSecurityManagerFactory 是 Factory的子類,DefaultSecurityManager是 SecurityManager的子類。
Factory 與 SecurityManager 及其子類的關係
從上圖能夠看到整個Shiro的認證流程
一、首先調用Subject.login(token)進行登陸,其會自動委託給Security Manager,調用以前必須經過SecurityUtils. setSecurityManager()設置;Realm:域,Shiro從從Realm獲取安全數據(如用戶、角色、權限),就是說SecurityManager要驗證用戶身份,那麼它須要從Realm獲取相應的用戶進行比較以肯定用戶身份是否合法;也須要從Realm獲得用戶相應的角色/權限進行驗證用戶是否能進行操做;能夠把Realm當作DataSource,即安全數據源。如咱們以前的ini配置方式將使用org.apache.shiro.realm.text.IniRealm。
org.apache.shiro.realm.Realm接口以下:
1
2
3
|
String getName();//返回一個惟一的Realm名字
booleansupports(AuthenticationToken token);//判斷此Realm是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throwsAuthenticationException; //根據Token獲取認證信息
|
1.咱們先定義一個Realm.
package org.credo.test.realm.single; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.realm.Realm; public class TestMySingleRealm implements Realm{ @Override public String getName() { return "TestMySingleReam"; } @Override public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } @Override public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String userName=String.valueOf(token.getPrincipal()); //注意token的Credentials是char[],z主要轉換. String passWord=String.valueOf((char[])token.getCredentials()); if(!userName.equals("credo")){ throw new UnknownAccountException("無效的帳戶名!"); } if(!passWord.equals("aaa")){ throw new IncorrectCredentialsException("密碼錯誤!"); } return new SimpleAuthenticationInfo(userName, passWord,getName()); } }
2.ini配置文件指定自定義Realm實現(文件名我定義爲:shiro-single-realm.ini)
1
2
|
singleRealm=org.credo.test.realm.single.TestMySingleRealm
securityManager.realms=$singleRealm
|
經過$name來引入以前的realm定義
3.Junit測試代碼
@Test public void testSingleMyRealm() { IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-single-realm.ini"); SecurityManager securityManager = securityManagerFactory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject shiroSubject = SecurityUtils.getSubject(); UsernamePasswordToken normalToken = new UsernamePasswordToken("credo", "aaa"); try { shiroSubject.login(normalToken); } catch (UnknownAccountException e) { System.out.println(e.getMessage()); } catch (IncorrectCredentialsException e) { System.out.println(e.getMessage()); } catch (AuthenticationException e) { e.printStackTrace(); } Assert.assertEquals(true, shiroSubject.isAuthenticated()); shiroSubject.logout(); //解除綁定Subject到線程,防止對下次測試形成影響 ThreadContext.unbindSubject(); }
realm A:
package org.credo.test.realm.multi; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.realm.Realm; public class RealmA implements Realm { @Override public String getName() { return "RealmA"; } @Override public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } @Override public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String userName=String.valueOf(token.getPrincipal()); //注意token的Credentials是char[],z主要轉換. String passWord=String.valueOf((char[])token.getCredentials()); System.out.println("realm A"); if(!userName.equals("credo")){ throw new UnknownAccountException("RealmA--無效的帳戶名!"); } if(!passWord.equals("123")){ throw new IncorrectCredentialsException("RealmA--密碼錯誤!"); } System.out.println("pass A"); return new SimpleAuthenticationInfo(userName, passWord,getName()); } }
realmB:
package org.credo.test.realm.multi; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.realm.Realm; public class RealmB implements Realm { @Override public String getName() { return "RealmsB"; } @Override public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } @Override public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String userName=String.valueOf(token.getPrincipal()); //注意token的Credentials是char[],z主要轉換. String passWord=String.valueOf((char[])token.getCredentials()); System.out.println("realm B"); if(!userName.equals("credo")){ throw new UnknownAccountException("RealmB--無效的帳戶名!"); } if(!passWord.equals("aaa")){ throw new IncorrectCredentialsException("RealmB--密碼錯誤!"); } System.out.println("pass B"); return new SimpleAuthenticationInfo(userName, passWord,getName()); } }
@Test public void testMultiMyRealm() { IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-multi-realm.ini"); SecurityManager securityManager = securityManagerFactory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject shiroSubject = SecurityUtils.getSubject(); UsernamePasswordToken normalToken = new UsernamePasswordToken("credo", "aaa"); try { shiroSubject.login(normalToken); } catch (UnknownAccountException e) { System.out.println(e.getMessage()); } catch (IncorrectCredentialsException e) { System.out.println(e.getMessage()); } catch (AuthenticationException e) { System.out.println(e.getMessage()); } Assert.assertEquals(true, shiroSubject.isAuthenticated()); shiroSubject.logout(); ThreadContext.unbindSubject(); }
測試結果能夠發現,只要其中一個realm經過就經過了.執行順序是按shiro.ini中指定的順序執行.先A後B.若是有realmC,realmD,但沒有指定,不會執行.
之後通常繼承AuthorizingRealm(受權)便可;其繼承了AuthenticatingRealm(即身份驗證),並且也間接繼承了CachingRealm(帶有緩存實現)。其中主要默認實現以下:
Authenticator的職責是驗證用戶賬號,是Shiro API中身份驗證核心的入口點:
package org.apache.shiro.authc; public interface Authenticator { /** * @throws AuthenticationException if there is any problem during the authentication process. * See the specific exceptions listed below to as examples of what could happen * in order to accurately handle these problems and to notify the user in an * appropriate manner why the authentication attempt failed. Realize an * implementation of this interface may or may not throw those listed or may * throw other AuthenticationExceptions, but the list shows the most common ones. * @see ExpiredCredentialsException * @see IncorrectCredentialsException * @see ExcessiveAttemptsException * @see LockedAccountException * @see ConcurrentAccessException * @see UnknownAccountException */ public AuthenticationInfo authenticate(AuthenticationToken authenticationToken) throws AuthenticationException; }
ModularRealmAuthenticator默認使用AtLeastOneSuccessfulStrategy策略。
自定義AuthenticationStrategy實現,首先看其API:
//在全部Realm驗證以前調用 AuthenticationInfo beforeAllAttempts( Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException; //在每一個Realm以前調用 AuthenticationInfo beforeAttempt( Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException; //在每一個Realm以後調用 AuthenticationInfo afterAttempt( Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException; //在全部Realm以後調用 AuthenticationInfo afterAllAttempts( AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException;
由於每一個AuthenticationStrategy實例都是無狀態的,全部每次都經過接口將相應的認證信息傳入下一次流程;經過如上接口能夠進行如合併/返回第一個驗證成功的認證信息。
自定義實現時通常繼承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy便可
測試案例:
修改shiro.ini
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator securityManager.authenticator=$authenticator allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy realmA=org.credo.test.realm.multi.RealmA realmB=org.credo.test.realm.multi.RealmB securityManager.realms=$realmA,$realmB
RealmB的getAuthenticationInfo方法返回值修改成:return new SimpleAuthenticationInfo(userName+"@qq.com", passWord,getName());
其餘不變,RealmA也不變.但驗證過程用戶名和密碼都寫正確的"credo","123"
@Test public void testAuthenticator() { IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-multi-realm.ini"); SecurityManager securityManager = securityManagerFactory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject shiroSubject = SecurityUtils.getSubject(); UsernamePasswordToken normalToken = new UsernamePasswordToken("credo", "aaa"); try { shiroSubject.login(normalToken); } catch (UnknownAccountException e) { System.out.println(e.getMessage()); } catch (IncorrectCredentialsException e) { System.out.println(e.getMessage()); } catch (AuthenticationException e) { System.out.println(e.getMessage()); } // 獲得一個PrincipalCollection,包含全部成功的. PrincipalCollection principalCollection = shiroSubject.getPrincipals(); for(Object obj:principalCollection){ System.out.println(obj.toString()); } Assert.assertEquals(2, principalCollection.asList().size()); Assert.assertEquals(true, shiroSubject.isAuthenticated()); shiroSubject.logout(); } @After public void tearDown() throws Exception { ThreadContext.unbindSubject(); }
1
2
3
4
5
6
|
realm A
pass A
realm B
pass B
credo
credo@qq.com
|
包含credo和credo@qq.com,兩個都經過了驗證.都有兩個信息.
學習資料參考以及部分文章的Copy: