Thực tiễn tốt nhất để xác thực dựa trên mã thông báo REST với JAX-RS và Jersey


459

Tôi đang tìm cách kích hoạt xác thực dựa trên mã thông báo ở Jersey. Tôi đang cố gắng không sử dụng bất kỳ khuôn khổ cụ thể. Điều đó có thể không?

Kế hoạch của tôi là: Một người dùng đăng ký dịch vụ web của tôi, dịch vụ web của tôi tạo mã thông báo, gửi cho khách hàng và khách hàng sẽ giữ lại. Sau đó, khách hàng, với mỗi yêu cầu, sẽ gửi mã thông báo thay vì tên người dùng và mật khẩu.

Tôi đã nghĩ đến việc sử dụng bộ lọc tùy chỉnh cho từng yêu cầu và @PreAuthorize("hasRole('ROLE')") tôi chỉ nghĩ rằng điều này gây ra rất nhiều yêu cầu đến cơ sở dữ liệu để kiểm tra xem mã thông báo có hợp lệ không.

Hoặc không tạo bộ lọc và trong mỗi yêu cầu đặt mã thông báo param? Vì vậy, mỗi API trước tiên sẽ kiểm tra mã thông báo và sau khi thực hiện một cái gì đó để lấy tài nguyên.

Câu trả lời:


1388

Cách xác thực dựa trên mã thông báo

Trong xác thực dựa trên mã thông báo, khách hàng trao đổi thông tin xác thực cứng (như tên người dùng và mật khẩu) cho một phần dữ liệu được gọi là mã thông báo . Đối với mỗi yêu cầu, thay vì gửi thông tin xác thực cứng, khách hàng sẽ gửi mã thông báo đến máy chủ để thực hiện xác thực và sau đó ủy quyền.

Trong một vài từ, một sơ đồ xác thực dựa trên các mã thông báo theo các bước sau:

  1. Máy khách sẽ gửi thông tin đăng nhập của họ (tên người dùng và mật khẩu) đến máy chủ.
  2. Máy chủ xác thực thông tin đăng nhập và nếu chúng hợp lệ, sẽ tạo mã thông báo cho người dùng.
  3. Máy chủ lưu trữ mã thông báo được tạo trước đó trong một số lưu trữ cùng với số nhận dạng người dùng và ngày hết hạn.
  4. Máy chủ gửi mã thông báo được tạo cho khách hàng.
  5. Máy khách gửi mã thông báo đến máy chủ trong mỗi yêu cầu.
  6. Máy chủ, trong mỗi yêu cầu, trích xuất mã thông báo từ yêu cầu đến. Với mã thông báo, máy chủ tra cứu chi tiết người dùng để thực hiện xác thực.
    • Nếu mã thông báo hợp lệ, máy chủ chấp nhận yêu cầu.
    • Nếu mã thông báo không hợp lệ, máy chủ sẽ từ chối yêu cầu.
  7. Khi xác thực đã được thực hiện, máy chủ thực hiện ủy quyền.
  8. Máy chủ có thể cung cấp điểm cuối để làm mới mã thông báo.

Lưu ý: Bước 3 không bắt buộc nếu máy chủ đã phát hành mã thông báo đã ký (chẳng hạn như JWT, cho phép bạn thực hiện xác thực không trạng thái ).

Bạn có thể làm gì với JAX-RS 2.0 (Jersey, REST EAS và Apache CXF)

Giải pháp này chỉ sử dụng API JAX-RS 2.0, tránh mọi giải pháp cụ thể của nhà cung cấp . Vì vậy, nó nên hoạt động với các triển khai JAX-RS 2.0, chẳng hạn như Jersey , REST EASApache CXF .

Điều đáng nói là nếu bạn đang sử dụng xác thực dựa trên mã thông báo, bạn không dựa vào các cơ chế bảo mật ứng dụng web Java EE tiêu chuẩn được cung cấp bởi bộ chứa servlet và có thể định cấu hình thông qua bộ web.xmlmô tả của ứng dụng . Đó là một xác thực tùy chỉnh.

Xác thực người dùng bằng tên người dùng và mật khẩu của họ và phát hành mã thông báo

Tạo phương thức tài nguyên JAX-RS để nhận và xác thực thông tin đăng nhập (tên người dùng và mật khẩu) và cấp mã thông báo cho người dùng:

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

