Jakarta EE 8 Security API – 雅加达EE 8安全API

最后修改: 2018年 6月 12日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

The Jakarta EE 8 Security API is the new standard and a portable way of handling security concerns in Java containers.

Jakarta EE 8安全API是新的标准,是处理Java容器中安全问题的可移植方式。

In this article, we’ll look at the three core features of the API:

在这篇文章中,我们将看一下API的三个核心功能。

  1. HTTP Authentication Mechanism
  2. Identity Store
  3. Security Context

We’ll first understand how to configure the provided implementations and then how to implement a custom one.

我们将首先了解如何配置所提供的实现,然后了解如何实现一个自定义的实现。

2. Maven Dependencies

2.Maven的依赖性

To set up the Jakarta EE 8 Security API, we need either a server-provided implementation or an explicit one.

为了设置Jakarta EE 8 Security API,我们需要一个服务器提供的实现或一个显式的实现。

2.1. Using the Server Implementation

2.1.使用服务器实现

Jakarta EE 8 compliant servers already provide an implementation for the Jakarta EE 8 Security API, and therefore we need only the Jakarta EE Web Profile API Maven artifact:

兼容Jakarta EE 8的服务器已经为Jakarta EE 8安全API提供了实现,因此我们只需要Jakarta EE Web Profile API Maven工件。

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

2.2. Using an Explicit Implementation

2.2.使用一个明确的实现

First, we specify the Maven artifact for the Jakarta EE 8 Security API:

首先,我们为Jakarta EE 8 Security API指定Maven工件。

<dependencies>
    <dependency>
        <groupId>javax.security.enterprise</groupId>
        <artifactId>javax.security.enterprise-api</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

And then, we’ll add an implementation, for example, Soteria – the reference implementation:

然后,我们将添加一个实现,例如,Soteria – 参考实现。

<dependencies>
    <dependency>
        <groupId>org.glassfish.soteria</groupId>
        <artifactId>javax.security.enterprise</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

3. HTTP Authentication Mechanism

3.HTTP认证机制

Prior to Jakarta EE 8, we’ve configured Authentication mechanisms declaratively through the web.xml file.

在Jakarta EE 8之前,我们已经通过web.xml文件声明性地配置了认证机制。

In this version, the Jakarta EE 8 Security API has designed the new HttpAuthenticationMechanism interface as a replacement. Therefore, web applications can now configure Authentication mechanisms by providing implementations of this interface.

在这个版本中,Jakarta EE 8 Security API设计了新的HttpAuthenticationMechanism接口作为替代。因此,Web应用程序现在可以通过提供这个接口的实现来配置认证机制。

Fortunately, the container already provides an implementation for each of the three authentication methods defined by the Servlet specification: Basic HTTP authentication, form-based authentication, and custom form-based authentication.

幸运的是,容器已经为Servlet规范所定义的三种认证方法中的每一种提供了一个实现。基本 HTTP 认证、基于表单的认证和基于表单的自定义认证。

It also provides an annotation to trigger each implementation:

它还提供了一个注解来触发每个实现。

  1. @BasicAuthenticationMechanismDefinition
  2. @FormAuthenticationMechanismDefinition
  3. @CustomFormAuthenrticationMechanismDefinition

3.1. Basic HTTP Authentication

3.1.基本HTTP认证

As mentioned above, a web application can configure the Basic HTTP Authentication just by using the @BasicAuthenticationMechanismDefinition annotation on a CDI bean:

如上所述,Web应用程序只需在CDI Bean上使用@BasicAuthenticationMechanismDefinition注解即可配置基本HTTP认证

@BasicAuthenticationMechanismDefinition(
  realmName = "userRealm")
@ApplicationScoped
public class AppConfig{}

At this point, the Servlet container searches and instantiates the provided implementation of HttpAuthenticationMechanism interface.

在这一点上,Servlet容器搜索并实例化所提供的HttpAuthenticationMechanism接口的实现。

Upon receipt of an unauthorized request, the container challenges the client for providing suitable authentication information via the WWW-Authenticate response header.

在收到未经授权的请求时,容器通过WWW-Authenticate响应头挑战客户提供适当的认证信息。

WWW-Authenticate: Basic realm="userRealm"

The client then sends the username and password, separated by a colon “:” and encoded in Base64, via the Authorization request header:

然后,客户端通过Authorization请求头发送用户名和密码,用冒号”: “分开,并以Base64编码。

