|
經驗豐富的 Java? 開發(fā)人員都知道,Java 從不缺乏 Java 安全機制。各種安全機制選項包括 Java Authorization for Container Contracts 規(guī)范 (JACC)、Java Authentication Service Provider Interface for Containers
(JASPIC),以及大量特定于第三方容器的安全 API 和配置管理解決方案。 我們面對的麻煩不是缺少選項,而是缺少一種企業(yè)標準。沒有標準,就無法激勵供應商一致地實現身份驗證等核心特性,并針對上下文和依賴注入 (CDI) 及
Expression Language (EL) 等新技術來升級專用解決方案,或者時刻跟上云和微服務架構的安全發(fā)展趨勢。 本系列將介紹新的 Java EE Security API,首先將概述該 API 及其 3
個主要接口:HttpAuthenticationMechanism、IdentityStore
和 SecurityContext。 獲取代碼 一個針對 Java EE 安全性的新標準開發(fā) Java EE 安全規(guī)范的運動源自 2014 年 Java EE 8 調查中的社區(qū)反饋。簡化和標準化 Java 企業(yè)安全是許多調查對象的優(yōu)先選項。JSR 375
專家小組在組建之后確定了以下問題: - 組成 Java EE 的各種 EJB 和 servlet 容器定義了類似的安全相關 API,但采用了稍微不同的語法。例如,一個檢查用戶角色的
servlet 會調用
HttpServletRequest.isUserInRole(String role),而一個 EJB 會調用
EJBContext.isCallerInRole(String roleName)。 - 諸如 JACC 之類的現有安全機制很難實現,而且 JASPIC 可能很難正確使用。
- 現有機制沒有充分利用現代 Java EE 編程的特性,比如上下文和依賴注入 (CDI)。
- 沒有一種可在容器間移植的方式來控制如何在后端執(zhí)行身份驗證。
- 對于身份存儲的管理或角色和權限的配置,沒有標準的支持。
- 對于自定義身份驗證規(guī)則的部署,也沒有標準的支持。
這些是 JSR 375 打算解決的主要問題。同時,該規(guī)范通過定義可移植的 API
在容器之間執(zhí)行身份驗證、身份存儲、角色和權限及授權,希望使開發(fā)人員能夠自行管理和控制安全性。 Java EE Security API 的美妙之處在于,它提供了一種配置身份存儲和身份驗證機制的備選方法,但沒有取代現有安全機制。Java EE
Security API 使開發(fā)人員能夠以一致、可移植的方式在 Java EE Web 應用程序中啟用安全性 —
無論是否使用特定于供應商的或專用的解決方案。 Java EE Security API 中包含的特性Java EE Security API V1.0 包含原始建議草案的一個子集,專注于與云原生應用程序相關的技術。這些特性包括: - 一個用于身份驗證的 API
- 一個身份存儲 API
- 一個安全上下文 API
這些特性通過新的標準化術語一起引入到所有 Java EE 安全實現中。Java EE Security
規(guī)范的下一個版本中即將包含的剩余特性包括: - 一個密碼混淆 API
- 一個角色/權限分配 API
- 一個授權攔截器 API
安全的 Web 身份驗證Java EE 平臺已為 Web 應用程序用戶的身份驗證指定了兩種機制:Servlet 4.0 (JSR 369)
提供了一種聲明性機制,適合一般應用程序配置。為了滿足執(zhí)行更可靠身份驗證的需求,JASPIC 定義了一個名為 ServerAuthModule
的服務提供程序接口,該接口支持開發(fā)身份驗證模塊來處理任何憑證類型。此外,Servlet Container Profile 指定了 JASPIC 應如何與 servlet 容器相集成。 這兩種機制都很有意義很有效,但對 Web 應用程序開發(fā)人員而言,每種機制都有其局限性。 servlet 容器機制被限定為僅支持 Servlet 4.0
定義的小范圍的憑證類型,而且它無法支持與調用方的復雜交互。它也無法讓應用程序確定調用方是否已針對預期身份存儲進行了身份驗證。 相反,JASPIC 非常強大,可塑性很強,但使用起來也非常復雜。對 AuthModule 進行編碼并針對 Web
容器來調整它,以便將它用于身份驗證,這可能很復雜。除此之外,JASPIC 沒有聲明性配置,沒有明確的方法來覆蓋通過編程方式注冊的
AuthModule。 Java EE Security API 通過新接口 HttpAuthenticationMechanism
解決了這些問題中的一部分。這個新接口實際上是 JASPIC ServerAuthModule 接口的一個簡化的
servlet 容器變體,它在減少現有機制的局限性的同時充分利用了現有機制。 HttpAuthenticationMechanism 實例是一個 CDI bean,由容器負責使其可用于注入。應用程序或
servlet 容器可以提供 HttpAuthenticationMechanism
接口的更多實現。請注意,HttpAuthenticationMechanism 僅指定用于 servlet 容器。
對 Servlet 4.0 身份驗證的支持一個 Java EE 容器必須為 Servlet 4.0 規(guī)范中定義的 3 種身份驗證機制提供
HttpAuthenticationMechanism 實現。這 3 種實現是: - 基本 HTTP 身份驗證(13.6.1 小節(jié))
- 基于表單的身份驗證(13.6.3 小節(jié))
- 自定義表單身份驗證(13.6.3.1 小節(jié))
每種實現都由與其相關的注解的存在而觸發(fā): @BasicAuthenticationMechanismDefinition@FormAuthenticationMechanismDefinition@CustomFormAuthenticationMechanismDefinition
遇到其中一個注解時,容器會實例化關聯機制的一個實例并立即提供該實例。 在新規(guī)范中,不再需要在 web.xml 文件中的 <login-config>
元素之間指定身份驗證機制,而 Servlet 4.0 需要這么做。實際上,如果存在這些 web.xml
配置,同時還存在一個基于 HttpAuthenticationMechanism 的注解,部署流程可能會失敗,或者至少會忽略這些配置。 讓我們看看可以如何使用每種機制的一些示例。 基本 HTTP 身份驗證@BasicAuthenticationMechanismDefinition 注解觸發(fā) Servlet 4.0
所定義的基本 HTTP 身份驗證。清單 1 給出了一個示例。唯一的配置參數是可選的,而且允許指定一個范圍。
清單 1. 基本 HTTP
身份驗證1 2 3 4 5 | @BasicAuthenticationMechanismDefinition(realmName="${'user-realm'}")
@WebServlet("/user")
@DeclareRoles({ "admin", "user", "demo" })
@ServletSecurity(@HttpConstraint(rolesAllowed = "user"))
public class UserServlet extends HttpServlet { … }
|
范圍是什么? 一種服務器資源可劃分為不同的受保護空間。在本例中,每個空間都有自己的身份驗證模式和授權數據庫,并包含由相同策略控制的用戶和組。這個由用戶和組構成的數據庫就稱為一個范圍。 基于表單的身份驗證@FormAuthenticationMechanismDefinition 注解用于基于表單的身份驗證。它有一個必要參數
loginToContinue,該參數用于配置 Web 應用程序的登錄頁、錯誤頁,以及重定向或轉發(fā)特征。在清單 2
中,可以看到登錄頁使用了一個 URI 定義,useForwardToLoginExpression 是使用一個
Expression Language (EL) 表達式來配置的。不需要向 @LoginToContinue
注解傳遞任何參數,因為該實現已提供了合理的默認參數。
清單 2. 基于表單的身份驗證1 2 3 4 5 6 7 8 9 | @FormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(
loginPage="/login-servlet",
errorPage="/error",
useForwardToLoginExpression="${appConfig.forward}"
)
)
@ApplicationScoped
public class ApplicationConfig { ...}
|
自定義表單身份驗證@CustomFormAuthenticationMechanismDefinition
注解觸發(fā)內置的自定義表單身份驗證。清單 3 給出了一個示例。
清單 3. 自定義表單身份驗證1 2 3 4 5 6 7 8 9 | @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
用于與身份存儲交互,以便驗證用戶和檢索組成員關系。 正如規(guī)范中所寫,IdentityStore 的意圖是供
HttpAuthenticationMechanism
實現使用,但這不是必須的。IdentityStore 可以獨立存在,并被其他任何身份驗證機制使用。但是,通過結合使用
IdentityStore 和
HttpAuthenticationMechanism,應用程序能以一種便攜的標準方式來控制其用于身份驗證的身份存儲,建議將此方式用于大多數用例場景。 IdentityStore API 包含一個 IdentityStoreHandler
接口,HttpAuthenticationMechanism
必須委托給該接口才能驗證用戶憑證。然后,IdentityStoreHandler 調用
IdentityStore 實例。Identity
存儲實現不會被直接使用,而是通過專用處理函數來交互。
IdentityStoreHandler 可以針對多個 IdentityStore
來執(zhí)行身份驗證,并以 CredentialValidationResult
實例的形式返回一個聚合結果。這個對象可以做的事情只是傳遞證書是否有效,或者它可能是一個包含以下任何信息的豐富對象:
按每個 IdentityStore
實現的優(yōu)先級確定的順序來查詢身份存儲。存儲列表被解析了兩次:第一次用于身份驗證,然后用于授權。 作為開發(fā)人員,您可以通過實現 IdentityStore 接口來實現自己的輕量型身份存儲,也可以使用用于 LDAP 和
RDBMS 的內置 IdentityStore 之一來實現。這些 IdentityStore
通過向合適的注解(@LdapIdentityStoreDefinition 或
@DataBaseIdentityStoreDefinition)傳遞配置細節(jié)來實現初始化。 配置內置 IdentityStore最簡單的身份存儲是數據庫存儲。它通過 @DataBaseIdentityStoreDefinition
注解來配置,如清單 4 所示。兩個內置的數據庫注解基于 Java EE 7 中已提供的
@DataStoreDefinition
注解。 清單 4 展示了如何配置一個數據庫身份存儲。這些配置選項是一目了然的,如果您配置過數據庫定義,應該很熟悉它們。 清單 4. 配置一個數據庫身份存儲1 2 3 4 5 6 7 8 9 10 | @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 中將優(yōu)先級設置為 10。此設置在找到多個身份存儲時使用,用于確定相對于其他存儲的迭代順序。數字越小,優(yōu)先級越高。 LDAP 配置很簡單,如清單 5 所示。如果您擁有使用 LDAP 配置語義的經驗,將會發(fā)現這里的選項很熟悉。 清單 5. 配置一個 LDAP
身份存儲1 2 3 4 5 6 7 8 | @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() 方法。該接口上有 4 個方法,所有方法都具有默認的方法實現。有效的身份存儲至少需要
validate() 方法。該方法接受一個 Credential 實例并返回一個
CredentialValidationResults 實例。 在清單 6 中,validate() 方法接收一個包含要驗證的登錄憑證的
UsernamePasswordCredential 實例。然后,它返回一個
CredentialValidationResults
實例。如果簡單的配置邏輯得到了成功的身份驗證,則會為此對象配置用戶名和用戶所屬的組集合。如果身份驗證失敗,那么
CredentialValidationResults 實例將會僅包含狀態(tài)標志
INVALID。 清單 6. 一個自定義的輕量型身份存儲1 2 3 4 5 6 7 8 9 10 | @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,可用于整個應用程序。 要使用您的輕量型身份存儲,可以將 IdentityStoreHandler 注入到一個自定義
HttpAuthenticationMechanism 中,如清單 7 所示。 清單 7. 將 LiteWeightIdentityStore 注入到一個自定義
HttpAuthenticationMechanism
中1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @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 APIIdentityStore 和 HttpAuthenticationMechanism
相結合,提供了非常強大的用戶身份驗證和授權功能,但聲明性模型本身并不夠。編程性安全使 Web
應用程序能執(zhí)行必要的檢查,以授權或拒絕對應用程序資源的訪問,SecurityContext API
提供了這一功能。
目前,Java EE 容器采用了不一致的方式來實現安全上下文對象。例如,servlet 容器提供一個
HttpServletRequest 實例,可以在該實例上調用
getUserPrincipal() 方法來獲取表示用戶身份的
UserPrincipal。然后,EJB
容器提供一個具有不同名稱的 EJBContext
實例,在該實例上調用同名的方法。類似地,如果您想測試用戶是否屬于某個角色,必須在 HttpServletRequest
實例上調用 isUserRole() 方法,然后在 EJBContext 實例上調用
isCallerInRole()。 安全上下文是什么? 在 Java
企業(yè)應用程序中,安全上下文提供與當前驗證的用戶有關聯的安全相關信息的訪問能力。SecurityContext API
的目的是在所有 servlet 和 EJB 容器中實現對應用程序的安全上下文的一致訪問。 新 SecurityContext 在所有 Java EE 容器中提供了一種獲取身份驗證和授權信息的一致機制。新 Java
EE Security 規(guī)范要求至少在 servlet 和 EJB 容器中提供
SecurityContext。服務器供應商也可以在其他容器中提供它。 SecurityContext 接口的方法SecurityContext 接口為編程性安全提供了一個入口點,而且是一種可注入的類型。它有 5
個方法,所有方法都沒有默認實現。下面列出了這些方法和它們的用途:
- Principal getCallerPrincipal();
返回表示當前驗證的用戶名的特定于平臺的主體,或者,如果當前調用方未經驗證,則返回 null。
- <T extends Principal> Set<T>
getPrincipalsByType(Class<T> pType);
返回來自經過驗證的調用方的主題中所有給定類型的主體;如果既未找到
pType
類型,當前用戶也未經驗證,則返回一個空集合。 - boolean isCallerInRole(String role);
確定調用方是否包含在指定的角色中;如果用戶未經授權,則返回 false。
- boolean hasAccessToWebResource(String resource, String...
methods); 確定調用方是否有權通過所提供的方法訪問給定 Web 資源。
- AuthenticationStatus authenticate(HttpServletRequest req,
HttpServletResponse res, AuthenticationParameters
param);:告知容器,它應該開始或繼續(xù)執(zhí)行與調用方的基于 HTTP 的身份驗證對話。由于依賴于
HttpServletRequest 和 HttpServletResponse
實例,此方法僅適用于 servlet 容器。
最后,我們將快速查看如何使用這些方法之一來檢查用戶對 Web 資源的訪問。 使用 SecurityContext:一個示例清單 8 展示了如何使用 hasAccessToWebResource() 方法來測試調用方使用指定的 HTTP 方法對給定
Web 資源的訪問。在本例中,SecurityContext 實例被注入到 servlet 中,并用在
doGet() 方法中,后一個方法中測試了調用方對位于 URI /secretServlet 的
servlet 的 GET 方法的訪問。 清單 8. 測試調用方對 Web
資源的訪問1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @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);
}
}
}
|
第 1 部分小結新 Java EE Security API 成功地結合使用了現有身份驗證和授權機制的強大功能,以及開發(fā)人員期望從現代 Java EE
特性和技術獲得的輕松開發(fā)能力。 盡管此 API 的原動力源自對以一致、可移植方式來解決安全相關問題的需求,但許多改進即將推出。在未來的版本中,JSR 375 專家小組打算集成 API
來實現密碼混淆、角色和權限分類,以及授權攔截 — 規(guī)范的 v1.0 中未包含的所有特性。 專家小組還希望集成密鑰管理和加密等特性,這些特性對于云原生和微服務應用程序中的常見用例至關重要。2016 年的 Java EE 社區(qū)調查還表明,在希望包含在 Java EE 8 中的特性中,OAuth2 和 OpenID
的重要性排第三。盡管由于時間限制,v1.0 中無法包含這些特性,但在即將推出的版本中,將會提供包含這些特性的充分理由和動機。 您已大致了解了新 Java EE Security API 的基本特性和組件,可以通過下面的快速測驗測試一下您學到的知識。下一篇文章將深入剖析
HttpAuthenticationMechanism 接口和它的 3 種支持 Servlet 4.0
的身份驗證機制。 測試您的了解情況- 3 種默認的
HttpAuthenticationMechanism 實現是哪些? @BasicFormAuthenticationMechanismDefinition@FormAuthenticationMechanismDefinition@LoginFormAuthenticationMechanismDefinition@CustomFormAuthenticationMechanismDefinition@BasicAuthenticationMechanismDefinition
- 以下哪兩個注解將會觸發(fā)內置的 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。
|