tomcat中AuthenticatorBase簡單的安全認證


有些web應用程序的內容是有限制的,只允許有權限的用戶在提供正確的用戶名和密碼的情況下才允許訪問。Servlet通過配置部署文件web.xml來對安全性提供技術支持。

    一個servlet通過一個叫authenticator的閥門(valve)來支持安全性限制。當容器啟動的時候,authenticator被添加到容器的流水線上(一整條流水線:StandardEngineValve->StandardHost->ErroReportValve->StandardContextValve->AuthenciatorValve->StandWrapperValve)。

    authenticator閥門會在包裝器閥門之前被調用。authenticator用於對用戶進行驗證,如果用戶輸入了正確的用戶名和密碼,authenticator閥門調用下一個用於處理請求servlet的閥門。如果驗證失敗,authenticator不喚醒下一個閥門直接返回。由於驗證失敗,用戶並不能看到請求的servlet。

    在用戶驗證的時候authenticator閥門調用的是上下文域(realm)內的authenticate方法,將用戶名和密碼傳遞給它。該容器域可以訪問合法的用戶名密碼。

域 Realm

    域是用於進行用戶驗證的一個組件,它可以告訴你一個用戶名密碼對是否是合法的。一個域跟一個上下文容器相聯系,一個容器可以只有一個域。可以使用容器的setRealm方法來建立它們之間的聯系。

    一個域是如何驗證一個用戶的合法性的?一個域擁有所有的合法用戶的密碼或者是可以訪問它們。至於它們存放在哪里則取決於域的實現。在Tomcat的默認實現里,合法用戶被存儲在tomcat-users.xml文件里。但是可以使用域的其它實現來訪問其它的源,如關系數據庫。

在Catalina中,一個域用接口org.apache.catalina.Realm表示。該接口最重要的方法是四個authenticate方法:

 

第一個方法是最常用的方法,Realm接口還有一個getRole方法,簽名如下:

    public boolean hasRole(Principal principal, String role);

另外,域還有getContainer和setContainer方法用於建立域與容器的聯系。

    一個域的基本上實現是抽象類org.apache.catalina.realm.RealmBase。org.apache.catalina.realm包中還提供了其它一些類繼承了RealmBase如:JDBCRealm, JNDIRealm, MemoryRealm,和 UserDatabaseRealm。默認情況下使用的域是MemoryRealm。

GenericPrincipal

    一個principal使用java.security.Principal接口來表示,Tomcat中該接口的實現為org.apache.catalina.realm.GenericPrincipal接口。一個GenericPrincipal必須跟一個域相關聯,這個是通過構造函數實現的:

 

    GenericPrincipal必須擁有一個用戶名和一個密碼,此外還可選擇性的傳遞一列角色。可以使用hasRole方法來檢查一個principal是否有一個特定的角色,傳遞的參數為角色的字符串表示形式。這里是Tomcat4中的hasRole方法:

                

LoginConfig類

        一個login configuration包括一個域名,用org.apache.catalina.deploy.LoginConfig類表示。LoginConfig的實例封裝了域名和驗證要用的方法。可以使用LoginConfig實例的getRealmName方法來獲得域名,可以使用getAuthName方法來驗證用戶。一個驗證(authentication)的名字必須是下面的之一:BASIC, DIGEST, FORM, o或者CLIENT-CERT。如果用到的是基於表單(form)的驗證,該LoginConfig對象還包括登錄或者錯誤頁面像對應的URL。

Tomcat一個部署啟動的時候,先讀取web.xml。如果web.xml包括一個login-confgi元素,Tomcat創建一LoginConfig對象並相應的設置它的屬性。驗證閥門調用LoginConfig的getRealmName方法並將域名發送給瀏覽器顯示登錄表單。如果getRealmName名字返回值為null,則發送給瀏覽器服務器的名字和端口名

Authenticator類

        org.apache.catalina.Authenticator接口用來表示一個驗證器。該方接口並沒有方法,只是一個組件的標志器,這樣就能使用instanceof來檢查一個組件是否為驗證器。

        Catalina提供了Authenticator接口的基本實現:org.apache.catalina.authenticator.AuthenticatorBase類。除了實現Authenticator接口外,AuthenticatorBase還繼承了org.apache.catalina.valves.ValveBase類。這就是說AuthenticatorBase也是一個閥門。可以在org.apache.catalina.authenticator包中找到該接口的幾個類:BasicAuthenticator用於基本驗證, FormAuthenticator用於基於表單的驗證, DigestAuthentication用於摘要(digest)驗證, SSLAuthenticator用於SSL驗證。NonLoginAuthenticator用於Tomcat沒有指定驗證元素的時候。NonLoginAuthenticator類表示只是檢查安全限制的驗證器,但是不進行用戶驗證。

        org.apache.catalina.authenticator包中類的UML結構圖

    

 

 

