Shiro之實現認證

與其它java開源框架相似,將shiro的jar包加入項目就可使用shiro提供的功能了。shiro-core是核心包必須選用,還提供了與web整合的shiro-web、與spring整合的shiro-spring、與任務調度quartz整合的shiro-quartz等jar包。java

寫在前面的話:本篇文章只是經過一個入門程序教你們學會使用Shiro實現認證功能(包括普通認證和通過散列算法對密碼進行加密後的認證),沒有將Shiro同Spring整合起來使用(後面的文章咱們會講解Shiro整合web開發環境的項目中如何實現認證功能,見Shiro整合Web項目及整合後的開發),因此咱們採用的jar包不多:shiro-core.jar、junit測試jar包、commons-logging.jar。web

1.認證的概念

身份認證,即在應用中誰能證實他就是他本人。通常提供如他們的身份ID一些標識信息來代表他就是他本人,如提供身份證,用戶名/密碼來證實。
在shiro中,用戶須要提供principals (身份)和credentials(證實)給shiro,從而應用能驗證用戶身份:算法

  • principals:身份,即主體的標識屬性,能夠是任何東西,如用戶名、郵箱等,惟一便可。一個主體能夠有多個principals,但只有一個Primary principals,通常是用戶名/密碼/手機號。
  • credentials:證實/憑證,即只有主體知道的安全值,如密碼/數字證書等。

最多見的principals和credentials組合就是用戶名/密碼了。接下來先經過一個入門程序進行一個基本的身份認證。spring

另外兩個相關的概念是以前提到的Subject及Realm,分別是主體及驗證主體的數據源。數據庫

2.Shiro認證流程

Shiro認證流程圖以下:apache

文字描述認證流程:安全

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

經過Shiro認證流程圖,咱們在用Shiro完成認證功能時就要徹底按照這個流程去寫代碼。導入上述所說的3個jar包後咱們就能夠開始寫Shiro認證的入門程序了。架構

3.Shiro認證入門程序

本入門程序是經過模仿用戶的登陸與退出來完成認證。咱們將用戶登陸時輸入的信息與數據庫中的信息進行比較,只有兩者信息符合時就說明用戶認證經過了。框架

3.1建立shiro-first.ini

在src包下建立名爲shiro-first.ini文件,程序中咱們沒有鏈接到數據庫,因此採用後綴爲ini的文件來模擬數據庫中的數據。該文件中的數據以下:ide

在.ini文件中經過[users]指定了兩個主體(Subject):用戶名zhangsan和密碼1111十一、用戶名lisi和密碼22222。這樣在.ini文件中配置就至關於模擬了數據庫中的兩條用戶信息。

3.2編寫入門程序

咱們接下來就參照上述認證流程圖來寫認證的程序,以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class AuthenticationTest
{
    //用戶的登陸和退出
    @Test
    public void testLoginAndLogout()
    {
        //建立securityManager工廠
        Factory<SecurityManager> factory=new IniSecurityManagerFactory(
                "classpath:shiro-first.ini");

        //建立SecurityManager
        SecurityManager securityManager = factory.getInstance();

        //將securityManager設置到當前的運行環境中
        SecurityUtils.setSecurityManager(securityManager);

        //叢SecurityUtils裏邊的到一個subject
        Subject subject = SecurityUtils.getSubject();

        // 在認證提交前準備token(令牌)
        // 模擬用戶輸入的帳號和密碼,未來是由用戶輸入進去從頁面傳送過來
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
                "111111");

        try {
            // 執行認證提交
            subject.login(token);
        } catch (AuthenticationException e) {
            // 認證失敗
            e.printStackTrace();
        }

        // 是否定證經過
        boolean isAuthenticated = subject.isAuthenticated();

        System.out.println("是否定證經過:" + isAuthenticated);

        // 退出操做
        subject.logout();
    }
}

 

運行程序,輸出是否定證經過:true。由於該用戶輸入的信息和數據庫中的信息匹配,因此認證成功。代碼講解:

  1. 首先經過new IniSecurityManagerFactory並指定一個ini配置文件來建立一個SecurityManager工廠。
  2. 接着獲取SecurityManager並綁定到SecurityUtils,這是一個全局設置,設置一次便可。
  3. 經過SecurityUtils獲得Subject,其會自動綁定到當前線程;若是在web環境在請求結束時須要解除綁定;而後獲取身份驗證的Token(即用戶輸入的信息),如用戶名/密碼。
  4. 調用subject.login方法進行登陸,其會自動委託給SecurityManager.login方法進行登陸。
  5. 若是身份驗證失敗請捕獲AuthenticationException或其子類異常,常見的如 :DisabledAccountException(禁用的賬號)LockedAccountException(鎖定的賬號)UnknownAccountException(錯誤的賬號)ExcessiveAttemptsException(登陸失敗次數過多)IncorrectCredentialsException (錯誤的憑證)ExpiredCredentialsException(過時的憑證)等,具體請查看其繼承關係;對於頁面的錯誤消息展現,最好使用如「用戶名/密碼錯誤」而不是「用戶名錯誤」/「密碼錯誤」,防止一些惡意用戶非法掃描賬號庫。
  6. 最後能夠調用subject.logout退出,其會自動委託給SecurityManager.logout方法退出。

