Java EE 8 Security API 入门,第 4 部分
跨 servlet 和 EJB 容器对用户访问进行身份验证和授权
Alex Theedom
2018 年 5 月 15 日发布
https://www.ibm.com/developerworks/cn/library/?series_title_by=**auto**
敬请期待该系列的后续内容。
敬请期待该系列的后续内容。
期盼已久的新 Java EE Security API (JSR 375) 推动 Java 企业安全性进入了云和微服务计算时代。本系列将展示新安全机制如何简化和标准化各种 Java EE 容器实现之间的安全处理,然后帮助您开始在受云支持的项目中使用它们。
本系列的上一篇文章介绍了 IdentityStore ,这是一个抽象,用于设置和配置对 Java™ Web 应用程序中的用户凭证数据的安全访问。尽管开发人员能结合使用 IdentityStore 和 HttpAuthenticationMechanism 来实现强大的内置身份验证和授权,但 HttpAuthenticationMechanism 的声明性安全模型无法满足某些安全需求。这时 SecurityContext API 就派上了用场。
在本文中,您将了解如何使用 SecurityContext 以编程方式扩展 HttpAuthenticationMechanism ,从而使您的 Web 应用程序能拒绝或允许访问应用程序资源。请注意,本文中的示例基于一个 servlet 容器。
获取代码
我们将使用 Java EE 8 Security API 参考实现 Soteria 来探索 SecurityContext 接口。您可以通过两种方式之一获取 Soteria。
使用以下 Maven 坐标在 POM 中指定 Soteria:
<dependency> <groupId>org.glassfish.soteria</groupId> <artifactId>javax.security.enterprise</artifactId> <version>1.0</version> </dependency>
符合 Java EE 8 规范的服务器将拥有自己的新 Java EE 8 Security API 实现,否则它们会依靠 Sotoria 的实现。无论如何,您都只需要 Java EE 8 坐标:
<dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>8.0</version> <scope>provided</scope> </dependency>
SecurityContext 接口位于 javax.security.enterprise 包中。
创建 SecurityContext API 是为了跨 servlet 和 EJB 容器提供一致的应用程序安全保护方法。 安全上下文 可用于访问与目前已验证用户有关的安全相关信息,这可以通过编程方式触发一个基于 Web 的身份验证流程的启动。
Servlet 和 EJB 容器实现安全上下文对象的方式类似,但存在差异。例如,要获取某个用户在 servlet 容器内的身份,将会使用一个 HttpServletRequest 实例并调用 getUserPrincipal() 方法来返回一个 UserPrincipal 对象。在 EJB 容器中,会在一个 EJBContext 实例上调用一个同名的方法。类似地,如果您想要测试某个用户是否属于某个容器角色,则会在 servlet 容器中的 HttpServletRequest 实例上调用 isUserRole() 方法。在 EJB 容器中,会在 EJBContext 实例上调用 isCallerInRole() 方法。
通过提供一种单一机制,以编程方式跨 servlet 和 EJB 容器获取身份验证和授权信息,新的 SecurityContext 解决了这些和其他差异。新的 Java EE 8 Security API 规范规定,servlet 中必须包含 SecurityContext ,而且 EJB 容器应与 Java EE 8 兼容。一些服务器供应商也可能在其他容器中提供 SecurityContext 。
SecurityContext 接口为编程性安全提供了一个入口点,而且是一种 可注入的类型 。它由以下 5 个方法组成,每个方法都没有默认实现。
getCallerPrincipal() 方法 获取表示当前已验证用户姓名的、特定于容器的主体。如果当前调用方未经身份验证,则返回 null 。返回的主体类型可能与 HttpAuthenticationMechanism 最初建立的类型不同。这个 getCallerPrincipal() 方法与 EJBContext 接口上的同名方法之间的重要区别是,它返回一个 Principal 实例,其中包含未经身份验证的用户的 null 名称。 getPrincipalsByType() 方法 从经身份验证的调用方的 Subject 返回所有指定类型的 Principal ;如果未找到该类型或当前用户未经身份验证,则返回一个空 集合 。当容器的调用方主体与应用程序的调用方主体具有不同类型时,或者应用程序只需要其调用方主体所提供的信息时,可以使用此方法。 isCallerInRole() 方法 确定调用方是否包含在以 String 形式传入的角色中。如果用户拥有该角色,则返回 true ;否则返回 false 。调用此方法所返回的结果,与执行特定于容器的调用时相同。如果 SecurityContext.isUserInRole() 返回 true,那么调用 HttpServletRequest.isUserInRole() 或 EJBContext.isCallerInRole() 将返回 true。 hasAccessToWebResource() 方法 确定调用方能否访问当前应用程序中的给定 HTTP 方法的给定 Web 资源。这是依据 Servlet 4.0 的安全约束规范在应用程序的安全约束中进行配置的。 authenticate() 方法 以编程方式触发容器开始或继续与调用方进行基于 HTTP 的身份验证对话,就好像客户端执行了调用来访问该资源一样。此方法依赖于一个有效的 servlet 上下文,因为它需要一个 HttpServletRequest 和 HttpServletResponse 实例。此方法仅在 servlet 容器中有效。 在大致了解这些方法和它们的功能后,我们将看一些代码示例。下面的所有示例都适用于 Servlet 4.0 Web 应用程序中的 SecurityContext 方法。
清单 3 将 SecurityContext 的 3 个用于测试调用方数据的方法组合到一个 servlet 中,以便演示它们的用法。在下面的示例中, SecurityContext 可用作一个 CDI bean,所以可注入到任何上下文感知的实例中。
@WebServlet("/securityContextServlet") @ServletSecurity(@HttpConstraint(rolesAllowed = "admin")) public class SecurityContextServlet extends HttpServlet { @Inject private SecurityContext securityContext; @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { // Example 1: Is the caller is one of the three roles: admin, user and demo PrintWriter pw = response.getWriter(); boolean role = securityContext.isCallerInRole("admin"); pw.write("User has role 'admin': " + role + "/n"); role = securityContext.isCallerInRole("user"); pw.write("User has role 'user': " + role + "/n"); role = securityContext.isCallerInRole("demo"); pw.write("User has role 'demo': " + role + "/n"); // Example 2: What is the caller principal name String contextName = null; if (securityContext.getCallerPrincipal() != null) { contextName = securityContext.getCallerPrincipal().getName(); } response.getWriter().write("context username: " + contextName + "/n"); // Example 3: Retrieve all CustomPrincipal Set<CustomPrincipal> customPrincipals = securityContext .getPrincipalsByType(CustomPrincipal.class); for (CustomPrincipal customPrincipal : customPrincipals) { response.getWriter().write((customPrincipal.getName())); } } }
在第一个示例中,安全上下文用于测试目前已经过身份验证的用户所参与的逻辑角色。测试的角色包括 admin、user 和 demo。
在第二个示例中,将了解如何使用 getCallerPrincipal() 方法来检索表示已经过身份验证的调用方的名称的、特定于平台的调用方主体。如果当前用户未经过身份验证,此方法将会返回 null ,所以必须执行适当的 null 检查。
最后一个示例将展示如何使用 getPrincipalsByType() 方法按类型检索一组主体。
接下来,在清单 4 中,您会看到一个实现 Principal 接口的自定义主体。对 getPrincipalsByType() 方法的调用将检索一组此类型的主体。
public class CustomPrincipal implements Principal { private final String name; public CustomPrincipal(String name) { this.name = name; } @Override public String getName() { return name; } }
清单 5 将展示如何使用 hasAccessToWebResource() 来测试调用方使用指定的 HTTP 方法对给定 Web 资源的访问。在这里,我将 SecurityContext 实例注入到了 servlet 中,并调用了 hasAccessToWebResource() 。我们想要测试调用方是否拥有位于 URI /secretServlet 上的资源的 GET 访问权(如清单 6 所示),所以我们将显示的参数传递给该方法。如果调用方拥有 admin 角色,该方法将返回 true ;否则会返回 false 。
@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"); } }
@WebServlet("/secretServlet") @ServletSecurity(@HttpConstraint(rolesAllowed = "admin")) public class SecretServlet extends HttpServlet { }
最后一个示例将展示如何使用 authenticate() 方法来验证用户输入的凭证。首先,用户将一个用户名和密码输入 JSF 中,如清单 7 所示。提交后, LoginBean 处理并验证凭证,如清单 8 所示。
<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>
输入的 username 和 password 是在 LoginBean 上设置的(清单 8),以便生成一个 Credential 实例。然后使用此凭证创建一个 AuthenticationParameters 实例。此实例与 HttpServletRequest 和 HttpServletResponse 实例一起传递给 authenticate() 方法,后两个实例是从 FacesContext 检索获得的。然后, AuthenticationParameters 实例返回 AuthenticationStatus 枚举的一个值。
AuthenticationStatus 枚举表明了身份验证流程的状态,并且可以是以下值之一:
NOT_DONE :调用了身份验证机制,但它决定不执行身份验证。通常,在采用抢先安全保护时会返回此状态。 SEND_CONTINUE :调用了身份验证机制,而且发起了与调用方的多步骤身份验证对话。 SUCCESS :调用了身份验证机制,而且已成功对调用方进行身份验证。调用方主体可用。 SEND_FAILURE :调用了身份验证机制,但调用方未成功通过身份验证,因此调用方主体不可用。 请注意,在 Java EE 8 中,JSF 2.3 已允许注入 FacesContext 。
@Named @RequestScoped @FacesConfig(version = JSF_2_3) 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"); } } private static HttpServletResponse getResponseFrom(FacesContext context) { return (HttpServletResponse) context .getExternalContext() .getResponse(); } private static HttpServletRequest getRequestFrom(FacesContext context) { return (HttpServletRequest) context .getExternalContext() .getRequest(); } // Getter and setters omitted }
在本系列中,您了解了新 Java EE 8 Security API 如何将一些最受欢迎且值得信赖的 Java EE 技术集成到常见的企业身份验证和授权例程中。除了其他特性之外,Java 开发人员社区想要一种在 servlet 和 EJB 容器间保持一致的简化的安全模型, SecurityContext 恰好提供了此模型。
尽管我的示例基于一个 servlet 容器,但 SecurityContext 使得跨 servlet 和 EJB 容器一致地询问调用方主体变得很简单。如果开发人员需要为最近的 Java EE 应用程序组合 XML 与基于注解的配置,他们会对迁移到纯注解框架感到高兴。新 Security API 也支持 XML 声明,这使得将旧项目迁移到 Java EE 8 变得相对简单和轻松,而没有任何更改安全性配置的迫切需求。
希望您喜欢本系列,并能将新知识应用到实践中。一定要在下面的最终测验中测试您的理解情况。
SecurityContext 接口?
getCallerPrincipal() isUserRole() getPrincipalsByType() isCallerInRole() isCallerPrincipal() hasAccessToWebResource() 方法对什么执行测试?
getPrincipalsByType() 方法会返回什么?
Subject 的一组给定类型的 Principal Principal Principal 列表 Null getCallerPrincipal() 方法的行为?
null Principal Principal 核对您的答案。