NewLife/NewLife.Remoting

Upgrade Nuget
大石头 authored at 2025-10-26 14:01:38
ee8099a
Tree
1 Parent(s) 8819014
Summary: 5 changed files with 62 additions and 10 deletions.
Modified +47 -7
Modified +11 -1
Modified +1 -1
Modified +1 -1
Modified +2 -0
Modified +47 -7
diff --git a/NewLife.Remoting.Extensions/Controllers/BaseController.cs b/NewLife.Remoting.Extensions/Controllers/BaseController.cs
index ea65df2..a91171a 100644
--- a/NewLife.Remoting.Extensions/Controllers/BaseController.cs
+++ b/NewLife.Remoting.Extensions/Controllers/BaseController.cs
@@ -1,4 +1,4 @@
-using System.Reflection;
+using System.Security.Claims;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Controllers;
@@ -16,7 +16,11 @@ namespace NewLife.Remoting.Extensions;
 
 /// <summary>业务接口控制器基类</summary>
 /// <remarks>
-/// 提供统一的令牌解码验证架构
+/// 提供统一的令牌解码/鉴权与异常收敛,前置解析令牌,后置记录异常并回收池化的 DeviceContext。
+/// 将 JWT 写入 Jwt/DeviceContext(Code、ClientId、设备/在线),并在 HttpContext.Items 暴露供中间件/日志使用。
+/// 鉴权成功后映射 ClaimsPrincipal(ApiToken),可配合 [Authorize] 与策略(例如 DeviceRequired)。
+/// 支持方法/控制器/Endpoint 的 AllowAnonymous 跳过鉴权;错误统一返回 code/message/traceId,并屏蔽 SQL细节。
+/// 可覆写 OnAuthorize/OnWriteError/WriteLog;控制器为每请求实例,结合对象池保证性能与线程安全。
 /// </remarks>
 [ApiFilter]
 [Route("[controller]")]
@@ -39,7 +43,7 @@ public abstract class BaseController : ControllerBase, IWebFilter, ILogProvider
     private readonly ITokenService _tokenService;
     private IDictionary<String, Object?>? _args;
     //private static readonly Action<String>? _setip;
-    private static readonly Pool<DeviceContext> _pool = new(64);
+    private static readonly Pool<DeviceContext> _pool = new(256);
     #endregion
 
     #region 构造
@@ -79,15 +83,26 @@ public abstract class BaseController : ControllerBase, IWebFilter, ILogProvider
         ctx.UserHost = ip;
         ctx["__ActionContext"] = context;
 
+        // 暴露到 HttpContext.Items,方便中间件/日志访问
+        HttpContext.Items[nameof(DeviceContext)] = ctx;
+
         var token = ctx.Token = ApiFilterAttribute.GetToken(context.HttpContext);
 
         try
         {
-            if (context.ActionDescriptor is ControllerActionDescriptor act && !act.MethodInfo.IsDefined(typeof(AllowAnonymousAttribute)))
+            if (context.ActionDescriptor is ControllerActionDescriptor act)
             {
-                // 匿名访问接口无需验证。例如星尘Node的SendCommand接口,并不使用Node令牌,而是使用App令牌
-                var rs = OnAuthorize(token, ctx);
-                if (!rs) throw new ApiException(ApiCode.Unauthorized, "认证失败");
+                var endpoint = context.HttpContext.GetEndpoint();
+                var allowAnon = act.MethodInfo.IsDefined(typeof(AllowAnonymousAttribute), true)
+                    || act.ControllerTypeInfo.IsDefined(typeof(AllowAnonymousAttribute), true)
+                    || endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null;
+
+                if (!allowAnon)
+                {
+                    // 匿名访问接口无需验证。例如星尘Node的SendCommand接口,并不使用Node令牌,而是使用App令牌
+                    var rs = OnAuthorize(token, ctx);
+                    if (!rs) throw new ApiException(ApiCode.Unauthorized, "认证失败");
+                }
             }
         }
         catch (Exception ex)
@@ -144,6 +159,28 @@ public abstract class BaseController : ControllerBase, IWebFilter, ILogProvider
 
         if (ex != null) throw ex;
 
+        // 将成功解析的身份映射为 ClaimsPrincipal,便于 [Authorize]/策略授权
+        if (jwt != null)
+        {
+            var claims = new List<Claim>(8);
+            if (!code.IsNullOrEmpty())
+            {
+                claims.Add(new Claim("code", code));
+                claims.Add(new Claim(ClaimTypes.NameIdentifier, code));
+            }
+            if (!context.ClientId.IsNullOrEmpty()) claims.Add(new Claim("client_id", context.ClientId));
+            //if (!context.UserHost.IsNullOrEmpty()) claims.Add(new Claim("ip", context.UserHost));
+            //if (context.Device != null)
+            //{
+            //    var name = context.Device.Name;
+            //    if (!name.IsNullOrEmpty()) claims.Add(new Claim(ClaimTypes.Name, name));
+            //}
+
+            var identity = new ClaimsIdentity(claims, "ApiToken");
+            var principal = new ClaimsPrincipal(identity);
+            HttpContext.User = principal;
+        }
+
         return jwt != null;
     }
 
