如何使用 JAX-RS 和 Jersey 实现基于 REST 令牌的身份验证 [英] How to implement REST token-based authentication with JAX-RS and Jersey

查看:119
本文介绍了如何使用 JAX-RS 和 Jersey 实现基于 REST 令牌的身份验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在寻找一种在泽西岛启用基于令牌的身份验证的方法.我尽量不使用任何特定的框架.这可能吗?

I'm looking for a way to enable token-based authentication in Jersey. I am trying not to use any particular framework. Is that possible?

我的计划是:一个用户注册我的网络服务,我的网络服务生成一个令牌,发送给客户端,客户端会保留它.然后,对于每个请求,客户端将发送令牌而不是用户名和密码.

My plan is: A user signs up for my web service, my web service generates a token, sends it to the client, and the client will retain it. Then the client, for each request, will send the token instead of username and password.

我想为每个请求和 @PreAuthorize("hasRole('ROLE')") 使用自定义过滤器,但我只是认为这会导致很多请求数据库以检查令牌是否有效.

I was thinking of using a custom filter for each request and @PreAuthorize("hasRole('ROLE')"), but I just thought that this causes a lot of requests to the database to check if the token is valid.

或者不创建过滤器并在每个请求中放置一个参数令牌?这样每个 API 首先检查令牌,然后执行某些操作以检索资源.

Or not create filter and in each request put a param token? So that each API first checks the token and after executes something to retrieve resource.

推荐答案

基于令牌的身份验证的工作原理

在基于令牌的身份验证中,客户端将硬凭证(例如用户名和密码)交换为一段称为令牌的数据.对于每个请求,客户端不会发送硬凭证,而是将令牌发送到服务器以执行身份验证和授权.

How token-based authentication works

In token-based authentication, the client exchanges hard credentials (such as username and password) for a piece of data called token. For each request, instead of sending the hard credentials, the client will send the token to the server to perform authentication and then authorization.

简而言之,基于令牌的身份验证方案遵循以下步骤:

In a few words, an authentication scheme based on tokens follow these steps:

  1. 客户端将其凭据(用户名和密码)发送到服务器.
  2. 服务器对凭据进行身份验证,如果凭据有效,则会为用户生成令牌.
  3. 服务器将先前生成的令牌与用户标识符和到期日期一起存储在某个存储中.
  4. 服务器将生成的令牌发送给客户端.
  5. 客户端在每个请求中向服务器发送令牌.
  6. 服务器在每个请求中从传入请求中提取令牌.使用令牌,服务器查找用户详细信息以执行身份验证.
    • 如果令牌有效,则服务器接受请求.
    • 如果令牌无效,服务器将拒绝请求.

您可以使用 JAX-RS 2.0(Jersey、RESTEasy 和 Apache CXF)做什么

此解决方案仅使用 JAX-RS 2.0 API,避免使用任何特定于供应商的解决方案.因此,它应该适用于 JAX-RS 2.0 实现,例如 JerseyRESTEasyApache CXF.

What you can do with JAX-RS 2.0 (Jersey, RESTEasy and Apache CXF)

This solution uses only the JAX-RS 2.0 API, avoiding any vendor specific solution. So, it should work with JAX-RS 2.0 implementations, such as Jersey, RESTEasy and Apache CXF.

值得一提的是,如果您使用的是基于令牌的身份验证,您就不会依赖 servlet 容器提供的标准 Java EE Web 应用程序安全机制,并且可以通过应用程序的 web.xml 进行配置描述符.这是自定义身份验证.

It is worthwhile to mention that if you are using token-based authentication, you are not relying on the standard Java EE web application security mechanisms offered by the servlet container and configurable via application's web.xml descriptor. It's a custom authentication.

创建一个 JAX-RS 资源方法,用于接收和验证凭据(用户名和密码)并为用户颁发令牌:

Create a JAX-RS resource method which receives and validates the credentials (username and password) and issue a token for the user:

