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

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

面向云和微服务平台的 Java 企业级安全

新的 HttpAuthenticationMechanism、IdentityStore 和 SecurityContext 接口概述html

关于这个系列:前端

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

经验丰富的 Java™ 开发者应该了解,Java 并不会受到缺少 Java 安全机制的影响。可选的方案有 Java 容器受权协议说明 (JACC),Java 身份认证服务提供器 (JASPIC),以及大量第三方特定于容器的安全 API 和配置管理解决方案。android

问题不在于缺少选择,而在于缺少企业标准。没有标准,致使几乎没有什么能够激励供应商始终如一地实现核心特性,好比,身份验证,像上下文和依赖注入(CDI)以及表达式语言(EL)那样独有解决方案的新技术更新,或者与云和微服务架构的安全发展保持同步。ios

本系列介绍了新的 Java EE Security API,首先会概述 API 及其三个主要接口:HttpAuthenticationMechanismIdentityStoreSecurityContextgit

获取代码github

Java EE 新的安全标准

Java EE 安全规范的开发得力于 2014 Java EE 8 问卷调查,社区的反馈推进了 Java EE 安全规范的开发步伐。简化和标准化 Java 企业级安全是许多调查对象优先考虑的事项。JSR 375专家组一旦成立,将肯定如下问题:web

  • 构成 Java EE 的各类 EJB 和 servlet 容器定义了相似的与安全相关的 API,但语法存在细微差异。例如,servlet 检查用户角色时,调用 HttpServletRequest.isUserInRole(String role),而 EJB 则调用 EJBContext.isCallerInRole(String roleName)
  • 实现像 JACC 这样的现有安全机制,困难重重,而 JASPIC 也很难被正确使用。
  • 现有机制没法充分利用现代 Java EE 的编程特性,例如上下文和依赖注入(CDI)。
  • 没有可移值性方法来控制如何在后端跨容器时,进行身份验证。
  • 对于管理标识存储或者角色和权限的配置,没有标准的支持。
  • 对于部署自定义身份验证规则,没有标准支持。

这些是 JSR 375 旨在解决的主要问题。同时,该规范经过定义用于身份验证、身份存储、角色和权限以及跨容器受权的可移值性 API,促使开发者可以自行管理和控制安全性。sql

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 用于与标识存储进行交互,以便对用户进行身份验证并检索组成员身份。

正如规范所描述的,IdentityStoreHttpAuthenticationMechanism 的实现所使用,但这不是必须的, IdentityStore 能够独立存在,供任何其余身份验证机制使用。尽管如此,使用 IdentityStoreHttpAuthenticationMechanism 使应用程序可以以可移植和标准化的方式控制用于身份验证的身份存储,在大部分用例场景中,都推荐使用。

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

IdentityStoreHttpAuthenticationMechanism 将用户的身份验证和受权完美结合,可是自身的声明式模型还没有成型。程序的安全性编码使 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 身份验证的方式进行会话。由于依赖于 HttpServletRequestHttpServletResponse 实例,因此此方法仅在 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 的三种身份验证机制。

测试您的理解

  1. 三种默认的 HttpAuthenticationMechanism 实现是什么?
    1. @BasicFormAuthenticationMechanismDefinition
    2. @FormAuthenticationMechanismDefinition
    3. @LoginFormAuthenticationMechanismDefinition
    4. @CustomFormAuthenticationMechanismDefinition
    5. @BasicAuthenticationMechanismDefinition
  2. 如下哪两个注解将触发内置 LDAP 和 RDBMS 标识存储?
    1. @LdapIdentityStore
    2. @DataBaseIdentityStore
    3. @DataBaseIdentityStoreDefinition
    4. @LdapIdentityStoreDefinition
    5. @RdbmsBaseIdentityStoreDefinition
  3. 如下哪一种说法是正确的?
    1. IdentityStore 只用于 HttpAuthenticationMechanism 的实现。
    2. IdentityStore 可用于任何内置或者定制的安全策略解决方案。
    3. IdentityStore 只能经过注入 IdentityStoreHandler的实现才能够访问。
    4. IdentityStore 没法经过 HttpAuthenticationMechanism 的实现来使用。
  4. SecurityContext 的目标是什么?
    1. 提供跨 servlet 和 EJB 容器上下文安全访问的一致性。
    2. 只提供针对 EJB 容器上下文安全访问的一致性。
    3. 提供对全部容器上下文安全访问的一致性。
    4. 提供对 Servlet 容器上下文安全访问的一致性。
    5. 提供跨 EJB 容器对上下文安全访问的一致性。
  5. 为何 HttpAuthenticationMechanism 实现必须是 @ApplicationScoped
    1. 为了确保它是 CDI 管理的 bean,并且能够供整个应用程序使用。
    2. 为了让 HttpAuthenticationMechanism 能够在全部应用程序级别上使用。
    3. 为了让每一个用户都有一个 HttpAuthenticationMechanism 实例。
    4. JsonAdapter.
    5. 这不是正确的说法。

检查您的答案

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索