一個驗證器的主要工作是驗證用戶。因此,AuthenticatorBase類的invoke方法調用了抽象方法authenticate,該方法的具體實現由子類完成。在BasicAuthenticator中,它authenticate使用基本驗證器來驗證用戶。

大量的 Web 應用都有安全相關的需求,正因如此,Servlet 規范建議容器要有滿足這些需求的機制和基礎設施,所以容器要對以下安全特性予以支持:

  • 身份驗證:驗證授權用戶的用戶名和密碼
  • 資源訪問控制:限制某些資源只允許部分用戶訪問
  • 數據完整性:能夠證明數據在傳輸過程中未被第三方修改
  • 機密性或數據隱私:傳輸加密(SSL),確保信息只能被信任用戶訪問

本文就以上問題,對 Tomcat 容器提供的認證和鑒權的設計與實現,以及內部單點登錄的原理進行分析。首發於微信公眾號頓悟源碼.

1. 授權

容器和 Web 應用采用的是基於角色的權限訪問控制方式,其中容器需要實現認證和鑒權的功能,而 Web 應用則要實現授權的功能。

在 Servlet 規范中描述了兩種授權方式:聲明式安全和編程式安全。聲明式安全就是在部署描述符中聲明角色、資源訪問權限和認證方式。以下代碼片段摘自 Tomcat 自帶的 Manager 應用的 web.xml:

