使用JAX-RS和Jersey进行基于REST令牌的身份验证的最佳实践

本文翻译自:Best practice for REST token-based authentication with JAX-RS and Jersey

I'm looking for a way to enable token-based authentication in Jersey. 我正在寻找一种在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. 我的计划是:用户注册我的Web服务,我的Web服务生成令牌,将其发送到客户端,客户端将保留它。 Then the client, for each request, will send the token instead of username and password. 然后,对于每个请求,客户端将发送令牌而不是用户名和密码。

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. 我正在考虑为每个请求使用自定义过滤器和@PreAuthorize("hasRole('ROLE')")但我只是认为这会导致很多请求数据库检查令牌是否有效。

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. 这样每个API首先检查令牌,然后执行一些东西来检索资源。


#1楼

参考:https://stackoom.com/question/1oLwR/使用JAX-RS和Jersey进行基于REST令牌的身份验证的最佳实践


#2楼

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. The client sends their credentials (username and password) to the server. 客户端将其凭据(用户名和密码)发送到服务器。
  2. The server authenticates the credentials and, if they are valid, generate a token for the user. 服务器验证凭据,如果它们有效,则为用户生成令牌。
  3. The server stores the previously generated token in some storage along with the user identifier and an expiration date. 服务器将先前生成的令牌与用户标识符和到期日期一起存储在一些存储器中。
  4. The server sends the generated token to the client. 服务器将生成的令牌发送到客户端。
  5. The client sends the token to the server in each request. 客户端在每个请求中将令牌发送到服务器。
  6. The server, in each request, extracts the token from the incoming request. 每个请求中的服务器从传入请求中提取令牌。 With the token, the server looks up the user details to perform authentication. 使用令牌,服务器查找用户详细信息以执行身份验证。
    • If the token is valid, the server accepts the request. 如果令牌有效,则服务器接受该请求。
    • If the token is invalid, the server refuses the request. 如果令牌无效,则服务器拒绝该请求。
  7. Once the authentication has been performed, the server performs authorization. 一旦执行了身份验证,服务器就会执行授权。
  8. The server can provide an endpoint to refresh tokens. 服务器可以提供端点来刷新令牌。

Note: The step 3 is not required if the server has issued a signed token (such as JWT, which allows you to perform stateless authentication). 注意:如果服务器已发出签名令牌(例如JWT,允许您执行无状态身份验证),则不需要执行步骤3。

What you can do with JAX-RS 2.0 (Jersey, RESTEasy and Apache CXF) 使用JAX-RS 2.0(Jersey,RESTEasy和Apache CXF)可以做些什么

This solution uses only the JAX-RS 2.0 API, avoiding any vendor specific solution . 此解决方案仅使用JAX-RS 2.0 API, 避免任何特定于供应商的解决方案 So, it should work with JAX-RS 2.0 implementations, such as Jersey , RESTEasy and Apache CXF . 因此,它应该适用于JAX-RS 2.0实现,例如JerseyRESTEasyApache CXF

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. 值得一提的是,如果使用基于令牌的身份验证,则不依赖于servlet容器提供的标准Java EE Web应用程序安全机制,并且可以通过应用程序的web.xml描述符进行配置。 It's a custom authentication. 这是一种自定义身份验证。

Authenticating a user with their username and password and issuing a token 使用用户名和密码验证用户并发出令牌

Create a JAX-RS resource method which receives and validates the credentials (username and password) and issue a token for the user: 创建一个JAX-RS资源方法,该方法接收并验证凭据(用户名和密码)并为用户发出令牌:

@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
    }
}

If any exceptions are thrown when validating the credentials, a response with the status 403 (Forbidden) will be returned. 如果在验证凭据时抛出任何异常,将返回状态为403 (Forbidden)的响应。

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. 如果成功验证凭据,将返回状态为200 (OK)的响应,并且已发布的令牌将在响应有效负载中发送到客户端。 The client must send the token to the server in every request. 客户端必须在每个请求中将令牌发送到服务器。

When consuming application/x-www-form-urlencoded , the client must to send the credentials in the following format in the request payload: 在使用application/x-www-form-urlencoded ,客户端必须在请求有效负载中以以下格式发送凭据:

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
}

And then consume it as JSON: 然后将其作为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"
}

Extracting the token from the request and validating it 从请求中提取令牌并验证它

The client should send the token in the standard HTTP Authorization header of the request. 客户端应该在请求的标准HTTP Authorization标头中发送令牌。 For example: 例如:

Authorization: Bearer <token-goes-here>

