针对云和微服务平台的 Java 企业安全性

概览

Java EE 8 Security API 入门,第 1 部分

关于本系列

期盼已久的新 Java EE Security API (JSR 375) 推动 Java 企业安全性进入了云和微服务计算时代。本系列将展示新安全机制如何简化和标准化各种 Java EE 容器实现之间的安全处理,然后帮助您开始在受云支持的项目中使用它们。

此内容是该系列 4 部分中的第 1 部分: Java EE 8 Security API 入门

经验丰富的 Java™ 开发人员都知道,Java 从不缺乏 Java 安全机制。各种安全机制选项包括 Java Authorization for Container Contracts 规范 (JACC)、Java Authentication Service Provider Interface for Containers (JASPIC),以及大量特定于第三方容器的安全 API 和配置管理解决方案。

我们面对的麻烦不是缺少选项,而是缺少一种企业标准。没有标准,就无法激励供应商一致地实现身份验证等核心特性,并针对上下文和依赖注入 (CDI) 及 Expression Language (EL) 等新技术来升级专用解决方案,或者时刻跟上云和微服务架构的安全发展趋势。

本系列将介绍新的 Java EE Security API,首先将概述该 API 及其 3 个主要接口:HttpAuthenticationMechanism、IdentityStore 和 SecurityContext。

获取代码

一个针对 Java EE 安全性的新标准

开发 Java EE 安全规范的运动源自 2014 年 Java EE 8 调查中的社区反馈。简化和标准化 Java 企业安全是许多调查对象的优先选项。JSR 375 专家小组在组建之后确定了以下问题:

  • 组成 Java EE 的各种 EJB 和 servlet 容器定义了类似的安全相关 API,但采用了稍微不同的语法。例如,一个检查用户角色的 servlet 会调用 HttpServletRequest.isUserInRole(String role),而一个 EJB 会调用 EJBContext.isCallerInRole(String roleName)。
  • 诸如 JACC 之类的现有安全机制很难实现,而且 JASPIC 可能很难正确使用。
  • 现有机制没有充分利用现代 Java EE 编程的特性,比如上下文和依赖注入 (CDI)。
  • 没有一种可在容器间移植的方式来控制如何在后端执行身份验证。
  • 对于身份存储的管理或角色和权限的配置,没有标准的支持。
  • 对于自定义身份验证规则的部署,也没有标准的支持。

这些是 JSR 375 打算解决的主要问题。同时,该规范通过定义可移植的 API 在容器之间执行身份验证、身份存储、角色和权限及授权,希望使开发人员能够自行管理和控制安全性。

Java EE Security API 的美妙之处在于,它提供了一种配置身份存储和身份验证机制的备选方法,但没有取代现有安全机制。Java EE Security API 使开发人员能够以一致、可移植的方式在 Java EE Web 应用程序中启用安全性 — 无论是否使用特定于供应商的或专用的解决方案。

Java EE Security API 中包含的特性

Java EE Security API V1.0 包含原始建议草案的一个子集,专注于与云原生应用程序相关的技术。这些特性包括:

  • 一个用于身份验证的 API
  • 一个身份存储 API
  • 一个安全上下文 API

这些特性通过新的标准化术语一起引入到所有 Java EE 安全实现中。Java EE Security 规范的下一个版本中即将包含的剩余特性包括:

  • 一个密码混淆 API
  • 一个角色/权限分配 API
  • 一个授权拦截器 API

安全的 Web 身份验证

Java EE 平台已为 Web 应用程序用户的身份验证指定了两种机制:Servlet 4.0 (JSR 369) 提供了一种声明性机制,适合一般应用程序配置。为了满足执行更可靠身份验证的需求,JASPIC 定义了一个名为 ServerAuthModule 的服务提供程序接口,该接口支持开发身份验证模块来处理任何凭证类型。此外,Servlet Container Profile 指定了 JASPIC 应如何与 servlet 容器相集成。