@@ -155,6 +192,9 @@ public abstract class BaseController : ControllerBase, IWebFilter, ILogProvider
         var ctx = Context;
         if (ctx != null)
         {
+            // 从 HttpContext.Items 移除暴露的 DeviceContext
+            HttpContext.Items.Remove(nameof(DeviceContext));
+
             ctx.Clear();
             _pool.Return(ctx);
             Context = null!;
Modified +11 -1
diff --git a/NewLife.Remoting.Extensions/RemotingExtensions.cs b/NewLife.Remoting.Extensions/RemotingExtensions.cs
index d424cd4..fdcf0e7 100644
--- a/NewLife.Remoting.Extensions/RemotingExtensions.cs
+++ b/NewLife.Remoting.Extensions/RemotingExtensions.cs
@@ -1,4 +1,5 @@
-using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.WebSockets;
 using Microsoft.Extensions.DependencyInjection.Extensions;
 using NewLife.Caching;
@@ -19,6 +20,7 @@ public static class RemotingExtensions
     /// 注册TokenService令牌服务,提供令牌颁发与验证服务;
     /// 注册密码提供者,用于通信过程中保护密钥,避免明文传输;
     /// 注册缓存提供者的默认实现;
+    /// 注册授权策略 DeviceRequired(RequireClaim("code")),便于使用 [Authorize(Policy = "DeviceRequired")]保护接口;
     /// </remarks>
     /// <param name="services"></param>
     /// <param name="setting"></param>
@@ -49,6 +51,14 @@ public static class RemotingExtensions
         // 注册缓存提供者,必须有默认实现
         services.TryAddSingleton<ICacheProvider, CacheProvider>();
 
+        // 授权策略:DeviceRequired => 要求存在 code 声明(由 BaseController 映射 ClaimsPrincipal 提供)
+        services.AddAuthorization();
+        services.PostConfigure<AuthorizationOptions>(options =>
+        {
+            if (options.GetPolicy("DeviceRequired") == null)
+                options.AddPolicy("DeviceRequired", policy => policy.RequireClaim("code"));
+        });
+
         // 添加模型绑定器
         //var binderProvider = new ServiceModelBinderProvider();
         services.Configure<MvcOptions>(mvcOptions =>
Modified +1 -1
diff --git a/NewLife.Remoting/ApiServer.cs b/NewLife.Remoting/ApiServer.cs
index 5f5e641..c693894 100644
--- a/NewLife.Remoting/ApiServer.cs
+++ b/NewLife.Remoting/ApiServer.cs
@@ -176,7 +176,7 @@ public class ApiServer : ApiHost, IServer, IServiceProvider
 
     /// <summary>开始服务</summary>
     /// <remarks>
-    /// 初始化 <see cref="Encoder"/> 与 <see cref="Handler"/>,创建并启动底层 <see cref="IApiServer"/>。
+    /// 初始化 <see cref="ApiHost.Encoder"/> 与 <see cref="Handler"/>,创建并启动底层 <see cref="IApiServer"/>。
     /// 若 <see cref="StatPeriod"/> 大于 0,启动统计定时器输出处理统计与网络状态。
     /// </remarks>
     public virtual void Start()
Modified +1 -1
diff --git a/NewLife.Remoting/IEncoder.cs b/NewLife.Remoting/IEncoder.cs
index 8ce427f..f7be29c 100644
--- a/NewLife.Remoting/IEncoder.cs
+++ b/NewLife.Remoting/IEncoder.cs
@@ -123,7 +123,7 @@ public abstract class EncoderBase
         if (msg.Reply && msg.Error) message.Code = reader.ReadInt32();
 
         // 参数或结果
-        if (reader.FreeCapacity > 0)
+        if (reader.Available > 0)
         {
             var len = reader.ReadInt32();
             if (len > 0) message.Data = msg.Payload.Slice(reader.Position, len);
Modified +2 -0
diff --git a/Samples/IoTZero/Controllers/ThingController.cs b/Samples/IoTZero/Controllers/ThingController.cs
index b05565a..624806f 100644
--- a/Samples/IoTZero/Controllers/ThingController.cs
+++ b/Samples/IoTZero/Controllers/ThingController.cs
@@ -1,5 +1,6 @@
 using IoT.Data;
 using IoTZero.Services;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using NewLife.IoT.Models;
 using NewLife.IoT.ThingModels;
@@ -16,6 +17,7 @@ namespace IoTZero.Controllers;
 /// <param name="thingService"></param>
 [ApiFilter]
 [ApiController]
+[Authorize(Policy = "DeviceRequired")]
 [Route("[controller]")]
 public class ThingController(IDeviceService deviceService, ThingService thingService, IServiceProvider serviceProvider) : BaseController(deviceService, null, serviceProvider)
 {