<security-constraint> <!-- 安全約束 --> <web-resource-collection> <!-- 限制訪問的資源集合 --> <web-resource-name>HTML Manager commands</web-resource-name> <url-pattern>/html/*</url-pattern> </web-resource-collection> <auth-constraint><!-- 授權可訪問此資源集合的角色 --> <role-name>manager-gui</role-name> </auth-constraint> </security-constraint> <login-config><!-- 配置驗證方法 --> <auth-method>BASIC</auth-method> <realm-name>Tomcat Manager Application</realm-name> </login-config> <security-role><!-- 定義一個安全角色 --> <description> The role that is required to access the HTML Manager pages </description> <role-name>manager-gui</role-name> </security-role> 

這些安全相關的配置,都會在應用部署時,初始化和設置到 StandardContext 對象中。更多詳細的內容可查看規范對部署描述文件的解釋,接下來看 Tomcat 怎么設計和實現認證及鑒權。

2. 認證和鑒權的設計

Servlet 規范雖然描述了 Web 應用聲明安全約束的機制,但沒有定義容器與關聯用戶和角色信息之間的接口。因此,Tomcat 定義了一個 Realm 接口,用於適配身份驗證的各種信息源。整體設計的類圖如下:

Tomcat 認證和鑒權類圖

上圖中,包含了各個類的核心方法,關鍵類或接口的作用如下:

  • Realm - 譯為,域有泛指某種范圍的意思,在這個范圍內存儲着用戶名、密碼、角色和權限,並且提供身份和權限驗證的功能,典型的這個范圍可以是某個配置文件或數據庫
  • CombinedRealm - 內部包含一個或多個 Realm,按配置順序執行身份驗證,任一 Realm 驗證成功,則表示成功驗證
  • LockOutRealm - 提供用戶鎖定機制,防止在一定時間段有過多身份驗證失敗的嘗試
  • Authenticator - 不同身份驗證方法的接口,主要有 BASIC、DIGEST、FORM、SSL 這幾種標准實現
  • Principal - 對認證主體的抽象,它包含用戶身份和權限信息
  • SingleSignOn - 用於支持容器內多應用的單點登錄功能

2.1 初始化

Realm 是容器的一個可嵌套組件,可以嵌套在 Engine、Host 和 Context 中,並且子容器可以覆蓋父容器配置的 Realm。默認的 server.xml 在 Engine 中配置了一個 LockOutRealm 組合域,內部包含一個 UserDatabaseRealm,它從配置的全局資源 conf/tomcat-users.xml 中提取用戶信息。

web.xml 中聲明的安全約束會初始化成對應的 SecurityConstraint、SecurityCollection 和 LoginConfig 對象,並關聯到一個 StandardContext 對象。

在上圖可以看到,AuthenticatorBase 還實現了 Valve 接口,StandardContext 對象在配置的過程中,如果發現聲明了標准的驗證方法,那么就會把它加入到自己的 Pipeline 中。

3. 一次請求認證和鑒權過程

Context 在 Tomcat 內部就代表着一個 Web 應用,假設配置使用 BASIC 驗證方法,那么 Context 內部的 Pipeline 就有 BasicAuthenticator 和 StandardContextValve 兩個閥門,當請求進入 Context 管道時,就首先進行認證和鑒權,方法調用如下:

認證和鑒權序列圖

整個過程的核心代碼就在 AuthenticatorBase 的 invoke 方法中:

public void invoke(Request request, Response response) throws IOException, ServletException { LoginConfig config = this.context.getLoginConfig(); // 0. Session 對象中是否緩存着一個已經進行身份驗證的 Principal if (cache) { Principal principal = request.getUserPrincipal(); if (principal == null) { Session session = request.getSessionInternal(false); if (session != null) { principal = session.getPrincipal(); if (principal != null) { request.setAuthType(session.getAuthType()); request.setUserPrincipal(principal); } } } } // 對於基於表單登錄,可能位於安全域之外的特殊情況進行處理 String contextPath = this.context.getPath(); String requestURI = request.getDecodedRequestURI(); if (requestURI.startsWith(contextPath) && requestURI.endsWith(Constants.FORM_ACTION)) { return; } } // 獲取安全域對象,默認配置是 LockOutRealm Realm realm = this.context.getRealm(); // 根據請求 URI 嘗試獲取配置的安全約束 SecurityConstraint [] constraints = realm.findSecurityConstraints(request, this.context); if ((constraints == null) /* && (!Constants.FORM_METHOD.equals(config.getAuthMethod())) */ ) { // 為 null 表示訪問的資源沒有安全約束,直接訪問下一個閥門 getNext().invoke(request, response); return; } // 確保受約束的資源不會被 Web 代理或瀏覽器緩存,因為緩存可能會造成安全漏洞 if (disableProxyCaching && !"POST".equalsIgnoreCase(request.getMethod())) { if (securePagesWithPragma) { response.setHeader("Pragma", "No-cache"); response.setHeader("Cache-Control", "no-cache"); } else { response.setHeader("Cache-Control", "private"); } response.setHeader("Expires", DATE_ONE); } int i; // 1. 檢查用戶數據的傳輸安全約束 if (!realm.hasUserDataPermission(request, response, constraints)) { // 驗證失敗 // Authenticator已經設置了適當的HTTP狀態代碼,因此我們不必做任何特殊的事情 return; } // 2. 檢查是否包含授權約束,也就是角色驗證 boolean authRequired = true; for(i=0; i < constraints.length && authRequired; i++) { if(!constraints[i].getAuthConstraint()) { authRequired = false; } else if(!constraints[i].getAllRoles()) { String [] roles = constraints[i].findAuthRoles(); if(roles == null || roles.length == 0) { authRequired = false; } } } // 3. 驗證用戶名和密碼 if(authRequired) { // authenticate 是一個抽象方法,由不同的驗證方法實現 if (!authenticate(request, response, config)) { return; } } // 4. 驗證用戶是否包含授權的角色 if (!realm.hasResourcePermission(request, response,constraints,this.context)) { return; } // 5. 已滿足任何和所有指定的約束 getNext().invoke(request, response); } 

另外,AuthenticatorBase 還有一個比較重要的 register() 方法,它會把認證后生成的 Principal 對象設置到當前 Session 中,如果配置了SingleSignOn 單點登錄的閥門,同時把用戶身份、權限信息關聯到 SSO 中。

4. 單點登錄

Tomcat 支持通過一次驗證就能訪問部署在同一個虛擬主機上的所有 Web 應用,可通過以下配置實現:

<Host name="localhost" ...> ... <Valve className="org.apache.catalina.authenticator.SingleSignOn"/> ... </Host> 

Tomcat 的單點登錄是利用 Cookie 實現的:

  • 當任一 Web 應用身份驗證成功后,都會把用戶身份信息緩存到 SSO 中,並生成一個名為 JSESSIONIDSSO 的 Cookie
  • 當用戶再次訪問這個主機時,會通過 Cookie 拿出存儲的用戶 token,獲取用戶 Principal 並關聯到 Request 對象中

5. 小結

本文介紹的是 Tomcat 內部實現的登錄認證和權限,而應用程序通常都是通過 Filter 或者自定義的攔截器(如 Spring 的 Interceptor)實現登錄,或者使用第三方安全框架,比如 Shiro,但是原理都差不多。


原文鏈接:https://blog.csdn.net/u012233580/article/details/79316212

https://www.cnblogs.com/chuonye/p/10877757.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM