[譯] 從 Java EE 8 Security API 開始 —— 第二部分

Starrier發表於2018-07-03

從 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),包括對新的高階介面的介紹:HttpAuthenticationMechanismIdentityStoreSecurityContext。本文將深入理解這三部分中的第一部分,您將學習如何在 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() 清除提供的主體和憑據的主題。

HttpServletRequestHttpServletResponseHttpMessageContext 方法都接受相同的引數型別。它們都對映在由容器提供的 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 描述了在 FilterHttpServlet 例項上呼叫每個方法的時間。

圖 1. 方法呼叫順序

方法呼叫順序

在執行 doFilter()service() 方法之前呼叫 validateRequest() 方法,並在 HttpServletResponse 例項上呼叫 authenticate()。此方法的目的是允許呼叫方進行身份驗證。為了進行這個操作,方法應該擁有呼叫方 HttpRequestHttpResponse 例項的訪問許可權。它可以使用這些來獲取請求的身份驗證資訊,也可以為了呼叫方重定向到 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 的三個主要元件中的第一個。接下來的兩篇文章將介紹 IdentityStoreSecurityContext API 的實踐。

測試您的掌握程度

  1. 三種預設的 HttpAuthenticationMechanism 實現是什麼?
    1. @BasicFormAuthenticationMechanismDefinition
    2. @FormAuthenticationMechanismDefinition
    3. @LoginFormAuthenticationMechanismDefinition
    4. @CustomFormAuthenticationMechanismDefinition
    5. @BasicAuthenticationMechanismDefinition
  2. 一下哪兩個註釋會引發基於表單的身份驗證?
    1. @BasicAuthenticationMechanismDefinition
    2. @BasicFormAuthenticationMechanismDefinition
    3. @FormAuthenticationMechanismDefinition
    4. @FormBasedAuthenticationMechanismDefinition
    5. @CustomFormAuthenticationMechanismDefinition
  3. 下列哪兩項是基於身份驗證的有效配置?
    1. @BasicAuthenticationMechanismDefinition(realmName="user-realm")
    2. @BasicAuthenticationMechanismDefinition(userRealm="user-realm")
    3. @BasicAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue)
    4. @BasicAuthenticationMechanismDefinition
    5. @BasicAuthenticationMechanismDefinition(realm="user-realm")
  4. 下列哪三項是基於表單的身份驗證的有效配置?
    1. @FormAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue)
    2. @FormAuthenticationMechanismDefinition
    3. @FormBasedAuthenticationMechanismDefinition
    4. @FormAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue(useForwardToLoginExpression = "${appConfigs.forward}"))
    5. @FormBasedAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue)
  5. 在 HTTP 請求期間,按照什麼順序,在 HttpAuthenticationMechanismFilterHttpServlet 實現上呼叫方法?
    1. doFilter(), validateRequest(), service(), secureResponse()
    2. validateRequest(), doFilter(), secureResponse(), service()
    3. validateRequest(), service(), doFilter(), secureResponse()
    4. validateRequest(), doFilter(), service(), secureResponse()
    5. service(), secureResponse(), doFilter(), validateRequest()
  6. 如何為 RememberMe cookie 設定最長有效時間?
    1. @RememberMe(cookieMaxAge = (units = SECONDS, value = 3600)
    2. @RememberMe(maxAgeSeconds = 3600)
    3. @RememberMe(cookieMaxAgeSeconds = 3600)
    4. @RememberMe(cookieMaxAgeMilliseconds = 3600000)
    5. @RememberMe(cookieMaxAgeSeconds = "3600")

檢查您的答案

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章