Nếu bất kỳ trường hợp ngoại lệ nào được đưa ra khi xác thực thông tin đăng nhập, phản hồi với trạng thái 403(Bị cấm) sẽ được trả về.

Nếu thông tin đăng nhập được xác thực thành công, phản hồi với trạng thái 200(OK) sẽ được trả về và mã thông báo đã phát hành sẽ được gửi cho khách hàng trong tải trọng phản hồi. Máy khách phải gửi mã thông báo đến máy chủ trong mọi yêu cầu.

Khi tiêu thụ application/x-www-form-urlencoded, khách hàng phải gửi thông tin đăng nhập theo định dạng sau trong tải trọng yêu cầu:

username=admin&password=123456

Thay vì tham số mẫu, có thể bọc tên người dùng và mật khẩu vào một lớp:

public class Credentials implements Serializable {

    private String username;
    private String password;

    // Getters and setters omitted
}

Và sau đó sử dụng nó dưới dạng 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
}

Sử dụng phương pháp này, khách hàng phải gửi thông tin đăng nhập theo định dạng sau trong tải trọng của yêu cầu:

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

Trích xuất mã thông báo từ yêu cầu và xác thực nó

Khách hàng nên gửi mã thông báo trong tiêu Authorizationđề HTTP tiêu chuẩn của yêu cầu. Ví dụ:

Authorization: Bearer <token-goes-here>

Tên của tiêu đề HTTP tiêu chuẩn là không may vì nó mang thông tin xác thực , không ủy quyền . Tuy nhiên, đó là tiêu đề HTTP tiêu chuẩn để gửi thông tin đăng nhập đến máy chủ.

JAX-RS cung cấp @NameBinding, một chú thích meta được sử dụng để tạo các chú thích khác để liên kết các bộ lọc và bộ chặn với các lớp và phương thức tài nguyên. Xác định một @Securedchú thích như sau:

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

Chú thích liên kết tên được xác định ở trên sẽ được sử dụng để trang trí một lớp bộ lọc, thực hiện ContainerRequestFilter, cho phép bạn chặn yêu cầu trước khi nó được xử lý bằng phương thức tài nguyên. Có ContainerRequestContextthể được sử dụng để truy cập các tiêu đề yêu cầu HTTP và sau đó trích xuất mã thông báo:

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

Nếu có bất kỳ vấn đề nào xảy ra trong quá trình xác thực mã thông báo, phản hồi với trạng thái 401(Không được phép) sẽ được trả lại. Nếu không, yêu cầu sẽ tiến hành một phương thức tài nguyên.

Đảm bảo các điểm cuối REST của bạn

Để liên kết bộ lọc xác thực với các phương thức tài nguyên hoặc các lớp tài nguyên, chú thích chúng với @Securedchú thích được tạo ở trên. Đối với các phương thức và / hoặc các lớp được chú thích, bộ lọc sẽ được thực thi. Điều đó có nghĩa là các điểm cuối như vậy sẽ chỉ đạt được nếu yêu cầu được thực hiện với mã thông báo hợp lệ.

Nếu một số phương thức hoặc lớp không cần xác thực, chỉ cần không chú thích chúng:

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

Trong ví dụ trình bày ở trên, bộ lọc sẽ được thực hiện chỉ cho mySecuredMethod(Long)phương pháp vì nó chú thích với @Secured.

Xác định người dùng hiện tại

Rất có khả năng bạn sẽ cần biết người dùng đang thực hiện yêu cầu lại API REST của bạn. Các cách tiếp cận sau đây có thể được sử dụng để đạt được nó:

Ghi đè bối cảnh bảo mật của yêu cầu hiện tại

Trong ContainerRequestFilter.filter(ContainerRequestContext)phương thức của bạn , một thể hiện mới SecurityContextcó thể được đặt cho yêu cầu hiện tại. Sau đó ghi đè lên SecurityContext.getUserPrincipal(), trả về một Principalthể hiện:

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

Sử dụng mã thông báo để tra cứu định danh người dùng (tên người dùng), đó sẽ là Principaltên của.

Tiêm SecurityContextvào bất kỳ lớp tài nguyên JAX-RS nào:

@Context
SecurityContext securityContext;

Điều tương tự có thể được thực hiện trong phương thức tài nguyên JAX-RS:

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

Và sau đó nhận được Principal:

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