@Path("/authentication")
public class AuthenticationEndpoint {

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response authenticateUser(@FormParam("username") String username, 
                                     @FormParam("password") String password) {

        try {

            // Authenticate the user using the credentials provided
            authenticate(username, password);

            // Issue a token for the user
            String token = issueToken(username);

            // Return the token on the response
            return Response.ok(token).build();

        } catch (Exception e) {
            return Response.status(Response.Status.FORBIDDEN).build();
        }      
    }

    private void authenticate(String username, String password) throws Exception {
        // Authenticate against a database, LDAP, file or whatever
        // Throw an Exception if the credentials are invalid
    }

    private String issueToken(String username) {
        // Issue a token (can be a random String persisted to a database or a JWT token)
        // The issued token must be associated to a user
        // Return the issued token
    }
}

如果在验证凭据时抛出任何异常,将返回状态为 403(禁止)的响应.

If any exceptions are thrown when validating the credentials, a response with the status 403 (Forbidden) will be returned.

如果凭据成功通过验证,将返回状态为 200 (OK) 的响应,并且所发出的令牌将在响应负载中发送到客户端.客户端必须在每次请求中向服务器发送令牌.

If the credentials are successfully validated, a response with the status 200 (OK) will be returned and the issued token will be sent to the client in the response payload. The client must send the token to the server in every request.

在使用 application/x-www-form-urlencoded 时,客户端必须在请求负载中以以下格式发送凭据:

When consuming application/x-www-form-urlencoded, the client must send the credentials in the following format in the request payload:

username=admin&password=123456

可以将用户名和密码包装到一个类中,而不是表单参数:

Instead of form params, it's possible to wrap the username and the password into a class:

public class Credentials implements Serializable {

    private String username;
    private String password;
    
    // Getters and setters omitted
}

然后将其作为 JSON 使用:

And then consume it as JSON:

@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response authenticateUser(Credentials credentials) {

    String username = credentials.getUsername();
    String password = credentials.getPassword();
    
    // Authenticate the user, issue a token and return a response
}

使用这种方法,客户端必须在请求的有效负载中以以下格式发送凭据:

Using this approach, the client must to send the credentials in the following format in the payload of the request:

{
  "username": "admin",
  "password": "123456"
}

从请求中提取令牌并验证它

客户端应在请求的标准 HTTP Authorization 标头中发送令牌.例如:

Extracting the token from the request and validating it

The client should send the token in the standard HTTP Authorization header of the request. For example:

Authorization: Bearer <token-goes-here>

标准 HTTP 标头的名称很不幸,因为它携带身份验证信息,而不是授权.但是,它是用于向服务器发送凭据的标准 HTTP 标头.

The name of the standard HTTP header is unfortunate because it carries authentication information, not authorization. However, it's the standard HTTP header for sending credentials to the server.

JAX-RS 提供 @NameBinding,一个元注解,用于创建其他注解以将过滤器和拦截器绑定到资源类和方法.定义一个 @Secured 注释如下:

JAX-RS provides @NameBinding, a meta-annotation used to create other annotations to bind filters and interceptors to resource classes and methods. Define a @Secured annotation as following:

@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }

上面定义的名称绑定注解将用于装饰一个过滤器类,它实现了ContainerRequestFilter,允许您在请求被资源方法处理之前拦截它.ContainerRequestContext 可用于访问 HTTP 请求标头,然后提取令牌:

The above defined name-binding annotation will be used to decorate a filter class, which implements ContainerRequestFilter, allowing you to intercept the request before it be handled by a resource method. The ContainerRequestContext can be used to access the HTTP request headers and then extract the token:

