通过 SecurityContext 询问调用方数据

概览

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

关于本系列

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

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

本系列的上一篇文章介绍了 IdentityStore,这是一个抽象,用于设置和配置对 Java™ Web 应用程序中的用户凭证数据的安全访问。尽管开发人员能结合使用 IdentityStore 和 HttpAuthenticationMechanism 来实现强大的内置身份验证和授权,但 HttpAuthenticationMechanism 的声明性安全模型无法满足某些安全需求。这时 SecurityContext API 就派上了用场。

在本文中,您将了解如何使用 SecurityContext 以编程方式扩展 HttpAuthenticationMechanism,从而使您的 Web 应用程序能拒绝或允许访问应用程序资源。请注意,本文中的示例基于一个 servlet 容器。

获取代码

安装 Soteria

我们将使用 Java EE 8 Security API 参考实现 Soteria 来探索 IdentityStore。您可以通过两种方式之一获取 Soteria。

1.在 POM 中显式指定 Soteria

使用以下 Maven 坐标在 POM 中指定 Soteria:

<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 坐标:

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>8.0</version>
    <scope>provided</scope>
</dependency>

SecurityContext 接口位于 javax.security.enterprise 包中。

SecurityContext 的用途

创建 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 的工作原理

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 方法。

示例 1:在 servlet 中测试调用方数据

SecurityContext 的 getCallerPrincipal()、getPrincipalsByType() 和 isCallerInRole() 方法

清单 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; 
    } 
}

示例 2:测试调用方对 Web 资源的访问

SecurityContext 的 hasAccessToWebResources() 方法

清单 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 { }

示例 3:身份验证调用方的访问

SecurityContext 的 authenticate() 方法

最后一个示例将展示如何使用 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 变得相对简单和轻松,而没有任何更改安全性配置的迫切需求。

希望您喜欢本系列,并能将新知识应用到实践中。一定要在下面的最终测验中测试您的理解情况。

测试您的知识

  1. 以下哪些方法属于 SecurityContext 接口?
    1. getCallerPrincipal()
    2. isUserRole()
    3. getPrincipalsByType()
    4. isCallerInRole()
    5. isCallerPrincipal()
  2. hasAccessToWebResource() 方法对什么执行测试?
    1. 指定的用户能否访问给定的资源
    2. servlet 是否有权访问资源
    3. 调用方能否访问指定的资源
    4. 调用方能否访问远程 Web 资源
  3. getPrincipalsByType() 方法会返回什么?
    1. 来自调用方 Subject 的一组给定类型的 Principal
    2. 来自上下文的给定类型的 Principal
    3. 一个给定类型的 Principal 列表
    4. 如果调用方未经授权,则返回 Null
    5. 如果调用方未经授权,则返回一个空集合
  4. 以下哪些是 getCallerPrincipal() 方法的行为?
    1. 返回经过验证的调用方的名称
    2. 如果当前调用方未经过身份验证,则返回 null
    3. 返回调用方的一组 Principal
    4. 返回经过验证的调用方的特定于平台的 Principal
问题 答案
1 134
2 3
3 15
4 24