從如上代碼可總結出身份驗證的步驟:

  1. 收集用戶身份/憑證,即如用戶名/密碼。
  2. 調用Subject.login進行登陸,若是失敗將獲得相應的AuthenticationException異常,根據異常提示用戶錯誤信息;不然登陸成功。
  3. 最後調用Subject.logout進行退出操做。

從上面的測試類中咱們發現的幾個問題:一、用戶名/密碼硬編碼在ini配置文件,之後須要改爲如數據庫存儲,且密碼須要加密存儲。二、用戶身份Token可能不只僅是用戶名/密碼,也可能還有其餘的,如登陸時容許用戶名/郵箱/手機號同時登陸。

上面咱們是直接訪問數據庫(.ini文件)的,而咱們真正用到Shiro時是經過Realm來訪問數據庫的,在這裏也應該是經過Realm來訪問.ini配置文件。因此下面咱們來說講經過上面提到的Realm完成認證功能。

4.自定義Realm

實際開發中咱們經過realm從數據庫中查詢用戶信息,因此realm的做用可想而知:根據token中的身份信息去查詢數據庫(入門程序咱們使用ini配置文件模擬數據庫),若是查到用戶則返回認證信息,若是查詢不到就返回null。

在Shiro架構中,realm接口中的java代碼以下:

1
2
3
4
5
6
String getName(); //返回一個惟一的Realm名字  

boolean supports(AuthenticationToken token); //判斷此Realm是否支持此Token  

AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)  
throws AuthenticationException;  //根據Token獲取認證信息

 

而每每咱們都是自定義Realm,因此咱們須要自定義一個CustomRealm.java文件並讓它繼承AuthorizingRealm抽象類,在sric包下建立一個realm包,在realm包中建立咱們的自定義Realm,java代碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class CustomRealm extends AuthorizingRealm
{

    //設置realm的名稱
    @Override
    public void setName(String name) {
        super.setName("customRealm");
    }

    //用於認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        //token是用戶輸入的
        //第一步:叢token中取出身份信息
        String userCode= (String) token.getPrincipal();

        //第二步:根據用戶輸入的userCode叢數據庫查詢

        //模擬從數據庫查詢到的密碼
        String password="111111";


        //若是查不到返回null,

        //若是查詢到,返回認證信息AuthenticationInfo

        SimpleAuthenticationInfo simpleAuthenticationInfo=new
                SimpleAuthenticationInfo(userCode,password,this.getName());


        return simpleAuthenticationInfo;
    }

    //用於受權,該功能在下篇文章中進行講解
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

}

 

4.1配置自定義Realm

自定義Realm的代碼實現後,咱們就須要將這個自定義的Realm進行配置,在src包下建立一個shiro-reaml.ini文件(你也能夠在先前咱們建立的shiro-first.ini文件下進行修改),而該配置文件跟咱們入門程序中的配置文件是不同的,入門程序中的配置文件是模擬的數據庫,而這裏的配置文件是將Realm進行配置,數據庫中的信息咱們在自定義Reaml的代碼中已進行模擬,內容以下:

內容中的解釋也在註釋中給出,內容中的屬性值customRealm任意取,不必定要跟Reaml實現java代碼中方法setName()的值同樣,固然如果你自定義了多個Realm,那麼配置文件中的內容以下:

1
2
3
4
5
#聲明多個realm  
customRealm1=realm.CustomRealme2  
customRealme=realm.CustomRealme2 
#指定securityManager的realms實現  
securityManager.realms=$myRealm1,$myRealm2

 