@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {

    private static final String REALM = "example";
    private static final String AUTHENTICATION_SCHEME = "Bearer";

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {

        // Get the Authorization header from the request
        String authorizationHeader =
                requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);

        // Validate the Authorization header
        if (!isTokenBasedAuthentication(authorizationHeader)) {
            abortWithUnauthorized(requestContext);
            return;
        }

        // Extract the token from the Authorization header
        String token = authorizationHeader
                            .substring(AUTHENTICATION_SCHEME.length()).trim();

        try {

            // Validate the token
            validateToken(token);

        } catch (Exception e) {
            abortWithUnauthorized(requestContext);
        }
    }

    private boolean isTokenBasedAuthentication(String authorizationHeader) {

        // Check if the Authorization header is valid
        // It must not be null and must be prefixed with "Bearer" plus a whitespace
        // The authentication scheme comparison must be case-insensitive
        return authorizationHeader != null && authorizationHeader.toLowerCase()
                    .startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
    }

    private void abortWithUnauthorized(ContainerRequestContext requestContext) {

        // Abort the filter chain with a 401 status code response
        // The WWW-Authenticate header is sent along with the response
        requestContext.abortWith(
                Response.status(Response.Status.UNAUTHORIZED)
                        .header(HttpHeaders.WWW_AUTHENTICATE, 
                                AUTHENTICATION_SCHEME + " realm="" + REALM + """)
                        .build());
    }

    private void validateToken(String token) throws Exception {
        // Check if the token was issued by the server and if it's not expired
        // Throw an Exception if the token is invalid
    }
}

如果令牌验证过程中发生任何问题,将返回状态为 401(未授权)的响应.否则请求将进入资源方法.

If any problems happen during the token validation, a response with the status 401 (Unauthorized) will be returned. Otherwise the request will proceed to a resource method.

要将身份验证过滤器绑定到资源方法或资源类,请使用上面创建的 @Secured 批注对其进行批注.对于被注解的方法和/或类,过滤器将被执行.这意味着如果使用有效令牌执行请求,则只有才能到达此类端点.

To bind the authentication filter to resource methods or resource classes, annotate them with the @Secured annotation created above. For the methods and/or classes that are annotated, the filter will be executed. It means that such endpoints will only be reached if the request is performed with a valid token.

如果某些方法或类不需要身份验证,请不要对其进行注释:

If some methods or classes do not need authentication, simply do not annotate them:

@Path("/example")
public class ExampleResource {

    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response myUnsecuredMethod(@PathParam("id") Long id) {
        // This method is not annotated with @Secured
        // The authentication filter won't be executed before invoking this method
        ...
    }

    @DELETE
    @Secured
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response mySecuredMethod(@PathParam("id") Long id) {
        // This method is annotated with @Secured
        // The authentication filter will be executed before invoking this method
        // The HTTP request must be performed with a valid token
        ...
    }
}

在上面显示的示例中,过滤器将mySecuredMethod(Long) 方法执行,因为它用 @Secured 注释.

In the example shown above, the filter will be executed only for the mySecuredMethod(Long) method because it's annotated with @Secured.

您很可能需要了解再次向您的 REST API 执行请求的用户.可以使用以下方法来实现它:

It's very likely that you will need to know the user who is performing the request agains your REST API. The following approaches can be used to achieve it:

在您的 ContainerRequestFilter.filter(ContainerRequestContext) 方法,一个新的SecurityContext 可以为当前请求设置实例.然后覆盖 SecurityContext.getUserPrincipal(),返回一个 Principal 实例:

Within your ContainerRequestFilter.filter(ContainerRequestContext) method, a new SecurityContext instance can be set for the current request. Then override the SecurityContext.getUserPrincipal(), returning a Principal instance:

final SecurityContext currentSecurityContext = requestContext.getSecurityContext();
requestContext.setSecurityContext(new SecurityContext() {

        @Override
        public Principal getUserPrincipal() {
            return () -> username;
        }

    @Override
    public boolean isUserInRole(String role) {
        return true;
    }

    @Override
    public boolean isSecure() {
        return currentSecurityContext.isSecure();
    }

    @Override
    public String getAuthenticationScheme() {
        return AUTHENTICATION_SCHEME;
    }
});

使用令牌查找用户标识符(用户名),即 委托人的名字.

Use the token to look up the user identifier (username), which will be the Principal's name.

注入SecurityContext 在任何 JAX-RS 资源类中:

Inject the SecurityContext in any JAX-RS resource class:

@Context
SecurityContext securityContext;

同样可以在 JAX-RS 资源方法中完成:

The same can be done in a JAX-RS resource method:

@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id, 
                         @Context SecurityContext securityContext) {
    ...
}

