如何在HotChocolate中实现订阅的动态授权及权限变更管控?
为HotChocolate订阅实现动态主题授权(基于SSE)
一、初始订阅权限校验
在订阅建立阶段直接拦截无权限请求,避免无效连接建立。通过自定义授权策略解析主题中的订单ID,校验用户权限:
- 定义授权需求与处理器:
public class OrderSubscriptionRequirement : IAuthorizationRequirement { } public class OrderSubscriptionAuthorizationHandler : AuthorizationHandler<OrderSubscriptionRequirement> { private readonly IOrderPermissionService _permissionService; public OrderSubscriptionAuthorizationHandler(IOrderPermissionService permissionService) { _permissionService = permissionService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OrderSubscriptionRequirement requirement) { if (context.Resource is ISubscriptionEventContext subContext) { // 从主题提取订单ID var orderId = subContext.Topic.Split('_').Last(); var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); if (await _permissionService.HasAccess(userId, orderId)) { context.Succeed(requirement); } else { context.Fail(); } } } }
- 注册授权策略并应用到订阅字段:
services.AddAuthorization(options => { options.AddPolicy("OrderSubscribe", policy => policy.AddRequirements(new OrderSubscriptionRequirement())); }); // 订阅字段配置 [Subscribe(With = typeof(OrderUpdatedSubscription))] [Authorize(Policy = "OrderSubscribe")] public OrderUpdated? OrderUpdated([EventMessage] OrderUpdated message) => message;
无权限用户发起订阅时会直接收到授权失败响应,无法建立连接。
二、动态权限失效的实时处理
针对权限随订单状态变化的场景,有两种核心实现方式:
方案1:推送前实时校验权限
在订阅事件流的处理逻辑中,每次推送前重新校验权限,无权限则终止流:
public class OrderUpdatedSubscription : ISubscriptionResolver<OrderUpdated> { private readonly IOrderPermissionService _permissionService; private readonly IHttpContextAccessor _httpContextAccessor; public OrderUpdatedSubscription(IOrderPermissionService permissionService, IHttpContextAccessor httpContextAccessor) { _permissionService = permissionService; _httpContextAccessor = httpContextAccessor; } public async ValueTask<IAsyncEnumerable<OrderUpdated>> ResolveAsync(ISubscriptionEventContext context, CancellationToken cancellationToken) { var orderId = context.Topic.Split('_').Last(); var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); var updateStream = await GetOrderUpdateStream(orderId, cancellationToken); await foreach (var update in updateStream.WithCancellation(cancellationToken)) { if (!await _permissionService.HasAccess(userId, orderId)) { // 终止流,断开订阅 break; } yield return update; } } }
方案2:权限变化时主动断开订阅
维护用户与订阅连接的映射,当权限失效时主动关闭对应连接:
public class OrderPermissionChangedHandler { private readonly ISubscriptionManager _subscriptionManager; public OrderPermissionChangedHandler(ISubscriptionManager subscriptionManager) { _subscriptionManager = subscriptionManager; } public async Task OnPermissionLost(string userId, string orderId) { var targetTopic = $"OrderUpdated_{orderId}"; var subscriptions = _subscriptionManager.GetSubscriptions(targetTopic); foreach (var sub in subscriptions) { var subUserId = sub.Context.User.FindFirstValue(ClaimTypes.NameIdentifier); if (subUserId == userId) { await _subscriptionManager.UnsubscribeAsync(sub.Id); } } } }
在订单状态变更触发权限失效时,调用该方法即可断开用户的对应订阅。
三、避免订单ID泄露的优化
- 禁止无权限用户建立订阅:初始校验不通过直接拒绝,用户无法获取任何关于主题存在性的信息。
- 加密主题中的订单ID:生成主题时使用加密后的订单ID,避免原始ID泄露:
var encryptedOrderId = Encrypt(orderId); var topic = $"OrderUpdated_{encryptedOrderId}";
四、SSE适配要点
- 初始权限校验要在SSE连接建立的HTTP请求阶段完成,避免无效连接占用资源。
- 权限失效时主动关闭SSE连接,返回403状态码,让客户端明确感知权限变化。
- 终止事件流时确保SSE连接正常关闭,避免客户端无意义等待。
内容的提问来源于stack exchange,提问作者cmeeren
相关产品推荐
相关产品推荐

