1. 程式人生 > >Shiro 登入認證原始碼詳解

Shiro 登入認證原始碼詳解

Apache Shiro 是一個強大且靈活的 Java 開源安全框架,擁有登入認證、授權管理、企業級會話管理和加密等功能,相比 Spring Security 來說要更加的簡單。

本文主要介紹 Shiro 的登入認證(Authentication)功能,主要從 Shiro 設計的角度去看這個登入認證的過程。

一、Shiro 總覽

首先,我們思考整個認證過程的業務邏輯:

  1. 獲取使用者輸入的使用者名稱,密碼;
  2. 從伺服器資料來源中獲取相應的使用者名稱和密碼;
  3. 判斷密碼是否匹配,決定是否登入成功。

我們現在來看看 Shiro 是如何設計這個過程的:

圖中包含三個重要的 Shiro 概念:Subject

SecurityManagerRealm。接下來,分別介紹這三者有何用:

  • Subject:表示“使用者”,表示當前執行的使用者。Subject 例項全部都繫結到了一個 SecurityManager 上,當和 Subject 互動時,它是委託給 SecurityManager 去執行的。
  • SecurityManager:Shiro 結構的心臟,協調它內部的安全元件(如登入,授權,資料來源等)。當整個應用配置好了以後,大多數時候都是直接和 Subject 的 API 打交道。
  • Realm:資料來源,也就是抽象意義上的 DAO 層。它負責和安全資料互動(比如儲存在資料庫的賬號、密碼,許可權等資訊),包括獲取和驗證。Shiro 支援多個 Realm,但是至少也要有一個。Shiro 自帶了很多開箱即用的 Reams,比如支援 LDAP、關係資料庫(JDBC)、INI 和 properties 檔案等。但是很多時候我們都需要實現自己的 Ream 去完成獲取資料和判斷的功能。

登入驗證的過程就是:Subject 執行 login 方法,傳入登入的「使用者名稱」和「密碼」,然後 SecurityManager 將這個 login 操作委託給內部的登入模組,登入模組就呼叫 Realm 去獲取安全的「使用者名稱」和「密碼」,然後對比,一致則登入,不一致則登入失敗。

Shiro 詳細結構

ShiroArchitecture

二、Shiro 登入示例

程式碼來自 Shiro 官網教程。Shiro 配置 INI 檔案:

# ----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# ----------------------------------------------------------------------------
[users]
wang=123

測試 main 方法:

public static void main(String[] args) {

    log.info("My First Apache Shiro Application");

    //1.從 Ini 配置檔案中獲取 SecurityManager 工廠
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");

    //2.獲取 SecurityManager 例項
    SecurityManager securityManager = factory.getInstance();

    //3.將 SecurityManager 例項繫結給 SecurityUtils
    SecurityUtils.setSecurityManager(securityManager);

    //4.獲取當前登入使用者
    Subject currentUser = SecurityUtils.getSubject();

    //5.判斷是否登入,如果未登入,則登入
    if (!currentUser.isAuthenticated()) {
        //6.建立使用者名稱/密碼驗證Token(Web 應用中即為前臺獲取的使用者名稱/密碼)
        UsernamePasswordToken token = new UsernamePasswordToken("wang", "123");
        try {
            //7.執行登入,如果登入未成功,則捕獲相應的異常
            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?
        }
    }

}

三、登入邏輯詳解

Shiro 登入過程主要涉及到 Subject.login 方法,接下來我們將通過檢視原始碼來分析整個登入過程。

  1. 建立 AuthenticationToken 介面的例項 token,比如例子中的 UsernamePasswordToken,包含了登入的使用者名稱和密碼;
  2. 獲取當前使用者 Subject,然後呼叫 Subject.login(AuthenticationToken) 方法;
  3. Subjectlogin 代理給 SecurityManagerlogin()

3.1 建立AuthenticationToken

第一步是建立 AuthenticationToken 介面的身份 token,比如例子中的 UsernamePasswordToken

