研究嵌入式设备访问Asp.Net Core Web Api的简单认证方式
研究嵌入式设备访问Asp.Net Core Web Api的简单认证方式
Asp.Net Core可以采用JWT保护Web Api,但是有的嵌入式设备硬件性能比较低,缺乏交互界面,无法使用JWT这么复杂的方案。根据嵌入式设备的约束条件,可以采用一些简单的网络安全保护措施:
1,采用https传输;
2,在header自定义认证参数;
3,设备端定期更换秘钥,加密存储;
服务端可以采用中间件、自定义认证、筛选器等方式对Web Api进行保护。
创建项目
VS2022创建Asp.Net Core Web Api服务端项目DeviceAuth和控制台项目DevClient。
DeviceAuth服务端定义一个简单的控制器,上传WeatherForecast。
创建设备认证服务类DevAuthService,从HttpRequest获取认证头,解码得到设备端认证参数,并跟服务端认证参数比较。如果相等则认证成功,否则失败。
D:\Software\gitee\DeviceAuth\DeviceAuth\DevAuthService.cs
/// <summary> /// 设备认证服务 /// </summary> public class DevAuthService { private readonly IConfiguration _configuration; private readonly ILogger<DevAuthService> _logger; public DevAuthService(IConfiguration configuration, ILogger<DevAuthService> logger) { _configuration = configuration; _logger = logger; } /// <summary> /// 处理设备认证 /// </summary> /// <param name="request"></param> /// <returns></returns> public bool HandleDevAuth(HttpRequest request) { //获取设备认证头 var devAuthHeader = request.Headers.Authorization.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x) && x.StartsWith("DevAuth")); if (string.IsNullOrWhiteSpace(devAuthHeader)) { _logger.LogWarning("找不到设备认证头"); return false; } //获取设备认证参数base64,实际项目中应该用加密取代base64 var devAuthHeaderValue = AuthenticationHeaderValue.Parse(devAuthHeader!); var paramBase64 = devAuthHeaderValue?.Parameter; if (string.IsNullOrWhiteSpace(paramBase64)) { _logger.LogWarning("设备认证参数为空"); return false; } //获取设备认证参数 byte[] paramAry = Convert.FromBase64String(paramBase64!); var devSecretKey = Encoding.ASCII.GetString(paramAry); var serverSecretKey = _configuration["DevAuth:SecretKey"]; if (devSecretKey == serverSecretKey) { _logger.LogInformation("设备认证成功"); return true; } else { _logger.LogWarning("设备认证参数错误"); return false; } } }
DevClient设备端通过HttpClient上传数据,添加设备认证头。
D:\Software\gitee\DeviceAuth\DevClient\Program.cs
/// <summary> /// 上传数据 /// </summary> /// <param name="isWithDevAuthHeader">是否携带设备认证头</param> /// <returns></returns> private static async Task<bool> UploadData(bool isWithDevAuthHeader = true) { try { HttpClient client = new HttpClient { BaseAddress = new Uri("https://localhost:7259") }; var forecast = new WeatherForecast() { Date = DateTimeOffset.Now, TemperatureC = System.Random.Shared.Next(0, 30), Summary = "多云" }; JsonContent jsonContent = JsonContent.Create(forecast); if (isWithDevAuthHeader) { //添加设备认证头 string authBase64 = Convert.ToBase64String(Encoding.ASCII.GetBytes("secret")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(DevAuthConst.DevAuth, authBase64); } for (int i = 1; i <= 3; i++) { string requestUri = $"/WeatherForecast/Add{i}"; var response = await client.PostAsync(requestUri, jsonContent); response.EnsureSuccessStatusCode(); string responseStr = await response.Content.ReadAsStringAsync(); Console.WriteLine($"{requestUri}上传数据成功, {responseStr}"); } return true; } catch (Exception ex) { Console.WriteLine($"上传数据失败, {ex}"); return false; } }
中间件方案
创建DevAuthMiddleware设备认证中间件,从HttpContext获取端点属性,如果标记了DevAuthAttribute属性,就调用DevAuthService服务进行设备认证。
D:\Software\gitee\DeviceAuth\DeviceAuth\DevAuthMiddleware.cs
/// <summary> /// 设备认证中间件 /// </summary> public class DevAuthMiddleware { //将服务注入中间件的 Invoke 或 InvokeAsync 方法。 使用构造函数注入会引发运行时异常 //private readonly DevAuthService _devAuthService; private readonly RequestDelegate _next; private readonly ILogger<DevAuthMiddleware> _logger; public DevAuthMiddleware(RequestDelegate next, //DevAuthService devAuthService, ILogger<DevAuthMiddleware> logger) { _next = next; //_devAuthService = devAuthService; _logger = logger; } public async Task InvokeAsync(HttpContext context, DevAuthService devAuthService) { bool isAuthSuccess = false; try { //如果端点标记了DevAuthAttribute属性,需要做设备认证 var devAuthAttr = context.GetEndpoint()?.Metadata?.GetMetadata<DevAuthAttribute>(); if (devAuthAttr is not null) isAuthSuccess = devAuthService.HandleDevAuth(context.Request); else isAuthSuccess = true; } catch (Exception ex) { _logger.LogWarning(ex, "设备认证错误"); isAuthSuccess = false; } if (isAuthSuccess) { //继续请求管道 await _next(context); } else { _logger.LogWarning($"{context.Request.Path}, 设备认证失败"); context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; } } } public static class DevAuthMiddlewareExtensions { public static IApplicationBuilder UseDevAuth(this IApplicationBuilder app) { ///使用设备认证中间件 return app.UseMiddleware<DevAuthMiddleware>(); } }
定义一个简单的DevAuthAttribute属性。
D:\Software\gitee\DeviceAuth\DeviceAuth\DevAuthAttribute.cs
D:\Software\gitee\DeviceAuth\DeviceAuth\DevAuthAttribute.cs /// <summary> /// 需要设备认证 /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class DevAuthAttribute : Attribute { }
在控制器函数上面标记DevAuthAttribute属性,表明需要做设备认证。
D:\Software\gitee\DeviceAuth\DeviceAuth\Controllers\WeatherForecastController.cs /// <summary> /// 采用中间件对标记了DevAuth属性的端点进行认证 /// </summary> /// <param name="weatherForecast"></param> /// <returns></returns> [DevAuth] [HttpPost("Add1")] public int Add1(WeatherForecast weatherForecast) { _logger.LogInformation($"{DateTimeOffset.Now}: Add1收到数据{weatherForecast}"); return 1; }
Program使用中间件app.UseDevAuth()。
自定义认证方案
创建DevAuthorizationHandler自定义设备认证处理器,获取Request的认证头,如果认证通过,创建一个AuthenticationTicket身份标识,包含设备角色Claim(ClaimTypes.Role, DevAuthConst.DevRole)。
D:\Software\gitee\DeviceAuth\DeviceAuth\DevAuthorizationHandler.cs
/// <summary> /// 自定义设备认证处理器 /// </summary> public class DevAuthorizationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { public const string DevAuthSchemeName = "DevAuthScheme"; public const string DevAuthPolicyName = "DevAuthPolicy"; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly DevAuthService _devAuthService; private readonly ILogger<DevAuthorizationHandler> _logger; public DevAuthorizationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory loggerFactory, UrlEncoder encoder, ISystemClock clock, IServiceScopeFactory serviceScopeFactory, DevAuthService devAuthService) : base(options, loggerFactory, encoder, clock) { _serviceScopeFactory = serviceScopeFactory; _devAuthService = devAuthService; _logger = loggerFactory.CreateLogger<DevAuthorizationHandler>(); } protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { await Task.CompletedTask; bool isAuthSuccess = false; try { //处理设备认证 isAuthSuccess = _devAuthService.HandleDevAuth(Request); } catch (Exception ex) { _logger.LogWarning(ex, "设备认证错误"); isAuthSuccess = false; } if (isAuthSuccess) { var claims = new List<Claim>() { new Claim(ClaimTypes.Role, DevAuthConst.DevRole) }; var identities = new List<ClaimsIdentity>() { new ClaimsIdentity(claims, Scheme.Name) }; var principal = new ClaimsPrincipal(identities); var ticket = new AuthenticationTicket(principal, Scheme.Name); return AuthenticateResult.Success(ticket); } else { _logger.LogWarning($"{Request.Path}, 设备认证失败"); return AuthenticateResult.Fail("设备认证失败"); } } }
在program注册自定义认证方案。
D:\Software\gitee\DeviceAuth\DeviceAuth\Program.cs
builder.Services.TryAddScoped<DevAuthorizationHandler>(); builder.Services.AddAuthentication(DevAuthorizationHandler.DevAuthSchemeName) //builder.Services.AddAuthentication(options => //{ // options.DefaultAuthenticateScheme = DevAuthorizationHandler.DevAuthSchemeName; // options.DefaultChallengeScheme = null; //}) .AddScheme<AuthenticationSchemeOptions, DevAuthorizationHandler>(DevAuthorizationHandler.DevAuthSchemeName, null); app.UseAuthentication(); app.UseAuthorization();
在控制器函数上面标记Authorize(Roles = DevAuthConst.DevRole),表明需要设备角色的身份标记的认证。
D:\Software\gitee\DeviceAuth\DeviceAuth\Controllers\WeatherForecastController.cs
/// <summary> /// 采用DevAuthorizationHandler自定义认证方案 /// </summary> /// <param name="weatherForecast"></param> /// <returns></returns> [Authorize(Roles = DevAuthConst.DevRole)] //[Authorize(DevAuthorizationHandler.DevAuthPolicyName)] [HttpPost("Add2")] public int Add2(WeatherForecast weatherForecast) { _logger.LogInformation($"{DateTimeOffset.Now}: Add2收到数据{weatherForecast}"); return 1; }
筛选器方案
创建DevAuthorizeFilter设备认证筛选器,检查HttpContext.Request的认证头。
D:\Software\gitee\DeviceAuth\DeviceAuth\DevAuthorizeFilter.cs
/// <summary> /// 设备认证筛选器 /// </summary> public class DevAuthorizeFilter : IAsyncAuthorizationFilter { private readonly DevAuthService _devAuthService; private readonly ILogger<DevAuthorizeFilter> _logger; public DevAuthorizeFilter(DevAuthService devAuthService, ILogger<DevAuthorizeFilter> logger) { _devAuthService = devAuthService; _logger = logger; } public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { await Task.CompletedTask; bool isAuthSuccess = false; try { //处理设备认证 isAuthSuccess = _devAuthService.HandleDevAuth(context.HttpContext.Request); } catch (Exception ex) { _logger.LogWarning(ex, "设备认证错误"); isAuthSuccess = false; } if (isAuthSuccess) { } else { _logger.LogWarning($"{context.HttpContext.Request.Path}, 设备认证失败"); context.Result = new UnauthorizedResult(); } } }
在控制器函数上面标记ServiceFilter<DevAuthorizeFilter>筛选器,表明需要设备认证。
D:\Software\gitee\DeviceAuth\DeviceAuth\Controllers\WeatherForecastController.cs
/// <summary> /// 采用DevAuthorizeFilter筛选器方案 /// </summary> /// <param name="weatherForecast"></param> /// <returns></returns> [ServiceFilter<DevAuthorizeFilter>] [HttpPost("Add3")] public int Add3(WeatherForecast weatherForecast) { _logger.LogInformation($"{DateTimeOffset.Now}: Add3收到数据{weatherForecast}"); return 1; } }
总结
测试3种认证方案都可以。中间件,自定义认证,筛选器都可以实现设备认证的目标。
中间件需要获取每次访问的端点,感觉比较麻烦。
自定义认证代码比较复杂。
推荐采用筛选器,代码简单。
DEMO代码地址:https://gitee.com/woodsun/deviceauth