通过 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 变得相对简单和轻松,而没有任何更改安全性配置的迫切需求。
希望您喜欢本系列,并能将新知识应用到实践中。一定要在下面的最终测验中测试您的理解情况。
测试您的知识
- 以下哪些方法属于 SecurityContext 接口?
- getCallerPrincipal()
- isUserRole()
- getPrincipalsByType()
- isCallerInRole()
- isCallerPrincipal()
- hasAccessToWebResource() 方法对什么执行测试?
- 指定的用户能否访问给定的资源
- servlet 是否有权访问资源
- 调用方能否访问指定的资源
- 调用方能否访问远程 Web 资源
- getPrincipalsByType() 方法会返回什么?
- 来自调用方 Subject 的一组给定类型的 Principal
- 来自上下文的给定类型的 Principal
- 一个给定类型的 Principal 列表
- 如果调用方未经授权,则返回 Null
- 如果调用方未经授权,则返回一个空集合
- 以下哪些是 getCallerPrincipal() 方法的行为?
- 返回经过验证的调用方的名称
- 如果当前调用方未经过身份验证,则返回 null
- 返回调用方的一组 Principal
- 返回经过验证的调用方的特定于平台的 Principal
问题 | 答案 |
---|---|
1 | 134 |
2 | 3 |
3 | 15 |
4 | 24 |