package org.apache.shiro.authc;

public interface AuthenticationToken extends Serializable {
    // 獲取“使用者名稱”
    Object getPrincipal();
    // 獲取“密碼”
    Object getCredentials();
}

3.2 獲取當前使用者並執行登入

獲取的 Subject 當前使用者是我們平時打交道最多的介面,有很多方法,但是這裡我們只分析 login 方法。

package org.apache.shiro.subject;

public interface Subject {

    void login(AuthenticationToken token) throws AuthenticationException;

}

login 方法接受一個 AuthenticationToken 引數,如果登入失敗則丟擲 AuthenticationException 異常,可通過判斷異常型別來知悉具體的錯誤型別。

接下來,分析 Subject 介面的實現類 DelegatingSubject 是如何實現 login 方法的:

public void login(AuthenticationToken token) throws AuthenticationException {
    clearRunAsIdentitiesInternal();
    // 代理給SecurityManager
    Subject subject = securityManager.login(this, token);
    ...
}

3.3 SecurityManager 介面

前面說過,整個 Shiro 安全框架的心臟就是 SecurityManager,我們看這個介面都有哪些方法:

package org.apache.shiro.mgt;

public interface SecurityManager extends Authenticator, Authorizer, SessionManager {

    Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;

    void logout(Subject subject);

    Subject createSubject(SubjectContext context);
}

SecurityManager 包含很多內建的模組來完成功能,比如登入(Authenticator),許可權驗證(Authorizer)等。這裡我們看到 SecurityManager 介面繼承了 Authenticator 登入認證的介面:

package org.apache.shiro.authc;

public interface Authenticator {

    public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException;
}

那麼,SecurityManager 的實現都是怎樣來實現 Authenticator 介面的呢?答案是:使用了組合。SecurityManager 都擁有一個 Authenticator 的屬性,這樣呼叫 SecurityManager.authenticate 的時候,是委託給內部的 Authenticator 屬性去執行的。

SecurityManager

3.4 SecurityManager.login 的實現

// DefaultSecurityManager.java
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
        info = authenticate(token);
    } catch (AuthenticationException ae) {
        try {
            onFailedLogin(token, ae, subject);
        } catch (Exception e) {
            if (log.isInfoEnabled()) {
                log.info("onFailedLogin method threw an " +
                        "exception.  Logging and propagating original AuthenticationException.", e);
            }
        }
        throw ae; //propagate
    }

    Subject loggedIn = createSubject(token, info, subject);

    onSuccessfulLogin(token, info, loggedIn);

    return loggedIn;
}

// AuthenticatingSecurityManager.java
/**
 * Delegates to the wrapped {@link org.apache.shiro.authc.Authenticator Authenticator} for authentication.
 */
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    return this.authenticator.authenticate(token);
}
  1. 呼叫自己的 authenticate 方法執行登入;
  2. authenticate 方法中代理給 Authenticator 介面型別的屬性去真正執行 authenticate(token) 方法。

3.5 Authenticator 登入模組

Authenticator 介面如下:

package org.apache.shiro.authc;

public interface Authenticator {

    public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException;
}

其實現類有 AbstractAuthenticatorModularRealmAuthenticator

Authenticator

下面來看看如何實現的 authenticate 方法:

// AbstractAuthenticator.java
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
        // 呼叫doAuthenticate方法
        info = doAuthenticate(token);
        if (info == null) {
            ...
        }
    } catch (Throwable t) {
        ...
    }
    ...
}

// ModularRealmAuthenticator.java
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
        // Realm唯一時
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    if (!realm.supports(token)) {
        ...
    }
    // 呼叫Realm的getAuthenticationInfo方法獲取AuthenticationInfo資訊
    AuthenticationInfo info = realm.getAuthenticationInfo(token);
    if (info == null) {
        ...
    }
    return info;
}

從原始碼中可以看出,最後會呼叫 RealmgetAuthenticationInfo(AuthenticationToken) 方法。

3.6 Realm 介面