//user=baeldung, password=baeldung
Authorization: Basic YmFlbGR1bmc6YmFlbGR1bmc=

Note that the dialog presented for providing credentials is coming from the browser and not from the server.

请注意,提供证书的对话框是来自浏览器,而不是来自服务器。

3.2. Form-based HTTP Authentication

3.2.基于表单的HTTP认证

The @FormAuthenticationMechanismDefinition annotation triggers a form-based authentication as defined by the Servlet specification.

@FormAuthenticationMechanismDefinition注解触发了Servlet规范所定义的基于表单的认证

Then we have the option to specify the login and error pages or use the default reasonable ones /login and /login-error:

然后我们可以选择指定登录和错误页面,或使用默认的合理页面/login/login-error

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(
    loginPage = "/login.html",
    errorPage = "/login-error.html"))
@ApplicationScoped
public class AppConfig{}

As a result of invoking loginPage, the server should send the form to the client:

作为调用loginPage的结果,服务器应该将表单发送到客户端。

<form action="j_security_check" method="post">
    <input name="j_username" type="text"/>
    <input name="j_password" type="password"/>
    <input type="submit">
</form>

The client then should send the form to a pre-defined backing authentication process provided by the container.

然后,客户端应该将表单发送到容器提供的预定义的后援认证程序。

3.3. Custom Form-based HTTP Authentication

3.3.基于表单的自定义HTTP认证

A web application can trigger the custom form-based authentication implementation by using the annotation @CustomFormAuthenticationMechanismDefinition:

Web应用程序可以通过使用注解@CustomFormAuthenticationMechanismDefinition:来触发基于表单的自定义认证实现。

@CustomFormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.xhtml"))
@ApplicationScoped
public class AppConfig {
}

But unlike the default form-based authentication, we’re configuring a custom login page and invoking the SecurityContext.authenticate() method as a backing authentication process.

但与默认的基于表单的认证不同,我们要配置一个自定义的登录页面,并调用SecurityContext.authenticate()方法作为后援认证过程。

Let’s have a look at the backing LoginBean as well, which contains the login logic:

让我们也来看看支持的LoginBean,它包含登录逻辑。

@Named
@RequestScoped
public class LoginBean {

    @Inject
    private SecurityContext securityContext;

    @NotNull private String username;

    @NotNull private String password;

    public void login() {
        Credential credential = new UsernamePasswordCredential(
          username, new Password(password));
        AuthenticationStatus status = securityContext
          .authenticate(
            getHttpRequestFromFacesContext(),
            getHttpResponseFromFacesContext(),
            withParams().credential(credential));
        // ...
    }
     
    // ...
}

As a result of invoking the custom login.xhtml page, the client submits the received form to the LoginBean’s login() method:

作为调用自定义login.xhtml页面的结果,客户端将收到的表单提交给LoginBeanslogin()方法。

//...
<input type="submit" value="Login" jsf:action="#{loginBean.login}"/>

3.4. Custom Authentication Mechanism

3.4.自定义认证机制

The HttpAuthenticationMechanism interface defines three methods. The most important is the validateRequest() which we must provide an implementation.

HttpAuthenticationMechanism接口定义了三个方法。最重要的是validateRequest(),我们必须提供一个实现。

The default behavior for the other two methods, secureResponse() and cleanSubject(), is sufficient in most cases.

其他两个方法的默认行为,secureResponse()cleanSubject()在大多数情况下是足够的。

Let’s have a look at an example implementation:

让我们来看看一个实现的例子。

@ApplicationScoped
public class CustomAuthentication 
  implements HttpAuthenticationMechanism {

    @Override
    public AuthenticationStatus validateRequest(
      HttpServletRequest request,
      HttpServletResponse response, 
      HttpMessageContext httpMsgContext) 
      throws AuthenticationException {
 
        String username = request.getParameter("username");
        String password = response.getParameter("password");
        // mocking UserDetail, but in real life, we can obtain it from a database
        UserDetail userDetail = findByUserNameAndPassword(username, password);
        if (userDetail != null) {
            return httpMsgContext.notifyContainerAboutLogin(
              new CustomPrincipal(userDetail),
              new HashSet<>(userDetail.getRoles()));
        }
        return httpMsgContext.responseUnauthorized();
    }
    //...
}

Here, the implementation provides the business logic of the validation process, but in practice, it’s recommended to delegate to the IdentityStore through the IdentityStoreHandler by invoking validate.

