新的 HttpAuthenticationMechanism、IdentityStore 和 SecurityContext 接口概述
关于这个系列:
期待已久的 Java EE Security API (JSR 375) 将 Java 企业级安全带入云计算和微服务的新纪元。本系列的文章将向您展示如何简化新的安全机制,以及 Java EE 跨容器安全的标准化处理,然后在启用云的项目中使用它们。
经验丰富的 Java™ 开发者应该了解,Java 并不会受到缺乏 Java 安全机制的影响。可选的方案有 Java 容器授权协议说明 (JACC), Java 身份认证服务提供器 (JASPIC),以及大量第三方特定于容器的安全 API 和配置管理解决方案。
问题不在于缺乏选择,而在于缺乏企业标准。没有标准,导致几乎没有什么可以激励供应商始终如一地实现核心特性,比如,身份验证,像上下文和依赖注入(CDI)以及表达式语言(EL)那样独有解决方案的新技术更新,或者与云和微服务架构的安全发展保持同步。
本系列介绍了新的 Java EE Security API,首先会概述 API 及其三个主要接口: HttpAuthenticationMechanism
、 IdentityStore
和 SecurityContext
。
获取代码
Java EE 安全规范的开发得力于 2014Java EE 8 问卷调查,社区的反馈推动了 Java EE 安全规范的开发步伐。简化和标准化 Java 企业级安全是许多调查对象优先考虑的事项。JSR 375专家组一旦成立,将确定以下问题:
HttpServletRequest.isUserInRole(String role)
,而 EJB 则调用 EJBContext.isCallerInRole(String roleName)
。 这些是 JSR 375 旨在解决的主要问题。同时,该规范通过定义用于身份验证、身份存储、角色和权限以及跨容器授权的可移值性 API,促使开发者能够自行管理和控制安全性。
Java EE Security API 的优点在于它提供了一种配置身份存储和身份验证机制的替代方法,但并不能取代现有的安全机制。Java EE Security API 允许开发人员以一致的和可移值的方式启用 Java EE web 应用程序的安全性 —— 无论是否具有特定于供应商的或者独有的解决方案。
Java EE Security API 1.0 版本包含了初始提交草案的一个子集,而且侧重于本地云应用程序相关的技术。这些特性是:
这些特性与所有 Java EE 安全实现的新的标准化术语结合在一起。剩余的特性(计划包含在下一个版本中)是:
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 容器指定。
Java EE 容器必须为 Servlet 4.0 规范中定义的三种身份认证机制提供 HttpAuthenticationMechanism
实现。这三种实现是:
每个实现都由相关注解的存在触发:
@BasicAuthenticationMechanismDefinition @FormAuthenticationMechanismDefinition @CustomFormAuthenticationMechanismDefinition
当遇到这些注解之一时,容器会实例化相关机制的实例,并使其立即可用。
在新规范中,不再需要像 Servlet 4.0 所要求的那样,在 web.xml
中的 <login-config>
元素之间指定身份验证机制。事实上,如果 web.xml
和基于 HttpAuthentication 机制的注解同时存在时,部署过程可能会失败 —— 至少要忽略 web.xml
配置。
让我们看看每种机制的示例是如何运行的。
@BasicAuthenticationMechanismDefinition
注解触发 Servlet 4.0 定义的基本 HTTP 身份验证。清单 1 列举了一个示例。唯一的配置参数是可选的,而且允许指定 realm。
@BasicAuthenticationMechanismDefinition(realmName="${'user-realm'}") @WebServlet("/user") @DeclareRoles({ "admin", "user", "demo" }) @ServletSecurity(@HttpConstraint(rolesAllowed = "user")) public class UserServlet extends HttpServlet { … }
服务器资源可以划分为单独的受保护控件。在这种情况下,每个用户都将拥有自己的身份验证模式和授权数据库,其中包含受同源策略控制的用户和组。这个用户和组的数据库称为 realm 。
@FormAuthenticationMechanismDefinition
注解用于基于表单的身份验证。它有一个必要的参数 loginToContinue
,用于配置 web 应用程序的登录页面、错误页面和重定向或转发特性。在清单 2 中,您可以看到登录页面是用 URL 定义的, useForwardToLoginExpression
是使用表达式语言(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 输入并处理。
标识存储是存储用户标识数据的数据库,如用户名、组成员和用于验证的凭据信息。Java EE Security API 提供了一个名为 IdentityStore
的抽象标识存储。类似于 JAAS LoginModule
接口, IdentityStore
用于与标识存储进行交互,以便对用户进行身份验证并检索组成员身份。
正如规范所描述的, IdentityStore
被 HttpAuthenticationMechanism
的实现所使用,但这不是必须的, IdentityStore
可以独立存在,供任何其他身份验证机制使用。尽管如此,使用 IdentityStore
和 HttpAuthenticationMechanism
使应用程序能够以可移植和标准化的方式控制用于身份验证的身份存储,在大部分用例场景中,都推荐使用。
IdentityStore
API 包括一个 IdentityStoreHandler
接口, HttpAuthenticationMechanism
必须委托它来验证用户凭据。之后, IdentityStoreHandler
调用 IdentityStore
实例。 Identity
存储实现不是直接使用的,而是通过专门的处理程序进行交互的。
IdentityStoreHandler
可以针对多个 IdentityStores
进行身份验证,并且以 CredentialValidationResult
实例的形式返回聚合结果。无论凭据是否有效,该对象可能只具有传递凭据的作用,或者它可以是包含下述任何信息的丰富对象:
CallerPrincipal
标识存储按顺序进行查询,这取决于每个 IdentityStore
实现的优先级。存储列表被解析了两次:首先用于身份验证,然后用于授权。
作为开发者,您可以通过实现 IdentityStore
接口来实现自己的轻量级标识存储,或者您可以使用为 LDAP 和 RDBMS 内置的 IdentityStores
的其中一种。它们是通过将配置细节传递给适当的注解来初始化的 —— @LdapIdentityStoreDefinition
或者 @DataBaseIdentityStoreDefinition
。
最简单的标识存储是 数据库存储
。它是通过 @DataBaseIdentityStoreDefinition
注解进行配置的。正如清单 4 所演示的那样,这两个内置的数据存储注解基于 Java EE 7 中已有的
@DataStoreDefinition
注解。
清单 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 语义配置方面的经验,您会发现这里的选项非常熟悉。
@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
接口,至少要实现 validate()
方法。接口上有四种方法,它们都有默认的实现方式。 validate()
方法是运行标识存储所需的最小条件。它接受 Credential
实例,然后返回 CredentialValidationResults
实例。
在清单 6 中, 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,该 bean 实例对整个应用程序来说,都是可用的。
要使用您自己轻量级标识存储,您可以向自定义 HttpAuthenticationMechanism
注入 IdentityStoreHandler
,就像清单 7 演示的那样。
@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(); } } }
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
接口提供了用于程序安全性的入口点,并且是可注入类型。它有五个方法(都默认为未实现),以下是方法的列表和用途:
pType
类型,或者当前用户未通过身份验证,则返回一个空集合。 HttpServletRequest
和 HttpServletResponse
实例,所以此方法仅在 servlet 容器中运行。 我们将简要总结使用这些方法的其中之一来检查用户对 web 资源的访问。
清单 8 演示了如何使用 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); } } }
新的 Java EE Security API 成功地将现有身份验证和授权机制与开发者期望的现代 Java EE 特性和技术的易用性相结合。
尽管这个 API 的初始目标是寻求以一致性和可移值性的方式解决安全性方面的问题,但仍需继续改进。在未来的版本中,JSR 375 专家组打算集成用于密码别名、角色和权限分配以及拦截器授权的 API —— 这些是还没有被纳入规范 v1.0 中的特性。
同时,专家组也希望集成诸如密码管理与加密等特性,这些特性对于本地云和微服务应用程序中的常见使用至关重要。此外,2016Java EE 社区调查还表明 OAuth2 和 OpenID 被选为 Java EE 8 中包含的第三个重要特性。虽然时间的限制将这些特性排除在 v1.0 中,但是在即将发布的版本中,包含这些特性确实是有着不可忽视的理由和动机。
您已经对新的 Java EE Security API 的基本特性和组件有了大致的了解,我鼓励您通过下面的快速测试来检测您所学的内容。下一篇文章将深入研究 HttpAuthenticationMechanism
接口及其支持的 Servlet 4.0 的三种身份验证机制。