Shiro學習筆記<1>入門--Hello Shiro

Apache Shiro是Apache的一個安全框架.對比Spring Security,可能沒有Spring Security功能多,可是在實際並不須要那麼重的東西.shiro簡小精悍.大多項目綽綽有餘.(JBOSS好像也有個什麼安全框架...名字忘了,去JBOSS官網找了半天也沒找到,找到個jboss sso好像是單點登陸方面使用的安全框架) 

Shiro主要功能有認證,受權,加密,會話管理,與Web集成,緩存等. java

1.shiro入門測試

新建一個簡單的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>




在src/test/java下建包,類TestHelloShiro.java .在src/test/resources下新建名爲 shiro.ini 的文件.
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
知識點:
  1. shiro.ini--是shiro必須配置的一個重要文件.
  2. [users]:是shiro.ini配置裏一個標註.做用就指定用戶身份/憑證.
  3. IniSecurityManagerFactory就是Factory<SecurityManager>:經過new IniSecurityManagerFactory實例化的SecurityManager工廠,關於這個工廠下面有源碼解釋.

2.shiro處理流程的簡單理解

從外部觀察shiro,shiro的結構就是  外部代碼--->Subject---->SecurityManager---->Realm apache

知識點: 設計模式

  1. Subject:與外部代碼交互的一層.應該理解爲一個"用戶",但這個用戶不必定是指傳統意義的用戶.應該理解爲與咱們當前系統交互的一個"對象".
  2. SecurityManager:SecurityManager是整個shiro核心控制器,其控制全部的Subject,或者說全部的Subject的操做其實都是交給SecurityManager來處理.在一個應用中只有一個單例的SecurityManager實例存在,Apache Shiro經過SecurityManager來管理內部組件實例,並經過它來提供安全管理的各類服務。
  3. Realm:Shiro須要Realm獲取安全數據,如用戶,角色,權限.Realm能夠理解爲"域".簡單的理解就是,Realm是像一個數據池,若是Shiro要驗證一個當前系統對象的權限,密碼,角色,那他就須要從Realm中獲取對應的數據.

今後咱們就能夠理解shiro的處理流程. api

  • 1.外部代碼訪問shiro,經過與Subject的交互來進行安全方面的操做,如受權,認證,資源的權限等.
  • 2.Subject相關的交互信息交由SecurityManager來處理.
  • 3.SecurityManager從Realm中獲取對應的"數據"進行處理,返回給外部代碼.

咱們能夠更進一步理解,Realm的數據是怎麼來的?固然是咱們本身定義的,也就是說,咱們須要本身定義權限,角色,受權方面的數據資源(數據庫存儲或shiro.ini文件存儲). 緩存

3.IniSecurityManagerFactory就是Factory<SecurityManager>源碼解析

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接口:

  1. org.apache.shiro.jndi.JndiObjectFactory,泛型類.用於JNDI查找.
  2. 抽象類,咱們須要關注的org.apache.shiro.util.AbstractFactory,就是abstract class AbstractFactory<T> implements Factory<T> .

接着是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 及其子類的關係

4.Shiro內部的認證流程

從上圖能夠看到整個Shiro的認證流程

一、首先調用Subject.login(token)進行登陸,其會自動委託給Security Manager,調用以前必須經過SecurityUtils. setSecurityManager()設置; 
二、SecurityManager負責真正的身份驗證邏輯;它會委託給Authenticator進行身份驗證; 
三、  Authenticator 纔是真正的身份驗證者,Shiro API中核心的身份認證入口點,此處能夠自定義插入本身的實現; 
四、Authenticator可能會委託給相應的  AuthenticationStrategy 進行多Realm身份驗證,默認  ModularRealmAuthenticator 會調用AuthenticationStrategy進行多Realm身份驗證; 
五、Authenticator會把相應的token傳入  Realm ,從Realm獲取身份驗證信息,若是沒有返回/拋出異常表示身份驗證失敗了。  此處能夠配置多個Realm,將按照相應的順序及策略進行訪問。

5.Realm

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獲取認證信息

A:單realm實現使用

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();
}




B:多個Realms的使用

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());
    }
 
}




shiro.ini配置(文件名:shiro-multi-realm.ini):
@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,但沒有指定,不會執行.

6.Shiro默認提供的Realm


之後通常繼承AuthorizingRealm(受權)便可;其繼承了AuthenticatingRealm(即身份驗證),並且也間接繼承了CachingRealm(帶有緩存實現)。其中主要默認實現以下:

  • org.apache.shiro.realm.text.IniRealm:[users]部分指定用戶名/密碼及其角色;[roles]部分指定角色即權限信息;
  • org.apache.shiro.realm.text.PropertiesRealm: user.username=password,role1,role2指定用戶名/密碼及其角色;role.role1=permission1,permission2指定角色及權限信息;
  • org.apache.shiro.realm.jdbc.JdbcRealm:經過sql查詢相應的信息,
  1. 如「select password from users where username = ?」獲取用戶密碼,
  2. 「select password, password_salt from users where username = ?」獲取用戶密碼及鹽;
  3. 「select role_name from user_roles where username = ?」獲取用戶角色;
  4. 「select permission from roles_permissions where role_name = ?」獲取角色對應的權限信息;
  5. 也能夠調用相應的api進行自定義sql;

7.Authenticator及AuthenticationStrategy

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;
}




若是驗證成功,將返回AuthenticationInfo驗證信息;此信息中包含了身份及憑證;若是驗證失敗將拋出相應的AuthenticationException實現。 

SecurityManager接口繼承了Authenticator,另外還有一個ModularRealmAuthenticator實現,其委託給多個Realm進行驗證,驗證規則經過AuthenticationStrategy接口指定,默認提供的實現:
  1. FirstSuccessfulStrategy:只要有一個Realm驗證成功便可,只返回第一個Realm身份驗證成功的認證信息,其餘的忽略;
  2. AtLeastOneSuccessfulStrategy:只要有一個Realm驗證成功便可,和FirstSuccessfulStrategy不一樣,返回全部Realm身份驗證成功的認證信息;
  3. AllSuccessfulStrategy:全部Realm驗證成功纔算成功,且返回全部Realm身份驗證成功的認證信息,若是有一個失敗就失敗了。

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




Junit測試代碼:

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:
  • http://shiro.apache.org/
  • http://jinnianshilongnian.iteye.com/blog/2018936
  • http://blog.csdn.net/teamlet/article/details/7773341
相關文章
相關標籤/搜索