Sử dụng CDI (Bối cảnh và tiêm phụ thuộc)

Nếu, vì một số lý do, bạn không muốn ghi đè lên SecurityContext, bạn có thể sử dụng CDI (Bối cảnh và phụ thuộc tiêm), cung cấp các tính năng hữu ích như sự kiện và nhà sản xuất.

Tạo vòng loại CDI:

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

Trong phần AuthenticationFilterđã tạo của bạn ở trên, hãy thêm một Eventchú thích với@AuthenticatedUser :

@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;

Nếu xác thực thành công, hãy kích hoạt sự kiện chuyển tên người dùng làm tham số (hãy nhớ, mã thông báo được cấp cho người dùng và mã thông báo sẽ được sử dụng để tra cứu định danh người dùng):

userAuthenticatedEvent.fire(username);

Rất có khả năng có một lớp đại diện cho người dùng trong ứng dụng của bạn. Hãy gọi lớp này User.

Tạo một hạt CDI để xử lý sự kiện xác thực, tìm một Userthể hiện với tên người dùng tương ứng và gán nó cho trường authenticatedUsernhà sản xuất:

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

Các authenticatedUserlĩnh vực sản xuất ra một Userví dụ có thể được tiêm vào container quản lý đậu, chẳng hạn như các dịch vụ JAX-RS, đậu CDI, servlets và EJB. Sử dụng đoạn mã sau để thêm một Userthể hiện (trên thực tế, đó là proxy CDI):

@Inject
@AuthenticatedUser
User authenticatedUser;

Lưu ý rằng @Produceschú thích CDI khác với @Produceschú thích JAX-RS :

Hãy chắc chắn rằng bạn sử dụng @Produceschú thích CDI trong AuthenticatedUserProducerbean của bạn .

Chìa khóa ở đây là bean được chú thích @RequestScoped, cho phép bạn chia sẻ dữ liệu giữa các bộ lọc và các bean của bạn. Nếu bạn không sử dụng các sự kiện, bạn có thể sửa đổi bộ lọc để lưu trữ người dùng được xác thực trong một bean có phạm vi yêu cầu và sau đó đọc nó từ các lớp tài nguyên JAX-RS của bạn.

So với cách tiếp cận ghi đè SecurityContext, phương pháp CDI cho phép bạn có được người dùng được xác thực từ các loại đậu khác ngoài các nhà cung cấp và tài nguyên của JAX-RS.

Hỗ trợ ủy quyền dựa trên vai trò

Vui lòng tham khảo câu trả lời khác của tôi để biết chi tiết về cách hỗ trợ ủy quyền dựa trên vai trò.

Phát hành mã thông báo

Mã thông báo có thể là:

  • Opaque: Hiển thị không có chi tiết nào ngoài giá trị (như một chuỗi ngẫu nhiên)
  • Tự chứa: Chứa các chi tiết về chính mã thông báo (như JWT).

Xem chi tiết bên dưới:

Chuỗi ngẫu nhiên dưới dạng mã thông báo

Mã thông báo có thể được phát hành bằng cách tạo một chuỗi ngẫu nhiên và lưu nó vào cơ sở dữ liệu cùng với mã định danh người dùng và ngày hết hạn. Một ví dụ điển hình về cách tạo một chuỗi ngẫu nhiên trong Java có thể được nhìn thấy ở đây . Bạn cũng có thể sử dụng:

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

JWT (Mã thông báo web JSON)

JWT (JSON Web Token) là một phương pháp tiêu chuẩn để thể hiện các khiếu nại một cách an toàn giữa hai bên và được xác định bởi RFC 7519 .

Đó là mã thông báo độc lập và nó cho phép bạn lưu trữ thông tin chi tiết trong khiếu nại . Các khiếu nại này được lưu trữ trong tải trọng mã thông báo được mã hóa JSON là Base64 . Dưới đây là một số khiếu nại được đăng ký trong RFC 7519 và ý nghĩa của chúng (đọc RFC đầy đủ để biết thêm chi tiết):

  • iss: Hiệu trưởng đã phát hành mã thông báo.
  • sub: Hiệu trưởng là chủ đề của JWT.
  • exp: Ngày hết hạn cho mã thông báo.
  • nbf: Thời gian mã thông báo sẽ bắt đầu được chấp nhận để xử lý.
  • iat: Thời gian mã thông báo được phát hành.
  • jti: Mã định danh duy nhất cho mã thông báo.