The name of the standard HTTP header is unfortunate because it carries authentication information, not authorization . 标准HTTP标头的名称很不幸,因为它带有身份验证信息,而不是授权 However, it's the standard HTTP header for sending credentials to the server. 但是,它是用于将凭据发送到服务器的标准HTTP标头。

JAX-RS provides @NameBinding , a meta-annotation used to create other annotations to bind filters and interceptors to resource classes and methods. JAX-RS提供@NameBinding ,这是一个元注释,用于创建其他注释以将过滤器和拦截器绑定到资源类和方法。 Define a @Secured annotation as following: 定义@Secured注释如下:

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

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. 上面定义的名称绑定注释将用于修饰过滤器类,该类实现ContainerRequestFilter ,允许您在资源方法处理请求之前拦截请求。 The ContainerRequestContext can be used to access the HTTP request headers and then extract the token: ContainerRequestContext可用于访问HTTP请求标头,然后提取标记:

@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
    }
}

If any problems happen during the token validation, a response with the status 401 (Unauthorized) will be returned. 如果在令牌验证期间发生任何问题,将返回状态为401 (未授权)的响应。 Otherwise the request will proceed to a resource method. 否则,请求将继续进行资源方法。

Securing your REST endpoints 保护REST端点

To bind the authentication filter to resource methods or resource classes, annotate them with the @Secured annotation created above. 要将身份验证筛选器绑定到资源方法或资源类,请使用上面创建的@Secured注释对其进行注释。 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
        ...
    }
}

In the example shown above, the filter will be executed only for the mySecuredMethod(Long) method because it's annotated with @Secured . 在上面显示的示例中,过滤器针对mySecuredMethod(Long)方法执行,因为它使用@Secured注释。

Identifying the current user 识别当前用户

It's very likely that you will need to know the user who is performing the request agains your REST API. 您很可能需要知道在REST API中再次执行请求的用户。 The following approaches can be used to achieve it: 可以使用以下方法来实现它:

Overriding the security context of the current request 覆盖当前请求的安全上下文

Within your ContainerRequestFilter.filter(ContainerRequestContext) method, a new SecurityContext instance can be set for the current request. ContainerRequestFilter.filter(ContainerRequestContext)方法中,可以为当前请求设置新的SecurityContext实例。 Then override the SecurityContext.getUserPrincipal() , returning a Principal instance: 然后重写SecurityContext.getUserPrincipal() ,返回一个Principal实例:

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. 使用令牌查找用户标识符(用户名),这将是Principal的名称。

Inject the SecurityContext in any JAX-RS resource class: 在任何JAX-RS资源类中注入SecurityContext

@Context
SecurityContext securityContext;

The same can be done in a JAX-RS resource method: 可以在JAX-RS资源方法中完成相同的操作:

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

And then get the Principal : 然后得到Principal

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

Using CDI (Context and Dependency Injection) 使用CDI(上下文和依赖注入)

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. 如果由于某种原因,您不想覆盖SecurityContext ,则可以使用CDI(上下文和依赖注入),它提供有用的功能,如事件和生成器。

Create a CDI qualifier: 创建CDI限定符:

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

In your AuthenticationFilter created above, inject an Event annotated with @AuthenticatedUser : 在上面创建的AuthenticationFilter ,注入一个使用@AuthenticatedUser注释的Event

@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);

It's very likely that there's a class that represents a user in your application. 很可能在您的应用程序中有一个代表用户的类。 Let's call this class User . 我们称这个类为User

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: 创建一个CDI bean来处理身份验证事件,找到一个具有相应用户名的User实例,并将其分配给authenticatedUser producer字段:

