使用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,則無需編寫自己的過濾器,只需使用現有實現即可。

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