Chiến lược ủy quyền API REST


8

Có rất nhiều câu hỏi ở đây liên quan đến các cơ chế xác thực và ủy quyền của API RESTful nhưng không có câu hỏi nào xuất hiện để đi vào chi tiết về cách triển khai các dịch vụ bảo mật ở cấp ứng dụng.

Ví dụ: giả sử ứng dụng web của tôi (tôi đã có Java nhưng điều này áp dụng cho bất kỳ phụ trợ nào thực sự) có hệ thống xác thực an toàn cho phép người dùng API đăng nhập bằng tên người dùng và mật khẩu. Khi người dùng đưa ra yêu cầu, tại bất kỳ thời điểm nào trong quá trình xử lý yêu cầu, tôi có thể gọi một getAuthenticatedUser()phương thức sẽ trả về người dùng null nếu người dùng không đăng nhập hoặc đối tượng miền Người dùng đại diện cho người dùng đã đăng nhập.

API cho phép người dùng được xác thực truy cập dữ liệu của họ, ví dụ như GET /api/orders/sẽ trả về danh sách đơn đặt hàng của người dùng đó. Tương tự, một GET để /api/tasks/{task_id}trả về dữ liệu liên quan đến nhiệm vụ cụ thể đó.

Giả sử rằng có một số đối tượng miền khác nhau có thể được liên kết với tài khoản của người dùng (đơn hàng và tác vụ là hai ví dụ, chúng tôi cũng có thể có khách hàng, hóa đơn, v.v.). Chúng tôi chỉ muốn người dùng được xác thực có thể truy cập dữ liệu về các đối tượng của chính họ vì vậy khi người dùng thực hiện cuộc gọi đến /api/invoices/{invoice_id}chúng tôi cần kiểm tra xem người dùng có được phép truy cập tài nguyên đó trước khi chúng tôi phục vụ không.

Câu hỏi của tôi là sau đó, các mô hình hoặc chiến lược tồn tại để giải quyết vấn đề ủy quyền này? Một tùy chọn tôi đang xem xét là tạo giao diện trợ giúp (nghĩa là SecurityUtils.isUserAuthorized(user, object)), có thể được gọi trong quá trình xử lý yêu cầu để đảm bảo rằng người dùng được ủy quyền để tìm nạp đối tượng. Điều này không lý tưởng vì nó gây ô nhiễm mã điểm cuối của ứng dụng với nhiều cuộc gọi này, vd