@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
    }
}

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. authenticatedUser字段生成一个可以注入容器托管bean的User实例,例如JAX-RS服务,CDI bean,servlet和EJB。 Use the following piece of code to inject a User instance (in fact, it's a CDI proxy): 使用以下代码注入User实例(实际上,它是一个CDI代理):

@Inject
@AuthenticatedUser
User authenticatedUser;

Note that the CDI @Produces annotation is different from the JAX-RS @Produces annotation: 需要注意的是,CDI @Produces注释是从JAX-RS 不同的 @Produces注解:

Be sure you use the CDI @Produces annotation in your AuthenticatedUserProducer bean. 确保在AuthenticatedUserProducer bean中使用CDI @Produces批注。

The key here is the bean annotated with @RequestScoped , allowing you to share data between filters and your beans. 这里的关键是使用@RequestScoped注释的bean,允许您在过滤器和bean之间共享数据。 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. 如果您不想使用事件,则可以修改过滤器以将经过身份验证的用户存储在请求范围的bean中,然后从JAX-RS资源类中读取它。

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

Supporting role-based authorization 支持基于角色的授权

Please refer to my other answer for details on how to support role-based authorization. 有关如何支持基于角色的授权的详细信息,请参阅我的其他答案

Issuing tokens 发行代币

A token can be: 令牌可以是:

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

See details below: 详情如下:

Random string as token 随机字符串作为标记

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 . 这里可以看到如何在Java中生成随机字符串的一个很好的例子。 You also could use: 你也可以使用:

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

JWT (JSON Web Token) JWT(JSON Web令牌)

JWT (JSON Web Token) is a standard method for representing claims securely between two parties and is defined by the RFC 7519 . JWT(JSON Web令牌)是一种在双方之间安全地表示声明的标准方法,由RFC 7519定义。

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 . 这些声明存储在令牌有效载荷中,该有效载荷是编码为Base64的JSON。 Here are some claims registered in the RFC 7519 and what they mean (read the full RFC for further details): 以下是RFC 7519中注册的一些声明及其含义(请阅读完整的RFC以获取更多详细信息):

  • iss : Principal that issued the token. iss :发出令牌的委托人。
  • sub : Principal that is the subject of the JWT. sub :作为JWT主题的校长。
  • exp : Expiration date for the token. exp :令牌的到期日期。
  • nbf : Time on which the token will start to be accepted for processing. nbf :开始接受令牌进行处理的时间。
  • iat : Time on which the token was issued. iat :发出令牌的时间。
  • jti : Unique identifier for the token. 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. 签名是防止令牌被篡改的原因。

You won't need to persist JWT tokens if you don't need to track them. 如果您不需要跟踪JWT令牌,则无需持久保存JWT令牌。 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. 要保持JWT令牌的跟踪,您可以将令牌标识符( jti声明)与其他一些详细信息(例如您发出令牌的用户,到期日期等)一起保留,而不是将整个令牌保留在服务器上。

When persisting tokens, always consider removing the old ones in order to prevent your database from growing indefinitely. 持久化令牌时,请始终考虑删除旧令牌,以防止数据库无限增长。

Using JWT 使用JWT

There are a few Java libraries to issue and validate JWT tokens such as: 有一些Java库可以发布和验证JWT令牌,例如:

To find some other great resources to work with JWT, have a look at http://jwt.io . 要找到一些与JWT合作的其他优秀资源,请查看http://jwt.io

Handling token refreshment with JWT 使用JWT处理令牌刷新

Accept only valid (and non-expired) tokens for refreshment. 接受有效(和未过期)令牌以进行更新。 It's responsability of the client to refresh the tokens before the expiration date indicated in the exp claim. 客户有责任在exp索赔中指明的到期日期之前刷新令牌。

You should prevent the tokens from being refreshed indefinitely. 您应该防止令牌无限期刷新。 See below a few approaches that you could consider. 请参阅以下几种您可以考虑的方法。

You could keep the track of token refreshment by adding two claims to your token (the claim names are up to you): 您可以通过向令牌添加两个声明(声明名称由您决定)来保持令牌更新的跟踪:

  • refreshLimit : Indicates how many times the token can be refreshed. refreshLimit :表示可以刷新令牌的次数。
  • refreshCount : Indicates how many times the token has been refreshed. refreshCount :表示令牌刷新的次数。

So only refresh the token if the following conditions are true: 因此,只有在满足以下条件时才刷新令牌:

  • The token is not expired ( exp >= now ). 令牌未过期( exp >= now )。
  • The number of times that the token has been refreshed is less than the number of times that the token can be refreshed ( refreshCount < refreshLimit ). 刷新令牌的次数少于令牌可刷新的次数( refreshCount < refreshLimit )。

And when refreshing the token: 刷新令牌时:

  • Update the expiration date ( exp = now + some-amount-of-time ). 更新到期日期( exp = now + some-amount-of-time )。
  • Increment the number of times that the token has been refreshed ( refreshCount++ ). 增加令牌刷新的次数( refreshCount++ )。

Alternatively to keeping​ the track of the number of refreshments, you could have a claim that indicates the absolute expiration date (which works pretty similar to the refreshLimit claim described above). 或者,为了跟踪茶点的数量,您可以声明指示绝对到期日期 (其工作方式与上述的refreshLimit声明非常相似)。 Before the absolute expiration date , any number of refreshments is acceptable. 绝对有效期之前 ,可以接受任意数量的茶点。

Another approach involves issuing a separate long-lived refresh token that is used to issue short-lived JWT tokens. 另一种方法涉及发布一个单独的长期刷新令牌,用于发布短期JWT令牌。

The best approach depends on your requirements. 最好的方法取决于您的要求。

Handling token revocation with JWT 使用JWT处理令牌撤销

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 . 对于令牌标识符,您可以使用UUID

The jti claim should be used to store the token identifier on the token. jti声明应该用于将令牌标识符存储在令牌上。 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. 验证令牌时,请通过检查服务器端的令牌标识符的jti声明值来确保它未被撤销。

For security purposes, revoke all the tokens for a user when they change their password. 出于安全考虑,请在用户更改密码时撤消所有令牌。

Additional information 附加信息

  • 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 . 始终在HTTPS连接的顶部进行,以防止中间人攻击
  • 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. 在本文中,您将找到有关基于令牌的身份验证的一些有用信息。

#3楼

This answer is all about authorization and it is a complement of my previous answer about authentication 这个答案都是关于授权的 ,它是我之前关于身份验证的 答案的补充

Why another answer? 为什么另一个答案 I attempted to expand my previous answer by adding details on how to support JSR-250 annotations. 我试图通过添加有关如何支持JSR-250注释的详细信息来扩展我之前的答案。 However the original answer became the way too long and exceeded the maximum length of 30,000 characters . 然而,最初的答案变得太长 ,超过了30,000个字符最大长度 So I moved the whole authorization details to this answer, keeping the other answer focused on performing authentication and issuing tokens. 所以我将整个授权细节移到了这个答案,另一个答案集中在执行身份验证和发布令牌。


Supporting role-based authorization with the @Secured annotation 使用@Secured注释支持基于角色的授权

Besides authentication flow shown in the other answer , role-based authorization can be supported in the REST endpoints. 除了另一个答案中显示的身份验证流程外,REST端点可以支持基于角色的授权。

Create an enumeration and define the roles according to your needs: 创建枚举并根据您的需要定义角色:

public enum Role {
    ROLE_1,
    ROLE_2,
    ROLE_3
}

Change the @Secured name binding annotation created before to support roles: 更改之前创建的@Secured名称绑定注释以支持角色:

@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured {
    Role[] value() default {};
}

And then annotate the resource classes and methods with @Secured to perform the authorization. 然后使用@Secured注释资源类和方法以执行授权。 The method annotations will override the class annotations: 方法注释将覆盖类注释:

@Path("/example")
@Secured({Role.ROLE_1})
public class ExampleResource {

    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response myMethod(@PathParam("id") Long id) {
        // This method is not annotated with @Secured
        // But it's declared within a class annotated with @Secured({Role.ROLE_1})
        // So it only can be executed by the users who have the ROLE_1 role
        ...
    }

    @DELETE
    @Path("{id}")    
    @Produces(MediaType.APPLICATION_JSON)
    @Secured({Role.ROLE_1, Role.ROLE_2})
    public Response myOtherMethod(@PathParam("id") Long id) {
        // This method is annotated with @Secured({Role.ROLE_1, Role.ROLE_2})
        // The method annotation overrides the class annotation
        // So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles
        ...
    }
}

Create a filter with the AUTHORIZATION priority, which is executed after the AUTHENTICATION priority filter defined previously. 创建具有AUTHORIZATION优先级的过滤器,该优先级在先前定义的AUTHENTICATION优先级过滤器之后执行。

The ResourceInfo can be used to get the resource Method and resource Class that will handle the request and then extract the @Secured annotations from them: ResourceInfo可用于获取将处理请求的资源Method和resource Class ,然后从中提取@Secured注释:

@Secured
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

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

        // Get the resource class which matches with the requested URL
        // Extract the roles declared by it
        Class<?> resourceClass = resourceInfo.getResourceClass();
        List<Role> classRoles = extractRoles(resourceClass);

        // Get the resource method which matches with the requested URL
        // Extract the roles declared by it
        Method resourceMethod = resourceInfo.getResourceMethod();
        List<Role> methodRoles = extractRoles(resourceMethod);

        try {

            // Check if the user is allowed to execute the method
            // The method annotations override the class annotations
            if (methodRoles.isEmpty()) {
                checkPermissions(classRoles);
            } else {
                checkPermissions(methodRoles);
            }

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

    // Extract the roles from the annotated element
    private List<Role> extractRoles(AnnotatedElement annotatedElement) {
        if (annotatedElement == null) {
            return new ArrayList<Role>();
        } else {
            Secured secured = annotatedElement.getAnnotation(Secured.class);
            if (secured == null) {
                return new ArrayList<Role>();
            } else {
                Role[] allowedRoles = secured.value();
                return Arrays.asList(allowedRoles);
            }
        }
    }

    private void checkPermissions(List<Role> allowedRoles) throws Exception {
        // Check if the user contains one of the allowed roles
        // Throw an Exception if the user has not permission to execute the method
    }
}

If the user has no permission to execute the operation, the request is aborted with a 403 (Forbidden). 如果用户没有执行操作的权限,请求将以403 (Forbidden)中止。

To know the user who is performing the request, see my previous answer . 要了解正在执行请求的用户,请参阅我之前的回答 You can get it from the SecurityContext (which should be already set in the ContainerRequestContext ) or inject it using CDI, depending on the approach you go for. 您可以从SecurityContext (应该已经在ContainerRequestContext设置)获取它,或者使用CDI注入它,具体取决于您的方法。

If a @Secured annotation has no roles declared, you can assume all authenticated users can access that endpoint, disregarding the roles the users have. 如果@Secured注释未声明任何角色,则可以假定所有经过身份验证的用户都可以访问该端点,而忽略用户拥有的角色。

Supporting role-based authorization with JSR-250 annotations 使用JSR-250注释支持基于角色的授权

Alternatively to defining the roles in the @Secured annotation as shown above, you could consider JSR-250 annotations such as @RolesAllowed , @PermitAll and @DenyAll . 或者如上所示定义@Secured注释中的角色,您可以考虑JSR-250注释,例如@RolesAllowed@PermitAll@DenyAll

JAX-RS doesn't support such annotations out-of-the-box, but it could be achieved with a filter. JAX-RS不支持这种开箱即用的注释,但可以使用过滤器实现。 Here are a few considerations to keep in mind if you want to support all of them: 如果您想支持所有这些,请记住以下几点注意事项:

So an authorization filter that checks JSR-250 annotations could be like: 因此,检查JSR-250注释的授权过滤器可能类似于:

@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

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

        Method method = resourceInfo.getResourceMethod();

        // @DenyAll on the method takes precedence over @RolesAllowed and @PermitAll
        if (method.isAnnotationPresent(DenyAll.class)) {
            refuseRequest();
        }

        // @RolesAllowed on the method takes precedence over @PermitAll
        RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
        if (rolesAllowed != null) {
            performAuthorization(rolesAllowed.value(), requestContext);
            return;
        }

        // @PermitAll on the method takes precedence over @RolesAllowed on the class
        if (method.isAnnotationPresent(PermitAll.class)) {
            // Do nothing
            return;
        }

        // @DenyAll can't be attached to classes

        // @RolesAllowed on the class takes precedence over @PermitAll on the class
        rolesAllowed = 
            resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
        if (rolesAllowed != null) {
            performAuthorization(rolesAllowed.value(), requestContext);
        }

        // @PermitAll on the class
        if (resourceInfo.getResourceClass().isAnnotationPresent(PermitAll.class)) {
            // Do nothing
            return;
        }

        // Authentication is required for non-annotated methods
        if (!isAuthenticated(requestContext)) {
            refuseRequest();
        }
    }

    /**
     * Perform authorization based on roles.
     *
     * @param rolesAllowed
     * @param requestContext
     */
    private void performAuthorization(String[] rolesAllowed, 
                                      ContainerRequestContext requestContext) {

        if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
            refuseRequest();
        }

        for (final String role : rolesAllowed) {
            if (requestContext.getSecurityContext().isUserInRole(role)) {
                return;
            }
        }

        refuseRequest();
    }

    /**
     * Check if the user is authenticated.
     *
     * @param requestContext
     * @return
     */
    private boolean isAuthenticated(final ContainerRequestContext requestContext) {
        // Return true if the user is authenticated or false otherwise
        // An implementation could be like:
        // return requestContext.getSecurityContext().getUserPrincipal() != null;
    }

    /**
     * Refuse the request.
     */
    private void refuseRequest() {
        throw new AccessDeniedException(
            "You don't have permissions to perform this action.");
    }
}

Note: The above implementation is based on the Jersey RolesAllowedDynamicFeature . 注意:以上实现基于Jersey RolesAllowedDynamicFeature If you use Jersey, you don't need to write your own filter, just use the existing implementation. 如果您使用Jersey,则无需编写自己的过滤器,只需使用现有实现即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章