從 Java EE 8 Security API 開始 —— 第一部分
面向雲和微服務平臺的 Java 企業級安全
新的 HttpAuthenticationMechanism、IdentityStore 和 SecurityContext 介面概述
關於這個系列:
期待已久的 Java EE Security API (JSR 375) 將 Java 企業級安全帶入雲端計算和微服務的新紀元。本系列的文章將向您展示如何簡化新的安全機制,以及 Java EE 跨容器安全的標準化處理,然後在啟用雲的專案中使用它們。
經驗豐富的 Java™ 開發者應該瞭解,Java 並不會受到缺乏 Java 安全機制的影響。可選的方案有 Java 容器授權協議說明 (JACC),Java 身份認證服務提供器 (JASPIC),以及大量第三方特定於容器的安全 API 和配置管理解決方案。
問題不在於缺乏選擇,而在於缺乏企業標準。沒有標準,導致幾乎沒有什麼可以激勵供應商始終如一地實現核心特性,比如,身份驗證,像上下文和依賴注入(CDI)以及表示式語言(EL)那樣獨有解決方案的新技術更新,或者與雲和微服務架構的安全發展保持同步。
本系列介紹了新的 Java EE Security API,首先會概述 API 及其三個主要介面:HttpAuthenticationMechanism
、IdentityStore
和 SecurityContext
。
Java EE 新的安全標準
Java EE 安全規範的開發得力於 2014 Java EE 8 問卷調查,社群的反饋推動了 Java EE 安全規範的開發步伐。簡化和標準化 Java 企業級安全是許多調查物件優先考慮的事項。JSR 375專家組一旦成立,將確定以下問題:
- 構成 Java EE 的各種 EJB 和 servlet 容器定義了類似的與安全相關的 API,但語法存在細微差別。例如,servlet 檢查使用者角色時,呼叫
HttpServletRequest.isUserInRole(String role)
,而 EJB 則呼叫EJBContext.isCallerInRole(String roleName)
。 - 實現像 JACC 這樣的現有安全機制,困難重重,而 JASPIC 也很難被正確使用。
- 現有機制無法充分利用現代 Java EE 的程式設計特性,例如上下文和依賴注入(CDI)。
- 沒有可移值性方法來控制如何在後端跨容器時,進行身份驗證。
- 對於管理標識儲存或者角色和許可權的配置,沒有標準的支援。
- 對於部署自定義身份驗證規則,沒有標準支援。
這些是 JSR 375 旨在解決的主要問題。同時,該規範通過定義用於身份驗證、身份儲存、角色和許可權以及跨容器授權的可移值性 API,促使開發者能夠自行管理和控制安全性。
Java EE Security API 的優點在於它提供了一種配置身份儲存和身份驗證機制的替代方法,但並不能取代現有的安全機制。Java EE Security API 允許開發人員以一致的和可移值的方式啟用 Java EE web 應用程式的安全性 —— 無論是否具有特定於供應商的或者獨有的解決方案。
Java EE Security API 中有什麼?
Java EE Security API 1.0 版本包含了初始提交草案的一個子集,而且側重於本地雲應用程式相關的技術。這些特性是:
- 用於身份驗證的 API
- 標識儲存 API
- 上下文安全的 API
這些特性與所有 Java EE 安全實現的新的標準化術語結合在一起。剩餘的特性(計劃包含在下一個版本中)是:
- 密碼別名 API
- 角色/許可權分配 API
- 授權攔截器 API
Web 安全認證
Java EE 平臺已經指定了兩種用於驗證 Web 應用程式使用者的機制:Servlet 4.0 (JSR 369) 提供適用於一般應用程式配置的宣告式機制。對於健壯性有更高需求的場景,JASPIC 定義了一個叫作 ServerAuthModule
的服務提供者介面,它支援開發認證模組來處理任何憑證型別。此外,Servlet 容器配置檔案指定了如何將 JASPIC 與 servlet 容器整合。
這兩種機制都是有意義和有效的,但對於 web 應用程式開發者來說,每種機制都存在其自身的侷限性。
Servlet 容器機制被限制為只支援 Servlet 4.0 定義的小部分憑據型別,而且它無法支援與呼叫方的複雜互動。它也無法為應用程式提供一種方法,以確定呼叫者是根據所需的標識儲存進行身份驗證的。
相反,JASPIC 非常優秀,而且有很好的延展性,但它的使用也相當複雜。編碼 AuthModule
,並且將其與 web 容器對齊以進行身份驗證使用,可能會非常難以處理。除此以外,JASPIC 沒有宣告式配置,也沒有明確的方式來過載註冊 AuthModule
的編碼方式。
Java EE Security API 通過一個新的介面 HttpAuthenticationMechanism
解決了其中一些問題。新介面本質上是 JASPIC ServerAuthModule
介面的一個簡化版 servlet 容器變體,它利用了現有的機制,同時削弱了它們的限制。
HttpAuthenticationMechanism
例項是容器負責提供注入的 CDI bean。HttpAuthenticationMechanism
介面的其他實現可以由應用程式或 servlet 容器提供。注意,HttpAuthenticationMechanism
僅為 servlet 容器指定。
對 Servlet 4.0 身份驗證的支援
Java EE 容器必須為 Servlet 4.0 規範中定義的三種身份認證機制提供 HttpAuthenticationMechanism
實現。這三種實現是:
- 基本 HTTP 身份驗證(第 13.6.1 章節)
- 基於表單的身份驗證(第 13.6.3 章節)
- 自定義表單身份驗證(第 13.6.3.1 章節)
每個實現都由相關注解的存在觸發:
@BasicAuthenticationMechanismDefinition
@FormAuthenticationMechanismDefinition
@CustomFormAuthenticationMechanismDefinition
當遇到這些註解之一時,容器會例項化相關機制的例項,並使其立即可用。
在新規範中,不再需要像 Servlet 4.0 所要求的那樣,在 web.xml
中的 <login-config>
元素之間指定身份驗證機制。事實上,如果 web.xml
和基於 HttpAuthentication 機制的註解同時存在時,部署過程可能會失敗 —— 至少要忽略 web.xml
配置。
讓我們看看每種機制的示例是如何執行的。
基本的 HTTP 身份驗證
@BasicAuthenticationMechanismDefinition
註解觸發 Servlet 4.0 定義的基本 HTTP 身份驗證。清單 1 列舉了一個示例。唯一的配置引數是可選的,而且允許指定 realm。
清單 1. 基本的 HTTP 身份驗證
@BasicAuthenticationMechanismDefinition(realmName="${'user-realm'}")
@WebServlet("/user")
@DeclareRoles({ "admin", "user", "demo" })
@ServletSecurity(@HttpConstraint(rolesAllowed = "user"))
public class UserServlet extends HttpServlet { … }
複製程式碼
什麼是 realm?
伺服器資源可以劃分為單獨的受保護控制元件。在這種情況下,每個使用者都將擁有自己的身份驗證模式和授權資料庫,其中包含受同源策略控制的使用者和組。這個使用者和組的資料庫稱為 realm。
基於表單的身份驗證
@FormAuthenticationMechanismDefinition
註解用於基於表單的身份驗證。它有一個必要的引數 loginToContinue
,用於配置 web 應用程式的登入頁面、錯誤頁面和重定向或轉發特性。在清單 2 中,您可以看到登入頁面是用 URL 定義的,useForwardToLoginExpression
是使用表示式語言(EL)配置的。不需要向 @LoginToContinue
註解傳遞任何引數,因為實現會提供預設值。
清單 2. 基於表單的身份驗證
@FormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(
loginPage="/login-servlet",
errorPage="/error",
useForwardToLoginExpression="${appConfig.forward}"
)
)
@ApplicationScoped
public class ApplicationConfig { ... }
複製程式碼
自定義表單認證
@CustomFormAuthenticationMechanismDefinition
註解觸發內建自定義表單身份驗證。清單 3 給出了一個示例。
清單 3. 自定義表單認證
@CustomFormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(
loginPage="/login.do"
)
)
@WebServlet("/admin")
@DeclareRoles({ "admin", "user", "demo" })
@ServletSecurity(@HttpConstraint(rolesAllowed = "admin"))
public class AdminServlet extends HttpServlet { ... }
複製程式碼
自定義表單身份驗證旨在更好地與 JavaServer Pages (JSF) 和相關的 Java EE 技術保持一致性。login.do
頁面顯示後,使用者名稱和密碼由登入頁面的後臺 bean 輸入並處理。
IdentityStore API
標識儲存是儲存使用者標識資料的資料庫,如使用者名稱、組成員和用於驗證的憑據資訊。Java EE Security API 提供了一個名為 IdentityStore
的抽象標識儲存。類似於 JAAS LoginModule
介面,IdentityStore
用於與標識儲存進行互動,以便對使用者進行身份驗證並檢索組成員身份。
正如規範所描述的,IdentityStore
被 HttpAuthenticationMechanism
的實現所使用,但這不是必須的, IdentityStore
可以獨立存在,供任何其他身份驗證機制使用。儘管如此,使用 IdentityStore
和 HttpAuthenticationMechanism
使應用程式能夠以可移植和標準化的方式控制用於身份驗證的身份儲存,在大部分用例場景中,都推薦使用。
IdentityStore
API 包括一個 IdentityStoreHandler
介面,HttpAuthenticationMechanism
必須委託它來驗證使用者憑據。之後,IdentityStoreHandler
呼叫 IdentityStore
例項。Identity
儲存實現不是直接使用的,而是通過專門的處理程式進行互動的。
IdentityStoreHandler
可以針對多個 IdentityStores
進行身份驗證,並且以 CredentialValidationResult
例項的形式返回聚合結果。無論憑據是否有效,該物件可能只具有傳遞憑據的作用,或者它可以是包含下述任何資訊的豐富物件:
CallerPrincipal
- 主體所屬的一組集合
- 呼叫者的名稱或者 LDAP 可分辨的名稱
- 標識儲存中呼叫方的唯一標識
標識儲存按順序進行查詢,這取決於每個 IdentityStore
實現的優先順序。儲存列表被解析了兩次:首先用於身份驗證,然後用於授權。
作為開發者,您可以通過實現 IdentityStore
介面來實現自己的輕量級標識儲存,或者您可以使用為 LDAP 和 RDBMS 內建的 IdentityStores
的其中一種。它們是通過將配置細節傳遞給適當的註解來初始化的 —— @LdapIdentityStoreDefinition
或者 @DataBaseIdentityStoreDefinition
。
配置內建的 IdentityStore
最簡單的標識儲存是資料庫儲存。它是通過 @DataBaseIdentityStoreDefinition
註解進行配置的。正如清單 4 所演示的那樣,這兩個內建的資料儲存註解基於 Java EE 7 中已有的 @DataStoreDefinition
註解。
清單 4 演示瞭如何配置資料庫身份儲存。這些配置選項本身就進行了自我解釋,而且如果您曾經配置過資料庫定義,應該會很熟悉。
清單 4. 配置資料庫標識儲存
@DatabaseIdentityStoreDefinition(
dataSourceLookup = "${'java:global/permissions_db'}",
callerQuery = "#{'select password from caller where name = ?'}",
groupsQuery = "select group_name from caller_groups where caller_name = ?",
hashAlgorithm = PasswordHash.class,
priority = 10
)
@ApplicationScoped
@Named
public class ApplicationConfig { ... }
複製程式碼
注意,清單 4 中的優先順序要設定為 10。在發現多個標識儲存並確定相對於其他儲存的迭代順序時使用。數目越少,優先順序越高。
LDAP 的配置如清單 5 所描述的那樣,非常簡單。如果您有 LDAP 語義配置方面的經驗,您會發現這裡的選項非常熟悉。
清單 5. 配置 LDAP 標識儲存
@LdapIdentityStoreDefinition(
url = "ldap://localhost:33389/",
callerBaseDn = "ou=caller,dc=jsr375,dc=net",
groupSearchBase = "ou=group,dc=jsr375,dc=net"
)
@DeclareRoles({ "admin", "user", "demo" })
@WebServlet("/admin")
public class AdminServlet extends HttpServlet { ... }
複製程式碼
自定義 IdentityStore
設計您自己的輕量級標識儲存非常簡單。您需要實現 IdentityStore
介面,至少要實現 validate()
方法。介面上有四種方法,它們都有預設的實現方式。validate()
方法是執行標識儲存所需的最小條件。它接受 Credential
例項,然後返回 CredentialValidationResults
例項。
在清單 6 中,validate()
方式接收一個包含要驗證的登入憑據的 UsernamePasswordCredential
例項,然後返回一個 CredentialValidationResults
的例項。如果簡單的配置邏輯促使身份驗證成功,則使用使用者名稱和使用者所屬組配置該物件。如果身份驗證失敗,那麼 CredentialValidationResults
例項只包含狀態標誌 INVALID
。
清單 6. 定製化的輕量級標識儲存
@ApplicationScoped
public class LiteWeightIdentityStore implements IdentityStore {
public CredentialValidationResult validate(UsernamePasswordCredential userCredential) {
if (userCredential.compareTo("admin", "pwd1")) {
return new CredentialValidationResult("admin",
new HashSet<>(asList("admin", "user", "demo")));
}
return INVALID_RESULT;
}
}
複製程式碼
注意,實現是基於 @ApplicationScope
註解的。這是必需的,因為 IdentityStoreHandler
儲存對 CDI 容器管理的所有 IdentityStore
bean 例項的引用。@ApplicationScope
註解確保例項是 CDI 管理的 bean,該 bean 例項對整個應用程式來說,都是可用的。
要使用您自己輕量級標識儲存,您可以向自定義 HttpAuthenticationMechanism
注入 IdentityStoreHandler
,就像清單 7 演示的那樣。
清單 7. 向自定義 HttpAuthenticationMechanism 注入 LiteWeightIdentityStore
@ApplicationScoped
public class LiteAuthenticationMechanism implements HttpAuthenticationMechanism {
@Inject
private IdentityStoreHandler idStoreHandler;
@Override
public AuthenticationStatus validateRequest(HttpServletRequest req,
HttpServletResponse res,
HttpMessageContext context) {
CredentialValidationResult result = idStoreHandler.validate(
new UsernamePasswordCredential(
req.getParameter("name"), req.getParameter("password")));
if (result.getStatus() == VALID) {
return context.notifyContainerAboutLogin(result);
} else {
return context.responseUnauthorized();
}
}
}
複製程式碼
SecurityContext API
IdentityStore
和 HttpAuthenticationMechanism
將使用者的身份驗證和授權完美結合,但是自身的宣告式模型尚未成型。程式的安全性編碼使 web 應用程式能執行授權或拒絕訪問應用程式資源所需的檢查,SecurityContext
API 提供了這一功能性需求。
目前,Java EE 容器在實現安全上下文物件的方式上並不一致。例如,servlet 容器提供一個 HttpServletRequest
例項,在該例項上呼叫 getUserPrincipal()
方法來獲取表示使用者身份的 UserPrincipal
。EJB 容器提供了不同命名的 EJBContext
例項,在該例項上呼叫同名方法。同樣的,如果需要測試使用者是否屬於某個角色,則必須在 HttpServletRequest
例項上呼叫 isUserRole()
方法,然後在 EJBContext 例項上呼叫 isCallerInRole()
。
什麼是上下文安全
在 Java 企業級應用程式中,上下文安全 提供了對與當前經過身份驗證的使用者關聯的安全相關資訊的訪問。SecurityContext API 的目標是在所有 servlet 和 EJB 容器中提供對應應用程式安全上下文的訪問一致性。
新的 SecurityContext
提供了跨 Java EE 容器的一致性機制,用於獲取身份驗證和授權資訊。新的 Java EE Security 規範要求至少在 servlet 和 EJB 容器中使用 SecurityContext
。伺服器供應商也可以在使其在其他容器中可用。
SecurityContext 介面中的方法
SecurityContext
介面提供了用於程式安全性的入口點,並且是可注入型別。它有五個方法(都預設為未實現),以下是方法的列表和用途:
- Principal getCallerPrincipal(); 如果當前呼叫者未進行身份驗證,則返回 null,否則返回特定於平臺的主體,表明當前使用者的名稱已通過驗證。
- Set getPrincipalsByType(Class pType); 從通過身份驗證的呼叫者的主題中,返回給定型別的所有主體;如果未找到
pType
型別,或者當前使用者未通過身份驗證,則返回一個空集合。 - boolean isCallerInRole(String role); 確定指定角色中是否包括呼叫方;如果未授權,則返回 false。
- boolean hasAccessToWebResource(String resource, String... methods); 確定呼叫方是否可以通過所提供的方法訪問給定的 web 資源。
- AuthenticationStatus authenticate(HttpServletRequest req, HttpServletResponse res, AuthenticationParameters param);: 通知容器應該啟動或與呼叫方繼續以基於 HTTP 身份驗證的方式進行會話。因為依賴於
HttpServletRequest
和HttpServletResponse
例項,所以此方法僅在 servlet 容器中執行。
我們將簡要總結使用這些方法的其中之一來檢查使用者對 web 資源的訪問。
使用 SecutiytContext:示例
清單 8 演示瞭如何使用 hasAccessToWebResource()
方法測試呼叫方對指定 HTTP 方法的給定 web 資源的訪問。在這種情況下,將 SecurityContext
例項注入到 servlet 中,並在 doGet()
方法中使用,測試呼叫方 URI /secretServlet
的 servlet 的 GET
方法的訪問。
清單 8. 呼叫方的 web 資源訪問測試
@DeclareRoles({"admin", "user", "demo"})
@WebServlet("/hasAccessServlet")
public class HasAccessServlet extends HttpServlet {
@Inject
private SecurityContext securityContext;
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
boolean hasAccess = securityContext.hasAccessToWebResource("/secretServlet", "GET");
if (hasAccess) {
req.getRequestDispatcher("/secretServlet").forward(req, res);
} else {
req.getRequestDispatcher("/logout").forward(req, res);
}
}
}
複製程式碼
第一部分的總結
新的 Java EE Security API 成功地將現有身份驗證和授權機制與開發者期望的現代 Java EE 特性和技術的易用性相結合。
儘管這個 API 的初始目標是尋求以一致性和可移值性的方式解決安全性方面的問題,但仍需繼續改進。在未來的版本中,JSR 375 專家組打算整合用於密碼別名、角色和許可權分配以及攔截器授權的 API —— 這些是還沒有被納入規範 v1.0 中的特性。
同時,專家組也希望整合諸如密碼管理與加密等特性,這些特性對於本地雲和微服務應用程式中的常見使用至關重要。此外,2016 Java EE 社群調查還表明 OAuth2 和 OpenID 被選為 Java EE 8 中包含的第三個重要特性。雖然時間的限制將這些特性排除在 v1.0 中,但是在即將釋出的版本中,包含這些特性確實是有著不可忽視的理由和動機。
您已經對新的 Java EE Security API 的基本特性和元件有了大致的瞭解,我鼓勵您通過下面的快速測試來檢測您所學的內容。下一篇文章將深入研究 HttpAuthenticationMechanism
介面及其支援的 Servlet 4.0 的三種身份驗證機制。
測試您的理解
- 三種預設的
HttpAuthenticationMechanism
實現是什麼?@BasicFormAuthenticationMechanismDefinition
@FormAuthenticationMechanismDefinition
@LoginFormAuthenticationMechanismDefinition
@CustomFormAuthenticationMechanismDefinition
@BasicAuthenticationMechanismDefinition
- 以下哪兩個註解將觸發內建 LDAP 和 RDBMS 標識儲存?
@LdapIdentityStore
@DataBaseIdentityStore
@DataBaseIdentityStoreDefinition
@LdapIdentityStoreDefinition
@RdbmsBaseIdentityStoreDefinition
- 以下哪種說法是正確的?
IdentityStore
只用於HttpAuthenticationMechanism
的實現。IdentityStore
可用於任何內建或者定製的安全策略解決方案。IdentityStore
只能通過注入IdentityStoreHandler
的實現才可以訪問。IdentityStore
無法通過HttpAuthenticationMechanism
的實現來使用。
SecurityContext
的目標是什麼?- 提供跨 servlet 和 EJB 容器上下文安全訪問的一致性。
- 只提供針對 EJB 容器上下文安全訪問的一致性。
- 提供對所有容器上下文安全訪問的一致性。
- 提供對 Servlet 容器上下文安全訪問的一致性。
- 提供跨 EJB 容器對上下文安全訪問的一致性。
- 為什麼
HttpAuthenticationMechanism
實現必須是@ApplicationScoped
?- 為了確保它是 CDI 管理的 bean,而且可以供整個應用程式使用。
- 為了讓
HttpAuthenticationMechanism
可以在所有應用程式級別上使用。 - 為了讓每個使用者都有一個
HttpAuthenticationMechanism
例項。 JsonAdapter
.- 這不是正確的說法。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。