Vert.x có một tính năng không khung nào khác có: nối thẳng event bus ra trình duyệt. Trang web eb.send("tin.moi", ...) và một verticle ở máy chủ nhận được. Bài này đo cái giá bảo mật của tiện lợi đó.

Cầu nối là hai dòng

Router r = Router.router(vertx);
r.route("/eventbus/*").subRouter(
    SockJSHandler.create(vertx).bridge(new SockJSBridgeOptions()
        .addInboundPermitted(new PermittedOptions().setAddress("tin.cong-khai"))
        .addOutboundPermitted(new PermittedOptions().setAddress("tin.cong-khai"))));

inbound là thông điệp từ trình duyệt vào, outboundtừ máy chủ ra. Hai chiều khai riêng, và đó là thiết kế đúng.

Ba mức cấu hình, đo bằng bộ đếm phía máy chủ

Máy chủ có hai địa chỉ: tin.cong-khai (dành cho trang web) và quan-tri.xoa-tat-ca (nội bộ). Tôi viết một client giả lập trình duyệt, nối vào /eventbus/websocket rồi gửi khung SockJS thẳng tới cả hai:

Cấu hình tin.cong-khai quan-tri.xoa-tat-ca
Không khai quyền nào 0 0
Chỉ cho phép tin.cong-khai 1 0
setAddressRegex(".*") 1 1

Số trong bảng là số lần consumer phía máy chủ thật sự được gọi.

Dòng đầu là tin tốt: mặc định là từ chối tất cả. Dựng cầu nối mà quên khai quyền thì không gì đi qua được — Vert.x chọn mặc định an toàn, khác với nhiều thư viện.

Dòng cuối là vấn đề. Với .*, client trong trình duyệt gọi được quan-tri.xoa-tat-ca — một địa chỉ chưa bao giờ dành cho ai ngoài máy chủ.

Event bus là không gian tên phẳng. Nó không có khái niệm "địa chỉ nội bộ" hay "địa chỉ công khai"; mọi verticle trong ứng dụng — và trong cả cụm, như phần 21 đo được — đều nghe trên cùng một không gian đó. Một dòng .* trong cầu nối biến toàn bộ không gian ấy thành API công khai.

.* không phải chuyện giả tưởng: nó là thứ người ta viết khi đang phát triển để "cho chạy đã", rồi quên gỡ.

Ba luật khi mở cầu nối

Liệt kê từng địa chỉ, đừng dùng regex bắt tất. Nếu phải dùng regex thì neo nó: ^tin\. chứ không phải tin.*, và không bao giờ .*.

Đặt tiền tố rõ ràng cho địa chỉ ra ngoài. Ví dụ mọi thứ trình duyệt được chạm phải bắt đầu bằng web.. Khi ấy quyền viết được một lần, đọc hiểu ngay, và một địa chỉ nội bộ mới thêm vào sẽ không vô tình lọt ra.

Thêm bộ lọc xác thực. bridge(opts, event -> ...) cho bạn xem từng sự kiện trước khi nó đi qua — kiểm token, kiểm quyền theo người dùng, rồi event.complete(true) hoặc false. Danh sách địa chỉ nói được cái gì đi qua; chỉ bộ lọc mới nói được ai được phép.

SockJSHandler.create(vertx).bridge(opts, event -> {
    if (event.type() == BridgeEventType.SEND) {
        String token = event.getRawMessage().getJsonObject("headers").getString("Authorization");
        event.complete(kiemTra(token));
        return;
    }
    event.complete(true);
});

Khi nào dùng cầu nối này

Hợp: bảng điều khiển thời gian thực, thông báo đẩy, cộng tác trong một ứng dụng nội bộ — nơi bạn sở hữu cả hai đầu và danh sách địa chỉ ngắn.

Không hợp: API công khai. Phần 14 đo được SSE tốn bộ nhớ ngang WebSocket và đơn giản hơn nhiều; nếu bạn chỉ cần đẩy dữ liệu xuống thì SSE không mở ra bề mặt tấn công nào cả.

Nói cách khác: cầu nối SockJS đổi một chút tiện lợi lấy việc để lộ kiến trúc nội bộ ra Internet. Đáng khi bạn thật sự cần hai chiều và kiểm soát được danh sách địa chỉ; còn lại thì một endpoint HTTP bình thường an toàn hơn nhiều.

Bài sau: mẫu hỏi đáp và xử lý lỗi qua event bus.

Thử ba mươi giây

# tim regex bat tat trong cau noi cua ban
grep -rn "setAddressRegex" src/ | grep -vE "setAddressRegex\(\"\^" | head

Mỗi dòng có regex không bắt đầu bằng ^ là một dấu hỏi. Dòng nào chứa đúng ".*" thì mọi địa chỉ event bus trong ứng dụng của bạn đang mở ra trình duyệt.