然后获取Principal:

Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();

使用 CDI(上下文和依赖注入)

如果出于某种原因,您不想覆盖 SecurityContext,可以使用CDI(Context and Dependency Injection),它提供了事件和生产者等有用的特性.

Using CDI (Context and Dependency Injection)

If, for some reason, you don't want to override the SecurityContext, you can use CDI (Context and Dependency Injection), which provides useful features such as events and producers.

创建 CDI 限定符:

Create a CDI qualifier:

@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }

在上面创建的 AuthenticationFilter 中,注入一个 Event@AuthenticatedUser 注释:

In your AuthenticationFilter created above, inject an Event annotated with @AuthenticatedUser:

@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;

如果认证成功,则触发传递用户名作为参数的事件(记住,令牌是为用户颁发的,令牌将用于查找用户标识符):

If the authentication succeeds, fire the event passing the username as parameter (remember, the token is issued for a user and the token will be used to look up the user identifier):

userAuthenticatedEvent.fire(username);

很可能有一个类代表您的应用程序中的用户.让我们称这个类为 User.

It's very likely that there's a class that represents a user in your application. Let's call this class User.

创建一个 CDI bean 来处理身份验证事件,找到一个具有对应用户名的 User 实例并将其分配给 authenticatedUser 生产者字段:

Create a CDI bean to handle the authentication event, find a User instance with the correspondent username and assign it to the authenticatedUser producer field:

@RequestScoped
public class AuthenticatedUserProducer {

    @Produces
    @RequestScoped
    @AuthenticatedUser
    private User authenticatedUser;
    
    public void handleAuthenticationEvent(@Observes @AuthenticatedUser String username) {
        this.authenticatedUser = findUser(username);
    }

    private User findUser(String username) {
        // Hit the the database or a service to find a user by its username and return it
        // Return the User instance
    }
}

authenticatedUser 字段生成一个 User 实例,该实例可以注入容器管理的 bean,例如 JAX-RS 服务、CDI bean、servlet 和 EJB.使用下面一段代码注入一个User实例(其实就是一个CDI代理):

The authenticatedUser field produces a User instance that can be injected into container managed beans, such as JAX-RS services, CDI beans, servlets and EJBs. Use the following piece of code to inject a User instance (in fact, it's a CDI proxy):

@Inject
@AuthenticatedUser
User authenticatedUser;

注意 CDI @Produces 注释与 JAX-RS @Produces 注释:

Note that the CDI @Produces annotation is different from the JAX-RS @Produces annotation:

确保您使用 CDI AuthenticatedUserProducer bean 中的 @Produces 注释.

Be sure you use the CDI @Produces annotation in your AuthenticatedUserProducer bean.

这里的关键是用 @RequestScoped,允许您在过滤器和 bean 之间共享数据.如果您不想使用事件,您可以修改过滤器以将经过身份验证的用户存储在请求范围的 bean 中,然后从您的 JAX-RS 资源类中读取它.

The key here is the bean annotated with @RequestScoped, allowing you to share data between filters and your beans. If you don't wan't to use events, you can modify the filter to store the authenticated user in a request scoped bean and then read it from your JAX-RS resource classes.

与覆盖的方法相比SecurityContext,CDI 方法允许您从 JAX-RS 资源和提供程序以外的 bean 获取经过身份验证的用户.

Compared to the approach that overrides the SecurityContext, the CDI approach allows you to get the authenticated user from beans other than JAX-RS resources and providers.

有关如何支持基于角色的授权的详细信息,请参阅我的其他答案.

Please refer to my other answer for details on how to support role-based authorization.

令牌可以是:

  • 不透明:除了值本身(如随机字符串)外不显示任何细节
  • 自包含:包含有关令牌本身的详细信息(如 JWT).
  • Opaque: Reveals no details other than the value itself (like a random string)
  • Self-contained: Contains details about the token itself (like JWT).

查看以下详细信息:

可以通过生成随机字符串并将其与用户标识符和到期日期一起保存到数据库中来发布令牌.此处是如何在 Java 中生成随机字符串的一个很好的示例.您也可以使用:

A token can be issued by generating a random string and persisting it to a database along with the user identifier and an expiration date. A good example of how to generate a random string in Java can be seen here. You also could use:

Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);