这两种机制都很有意义很有效,但对 Web 应用程序开发人员而言,每种机制都有其局限性。

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 规范中定义的 3 种身份验证机制提供 HttpAuthenticationMechanism 实现。这 3 种实现是:

  • 基本 HTTP 身份验证(13.6.1 小节)
  • 基于表单的身份验证(13.6.3 小节)
  • 自定义表单身份验证(13.6.3.1 小节)

每种实现都由与其相关的注解的存在而触发:

  • @BasicAuthenticationMechanismDefinition
  • @FormAuthenticationMechanismDefinition
  • @CustomFormAuthenticationMechanismDefinition

遇到其中一个注解时,容器会实例化关联机制的一个实例并立即提供该实例。

在新规范中,不再需要在 web.xml 文件中的 <login-config> 元素之间指定身份验证机制,而 Servlet 4.0 需要这么做。实际上,如果存在这些 web.xml 配置,同时还存在一个基于 HttpAuthenticationMechanism 的注解,部署流程可能会失败,或者至少会忽略这些配置。

让我们看看可以如何使用每种机制的一些示例。

基本 HTTP 身份验证

@BasicAuthenticationMechanismDefinition 注解触发 Servlet 4.0 所定义的基本 HTTP 身份验证。清单 1 给出了一个示例。唯一的配置参数是可选的,而且允许指定一个范围。

范围是什么?

一种服务器资源可划分为不同的受保护空间。在本例中,每个空间都有自己的身份验证模式和授权数据库,并包含由相同策略控制的用户和组。这个由用户和组构成的数据库就称为一个范围。

@BasicAuthenticationMechanismDefinition(realmName="${'user-realm'}")
@WebServlet("/user")
@DeclareRoles({ "admin", "user", "demo" })
@ServletSecurity(@HttpConstraint(rolesAllowed = "user"))
public class UserServlet extends HttpServlet {}

基于表单的身份验证

@FormAuthenticationMechanismDefinition 注解用于基于表单的身份验证。它有一个必要参数 loginToContinue,该参数用于配置 Web 应用程序的登录页、错误页,以及重定向或转发特征。在清单 2 中,可以看到登录页使用了一个 URI 定义,useForwardToLoginExpression 是使用一个 Expression Language (EL) 表达式来配置的。不需要向 @LoginToContinue 注解传递任何参数,因为该实现已提供了合理的默认参数。

@FormAuthenticationMechanismDefinition(
   loginToContinue = @LoginToContinue(
       loginPage="/login-servlet",
       errorPage="/error",
       useForwardToLoginExpression="${appConfig.forward}"
   )
)
@ApplicationScoped
public class ApplicationConfig { ...}

自定义表单身份验证

@CustomFormAuthenticationMechanismDefinition 注解触发内置的自定义表单身份验证。清单 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 用于与身份存储交互,以便验证用户和检索组成员关系。

正如规范中所写,IdentityStore 的意图是供 HttpAuthenticationMechanism 实现使用,但这不是必须的。IdentityStore 可以独立存在,并被其他任何身份验证机制使用。但是,通过结合使用 IdentityStore 和 HttpAuthenticationMechanism,应用程序能以一种便携的标准方式来控制其用于身份验证的身份存储,建议将此方式用于大多数用例场景。

IdentityStore API 包含一个 IdentityStoreHandler 接口,HttpAuthenticationMechanism 必须委托给该接口才能验证用户凭证。然后,IdentityStoreHandler 调用 IdentityStore 实例。Identity 存储实现不会被直接使用,而是通过专用处理函数来交互。

IdentityStoreHandler 可以针对多个 IdentityStore 来执行身份验证,并以 CredentialValidationResult 实例的形式返回一个聚合结果。这个对象可以做的事情只是传递证书是否有效,或者它可能是一个包含以下任何信息的丰富对象:

  • CallerPrincipal
  • 主体所属的组的集合
  • 调用方的名称或 LDAP 可识别的名称
  • 来自身份存储的调用方唯一标识符

