[译] 从 Java EE 8 Security API 开始 —— 第二部分

从 Java EE 8 Security API 开始 —— 第二部分

基于 HttpAuthenticationMechanism 认证

使用 Java EE 8 新的注解驱动的 HTTP 身份验证机制的经典和自定义 Servlet 身份验证。html

关于本系列:前端

期待已久的 Java EE Security API (JSR 375) 将 Java 企业级安全带入云计算和微服务时代的新纪元。本系列的文章将向您展现如何简化新的安全机制,以及 Java EE 跨容器安全的标准化处理,而后在启用云的项目中使用它们。java

本系列的第一篇文章概述了 Java EE Security API (JSR 375),包括对新的高级接口的介绍:HttpAuthenticationMechanismIdentityStoreSecurityContext。本文将深刻理解这三部分中的第一部分,您将学习如何在 Java web 示例应用程序中使用 HttpAuthenticationMechanism 来设置并配置用户身份验证。android

HttpAuthenticationMechanism 接口是 Java™ EE HTTP 新的身份验证机制的核心。它拥有三个内置的 CDI(上下文和依赖注入)实现,它们会自动实例化,而后供 CDI 容器调用。这些内置实现支持 Servlet 4.0 指定的三种经典身份验证方案:基本 HTTP 身份验证、基于表单的身份验证和自定义表单身份验证ios

除了内置的身份验证方法,您还可使用 HttpAuthenticationMechanism 来开发自定义身份验证。若是须要支持指定协议和身份验证令牌,能够选择此选项。一些 servlet 容器还能够提供自定义的 HttpAuthenticationMechanism 实现。git

本文中,您将亲自体验 HttpAuthenticationMechanism 接口及其三个内置实现。我还将向您演示如何编写自定义 HttpAuthenticationMechanism 身份验证机制。github

获取代码web

安装 Soteria

咱们将使用 Java EE 8 Security API 指南来实现 Soteria,经过 HttpAuthenticationMechanism 来研究可访问的内置身份验证机制和自定义的身份验证机制。您可使用两种方法中的一种来获取 Soteria。后端

1. 在您的 POM 中,显式指定 Soteria

在您的 POM 中,使用如下 Maven 坐标来指定 Soteria:api

清单 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索