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
核对您的答案。