按每个 IdentityStore 实现的优先级确定的顺序来查询身份存储。存储列表被解析了两次:第一次用于身份验证,然后用于授权。

作为开发人员,您可以通过实现 IdentityStore 接口来实现自己的轻量型身份存储,也可以使用用于 LDAP 和 RDBMS 的内置 IdentityStore 之一来实现。这些 IdentityStore 通过向合适的注解(@LdapIdentityStoreDefinition 或 @DataBaseIdentityStoreDefinition)传递配置细节来实现初始化。

配置内置 IdentityStore

最简单的身份存储是数据库存储。它通过 @DataBaseIdentityStoreDefinition 注解来配置,如清单 4 所示。两个内置的数据库注解基于 Java EE 7 中已提供的 @DataStoreDefinition 注解。

如下代码展示了如何配置一个数据库身份存储。这些配置选项是一目了然的,如果您配置过数据库定义,应该很熟悉它们。

@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 { ...}

请注意,以上代码中将优先级设置为 10。此设置在找到多个身份存储时使用,用于确定相对于其他存储的迭代顺序。数字越小,优先级越高。

LDAP 配置很简单,如下代码所示。如果您拥有使用 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() 方法。该接口上有 4 个方法,所有方法都具有默认的方法实现。有效的身份存储至少需要 validate() 方法。该方法接受一个 Credential 实例并返回一个 CredentialValidationResults 实例。

在如下代码中,validate() 方法接收一个包含要验证的登录凭证的 UsernamePasswordCredential 实例。然后,它返回一个 CredentialValidationResults 实例。如果简单的配置逻辑得到了成功的身份验证,则会为此对象配置用户名和用户所属的组集合。如果身份验证失败,那么 CredentialValidationResults 实例将会仅包含状态标志 INVALID。

@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 中,如下代码所示。

@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

IdentityStore 和 HttpAuthenticationMechanism 相结合,提供了非常强大的用户身份验证和授权功能,但声明性模型本身并不够。编程性安全使 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 接口为编程性安全提供了一个入口点,而且是一种可注入的类型。它有 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);:告知容器,它应该开始或继续执行与调用方的基于 HTTP 的身份验证对话。由于依赖于 HttpServletRequest 和 HttpServletResponse 实例,此方法仅适用于 servlet 容器。\

最后,我们将快速查看如何使用这些方法之一来检查用户对 Web 资源的访问。

使用 SecurityContext:一个示例

如下代码展示了如何使用 hasAccessToWebResource() 方法来测试调用方使用指定的 HTTP 方法对给定 Web 资源的访问。在本例中,SecurityContext 实例被注入到 servlet 中,并用在 doGet() 方法中,后一个方法中测试了调用方对位于 URI /secretServlet 的 servlet 的 GET 方法的访问。

@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 成功地结合使用了现有身份验证和授权机制的强大功能,以及开发人员期望从现代 Java EE 特性和技术获得的轻松开发能力。

尽管此 API 的原动力源自对以一致、可移植方式来解决安全相关问题的需求,但许多改进即将推出。在未来的版本中,JSR 375 专家小组打算集成 API 来实现密码混淆、角色和权限分类,以及授权拦截 — 规范的 v1.0 中未包含的所有特性。

专家小组还希望集成密钥管理和加密等特性,这些特性对于云原生和微服务应用程序中的常见用例至关重要。2016 年的 Java EE 社区调查还表明,在希望包含在 Java EE 8 中的特性中,OAuth2 和 OpenID 的重要性排第三。尽管由于时间限制,v1.0 中无法包含这些特性,但在即将推出的版本中,将会提供包含这些特性的充分理由和动机。

您已大致了解了新 Java EE Security API 的基本特性和组件,可以通过下面的快速测验测试一下您学到的知识。下一篇文章将深入剖析 HttpAuthenticationMechanism 接口和它的 3 种支持 Servlet 4.0 的身份验证机制。

