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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/26777083/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 03:16:03  来源:igfitidea点击:

Best practice for REST token-based authentication with JAX-RS and Jersey

javarestauthenticationjax-rsjersey-2.0

提问by DevOps85

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.

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

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 首先检查令牌,然后执行某些操作以检索资源。

采纳答案by cassiomolin

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.

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

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.
  1. 客户端将其凭据(用户名和密码)发送到服务器。
  2. 服务器验证凭据,如果它们有效,则为用户生成令牌。
  3. 服务器将先前生成的令牌与用户标识符和到期日期一起存储在某个存储器中。
  4. 服务器将生成的令牌发送给客户端。
  5. 客户端在每个请求中将令牌发送到服务器。
  6. 服务器在每个请求中从传入请求中提取令牌。使用令牌,服务器查找用户详细信息以执行身份验证。
    • 如果令牌有效,则服务器接受请求。
    • 如果令牌无效,则服务器拒绝请求。
  7. 一旦执行了身份验证,服务器就会执行授权。
  8. 服务器可以提供一个端点来刷新令牌。

Note:The step 3 is not required if the server has issued a signed token (such as JWT, which allows you to perform statelessauthentication).

注意:如果服务器已经发布了签名令牌(例如 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. So, it should work with JAX-RS 2.0 implementations, such as Jersey, RESTEasyand Apache CXF.

此解决方案仅使用 JAX-RS 2.0 API,避免使用任何特定于供应商的解决方案。因此,它应该适用于 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.xmldescriptor. It's a custom authentication.

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

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. The client must send the token to the server in every request.

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

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 Authorizationheader of the request. For example:

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

Authorization: Bearer <token-goes-here>

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

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

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

JAX-RS 提供@NameBinding了一个元注释,用于创建其他注释以将过滤器和拦截器绑定到资源类和方法。定义一个@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. The ContainerRequestContextcan be used to access the HTTP request headers and then extract the token:

上面定义的名称绑定注解将用于装饰过滤器类,它实现ContainerRequestFilter,允许您在请求被资源方法处理之前拦截请求。在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. Otherwise the request will proceed to a resource method.

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

Securing your REST endpoints

保护您的 REST 端点

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

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

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 onlyfor 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. The following approaches can be used to achieve it:

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

Overriding the security context of the current request

覆盖当前请求的安全上下文

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

在您的ContainerRequestFilter.filter(ContainerRequestContext)方法中,SecurityContext可以为当前请求设置一个新实例。然后覆盖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 SecurityContextin any JAX-RS resource class:

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

@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 AuthenticationFiltercreated above, inject an Eventannotated with @AuthenticatedUser:

在你AuthenticationFilter上面创建的,注入一个带Event注释的@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);

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 Userinstance with the correspondent username and assign it to the authenticatedUserproducer field:

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

@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 authenticatedUserfield produces a Userinstance 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 Userinstance (in fact, it's a CDI proxy):

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

@Inject
@AuthenticatedUser
User authenticatedUser;

Note that the CDI @Producesannotation is differentfrom the JAX-RS @Producesannotation:

请注意,CDI@Produces注释是不同从JAX-RS@Produces注解:

Be sure you use the CDI @Producesannotation in your AuthenticatedUserProducerbean.

确保@ProducesAuthenticatedUserProducerbean 中使用 CDI注释。

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.

这里的关键是用 注释的 bean @RequestScoped,允许您在过滤器和 bean 之间共享数据。如果您不想使用事件,您可以修改过滤器以将经过身份验证的用户存储在请求范围的 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 answerfor 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. You also could use:

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

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

JWT (JSON Web Token)

JWT(JSON 网络令牌)

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 Token)是一种用于在两方之间安全地表示声明的标准方法,由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. Here are some claims registered in the RFC 7519and what they mean (read the full RFC for further details):

它是一个自包含的令牌,它使您能够在claim 中存储详细信息。这些声明存储在令牌有效负载中,该有效负载是编码为Base64的 JSON 。以下是在RFC 7519 中注册的一些声明及其含义(阅读完整的 RFC 以获取更多详细信息):

  • iss: Principal that issued the token.
  • sub: Principal that is the subject of the JWT.
  • exp: Expiration date for the token.
  • nbf: Time on which the token will start to be accepted for processing.
  • iat: Time on which the token was issued.
  • jti: Unique identifier for the token.
  • iss:发行令牌的委托人。
  • sub: Principal 是 JWT 的主题。
  • 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.

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

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 (jticlaim) along with some other details such as the user you issued the token for, the expiration date, etc.

如果您不需要跟踪 JWT 令牌,则不需要保留 JWT 令牌。尽管如此,通过持久化令牌,您将有可能使它们的访问无效和撤销。为了跟踪 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 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 jticlaim 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 jticlaim against the token identifiers you have on server side.

jti声明应用于在令牌上存储令牌标识符。验证令牌时,请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. Alwaysdo it on the top of a HTTPS connection to prevent the man-in-the-middle attack.
  • Take a look at this questionfrom Information Security for more information about tokens.
  • In this articleyou will find some useful information about token-based authentication.
  • 您决定使用哪种类型的身份验证并不重要。始终在 HTTPS 连接的顶部执行此操作以防止中间人攻击
  • 有关令牌的更多信息,请查看Information Security 的这个问题
  • 在本文中,您将找到一些有关基于令牌的身份验证的有用信息。

回答by cassiomolin

This answer is all about authorizationand it is a complement of my previous answerabout authentication

Why anotheranswer?I attempted to expand my previous answer by adding details on how to support JSR-250 annotations. However the original answer became the way too longand exceeded the maximum length of 30,000 characters. So I moved the whole authorization details to this answer, keeping the other answer focused on performing authentication and issuing tokens.

这个答案是关于授权的,它是我之前关于身份验证的答案的补充

为什么是另一个答案?我试图通过添加有关如何支持 JSR-250 注释的详细信息来扩展我之前的答案。然而原来的答案变得太长,超过了30,000个字符最大长度。所以我将整个授权细节移到了这个答案,让另一个答案专注于执行身份验证和颁发令牌。



Supporting role-based authorization with the @Securedannotation

使用@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 @Securedname 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 @Securedto perform the authorization. The method annotations will override the class annotations:

然后注释资源类和方法@Secured以执行授权。方法注释将覆盖类注释:

@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 AUTHORIZATIONpriority, which is executed after the AUTHENTICATIONpriority filter defined previously.

创建具有AUTHORIZATION优先级的过滤器,该AUTHENTICATION过滤器在之前定义的优先级过滤器之后执行。

The ResourceInfocan be used to get the resource Methodand resource Classthat will handle the request and then extract the @Securedannotations from them:

ResourceInfo可用于获取资源Method和资源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(禁止)。

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 @Securedannotation 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 @Securedannotation as shown above, you could consider JSR-250 annotations such as @RolesAllowed, @PermitAlland @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. Here are a few considerations to keep in mind if you want to support all of them:

JAX-RS 不支持这种开箱即用的注释,但可以通过过滤器来实现。如果您想支持所有这些,请记住以下几点注意事项:

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. If you use Jersey, you don't need to write your own filter, just use the existing implementation.

注意:以上实现基于 Jersey RolesAllowedDynamicFeature。如果使用 Jersey,则无需编写自己的过滤器,只需使用现有实现即可。