完成自定義Realm的代碼實現並配置後,咱們即可進行該Reaml的測試了,測試代碼以下,仍然模擬的用戶登陸認證:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//用戶的登陸和退出
  @Test
  public void testCustomRealm()
  {
      //建立securityManager工廠
      Factory<SecurityManager> factory=new IniSecurityManagerFactory(
              "classpath:shiro-realm.ini");

      //建立SecurityManager
      SecurityManager securityManager = factory.getInstance();

      //將securityManager設置到當前的運行環境匯中
      SecurityUtils.setSecurityManager(securityManager);

      //叢SecurityUtils裏邊建立一個
      Subject subject = SecurityUtils.getSubject();

      // 在認證提交前準備token(令牌)
      // 這裏的帳號和密碼 未來是由用戶輸入進去
      UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
              "111111");

      try {
          // 執行認證提交
          subject.login(token);
      } catch (AuthenticationException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
      }

      // 是否定證經過
      boolean isAuthenticated = subject.isAuthenticated();

      System.out.println("是否定證經過:" + isAuthenticated);

  }

 

運行程序,控制檯輸出認證經過,說明用戶輸入的信息與數據庫(配置文件)中的信息符合,因此用戶認證成功。這樣咱們便經過Shiro完成了認證功能的實現。

擴展:看到這裏相信你就會使用Shiro完成認證功能了,可是這裏咱們是經過用戶輸入的密碼直接去查詢數據庫中的明文密碼,而每每爲了保護用戶信息因此數據庫中的密碼都是通過了MD5的算法進行加密後才放入數據庫的,因此實際開發中咱們須要經過Shiro獲取數據庫中通過md5加密後的密碼來和用戶輸入的密碼進行比對。因此接下來我要講Shiro中是如何將用戶輸入的信息與數據庫中的加密信息進行對比從而實現的認證。

5.散列算法

實際開發中爲了保護用戶信息的安全,咱們須要對用戶在註冊時輸入的密碼進行加密後再保存到數據庫,當用戶登陸時咱們也要將用戶輸入的密碼進行加密後再與數據庫中的密碼進行比對。即須要對密碼進行散列,經常使用的散列方法有md五、sha。

用md5算法對密碼進行散列的問題:若是知道散列後的值能夠經過窮舉算法獲得md5密碼對應的明文。解決方法:建議對md5進行散列時加salt(鹽),進行加密至關於對原始密碼+鹽進行散列。

5.1自定義realm支持散列算法

那麼接下來咱們就講解上述reaml支持散列算法的測試,在realm包下新建CustomRealmMd5.java類,並再裏面模仿數據庫中的用戶名和加密密文,內容以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class CustomRealmMd5 extends AuthorizingRealm
{

    // 設置realm的名稱
    @Override
    public void setName(String name) {
        super.setName("customRealmMd5");
    }

    // 用於認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {

        // token是用戶輸入的
        // 第一步從token中取出身份信息
        String userCode = (String) token.getPrincipal();

        // 第二步:根據用戶輸入的userCode從數據庫查詢
        // ....

        // 若是查詢不到返回null
        // 數據庫中用戶帳號是zhangsansan
		/*
		 * if(!userCode.equals("zhangsansan")){// return null; }
		 */

        // 模擬根據用戶名從數據庫查詢到的密碼,散列值
        String password = "f3694f162729b7d0254c6e40260bf15c";
        // 從數據庫獲取salt
        String salt = "qwerty";
        //上邊散列值和鹽對應的明文:111111

        // 若是查詢到返回認證信息AuthenticationInfo
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                userCode, password, ByteSource.Util.bytes(salt), this.getName());

        return simpleAuthenticationInfo;
    }

    // 用於受權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        // TODO Auto-generated method stub
        return null;
    }
}

 

5.2在realm中配置憑證匹配器

而後對該自定義支持md5算法的Realm進行配置,在src包下建立shiro-realm-md5.ini文件,內容以下:

1
2
3
4
5
6
7
8
9
10
11
12
[main]
#自定憑證匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列的算法
credentialsMatcher.hashAlgorithmName=md5
#散列的次數
credentialsMatcher.hashIterations=1

#將憑證匹配器設置到咱們定義的realm
customRealm=realm.CustomRealmMd5
customRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$customRealm

 

5.3測試

而後即可進行咱們支持md5算法的realm測試了,測試代碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MD5Test {

		//註解用的main方法進行測試,你也能夠經過junit.jar進行測試
    public static void main(String[] args)
    {
        //模擬用戶輸入的密碼
        String source="111111";

        //加入咱們的鹽salt
        String salt="qwerty";

        //密碼11111通過散列1次獲得的密碼:f3694f162729b7d0254c6e40260bf15c
        int hashIterations=1;


        //構造方法中:
        //第一個參數:明文,原始密碼
        //第二個參數:鹽,經過使用隨機數
        //第三個參數:散列的次數,好比散列兩次,至關 於md5(md5(''))
        Md5Hash md5Hash=new Md5Hash(source,salt,hashIterations);

        String password_md5=md5Hash.toString();

        System.out.println(password_md5);

    }
}
相關文章
相關標籤/搜索