在这里,实现提供了验证过程的业务逻辑,但是在实践中,建议通过IdentityStoreHandler by调用validate来委托给IdentityStore

We’ve also annotated the implementation with @ApplicationScoped annotation as we need to make it CDI-enabled.

我们还用@ApplicationScoped注解对该实现进行了注解,因为我们需要使其具有CDI功能。

After a valid verification of the credential, and an eventual retrieving of user roles, the implementation should notify the container then:

在对凭证进行有效的验证并最终检索到用户角色之后,实现应该通知容器

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

3.5. Enforcing Servlet Security

3.5.强制执行Servlet安全

A web application can enforce security constraints by using the @ServletSecurity annotation on a Servlet implementation:

Web应用程序可以通过在Servlet实现上使用@ServletSecurity注解来执行安全约束

@WebServlet("/secured")
@ServletSecurity(
  value = @HttpConstraint(rolesAllowed = {"admin_role"}),
  httpMethodConstraints = {
    @HttpMethodConstraint(
      value = "GET", 
      rolesAllowed = {"user_role"}),
    @HttpMethodConstraint(     
      value = "POST", 
      rolesAllowed = {"admin_role"})
  })
public class SecuredServlet extends HttpServlet {
}

This annotation has two attributes – httpMethodConstraints and valuehttpMethodConstraints is used to specify one or more constraints, each one representing an access control to an HTTP method by a list of allowed roles.

这个注解有两个属性–httpMethodConstraintsvaluehttpMethodConstraints被用来指定一个或多个约束,每个约束都代表了通过允许的角色列表对HTTP方法的访问控制。

The container will then check, for every url-pattern and HTTP method, if the connected user has the suitable role for accessing the resource.

然后,容器将为每个url-pattern和HTTP方法检查所连接的用户是否具有访问该资源的合适角色。

4. Identity Store

4.身份存储

This feature is abstracted by the IdentityStore interface, and it’s used to validate credentials and eventually retrieve group membership. In other words, it can provide capabilities for authentication, authorization or both.

该功能由IdentityStore接口抽象出来,它被用来验证凭证并最终检索组成员。换句话说,它可以为认证、授权或两者提供功能

IdentityStore is intended and encouraged to be used by the HttpAuthenticationMecanism through a called IdentityStoreHandler interface. A default implementation of the IdentityStoreHandler is provided by the Servlet container.

IdentityStore旨在并鼓励HttpAuthenticationMecanism通过调用IdentityStoreHandler接口来使用。Servlet容器提供了IdentityStoreHandler的一个默认实现。

An application can provide its implementation of the IdentityStore or uses one of the two built-in implementations provided by the container for Database and LDAP.

应用程序可以提供其对IdentityStore的实现,或者使用容器为数据库和LDAP提供的两种内置实现之一。

4.1. Built-in Identity Stores

4.1.内置身份存储

The Jakarta EE compliant server should provide implementations for the two Identity Stores: Database and LDAP.

兼容Jakarta EE的服务器应该为两种身份存储提供实现。数据库和LDAP

The database IdentityStore implementation is initialized by passing a configuration data to the @DataBaseIdentityStoreDefinition annotation:

数据库IdentityStore的实现是通过向@DataBaseIdentityStoreDefinition注释传递配置数据而初始化的:

@DatabaseIdentityStoreDefinition(
  dataSourceLookup = "java:comp/env/jdbc/securityDS",
  callerQuery = "select password from users where username = ?",
  groupsQuery = "select GROUPNAME from groups where username = ?",
  priority=30)
@ApplicationScoped
public class AppConfig {
}

As a configuration data, we need a JNDI data source to an external database, two JDBC statements for checking caller and his groups and finally a priority parameter which is used in case of multiples store are configured.

作为配置数据,我们需要一个指向外部数据库的JNDI数据源,两个JDBC语句用于检查调用者和他的组,最后是一个优先级参数,用于配置多重存储的情况。

IdentityStore with high priority is processed later by the IdentityStoreHandler.

具有高优先级的IdentityStore将由IdentityStoreHandler.随后处理。

Like the database, LDAP IdentityStore implementation is initialized through the @LdapIdentityStoreDefinition by passing configuration data:

与数据库一样,LDAP IdentityStore的实现是通过@LdapIdentityStoreDefinition传递配置数据而初始化的。

@LdapIdentityStoreDefinition(
  url = "ldap://localhost:10389",
  callerBaseDn = "ou=caller,dc=baeldung,dc=com",
  groupSearchBase = "ou=group,dc=baeldung,dc=com",
  groupSearchFilter = "(&(member=%s)(objectClass=groupOfNames))")
