從 Java EE 8 Security API 開始 —— 第二部分
基於 HttpAuthenticationMechanism 認證
使用 Java EE 8 新的註解驅動的 HTTP 身份驗證機制的經典和自定義 Servlet 身份驗證。
關於本系列:
期待已久的 Java EE Security API (JSR 375) 將 Java 企業級安全帶入雲端計算和微服務時代的新紀元。本系列的文章將向您展示如何簡化新的安全機制,以及 Java EE 跨容器安全的標準化處理,然後在啟用雲的專案中使用它們。
本系列的第一篇文章概述了 Java EE Security API (JSR 375),包括對新的高階介面的介紹:HttpAuthenticationMechanism
、IdentityStore
和 SecurityContext
。本文將深入理解這三部分中的第一部分,您將學習如何在 Java web 示例應用程式中使用 HttpAuthenticationMechanism
來設定並配置使用者身份驗證。
HttpAuthenticationMechanism
介面是 Java™ EE HTTP 新的身份驗證機制的核心。它擁有三個內建的 CDI(上下文和依賴注入)實現,它們會自動例項化,然後供 CDI 容器呼叫。這些內建實現支援 Servlet 4.0 指定的三種經典身份驗證方案:基本 HTTP 身份驗證、基於表單的身份驗證和自定義表單身份驗證
除了內建的身份驗證方法,您還可以使用 HttpAuthenticationMechanism
來開發自定義身份驗證。如果需要支援指定協議和身份驗證令牌,可以選擇此選項。一些 servlet 容器還可以提供自定義的 HttpAuthenticationMechanism
實現。
本文中,您將親自體驗 HttpAuthenticationMechanism
介面及其三個內建實現。我還將向您演示如何編寫自定義 HttpAuthenticationMechanism
身份驗證機制。
安裝 Soteria
我們將使用 Java EE 8 Security API 指南來實現 Soteria,通過 HttpAuthenticationMechanism
來研究可訪問的內建身份驗證機制和自定義的身份驗證機制。您可以使用兩種方法中的一種來獲取 Soteria。
1. 在您的 POM 中,顯式指定 Soteria
在您的 POM 中,使用以下 Maven 座標來指定 Soteria:
清單 1. Soteria 專案的 Maven 座標
<dependency>
<groupId>org.glassfish.soteria</groupId>
<artifactId>javax.security.enterprise</artifactId>
<version>1.0</version>
</dependency>
複製程式碼
2. 使用內建的 Java EE 8 座標
符合 Java EE 8 的伺服器將擁有自己的新的 Java EE 8 Security API 實現,或者它們依賴於 Sotoria 的實現。無法如何,你都需要 Java EE 8 的座標。
清單 2. Java EE 8 的 Maven 座標
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0</version>
<scope>provided</scope>
</dependency>
複製程式碼
內建身份驗證機制
內建的 HTTP 身份驗證機制支援 Servlet 4.0(第 13.6 章節)指定的身份驗證方式。下一章節我將向您演示如何使用註解來啟用三種身份驗證機制,以及如何在 Java web 應用程式中設定和實現每種機制。
@BasicAuthenticationMechanismDefinition
@BasicAuthenticationMechanismDefinition
註解觸發 Servlet 4.0(第 13.6.1 章節)定義的 HTTP 基本身份驗證。它有一個可選引數 realmName
,它通過 WWW-Authenticate
報頭指定傳送 realm 的名稱。清單 3 演示瞭如何為名為 user-realm
的 realm 觸發 HTTP 基本身份驗證。
清單 3. HTTP 基本身份驗證機制
@BasicAuthenticationMechanismDefinition(realmName="user-realm")
@WebServlet("/user")
@DeclareRoles({ "admin", "user", "demo" })
@ServletSecurity(@HttpConstraint(rolesAllowed = "user"))
public class UserServlet extends HttpServlet { … }
複製程式碼
@FormAuthenticationMechanismDefinition
@FormAuthenticationMechanismDefinition
註解引起 Servlet 4.0 規範定義中(第 13.6.3 章節)基於表單的身份驗證。它有一個必須配置的選項。loginToContinue
選項接受配置的 @LoginToContinue
註解,該註解允許應用程式提供 "login to continue" 的功能。您可以選擇使用合理的預設值或為此功能指定四個特性中的一個。
在清單 4 中,登入頁面 URI 被指定為 /login-servlet
。如果身份驗證失敗,流將傳遞到 /login-servlet-fail
。
清單 4. 基於表單的身份驗證機制
@FormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(
loginPage = "/login-servlet",
errorPage = "/login-servlet-fail"
)
)
@ApplicationScoped
public class ApplicationConfig { ... }
複製程式碼
要設定跳轉到登入頁面的方式,請使用 useForwardToLogin
選項。如果需要將此選項設定為“轉發”或者“重定向”,則應該顯式宣告 true
或者 false
,預設值為 true
。或者,您可以通過傳遞給選項 useForwardToLoginExpression
的 EL 表示式來設定該值。
@LoginToContinue
具有合理的預設值。登入頁面被設定為 /login
,同時錯誤頁面被設定為 /login-error
。
@CustomFormAuthenticationMechanismDefinition
@CustomFormAuthenticationMechanismDefinition
註解為自定義登入表單提供了配置選項。在清單 5 中,你可以發現網站的登入頁面被標識為 login.do
。登入頁面設定為 @CustomFormAuthenticationMechanismDefinition
註解的loginPage
引數的 loginToContinue
引數的值。注意,loginToContinue
是唯一的引數,而且是可選的。
清單 5. 自定義表單配置
@CustomFormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(
loginPage="/login.do"
)
)
@WebServlet("/admin")
@DeclareRoles({ "admin", "user", "demo" })
@ServletSecurity(@HttpConstraint(rolesAllowed = "admin"))
public class AdminServlet extends HttpServlet { ... }
複製程式碼
清單 6 演示了 login.do
的登入頁面,它是一個登入 backing bean 支援的 JSF(JavaServer Pages)頁面,如清單 7 所示。
清單 6. login.do JSF 登入頁面
<form jsf:id="form">
<p>
<strong>Username</strong>
<input jsf:id="username" type="text" jsf:value="#{loginBean.username}" />
</p>
<p>
<strong>Password</strong>
<input jsf:id="password" type="password" jsf:value="#{loginBean.password}" />
</p>
<p>
<input type="submit" value="Login" jsf:action="#{loginBean.login}" />
</p>
</form>
複製程式碼
登入 backing bean 使用 SecurityContext
例項來執行身份驗證,如清單 7 所示。如果驗證成功,將授予使用者對資源的訪問權;否則,流將傳遞給錯誤頁面。在本例中,它將使用者轉發到預設的 URI /login-error
。
清單 7. 登入 backing bean
@Named
@RequestScoped
public class LoginBean {
@Inject
private SecurityContext securityContext;
@Inject
private FacesContext facesContext;
private String username, password;
public void login() {
Credential credential = new UsernamePasswordCredential(username, new Password(password));
AuthenticationStatus status = securityContext.authenticate(
getRequestFrom(facesContext),
getResponseFrom(facesContext),
withParams().credential(credential));
if (status.equals(SEND_CONTINUE)) {
facesContext.responseComplete();
} else if (status.equals(SEND_FAILURE)) {
addError(facesContext, "Authentication failed");
}
}
// 為了簡潔而省略一些方法
}
複製程式碼
編寫一個自定義 HttpAuthenticationMechanism
在大多數場景中,您會發現這三個內建的實現已經足以滿足您的需求。在某些場景中,您可能更喜歡編寫自己的 HttpAuthenticationMechanism
介面實現。本節中,我將介紹如何編寫自定義的 HttpAuthenticationMechanism
介面。
為了確保 Java 應用程式可以使用它,您需要將 HttpAuthenticationMechanism
介面實現為具有 @ApplicationScope
的 CDI bean。介面定義了以下三種方法:
validateRequest()
身份驗證的 HTTP 請求。secureResponse()
保護 HTTP 相應訊息。cleanSubject()
清除提供的主體和憑據的主題。
HttpServletRequest
、HttpServletResponse
和 HttpMessageContext
方法都接受相同的引數型別。它們都對映在由容器提供的 JASPIC Server Auth Module 介面所定義的對應方法上。當在 Server Auth
上呼叫 JASPIC 方法時,它將委託給您自定義的 HttpAuthenticationMechanism
。
清單 8. 自定義 HttpAuthenticationMechanism 的實現
@ApplicationScoped
public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {
@Inject
private IdentityStoreHandler idStoreHandler;
@Override
public AuthenticationStatus validateRequest(HttpServletRequest req,
HttpServletResponse res,
HttpMessageContext msg) {
// use idStoreHandler to authenticate and authorize access
return msg.responseUnauthorized(); // other responses available
}
}
複製程式碼
在 HTTP 請求期間執行方法
在 HTTP 請求期間,在固定時刻呼叫 HttpAuthenticationMechanism
實現的方法。圖 1 描述了在 Filter
和 HttpServlet
例項上呼叫每個方法的時間。
圖 1. 方法呼叫順序
在執行 doFilter()
或 service()
方法之前呼叫 validateRequest()
方法,並在 HttpServletResponse
例項上呼叫 authenticate()
。此方法的目的是允許呼叫方進行身份驗證。為了進行這個操作,方法應該擁有呼叫方 HttpRequest
和 HttpResponse
例項的訪問許可權。它可以使用這些來獲取請求的身份驗證資訊,也可以為了呼叫方重定向到 OAuth 提供者而進行寫入操作。完成身份驗證之後,它可以使用 HttpMessageContext
例項來告知身份驗證的狀態。
在執行 doFilter()
或者 service()
之後呼叫 secureResponse()
方法。它在 servlet 或 過濾器生成的響應上提供後置處理功能。加密是該方法的潛在功能。
在呼叫 HttpServletRequest
例項上的 logout()
方法之後,呼叫 cleanSubject()
方法。此方法還可用於刪除登出時間後與使用者相關的狀態。
HttpMessageContext
介面有一個 HttpAuthenticationMechanism
例項可以用來與呼叫它的 ServerAuthModule
進行通訊的方法。
自定義示例:使用 cookie 進行身份驗證
正如我之前提及的那樣,您通常會編寫一個自定義實現來提供內建選項中不可用的功能。一個示例是,在身份驗證流中使用 cookie。
在類的級別中,您可以使用可選的 @RememberMe
註解來有效地“記住”使用者身份驗證,並在每個請求中自動應用它。
清單 9. 在自定義的 HttpAuthenticationMechanism 中使用 @RememberMe
@RememberMe(
cookieMaxAgeSeconds = 3600
)
@ApplicationScoped
public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism { … }
複製程式碼
這個註解有 8 個配置選項,每一個選項都有合理的預設值,因此您不必手動實現它們:
cookieMaxAgeSeconds
設定 “remember me” cookie 的生命週期。cookieMaxAgeSecondsExpression
是 cookieMaxAgeSeconds的 EL 版本。cookieSecureOnly
指定只能通過安全方法(HTTPS)訪問 cookie。cookieSecureOnlyExpression
是 cookieSecureOnly 的 EL 版本。cookieHttpOnly
表示只有 HTTP 請求才能傳送 cookie。cookieHttpOnlyExpression
是 cookieHttpOnly 的 EL 版本。cookieName
設定 cookie 的名稱、isRememberMe
"remember me" 的開關。isRememberMeExpression
是 isRememberMe 的 EL 版本。
RememberMe
功能被作為攔截器繫結而實現。容器將攔截對 validateRequest()
和 cleanSubject()
方法的呼叫。當對包含 RememberMe
cookie 實現的呼叫,呼叫 validateRequest()
方法時,它將嘗試對呼叫方進行身份驗證。如果成功,通知 HttpMessageConext
登入事件;否則 cookie 將被移出。攔截 cleanSubject()
方法只需刪除 cookie 並完成登出請求。
第二部分結論
新的 HttpAuthenticationMechanism
介面是 Java EE 8 中 web 身份驗證的核心。它內建的三種身份驗證支援 Servlet 4.0 中指定的經典身份驗證方法,而且也很容易為自定義實現進行介面擴充套件。在本教程中,您學習瞭如何使用註解來呼叫和配置 HttpAuthenticationMechanism
的內建機制,以及如何為特殊用例編寫自定義機制。我鼓勵您用下面的小測驗來測試您所學到的東西。
這篇文章深入地介紹新的 Java EE 8 Security API 的三個主要元件中的第一個。接下來的兩篇文章將介紹 IdentityStore
和 SecurityContext
API 的實踐。
測試您的掌握程度
- 三種預設的
HttpAuthenticationMechanism
實現是什麼?@BasicFormAuthenticationMechanismDefinition
@FormAuthenticationMechanismDefinition
@LoginFormAuthenticationMechanismDefinition
@CustomFormAuthenticationMechanismDefinition
@BasicAuthenticationMechanismDefinition
- 一下哪兩個註釋會引發基於表單的身份驗證?
@BasicAuthenticationMechanismDefinition
@BasicFormAuthenticationMechanismDefinition
@FormAuthenticationMechanismDefinition
@FormBasedAuthenticationMechanismDefinition
@CustomFormAuthenticationMechanismDefinition
- 下列哪兩項是基於身份驗證的有效配置?
@BasicAuthenticationMechanismDefinition(realmName="user-realm")
@BasicAuthenticationMechanismDefinition(userRealm="user-realm")
@BasicAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue)
@BasicAuthenticationMechanismDefinition
@BasicAuthenticationMechanismDefinition(realm="user-realm")
- 下列哪三項是基於表單的身份驗證的有效配置?
@FormAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue)
@FormAuthenticationMechanismDefinition
@FormBasedAuthenticationMechanismDefinition
@FormAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue(useForwardToLoginExpression = "${appConfigs.forward}"))
@FormBasedAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue)
- 在 HTTP 請求期間,按照什麼順序,在
HttpAuthenticationMechanism
、Filter
和HttpServlet
實現上呼叫方法?doFilter()
,validateRequest()
,service()
,secureResponse()
validateRequest()
,doFilter()
,secureResponse()
,service()
validateRequest()
,service()
,doFilter()
,secureResponse()
validateRequest()
,doFilter()
,service()
,secureResponse()
service()
,secureResponse()
,doFilter()
,validateRequest()
- 如何為
RememberMe
cookie 設定最長有效時間?@RememberMe(cookieMaxAge = (units = SECONDS, value = 3600)
@RememberMe(maxAgeSeconds = 3600)
@RememberMe(cookieMaxAgeSeconds = 3600)
@RememberMe(cookieMaxAgeMilliseconds = 3600000)
@RememberMe(cookieMaxAgeSeconds = "3600")
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。