1. 程式人生 > >Shiro 登錄認證源碼詳解

Shiro 登錄認證源碼詳解

owa fontsize ade 加密算 static end pub ons tle

Shiro 登錄認證源碼詳解

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

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

一、Shiro 總覽

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

  1. 獲取用戶輸入的用戶名,密碼;
  2. 從服務器數據源中獲取相應的用戶名和密碼;
  3. 判斷密碼是否匹配,決定是否登錄成功。

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

技術分享圖片

圖中包含三個重要的 Shiro 概念:SubjectSecurityManagerRealm。接下來,分別介紹這三者有何用:

  • 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 詳細結構

技術分享圖片

二、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其中的一個實現類AuthenticatingSecurityManager中擁有一個 Authenticator 的屬性,這樣調用 authenticate 的時候,是委托給內部的 Authenticator 屬性去執行的。

技術分享圖片

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. 調用AuthenticatingSecurityManager接口的 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

技術分享圖片

下面來看看如何實現的 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 實現,具體的類圖如下:

技術分享圖片

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 登錄認證源碼詳解