@ApplicationScoped
public class AppConfig {
}

Here we need the URL of an external LDAP server, how to search the caller in the LDAP directory, and how to retrieve his groups.

这里我们需要一个外部LDAP服务器的URL,如何在LDAP目录中搜索呼叫者,以及如何检索他的组。

4.2. Implementing a Custom IdentityStore

4.2.实现一个自定义的身份存储

The IdentityStore interface defines four default methods:

IdentityStore接口定义了四个默认方法:

default CredentialValidationResult validate(
  Credential credential)
default Set<String> getCallerGroups(
  CredentialValidationResult validationResult)
default int priority()
default Set<ValidationType> validationTypes()

The priority() method returns a value for the order of iteration this implementation is processed by IdentityStoreHandler. An IdentityStore with lower priority is treated first.

priority()方法返回该实现被IdentityStoreHandler处理的迭代顺序的值。优先级较低的IdentityStore被首先处理。

By default, an IdentityStore processes both credentials validation (ValidationType.VALIDATE) and group retrieval(ValidationType.PROVIDE_GROUPS). We can override this behavior so that it can provide only one capability.

默认情况下,IdentityStore同时处理凭证验证(ValidationType.VALIDATE)和组检索(ValidationType.PROVIDE_GROUPS)。我们可以覆盖这一行为,使其只提供一种能力。

Thus, we can configure the IdentityStore to be used only for credentials validation:

因此,我们可以将IdentityStore配置为只用于凭证验证。

@Override
public Set<ValidationType> validationTypes() {
    return EnumSet.of(ValidationType.VALIDATE);
}

In this case, we should provide an implementation for the validate() method:

在这种情况下,我们应该为validate()方法提供一个实现。

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map<String, UserDetails> users = new HashMap<>();

    @Override
    public int priority() {
        return 70;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.VALIDATE);
    }

    public CredentialValidationResult validate( 
      UsernamePasswordCredential credential) {
 
        UserDetails user = users.get(credential.getCaller());
        if (credential.compareTo(user.getLogin(), user.getPassword())) {
            return new CredentialValidationResult(user.getLogin());
        }
        return INVALID_RESULT;
    }
}

Or we can choose to configure the IdentityStore so that it can be used only for group retrieval:

或者我们可以选择配置IdentityStore,使其只能用于组检索。

@Override
public Set<ValidationType> validationTypes() {
    return EnumSet.of(ValidationType.PROVIDE_GROUPS);
}

We should then provide an implementation for the getCallerGroups() methods:

然后我们应该为getCallerGroups()方法提供一个实现。

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map<String, UserDetails> users = new HashMap<>();

    @Override
    public int priority() {
        return 90;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.PROVIDE_GROUPS);
    }

    @Override
    public Set<String> getCallerGroups(CredentialValidationResult validationResult) {
        UserDetails user = users.get(
          validationResult.getCallerPrincipal().getName());
        return new HashSet<>(user.getRoles());
    }
}

Because IdentityStoreHandler expects the implementation to be a CDI bean, we decorate it with ApplicationScoped annotation.

因为IdentityStoreHandler期望实现是一个CDI bean,我们用ApplicationScoped注解来装饰它。

5. Security Context API

5.安全语境API

The Jakarta EE 8 Security API provides an access point to programmatic security through the SecurityContext interface. It’s an alternative when the declarative security model enforced by the container isn’t sufficient.

Jakarta EE 8 Security API通过SecurityContext接口提供了对程序化安全的访问点。当容器强制执行的声明性安全模型不够用时,它是一个替代方案。

A default implementation of the SecurityContext interface should be provided at runtime as a CDI bean, and therefore we need to inject it:

SecurityContext接口的默认实现应该在运行时作为CDI bean提供,因此我们需要注入它。

@Inject
SecurityContext securityContext;

At this point, we can authenticate the user, retrieve an authenticated one, check his role membership and grant or deny access to web resource through the five available methods.

在这一点上,我们可以认证用户,检索已认证的用户,检查他的角色成员资格,并通过五个可用的方法授予或拒绝对网络资源的访问。

5.1. Retrieving Caller Data

5.1 检索呼叫者数据

In previous versions of Jakarta EE, we’d retrieve the Principal or check the role membership differently in each container.

在以前的Jakarta EE版本中,我们会检索Principal或在每个容器中检查不同的角色成员。