Xin lưu ý rằng bạn không được lưu trữ dữ liệu nhạy cảm, như mật khẩu, trong mã thông báo.

Tải trọng có thể được đọc bởi khách hàng và có thể dễ dàng kiểm tra tính toàn vẹn của mã thông báo bằng cách xác minh chữ ký của nó trên máy chủ. Chữ ký là thứ ngăn chặn mã thông báo bị giả mạo.

Bạn sẽ không cần phải duy trì mã thông báo JWT nếu bạn không cần theo dõi chúng. Mặc dù, bằng cách duy trì mã thông báo, bạn sẽ có khả năng vô hiệu hóa và thu hồi quyền truy cập của chúng. Để theo dõi mã thông báo JWT, thay vì duy trì toàn bộ mã thông báo trên máy chủ, bạn có thể duy trì mã định danh mã thông báo (jti xác nhận) cùng với một số chi tiết khác như người dùng bạn đã cấp mã thông báo, ngày hết hạn, v.v.

Khi các mã thông báo vẫn tồn tại, luôn luôn xem xét loại bỏ các mã cũ để ngăn cơ sở dữ liệu của bạn phát triển vô thời hạn.

Sử dụng JWT

Có một vài thư viện Java để phát hành và xác thực các mã thông báo JWT, chẳng hạn như:

Để tìm một số tài nguyên tuyệt vời khác để làm việc với JWT, hãy xem http://jwt.io .

Xử lý thu hồi mã thông báo với JWT

Nếu bạn muốn thu hồi mã thông báo, bạn phải theo dõi chúng. Bạn không cần lưu trữ toàn bộ mã thông báo ở phía máy chủ, chỉ lưu trữ mã định danh mã thông báo (phải là duy nhất) và một số siêu dữ liệu nếu bạn cần. Đối với mã định danh mã thông báo, bạn có thể sử dụng UUID .

Yêu jticầu nên được sử dụng để lưu trữ mã định danh mã thông báo trên mã thông báo. Khi xác thực mã thông báo, đảm bảo rằng nó chưa bị thu hồi bằng cách kiểm tra giá trị của jtikhiếu nại đối với các mã định danh mã thông báo bạn có ở phía máy chủ.

Vì mục đích bảo mật, hãy thu hồi tất cả các mã thông báo cho người dùng khi họ thay đổi mật khẩu.

Thông tin thêm

  • Việc bạn quyết định sử dụng loại xác thực nào không quan trọng. Luôn luôn làm điều đó trên đầu kết nối HTTPS để ngăn chặn cuộc tấn công giữa chừng .
  • Hãy xem câu hỏi này từ Bảo mật thông tin để biết thêm thông tin về mã thông báo.
  • Trong bài viết này, bạn sẽ tìm thấy một số thông tin hữu ích về xác thực dựa trên mã thông báo.

The server stores the previously generated token in some storage along with the user identifier and an expiration date. The server sends the generated token to the client. Làm thế nào là RESTful này?
scottysseus

3
@scottyseus Xác thực dựa trên mã thông báo hoạt động bằng cách máy chủ ghi nhớ mã thông báo đã phát hành. Bạn có thể sử dụng mã thông báo JWT để xác thực không trạng thái.
cassiomolin

Điều gì về việc gửi mật khẩu băm thay vì đơn giản (băm với nonce do máy chủ tạo)? Nó có tăng mức độ bảo mật (ví dụ khi không sử dụng https) không? Trong trường hợp người đàn ông ở giữa - anh ta sẽ có thể chiếm quyền điều khiển một phiên, nhưng ít nhất anh ta sẽ không nhận được mật khẩu
Denis Itskovich

15
Tôi không thể tin rằng điều này không có trong tài liệu chính thức.
Daniel M.

2
@grep Trong REST, không có phiên nào như phiên ở phía máy chủ. Do đó, trạng thái phiên được quản lý ở phía khách hàng.
cassiomolin

98

Câu trả lời này là tất cả về ủy quyền và nó là phần bổ sung cho câu trả lời trước đây của tôi về xác thực

