研究嵌入式裝置訪問Asp.Net Core Web Api的簡單認證方式

SunnyTrudeau發表於2024-06-09

研究嵌入式裝置訪問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

相關文章