You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Quarkus Mutiny上下文传播与SSE:如何保存订阅时的SecurityIdentity

问题

我正尝试搭建一个基于订阅客户端身份过滤事件的SSE(Server-Sent Events)通道,但订阅时获取的SecurityIdentity无法在事件分发时传播,导致调用channel.send时触发UnauthorizedException。

当前实现中,服务器调用channel.send(event)时没有可注入的SecurityIdentity,assert ! _identity.isAnonymous();断言失败(实际是因为@Authenticated注解被检测到,导致分发失败)。请问如何保存订阅REST请求时的安全身份,以便后续事件分发时使用?

当前代码实现

REST资源类

@GET
@Path("stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
@RestStreamElementType(MediaType.APPLICATION_JSON)
public Multi<PassportEvent> stream(@QueryParam("order") Integer orderId) {
    return _passportService.stream(orderId);
}

服务类

@Inject
@Channel("passports")
Multi<PassportEvent> passportEvents;
    
@Inject
SecurityIdentity _identity;

@Authenticated
public Multi<PassportEvent> stream(Integer orderId)
{
    assert ! _identity.isAnonymous();

    return passportEvents
        .filter(event -> Objects.equals(event.tenantId, getTenantId())
            && (orderId == null || Objects.equals(orderId, event.orderId)));
}

事件发送服务

@Inject
@Channel("passports")
Emitter<PassportEvent> passportEvents;

// no security identity here
createPassport(...) {
    ...
    if (passportEvents.hasRequests()) {
       PassportEvent event = ...;
            passportEvents.send(event);
        }
    }
}
解决方案

问题核心是事件发送时的上下文和订阅请求的上下文完全分离,不能直接依赖注入SecurityIdentity获取订阅者身份。正确思路是在订阅阶段就完成身份校验和事件过滤绑定,而非在发送事件时处理权限逻辑。以下是几种可行方案:

1. 简化逻辑:订阅时完成权限校验与过滤

你当前的@Authenticated注解只需在订阅请求时生效,确保只有认证用户能创建订阅;事件发送端仅负责广播事件,Quarkus的SSE机制会自动将事件推送给每个订阅者已过滤的流。

调整服务类实现,明确权限校验只在订阅时执行:

@Inject
@Channel("passports")
Multi<PassportEvent> passportEvents;
    
@Inject
SecurityIdentity _identity;

@Authenticated
public Multi<PassportEvent> stream(Integer orderId)
{
    // 订阅阶段校验身份,确保是已认证用户
    assert ! _identity.isAnonymous();
    String currentTenantId = getTenantId();

    // 返回已过滤的流,仅包含当前租户和指定订单的事件
    return passportEvents
        .filter(event -> Objects.equals(event.tenantId, currentTenantId)
            && (orderId == null || Objects.equals(orderId, event.orderId)));
}

这种方式下,事件发送端无需关心任何身份信息,正常发送事件即可,不会触发分发阶段的权限检查。

2. 自定义订阅注册表:绑定订阅者身份

如果需要更细粒度的权限控制,可以创建订阅注册表,在订阅时保存每个订阅者的身份信息,发送事件时基于身份判断是否推送:

首先实现订阅注册表:

@ApplicationScoped
public class PassportSubscriptionRegistry {
    private final Map<String, SecurityIdentity> subscriberIdentities = new ConcurrentHashMap<>();

    public void registerSubscriber(String subscriberId, SecurityIdentity identity) {
        subscriberIdentities.put(subscriberId, identity);
    }

    public void unregisterSubscriber(String subscriberId) {
        subscriberIdentities.remove(subscriberId);
    }

    public boolean isAuthorized(String subscriberId, PassportEvent event) {
        SecurityIdentity identity = subscriberIdentities.get(subscriberId);
        if (identity == null || identity.isAnonymous()) {
            return false;
        }
        String tenantId = getTenantIdFromIdentity(identity);
        return Objects.equals(event.tenantId, tenantId);
    }
}

然后在订阅方法中注册并绑定身份:

@Authenticated
public Multi<PassportEvent> stream(Integer orderId, @Context SseEventSink sink) {
    String subscriberId = UUID.randomUUID().toString();
    _subscriptionRegistry.registerSubscriber(subscriberId, _identity);

    // 订阅取消时移除注册信息
    sink.onClose(() -> _subscriptionRegistry.unregisterSubscriber(subscriberId));

    return passportEvents
        .filter(event -> _subscriptionRegistry.isAuthorized(subscriberId, event)
            && (orderId == null || Objects.equals(orderId, event.orderId)));
}

该方案适合需要针对单个订阅者做复杂权限判断的场景。

3. 事件嵌入租户标识:发送端无需身份上下文

既然过滤逻辑基于租户ID和订单ID,可直接在事件中嵌入租户标识,订阅者在订阅阶段基于自身租户ID过滤流。事件发送端只需广播带有租户标识的事件,完全不需要关心身份信息,这是最贴合SSE设计思路的方案。

内容的提问来源于stack exchange,提问作者Franck

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 19:03:13