JWT(JSON 网络令牌)

JWT(JSON 网络令牌)是一种在两方之间安全地表示声明的标准方法,由 RFC 7519.

它是一个独立的令牌,它使您能够在声明中存储详细信息.这些声明存储在令牌负载中,该负载是编码为 Base64 的 JSON.以下是在 RFC 7519 中注册的一些声明及其含义(阅读完整的 RFC详情):

It's a self-contained token and it enables you to store details in claims. These claims are stored in the token payload which is a JSON encoded as Base64. Here are some claims registered in the RFC 7519 and what they mean (read the full RFC for further details):

  • iss:颁发令牌的委托人.
  • sub:作为 JWT 主题的 Principal.
  • exp:令牌的到期日期.
  • nbf:开始接受令牌进行处理的时间.
  • iat:令牌的发行时间.
  • jti:令牌的唯一标识符.

请注意,您不得在令牌中存储敏感数据,例如密码.

Be aware that you must not store sensitive data, such as passwords, in the token.

客户端可以读取有效负载,并且可以通过在服务器上验证其签名来轻松检查令牌的完整性.签名是防止令牌被篡改的原因.

The payload can be read by the client and the integrity of the token can be easily checked by verifying its signature on the server. The signature is what prevents the token from being tampered with.

如果您不需要跟踪 JWT 令牌,则不需要保留它们.尽管如此,通过持久化令牌,您将有可能使它们的访问无效和撤销.为了跟踪 JWT 令牌,您可以将令牌标识符(jti 声明)以及一些其他详细信息,例如您为其颁发令牌的用户、到期日期等.

You won't need to persist JWT tokens if you don't need to track them. Althought, by persisting the tokens, you will have the possibility of invalidating and revoking the access of them. To keep the track of JWT tokens, instead of persisting the whole token on the server, you could persist the token identifier (jti claim) along with some other details such as the user you issued the token for, the expiration date, etc.

在持久化令牌时,请始终考虑删除旧令牌,以防止您的数据库无限增长.

When persisting tokens, always consider removing the old ones in order to prevent your database from growing indefinitely.

有一些 Java 库可以发布和验证 JWT 令牌,例如:

There are a few Java libraries to issue and validate JWT tokens such as:

要找到其他一些与 JWT 合作的重要资源,请查看 http://jwt.io.

To find some other great resources to work with JWT, have a look at http://jwt.io.

如果您想撤销令牌,则必须对其进行跟踪.您不需要在服务器端存储整个令牌,只需存储令牌标识符(必须是唯一的)和一些元数据(如果需要).对于令牌标识符,您可以使用 UUID.

If you want to revoke tokens, you must keep the track of them. You don't need to store the whole token on server side, store only the token identifier (that must be unique) and some metadata if you need. For the token identifier you could use UUID.

jti声明应该用于在令牌上存储令牌标识符.验证令牌时,通过检查 jti 针对您在服务器端拥有的令牌标识符声明.

The jti claim should be used to store the token identifier on the token. When validating the token, ensure that it has not been revoked by checking the value of the jti claim against the token identifiers you have on server side.

为了安全起见,当用户更改密码时,撤销用户的所有令牌.

For security purposes, revoke all the tokens for a user when they change their password.

  • 您决定使用哪种类型的身份验证并不重要.始终在 HTTPS 连接的顶部执行此操作以防止 中间人攻击.
  • 查看来自 Information Security 的这个问题,了解有关令牌的更多信息.
  • 在本文中,您将找到一些关于基于令牌的身份验证的有用信息.
  • It doesn't matter which type of authentication you decide to use. Always do it on the top of a HTTPS connection to prevent the man-in-the-middle attack.
  • Take a look at this question from Information Security for more information about tokens.
  • In this article you will find some useful information about token-based authentication.

这篇关于如何使用 JAX-RS 和 Jersey 实现基于 REST 令牌的身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