While we use the getUserPrincipal() and isUserInRole() methods of the HttpServletRequest in a servlet container, a similar methods getCallerPrincipal() and isCallerInRole() methods of the EJBContext are used in EJB Container.

当我们在servlet容器中使用HttpServletRequestgetUserPrincipal()isUserInRole()方法时,EJB容器中使用EJBContext的类似方法getCallerPrincipalisCallerInRole方法。

The new Jakarta EE 8 Security API has standardized this by providing a similar method through the SecurityContext interface:

新的Jakarta EE 8安全API已经通过SecurityContext接口提供了一个类似的方法来规范这一点:

Principal getCallerPrincipal();
boolean isCallerInRole(String role);
<T extends Principal> Set<T> getPrincipalsByType(Class<T> type);

The getCallerPrincipal() method returns a container specific representation of the authenticated caller while the getPrincipalsByType() method retrieves all principals of a given type.

getCallerPrincipal()方法返回已验证的调用者的特定容器表示,而getPrincipalsByType()方法检索特定类型的所有委托人。

It can be useful in case the application specific caller is different from the container one.

在应用程序特定的调用者与容器的调用者不同的情况下,它可能是有用的。

5.2. Testing for Web Resource Access

5.2.测试网络资源访问

First, we need to configure a protected resource:

首先,我们需要配置一个受保护的资源。

@WebServlet("/protectedServlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "USER_ROLE"))
public class ProtectedServlet extends HttpServlet {
    //...
}

And then, to check access to this protected resource we should invoke the hasAccessToWebResource() method:

然后,为了检查对这个受保护资源的访问,我们应该调用hasAccessToWebResource()方法:

securityContext.hasAccessToWebResource("/protectedServlet", "GET");

In this case, the method returns true if the user is in role USER_ROLE.

在这种情况下,如果用户处于角色USER_ROLE.,该方法返回true。

5.3. Authenticating the Caller Programmatically

5.3.以程序方式认证调用者

An application can programmatically trigger the authentication process by invoking authenticate():

一个应用程序可以通过调用authenticate(),以编程方式触发认证过程。

AuthenticationStatus authenticate(
  HttpServletRequest request, 
  HttpServletResponse response,
  AuthenticationParameters parameters);

The container is then notified and will, in turn, invoke the authentication mechanism configured for the application. AuthenticationParameters parameter provides a credential to HttpAuthenticationMechanism:

然后,容器被通知,并将反过来调用为应用程序配置的认证机制。AuthenticationParameters参数向HttpAuthenticationMechanism提供了一个凭证:

withParams().credential(credential)

The SUCCESS and SEND_FAILURE values of the AuthenticationStatus design a successful and failed authentication while SEND_CONTINUE  signals an in progress status of the authentication process.

SUCCESSSEND_FAILURE值指定了成功和失败的认证,而SEND_CONTINUE则表示认证过程的进行中状态。

6. Running the Examples

6.运行实例

For highlighting these examples, we’ve used the latest development build of the Open Liberty Server which supports Jakarta EE 8. This is downloaded and installed thanks to the liberty-maven-plugin which can also deploy the application and start the server.

为了突出这些例子,我们使用了Open Liberty服务器的最新开发版本,它支持Jakarta EE 8。这要归功于liberty-maven-plugin的下载和安装,它还可以部署应用程序并启动服务器。

To run the examples, just access to the corresponding module and invoke this command:

要运行这些例子,只要进入相应的模块并调用这个命令。

mvn clean package liberty:run

As a result, Maven will download the server, build, deploy, and run the application.

因此,Maven会下载服务器,构建、部署并运行应用程序。

7. Conclusion

7.结语

In this article, we covered the configuration and implementation of the main features of the new Jakarta EE 8 Security API.

在这篇文章中,我们介绍了新的Jakarta EE 8安全API的主要功能的配置和实现。

First, we started by showing how to configure the default built-in authentication mechanisms and how to implement a custom one. Later, we saw how to configure the built-in Identity Store and how to implement a custom one. And finally, we saw how to call methods of the SecurityContext.

首先,我们开始展示了如何配置默认的内置认证机制以及如何实现一个自定义的认证机制。后来,我们看到了如何配置内置的身份存储以及如何实现一个自定义的身份存储。最后,我们看到了如何调用SecurityContext.的方法。

As always, the code examples for this article are available over on GitHub.

像往常一样,本文的代码示例可在GitHub上获得