Realm 相當於資料來源,功能是通過 AuthenticationToken 獲取資料來源中的安全資料,這個過程中可以丟擲異常,告訴 shiro 登入失敗。

package org.apache.shiro.realm;

public interface Realm {

    // 獲取 shiro 唯一的 realm 名稱
    String getName();

    // 是否支援給定的 AuthenticationToken 型別
    boolean supports(AuthenticationToken token);

    // 獲取 AuthenticationInfo
    AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}

Shiro 自帶了很多開箱即用的 Realm 實現,具體的類圖如下:

Realm

3.7 總結

到此,我們把整個 Shiro 的登入認證流程分析了一遍。

  1. 建立 AuthenticationToken,然後呼叫 Subject.login 方法進行登入認證;
  2. Subject 委託給 SecurityManager
  3. SecurityManager 委託給 Authenticator 介面;
  4. Authenticator 介面呼叫 Realm 獲取登入資訊。

整個過程中,如果登入失敗,就丟擲異常,是使用異常來進行邏輯控制的。

四、登入密碼的儲存

  1. 頁面使用 Https 協議;
  2. 頁面傳送密碼時要先加密後再傳輸,最好是不可逆的加密演算法(MD5,SHA2);
  3. 後端儲存時要結合鹽(隨機數)一起加密儲存;
  4. 使用不可逆的加密演算法,而且可以加密多次;
  5. 把加密後的密碼和鹽一起儲存到資料庫;

五、學習 Shiro 原始碼感悟

  1. 從整體去思考框架的實現,帶著業務邏輯去看實現邏輯;
  2. 不要摳細節,要看抽象,學習其實現方法;
  3. 首先看官方文件,官方文件一般會從整體設計方面去說明,遇到具體的介面再去看Javadoc文件;
  4. 結合類圖等工具方便理解;

六、參考

相關推薦

Shiro 登入認證原始碼

Apache Shiro 是一個強大且靈活的 Java 開源安全框架,擁有登入認證、授權管理、企業級會話管理和加密等功能,相比 Spring Security 來說要更加的簡單。 本文主要介紹 Shiro 的登入認證(Authentication)功能,主要從