测试您的了解情况

  1. 3 种默认的 HttpAuthenticationMechanism 实现是哪些?

    a. @BasicFormAuthenticationMechanismDefinition

    b. @FormAuthenticationMechanismDefinition

    c. @LoginFormAuthenticationMechanismDefinition

    d. @CustomFormAuthenticationMechanismDefinition

    e. @BasicAuthenticationMechanismDefinition

  2. 以下哪两个注解将会触发内置的 LDAP 和 RDBMS 身份存储?

    a. @LdapIdentityStore

    b. @DataBaseIdentityStore

    c. @DataBaseIdentityStoreDefinition

    d. @LdapIdentityStoreDefinition

    e. @RdbmsBaseIdentityStoreDefinition

  3. 以下哪句陈述是正确的?

    a. IdentityStore 只能被 HttpAuthenticationMechanism 的实现使用。

    b. IdentityStore 能被任何内置或定制的安全选项使用。

    c. IdentityStore 只能通过注入的 IdentityStoreHandler 实现进行访问。

    d. IdentityStore 不能被 HttpAuthenticationMechanism 的实现使用。

  4. SecurityContext 的目标是什么?

    a. 在 servlet 和 EJB 容器之间提供对安全上下文的一致访问。

    b. 仅向 EJB 容器提供对安全上下文的一致访问。

    c. 在所有容器之间提供对安全上下文的一致访问。

    d. 向 servlet 容器提供对安全上下文的一致访问。

    e. 在 EJB 容器之间提供对安全上下文的一致访问。

  5. 为什么 HttpAuthenticationMechanism 实现必须是 @ApplicationScoped?

    a. 为了确保它是一个 CDI 管理的 bean,而且可用于整个应用程序。

    b. 以便 HttpAuthenticationMechanism 能在所有应用程序级别上使用。

    c. 以便每个用户有一个对应的 HttpAuthenticationMechanism 实例。

    d. JsonAdapter。

    e. 这是一句错误的陈述。


针对云和微服务平台的 Java 企业安全性:测验答案

  1. 3 种默认的 HttpAuthenticationMechanism 实现是哪些?

    a. @BasicFormAuthenticationMechanismDefinition

    b. @FormAuthenticationMechanismDefinition

    c. @LoginFormAuthenticationMechanismDefinition

    d. @CustomFormAuthenticationMechanismDefinition

    e. @BasicAuthenticationMechanismDefinition

    答案:b、c、e

  2. 以下哪两个注解将会触发内置的 LDAP 和 RDBMS 身份存储?

    a. @LdapIdentityStore

    b. @DataBaseIdentityStore

    c. @DataBaseIdentityStoreDefinition

    d. @LdapIdentityStoreDefinition

    e. @RdbmsBaseIdentityStoreDefinition

    答案:c、d

  3. 以下哪句陈述是正确的?

    a. IdentityStore 只能被 HttpAuthenticationMechanism 的实现使用。

    b. IdentityStore 能被任何内置或定制的安全选项使用。

    c. IdentityStore 只能通过注入的 IdentityStoreHandler 实现进行访问。

    d. IdentityStore 不能被 HttpAuthenticationMechanism 的实现使用。

> 答案:b、c
  1. SecurityContext 的目标是什么?

    a. 在 servlet 和 EJB 容器之间提供对安全上下文的一致访问。

    b. 仅向 EJB 容器提供对安全上下文的一致访问。

    c. 在所有容器之间提供对安全上下文的一致访问。

    d. 向 servlet 容器提供对安全上下文的一致访问。

    e. 在 EJB 容器之间提供对安全上下文的一致访问。

    答案:a、c

  2. 为什么 HttpAuthenticationMechanism 实现必须是 @ApplicationScoped?

    a. 为了确保它是一个 CDI 管理的 bean,而且可用于整个应用程序。

    b. 以便 HttpAuthenticationMechanism 能在所有应用程序级别上使用。

    c. 以便每个用户有一个对应的 HttpAuthenticationMechanism 实例。

    d. JsonAdapter。

    e. 这是一句错误的陈述。

    答案:a