存取安全性已經在 Java 中實現了相當長的時間,提供這種安全性的體系結構稱為 JAAS - Java 身份驗證和授權服務。這篇評論將試圖揭開身份驗證、授權是什麼以及 JAAS 與它們有何關係的謎團。JAAS 如何與 Servlet API 成為朋友,以及他們的關係中存在哪些問題。
也就是說,在建立 Web 應用程式的 WAR 存檔時,插件將考慮此位置的檔案。此外,Gradle War Plugin 文件表示該目錄將是「存檔的根目錄」。我們已經可以在其中建立一個 WEB-INF 目錄並在其中新增 web.xml 檔案。這是什麼類型的文件?
檢查 Tomcat 是否選擇了 servlet 容器(請參閱#1)並檢查我們的 Web 應用程式在哪個位址可用(請參閱#2)非常重要。
也就是說,我們可以將 JAAS 登入組態設定放置在 Web 應用程式的資源中,並且藉助 Tomcat Context,我們將能夠使用它。該檔案必須可作為 ClassLoader 的資源使用,因此其路徑應如下所示:
介紹
在這篇評論中,我想討論 Web 應用程式安全這樣的主題。Java 有多種提供安全性的技術:-
“ Java SE Platform Security Architecture ”,更多詳細資訊可以閱讀Oracle的指南:“ JavaTM SE Platform Security Architecture ”。該架構描述了我們需要如何保護 Java SE 執行時間環境中的 Java 應用程式。但這不是我們今天談話的主題。
-
「Java Cryptography Architecture」是描述資料加密的Java擴充。您可以在 JavaRush 上的評論“ Java Cryptography Architecture:初識”或 Oracle 指南:“ Java Cryptography Architecture (JCA) Reference Guide ”中閱讀有關此擴充功能的更多資訊。
聯合航空暨太空總署
JAAS是 Java SE 的擴展,在Java 驗證和授權服務 (JAAS) 參考指南中進行了描述。正如該技術的名稱所示,JAAS 描述瞭如何執行身份驗證和授權:-
「Authentication」:譯自希臘語,「authentikos」的意思是「真實的、真實的」。也就是說,認證是對真實性的測試。無論誰被驗證都是他們所說的真實身分。
-
「授權」:翻譯自英文,意思是「許可」。即授權是認證成功後進行的存取控制。
- 他的駕駛執照代表一個人作為道路使用者
- 他的護照,代表一個人作為其國家的公民
- 他的外國護照,作為國際關係參與者的代表
- 他在圖書館的借書卡,代表一個人作為圖書館的讀者
Web應用程式
因此,我們需要一個網頁應用程式。Gradle自動專案建置系統將幫助我們建立它。由於使用 Gradle,我們可以透過執行小命令,以我們需要的格式組裝 Java 項目,自動建立必要的目錄結構等等。您可以在簡短概述:「Gradle 簡介」或官方文件「Gradle 入門」中閱讀有關 Gradle 的更多資訊。我們需要對專案進行初始化(Initialization),為此Gradle有一個特殊的插件:「Gradle Init Plugin」(Init是Initialization的縮寫,方便記憶)。要使用此插件,請在命令列上運行命令:gradle init --type java-application
成功完成後,我們將擁有一個Java專案。現在讓我們打開專案的建置腳本進行編輯。建置腳本是一個名為 的文件build.gradle
,它描述了應用程式建置的細微差別。因此得名建置腳本。我們可以說這是一個專案建置腳本。Gradle 是一個多功能工具,其基本功能可透過插件進行擴充。因此,首先讓我們專注於「插件」區塊:
plugins {
id 'java'
id 'application'
}
預設情況下,Gradle 依照我們指定的「--type java-application
」設定了一組核心插件,也就是那些包含在 Gradle 本身發行版中的插件。如果您前往gradle.org網站上的「文件」(即文件)部分,那麼在「參考」部分的主題清單左側,我們會看到「核心外掛程式」部分,即 部分包含這些非常基本的插件的描述。讓我們準確地選擇我們需要的插件,而不是 Gradle 為我們產生的插件。根據文檔,「Gradle Java Plugin」提供了Java程式碼的基本操作,例如編譯原始碼。另外,根據文檔,「Gradle 應用程式插件」為我們提供了使用「可執行 JVM 應用程式」的工具,即 可以作為獨立應用程式啟動的 java 應用程式(例如,控制台應用程式或具有自己的 UI 的應用程式)。事實證明,我們不需要「應用程式」插件,因為... 我們不需要獨立的應用程序,我們需要一個網頁應用程式。我們把它刪除吧。以及“mainClassName”設置,只有這個插件知道。此外,在提供應用程式插件文件連結的同一「打包和分發」部分中,還有一個 Gradle War 插件的連結。 Gradle War Plugin,如文件所述,提供對建立 war 格式的 Java Web 應用程式的支援。WAR 格式意味著將建立 WAR 存檔而不是 JAR 存檔。看來這就是我們所需要的。另外,正如文件所述,「War 外掛擴充了 Java 外掛」。即我們可以將java插件替換為war插件。因此,我們的插件塊最終將如下所示:
plugins {
id 'war'
}
另外,在「Gradle War Plugin」的文檔中,據說該插件使用了額外的「專案佈局」。佈局從英文翻譯為位置。也就是說,war 插件預設期望存在用於其任務的特定位置的檔案。它將使用以下目錄來儲存 Web 應用程式檔案:src/main/webapp
該插件的行為描述如下:
web.xml
- 這是一個「部署描述符」或「部署描述符」。該文件描述如何配置我們的 Web 應用程式以使其工作。該文件指定我們的應用程式將處理哪些請求、安全性設定等等。從本質上講,它有點類似於 JAR 檔案中的清單檔案(請參閱「使用清單檔案:基礎知識」)。Manifest 檔案告訴我們如何使用 Java 應用程式(即 JAR 存檔),而 web.xml 告訴我們如何使用 Java Web 應用程式(即 WAR 存檔)。「部署描述符」的概念本身並不是出現的,而是在文件「Servlet API 規範」中描述的任何Java Web應用程式都依賴這個「Servlet API」。重要的是要理解這是一個API——也就是說,它是一些互動契約的描述。Web應用程式不是獨立的應用程式。它們運行在Web伺服器上,它提供與使用者的網路通訊。也就是說,Web伺服器是Web應用程式的一種「容器」。這是合乎邏輯的,因為我們要編寫Web應用程式的邏輯,也就是使用者將看到哪些頁面以及如何查看他們應該對用戶的操作做出反應。我們不想編寫如何將訊息發送給用戶、如何傳輸信息字節以及其他低級且質量要求很高的事情的代碼。此外,事實證明,Web 應用程序都是不同的,但資料傳輸是相同的。也就是說,一百萬個程式設計師必須一遍又一遍地為同一目的編寫程式碼。因此,Web 伺服器負責一些使用者互動和資料交換,Web 應用程式和開發人員負責產生該數據。為了連接這兩個部分,即 Web 伺服器和 Web 應用程序,您需要為它們的互動簽訂合同,即 他們將遵循什麼規則來做到這一點?為了以某種方式描述契約,即 Web 應用程式和 Web 伺服器之間的互動應該是什麼樣子,Servlet API 被發明了。有趣的是,即使您使用像 Spring 這樣的框架,仍然有一個 Servlet API 在背景運行。也就是說,您使用 Spring,Spring 會為您使用 Servlet API。事實證明,我們的網路應用程式專案必須依賴Servlet API。在這種情況下,Servlet API 將成為依賴項。眾所周知,Gradle 還允許您以聲明方式描述專案依賴關係。插件描述如何管理依賴關係。例如,Java Gradle Plugin 引入了「testImplementation」依賴管理方法,該方法表示這樣的依賴僅在測試時需要。但是Gradle War Plugin加入了一個依賴管理方法“providedCompile”,它表示這樣的依賴將不會包含在我們的網路應用程式的WAR檔案中。為什麼我們不將 Servlet API 包含在我們的 WAR 檔案中?因為Servlet API將由Web伺服器本身提供給我們的Web應用程式。如果 Web 伺服器提供 Servlet API,則該伺服器稱為 Servlet 容器。因此,Web伺服器有責任為我們提供Servlet API,而我們有責任僅在程式碼編譯時提供ServletAPI。這就是為什麼providedCompile
。因此,依賴區塊將如下所示:
dependencies {
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
testImplementation 'junit:junit:4.12'
}
那麼,讓我們回到 web.xml 檔案。預設情況下,Gradle 不會建立任何部署描述符,因此我們需要自己建立。讓我們建立一個目錄src/main/webapp/WEB-INF
,並在其中建立一個名為 的 XML 檔案web.xml
。現在讓我們打開「Java Servlet 規範」本身和「第 14 章部署描述符」一章。如「14.3 部署描述符」所述,部署描述符的 XML 文件由模式http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd描述。XML 模式描述了文件可以包含哪些元素以及它們應該以什麼順序出現。哪些是強制性的,哪些不是。一般來說,它描述了文件的結構,並允許您檢查 XML 文件的組成是否正確。現在讓我們使用「 14.5範例」一章中的範例,但必須為3.1版本指定方案,即
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd
我們的空的web.xml
將如下所示:
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>JAAS Example</display-name>
</web-app>
現在讓我們來描述將使用 JAAS 保護的 servlet。之前,Gradle 為我們產生了 App 類別。讓我們把它變成servlet。正如《 CHAPTER 2 The Servlet Interface 》規範中所述,“對於大多數目的,開發人員將擴展 HttpServlet 來實現他們的 servlet ”,也就是說,要使一個類成為 servlet,您需要從以下位置繼承該類HttpServlet
:
public class App extends HttpServlet {
public String getGreeting() {
return "Secret!";
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().print(getGreeting());
}
}
正如我們所說,Servlet API 是伺服器和我們的 Web 應用程式之間的契約。這個契約允許我們描述當使用者聯繫伺服器時,伺服器將以物件的形式產生來自使用者的請求HttpServletRequest
並將其傳遞給servlet。它還將為 servlet 提供一個對象,HttpServletResponse
以便 servlet 可以為用戶編寫對其的回應。一旦 servlet 完成運行,伺服器就能夠向使用者提供基於它的回應HttpServletResponse
。即servlet不直接與用戶通信,只與伺服器通信。為了讓伺服器知道我們有一個 servlet 以及需要使用它來處理哪些請求,我們需要在部署描述符中告訴伺服器這一點:
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>jaas.App</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/secret</url-pattern>
</servlet-mapping>
在這種情況下,所有請求都/secret
不會透過 name 傳送到我們的一個 servlet app
,該 servlet 對應於 class jaas.App
。正如我們之前所說,Web 應用程式只能部署在 Web 伺服器上。Web 伺服器可以單獨安裝(獨立)。但出於本次審查的目的,另一種選擇是合適的 - 在嵌入式伺服器上運行。這意味著伺服器將以程式設計方式建立和啟動(插件將為我們執行此操作),同時我們的 Web 應用程式將部署在其上。Gradle 建置系統可讓您使用「Gradle Gretty Plugin」外掛程式來實現以下目的:
plugins {
id 'war'
id 'org.gretty' version '2.2.0'
}
此外,Gretty 外掛程式有很好的文件。首先,Gretty 外掛程式可讓您在不同的 Web 伺服器之間切換。文件對此進行了更詳細的描述:「在 servlet 容器之間切換」。讓我們切換到 Tomcat,因為... 它是最受歡迎的使用之一,也有很好的文件和許多範例和分析的問題:
gretty {
// Переключаемся с дефолтного Jetty на Tomcat
servletContainer = 'tomcat8'
// Укажем Context Path, он же Context Root
contextPath = '/jaas'
}
現在我們可以執行“gradle appRun”,然後我們的 Web 應用程式將在 http://localhost:8080/jaas/secret 上可用
驗證
身份驗證設定通常由兩部分組成:伺服器端的設定和在此伺服器上執行的 Web 應用程式端的設定。如果沒有其他原因,Web 應用程式的安全性設定只能與 Web 伺服器的安全設定互動。我們轉向 Tomcat 並沒有白費,因為… Tomcat 具有描述良好的架構(請參閱「Apache Tomcat 8 架構」)。從這個架構的描述可以清楚地看出,Tomcat作為一個Web伺服器,將Web應用程式表示為一定的上下文,稱為「Tomcat上下文」。此上下文允許每個 Web 應用程式擁有自己的設置,與其他 Web 應用程式隔離。此外,Web 應用程式可以影響此上下文的設定。靈活方便。為了更深入地了解,我們建議閱讀文章「了解 Tomcat 上下文容器」和 Tomcat 文件部分「上下文容器」。如上所述,我們的 Web 應用程式可以使用/META-INF/context.xml
. 我們可以影響的非常重要的設定之一是安全領域。 安全領域是一種「安全區域」。指定特定安全設定的區域。因此,在使用安全領域時,我們會應用為此領域定義的安全性設定。安全領域由容器管理,即 Web 伺服器,而不是我們的 Web 應用程式。我們只能告訴伺服器我們的應用程式需要擴展哪些安全範圍。Tomcat 文件中的「領域元件」部分將領域描述為有關使用者及其用於執行身份驗證的角色的資料集合。Tomcat 提供了一組不同的 Security Realm 實現,其中之一是「Jaas Realm」。了解了一些術語後,我們來描述文件中的 Tomcat 上下文/META-INF/context.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Realm className="org.apache.catalina.realm.JAASRealm"
appName="JaasLogin"
userClassNames="jaas.login.UserPrincipal"
roleClassNames="jaas.login.RolePrincipal"
configFile="jaas.config" />
</Context>
appName
- 應用程式名稱。Tomcat 將嘗試將此名稱與configFile
. configFile
- 這是「登入設定檔」。JAAS 文件中可以看到這樣的範例:「附錄 B:登入設定範例」。此外,重要的是,該文件將首先在資源中搜尋。因此,我們的網路應用程式可以自己提供這個檔案。屬性userClassNames
並roleClassNames
包含代表使用者主體的類別的指示。JAAS 將「使用者」和「角色」的概念分離為兩個不同的概念java.security.Principal
。讓我們來描述一下上面的類別。讓我們為用戶主體創建最簡單的實作:
public class UserPrincipal implements Principal {
private String name;
public UserPrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
我們將為 重複完全相同的實作RolePrincipal
。從介面中可以看到,Principal的主要作用是儲存並返回一些代表Principal的名稱(或ID)。現在,我們有一個安全領域,我們有主要的類別。configFile
仍然需要從“ ”屬性(又稱為 )填充文件login configuration file
。它的描述可以在Tomcat文檔中找到:「The Realm Component」。
\src\main\resources\jaas.config
讓我們設定該檔案的內容:
JaasLogin {
jaas.login.JaasLoginModule required debug=true;
};
值得注意的是,context.xml
這裡和 in 中使用了相同的名稱。這將安全性領域映射到登入模組。因此,Tomcat Context 告訴我們哪些類別代表主體,以及要使用哪個 LoginModule。我們要做的就是實作這個LoginModule。 LoginModule也許是 JAAS 中最有趣的東西之一。官方文件將幫助我們開發LoginModule:《Java Authentication and Authorization Service (JAAS): LoginModule Developer's Guide》。讓我們來實作登入模組。讓我們建立一個實作該介面的類別LoginModule
:
public class JaasLoginModule implements LoginModule {
}
首先我們來描述一下初始化方法LoginModule
:
private CallbackHandler handler;
private Subject subject;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, <String, ?> sharedState, Map<String, ?> options) {
handler = callbackHandler;
this.subject = subject;
}
此方法將保存Subject
,我們將進一步驗證並填充有關主體的資訊。我們還將保存給CallbackHandler
我們的以供將來使用。在幫助下,CallbackHandler
我們可以稍後要求有關身份驗證主題的各種資訊。CallbackHandler
您可以在文件的相應部分閱讀更多相關資訊:「 JAAS 參考指南:CallbackHandler」。login
接下來,執行認證方法Subject
。這是身份驗證的第一階段:
@Override
public boolean login() throws LoginException {
// Добавляем колбэки
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("login");
callbacks[1] = new PasswordCallback("password", true);
// При помощи колбэков получаем через CallbackHandler логин и пароль
try {
handler.handle(callbacks);
String name = ((NameCallback) callbacks[0]).getName();
String password = String.valueOf(((PasswordCallback) callbacks[1]).getPassword());
// Далее выполняем валидацию.
// Тут просто для примера проверяем определённые значения
if (name != null && name.equals("user123") && password != null && password.equals("pass123")) {
// Сохраняем информацию, которая будет использована в методе commit
// Не "пачкаем" Subject, т.к. не факт, что commit выполнится
// Для примера проставим группы вручную, "хардcodeно".
login = name;
userGroups = new ArrayList<String>();
userGroups.add("admin");
return true;
} else {
throw new LoginException("Authentication failed");
}
} catch (IOException | UnsupportedCallbackException e) {
throw new LoginException(e.getMessage());
}
}
重要的是login
我們不應該改變Subject
. 這種改變應該只發生在確認方法中commit
。接下來我們要來描述一下確認認證成功的方法:
@Override
public boolean commit() throws LoginException {
userPrincipal = new UserPrincipal(login);
subject.getPrincipals().add(userPrincipal);
if (userGroups != null && userGroups.size() > 0) {
for (String groupName : userGroups) {
rolePrincipal = new RolePrincipal(groupName);
subject.getPrincipals().add(rolePrincipal);
}
}
return true;
}
login
將方法和 分開似乎很奇怪commit
。但重點是登入模組可以組合。為了成功進行身份驗證,可能需要多個登入模組才能成功運作。並且只有當所有必要的模組都工作時,才會儲存變更。這是身份驗證的第二階段。讓我們以abort
和方法結束logout
:
@Override
public boolean abort() throws LoginException {
return false;
}
@Override
public boolean logout() throws LoginException {
subject.getPrincipals().remove(userPrincipal);
subject.getPrincipals().remove(rolePrincipal);
return true;
}
abort
當第一階段身份驗證失敗時呼叫 該方法。logout
當系統登出時呼叫該方法。實作Login Module
並配置它之後Security Realm
,現在我們需要表明web.xml
我們想要使用特定的一個Login Config
:
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>JaasLogin</realm-name>
</login-config>
我們指定了安全領域的名稱並指定了身份驗證方法 - BASIC。這是「 13.6 驗證」部分中 Servlet API 中所述的身份驗證類型之一。剩餘 n
GO TO FULL VERSION