Shiro安全框架入門篇(登入驗證例項原始碼

一、Shiro框架簡單介紹 Apache Shiro是Java的一個安全框架,旨在簡化身份驗證和授權。Shiro在JavaSE和JavaEE專案中都可以使用。它主要用來處理身份認證,授權,企業會話管理和加密等。Shiro的具體功能點如下: (1)身份認證/登

(轉) shiro權限框架04-shiro認證

software protected .get 打開 net 文件的 apach stc cdc http://blog.csdn.net/facekbook/article/details/54906635 shiro認證 本文介紹shiro的認證功能 認證流程

Shiro的Filter機制---原始碼分析

Shiro的Filter機制詳解 首先從spring-shiro.xml的filter配置說起,先回答兩個問題: 1, 為什麼相同url規則,後面定義的會覆蓋前面定義的(執行的時候只執行最後一個)。 2, 為什麼兩個url規則都可以匹配同一個url,只執行第一個呢。 下面分別從這兩個問題入手,最終閱讀原

(轉)shiro權限框架05-shiro授權

roles ktr ase sub turn stp exc protected user http://blog.csdn.net/facekbook/article/details/54910606 本文介紹 授權流程 授權方式 授權測試 自定義授權rea

(轉)shiro權限框架06-shiro與web項目整合(下)

tex web項目 ssd ndis form認證 lec rfi 出身 javadoc http://blog.csdn.net/facekbook/article/details/54962975 shiro和web項目整合,實現類似真實項目的應用 web項目中

k8s認證授權

k8s認證授權理解認證授權1.1 為什麽要認證想理解認證,我們得從認證解決什麽問題、防止什麽問題的發生入手。 防止什麽問題呢?是防止有人入侵你的集群,root你的機器後讓我們集群依然安全嗎?不是吧,root都到手了,那就為所欲為,防不勝防了。 其實網絡安全本身就是為了解決在某些假設成立的條件下如何防範的問題。

【轉】JDK的Parser來解析Java原始碼

轉自:https://www.jb51.net/article/92989.htm 這篇文章主要介紹了JDK的Parser來解析Java原始碼的相關資料,需要的朋友可以參考下 在JDK中,自帶了一套相關的編譯API,可以在Java中發起編譯流程,解析Java原始檔然後獲取其語法樹,在JDK的

Map容器家族(HashMap原始碼)

一、在Map集合家族的位置及描述         HashMap子類繼承自AbstractMap抽象類,實現了Map,Serializable,Cloneable介面,AbstractMap實現了Map介面的一部分方法,減輕了其子類的負擔。

zxing開源庫工作流程原始碼

程式碼獲取 作為移動客戶端開發者來說,對二維碼識別或二維碼生成相關的開發需求肯定並不陌生,Android開發二維碼相關的功能通常都會使用或參考大名鼎鼎的zxing庫。而本文則主要是通過原始碼分析一下該開源庫掃描二維碼的工作流程,對這塊能有個更深的瞭解。 首先使用git將專案程式碼clone到本地,新建專案

Collection容器家族(LinkedHashSet原始碼

一、在Collection集合體系中的位置及概述         LinkedHashSet 是非同步的有序的,分別是插入順序和訪問順序,LinkedHashSet的有序性可參考LinkedHashMap的有序性,繼承於HashSet,內部基

Collection容器家族(HashSet原始碼

一、在Collection集合體系中的位置及概述         HashSet繼承自AbstractSet抽象類,實現了Cloneable、Serializable介面,顯示的實現了Set介面。至於為什麼顯示的實現Set介面,我前面的文章講過。

Map容器家族(TreeMap原始碼)

一、在Map集合家族的位置及概述         TreeMap是一個有序的key-value集合,它內部是通過紅-黑樹實現的。TreeMap繼承與AbstractMap,實現了NavigableMap介面,意味著它支援一系列的導航方法

Map容器家族(LinkedHashMap原始碼)

一、在Map集合家族的位置及描述                                    Li

OkHttp原始碼之二完結篇

1. 請大家思考幾個問題 在開始本文之前,請大家思考如下幾個問題。並請大家帶著這幾個問題,去本文尋找答案。如果你對下面幾個問題的答案瞭如指掌那本文可以略過不看 在瀏覽器中輸入一個網址,按回車後發生了什麼? Okhttp的TCP連線建立發生在什麼時候? Okht

OkHttp原始碼之Okio原始碼

請在電腦上閱讀,效果更佳 本文將從兩個技術點講解OkHttp 1. 講解Okio,因為Okhttp的IO操作都是基於Okio,拋開Okio的OkHttp講解是不完美的 2. 講解OkHttp原始碼 Okio 1. Okio簡介 引用官方的一段介紹 Okio是一個補

wordcount 原始碼

1.原始碼解釋 package org.apache.hadoop.examples;import java.io.IOException;import java.util.StringTokenizer;import org.apache.hadoop.conf.Configuration;import

openTSDB原始碼之rowKey生成

openTSDB原始碼詳解之rowKey生成 openTSDB的一個非常好的設計就是其rowKey的生成。下面詳細介紹一下。 1.相關處理類 openTSDB往hbase中寫入資料的處理過程,我之前就已經分析過,主要涉及的類有: addPointInternal(

openTSDB原始碼之Deferred類簡單示例2

openTSDB原始碼詳解之Deferred類簡單示例2 1.示例2 1.1 程式碼 程式程式碼如下: public static void test2() { try { //注意這個時候由 dfd -> dfd List(lstDfd)。但是其型

openTSDB原始碼之Deferred類程式碼簡單示例1

openTSDB原始碼詳解之Deferred類程式碼簡單示例1 1.示例1 1.1 程式碼 /** * simplest with only 1 defer * 最簡單的,僅僅只有1個defer */ public static void test