Tại sao câu trả lời khác ? Tôi đã cố gắng mở rộng câu trả lời trước đây của mình bằng cách thêm chi tiết về cách hỗ trợ các chú thích JSR-250. Tuy nhiên, câu trả lời ban đầu đã trở thành quá dài và vượt quá độ dài tối đa 30.000 ký tự . Vì vậy, tôi đã chuyển toàn bộ chi tiết ủy quyền cho câu trả lời này, giữ cho câu trả lời khác tập trung vào việc thực hiện xác thực và phát hành mã thông báo.


Hỗ trợ ủy quyền dựa trên vai trò với @Securedchú thích

Bên cạnh luồng xác thực được hiển thị trong câu trả lời khác , ủy quyền dựa trên vai trò có thể được hỗ trợ trong các điểm cuối REST.

Tạo một bảng liệt kê và xác định vai trò theo nhu cầu của bạn:

public enum Role {
    ROLE_1,
    ROLE_2,
    ROLE_3
}

Thay đổi @Securedchú thích ràng buộc tên được tạo trước đó để hỗ trợ vai trò:

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

Và sau đó chú thích các lớp tài nguyên và phương thức @Securedđể thực hiện ủy quyền. Các chú thích phương thức sẽ ghi đè các chú thích lớp:

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

Tạo bộ lọc với AUTHORIZATIONmức độ ưu tiên, được thực hiện sau AUTHENTICATIONbộ lọc ưu tiên được xác định trước đó.

ResourceInfothể được sử dụng để lấy tài nguyên Methodvà tài nguyên Classsẽ xử lý yêu cầu và sau đó trích xuất các @Securedchú thích từ chúng:

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

Nếu người dùng không có quyền thực hiện thao tác, yêu cầu sẽ bị hủy bỏ bằng 403(Cấm).

Để biết người dùng đang thực hiện yêu cầu, hãy xem câu trả lời trước của tôi . Bạn có thể lấy nó từ SecurityContext(cần được đặt trong ContainerRequestContext) hoặc tiêm nó bằng CDI, tùy thuộc vào cách tiếp cận bạn thực hiện.

Nếu một @Secured chú thích không có vai trò nào được khai báo, bạn có thể cho rằng tất cả người dùng được xác thực có thể truy cập điểm cuối đó, bỏ qua các vai trò mà người dùng có.

Hỗ trợ ủy quyền dựa trên vai trò với các chú thích JSR-250

Ngoài ra, để xác định các vai trò trong @Securedchú thích như được hiển thị ở trên, bạn có thể xem xét các chú thích JSR-250 như @RolesAllowed, @PermitAll@DenyAll.

JAX-RS không hỗ trợ các chú thích như vậy, nhưng có thể đạt được bằng bộ lọc. Dưới đây là một số lưu ý cần lưu ý nếu bạn muốn hỗ trợ tất cả chúng:

Vì vậy, một bộ lọc ủy quyền kiểm tra các chú thích JSR-250 có thể giống như:

@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.");
    }
}

Lưu ý: Việc thực hiện trên được dựa trên Jersey RolesAllowedDynamicFeature. Nếu bạn sử dụng Jersey, bạn không cần phải viết bộ lọc của riêng mình, chỉ cần sử dụng triển khai hiện có.


Có bất kỳ kho lưu trữ github với giải pháp thanh lịch này có sẵn?
Daniel Ferreira Castro

6
@DanielFerreiraCastro Tất nhiên rồi. Có một cái nhìn ở đây .
cassiomolin

Có cách nào tốt để xác thực rằng một yêu cầu là từ người dùng được ủy quyền VÀ người dùng đó CÓ THỂ thay đổi dữ liệu vì anh ta "sở hữu" dữ liệu (ví dụ: tin tặc không thể sử dụng mã thông báo của mình để thay đổi tên của người dùng khác)? Tôi biết tôi có thể kiểm tra tại mọi điểm cuối nếu user_id== token.userId, hoặc một cái gì đó tương tự, nhưng điều này rất lặp đi lặp lại.
mFeinstein

@mFeinstein Một câu trả lời cho điều đó chắc chắn sẽ đòi hỏi nhiều nhân vật hơn tôi có thể gõ vào đây trong các bình luận. Chỉ cần cung cấp cho bạn một số hướng, bạn có thể tìm kiếm bảo mật cấp hàng .
cassiomolin

Tôi có thể thấy rất nhiều chủ đề trên cơ sở dữ liệu khi tôi tìm kiếm bảo mật cấp hàng, tôi sẽ mở nó dưới dạng một câu hỏi mới sau đó
mFeinstein
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.