Object someEndpoint(int objectId) {
    if (!SecurityUtils.isUserAuthorized(loggedInUser, objectDAO.get(objectId)) {
        throw new UnauthorizedException();
    }
    ...
}

... Và sau đó là câu hỏi về việc triển khai phương pháp này cho mọi loại tên miền có thể gây ra một chút khó khăn. Đây có thể là lựa chọn duy nhất nhưng tôi rất muốn nghe đề xuất của bạn!


Khi bạn nói người dùng "đăng nhập", bạn có nghĩa là bạn đang duy trì một phiên?
JimmyJames

Câu trả lời:


8

Xin cho tình yêu của Thiên Chúa không tạo ra một SecurityUtilslớp học!

Lớp học của bạn sẽ trở thành 10k dòng mã spaghetti trong vài tháng! Bạn sẽ cần phải có một Actionloại (tạo, đọc, cập nhật, hủy, liệt kê, v.v.) vào isUserAuthorized()phương thức của bạn , nó sẽ nhanh chóng trở thành một switchcâu lệnh dài hàng nghìn với logic ngày càng phức tạp, khó kiểm tra đơn vị. Đừng làm điều đó.


Nói chung, những gì tôi làm, ít nhất là trong Ruby on Rails, mỗi đối tượng miền phải chịu trách nhiệm cho các đặc quyền truy cập của riêng mình bằng cách có một lớp chính sách cho mỗi mô hình . Sau đó, bộ điều khiển hỏi lớp chính sách xem người dùng hiện tại cho yêu cầu có quyền truy cập vào tài nguyên hay không. Đây là một ví dụ trong Ruby, vì tôi chưa bao giờ triển khai bất cứ thứ gì giống như trong Java, nhưng ý tưởng sẽ xuất hiện rõ ràng:

class OrderPolicy

    class Scope < Struct.new(:user, :scope)

        def resolve

            # A user must be logged in to interact with this resource at all
            raise NotAuthorizedException unless user

            # Admin/moderator can see all orders
            if (user.admin? || user.moderator?)
                scope.all
            else
                # Only allow the user to see their own orders
                scope.where(orderer_id: user.id)
            end
        end
    end

    # Constructor, if you don't know Ruby
    def initialize(user, order)
        raise NotAuthorizedException unless user
        @user = user
        @order= order
    end

    # Whitelist what data can be manipulated by each type of user
    def valid_attributes
        if @user.admin?
            [:probably, :want, :to, :let, :admin, :update, :everything]
        elsif @user.moderator?
            [:fewer, :attributes, :but, :still, :most]
        else
            [:regualar, :user, :attributes]
        end
    end

    # Maybe restrict updatable attributes further
    def valid_update_attributes
    end

    # Who can create new orders
    def create?
        true # anyone, and they would have been authenticated already by #initialize
    end

    # Read operation
    def show?
        @user.admin? || @user.moderator? || owns_order
    end

    # Only superusers can update resources
    def update?
        @user.admin? || @user.moderator?
    end

    # Only admins can delete, because it's extremely destructive or whatever
    def destroy?
        @user.admin?
    end

    private

    # A user 'owns' an order if they were the person who submitted the order
    # E.g. superusers can access the order, but they didn't create it
    def owns_order
        @order.orderer_id == @user.id
    end
end

Ngay cả khi bạn có các tài nguyên lồng nhau phức tạp, một số tài nguyên phải 'sở hữu' các tài nguyên được lồng, do đó logic cấp cao nhất vốn đã sủi bọt. Tuy nhiên, các tài nguyên lồng nhau đó cần các lớp chính sách của riêng chúng trong trường hợp chúng có thể được cập nhật độc lập với tài nguyên 'cha mẹ'.

Trong ứng dụng của tôi, dành cho khoa của trường đại học của tôi, mọi thứ đều xoay quanh Courseđối tượng. Đây không phải là một Userứng dụng lập dị. Tuy nhiên, Users được đăng ký Course, vì vậy tôi chỉ có thể đảm bảo rằng:

@course.users.include? current_user && (whatever_other_logic_I_need)

đối với bất kỳ tài nguyên nào mà một tài nguyên cụ thể Usercần sửa đổi, vì hầu như tất cả các tài nguyên được gắn với a Course. Điều này được thực hiện trong lớp chính sách trong owns_whateverphương thức.

Tôi chưa thực hiện nhiều kiến ​​trúc Java, nhưng dường như bạn có thể tạo một Policygiao diện, trong đó các tài nguyên khác nhau cần được xác thực phải thực hiện giao diện. Sau đó, bạn có tất cả các phương thức cần thiết có thể trở nên phức tạp như bạn cần chúng cho mỗi đối tượng miền . Điều quan trọng là buộc logic vào chính mô hình, nhưng đồng thời giữ nó trong một lớp riêng (nguyên tắc trách nhiệm duy nhất (SRP)).

Hành động điều khiển của bạn có thể trông giống như:

public List<Order> index(OrderQuery query) {

    authorize(Order.class)
    // you should be auto-rescuing the NotAuthorizedException thrown by
    //the policy class at the controller level (or application level)

    // if the authorization didn't fail/rescue from exception, just render the resource
    List<Order> orders = db.search(query);
    return renderJSON(orders);
}

public Order show(int orderId) {

    authorize(Order.class)
    Order order = db.find(orderId);
    return renderJSON(order);
}

1

Một giải pháp thuận tiện hơn là sử dụng các chú thích để đánh dấu các phương thức đòi hỏi một số hình thức ủy quyền. Điều này nổi bật so với mã doanh nghiệp của bạn và có thể được xử lý bởi Spring Security hoặc mã AOP tùy chỉnh. Nếu bạn sử dụng các chú thích này trên các phương thức kinh doanh của mình thay vì các điểm cuối, bạn có thể chắc chắn nhận được một ngoại lệ khi người dùng trái phép cố gắng gọi chúng bất kể điểm nhập cảnh.


Không phải những gì tôi đang hỏi. Chú thích mùa xuân cho phép bạn đảm bảo rằng người dùng có cấp ủy quyền cụ thể (ví dụ: người dùng là quản trị viên) nhưng tôi không tin rằng họ giúp hạn chế quyền truy cập vào các thực thể cụ thể. Giả sử logic nghiệp vụ tìm nạp và trả về hóa đơn dựa trên id. Tôi đăng nhập và cung cấp id hóa đơn của người khác đến điểm cuối. Tôi không tin các chú thích mùa xuân ngăn chặn truy cập theo chiều ngang của hình thức đó?
HJCee

1
@HJCee Chú thích chứng khoán mùa xuân AOP khá biểu cảm . Bạn có thể định nghĩa một @PreAuthorization("hasRole('ADMIN') and #requestingUser.company.uuid == authentication.details.companyUuid")chú thích trong đó #requestingUserphân đoạn tham chiếu đến một đối tượng bị bỏ qua với tên trường requestingUsercó một phương thức getCompany()mà đối tượng trả về có một phương thức khác getUuid(). Các authenticationtham chiếu đến các Authenticationđối tượng được lưu trữ trong bối cảnh bảo mật.
Roman Vottner

1
@RomanVottner Điều gì xảy ra nếu bạn cần ủy quyền thực sự phức tạp? chẳng hạn, chỉ người điều hành trên Stack Exchange với huy hiệu vàng X trong thẻ Y mới có thể thực hiện các chỉnh sửa để xóa câu hỏi (hoặc bất cứ điều gì). Tôi sẽ chuyển vào chú thích một dòng gồm 300 ký tự.
Chris Cirefice

1
@ChrisCirefice Hoặc sử dụng @PreAuthorize("hasPermission(#user, 'allowDoSomething')")và triển khai trình đánh giá quyền tùy chỉnh của bạn hoặc viết trình xử lý biểu thức tùy chỉnh và root . Nếu bạn muốn thay đổi hành vi của các chú thích có sẵn, hãy xem chủ đề này
Roman Vottner

0

Sử dụng bảo mật dựa trên khả năng.

Một khả năng là một đối tượng không thể tha thứ, đóng vai trò là bằng chứng cho thấy người ta có thể thực hiện một hành động nhất định. Trong trường hợp này:

  • Làm cho mỗi vai trò (tập hợp các hành động được phép) là một giao diện.
  • Có các hoạt động yêu cầu xác thực là phương thức trên các giao diện tương ứng của chúng. Chúng nên ném một ngoại lệ nếu người nhận không phải là người dùng hiện tại của yêu cầu, nếu có thể.

Điều này khiến bạn không thể cố gắng làm điều gì đó mà người dùng hiện tại không được phép làm.

Đó là điều không thể


1
Không phải là người chỉ trích, nhưng đây không phải là TL; DR trong câu trả lời của tôi sao? Nếu có, bạn chỉ nên bình luận về câu trả lời của tôi thay vì tự viết :)
Chris Cirefice

Không hẳn. Ý tưởng ở đây là thể hiện các vai trò khác nhau mà người dùng có thể có trong hệ thống loại Java, sao cho bạn không thể gọi một phương thức trên người dùng yêu cầu đặc quyền mà người dùng không có.
Demi

Ngoài nhận xét của Chris, câu hỏi của tôi không phải là về hạn chế truy cập dựa trên vai trò (không quan trọng để thực hiện với bất kỳ khung web tốt nào) mà là về các hạn chế truy cập dựa trên mối liên hệ giữa người dùng và dữ liệu ('là đối tượng X thuộc sở hữu của người dùng Y' là một ví dụ thực sự đơn giản về một hiệp hội như vậy nhưng chúng có thể rất phức tạp). Đó là vấn đề tôi thực sự đang cố gắng để có được lời khuyên.
HJCee
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.