Blazor Webassembly实现自定义JWT用户认证方案

目前尚未发现有任意一篇博文详细介绍了Blazor的认证系统,基本上都是东拼西凑的,很难受,给新手带来了极大的困惑。这里介绍一个较为简单的认证方案,其实如果放在Vue+PHP/JAVA的组合的话,这个是完全没有一点坑的,只不过由于Blazor的相关资料较少,所以遇到的坑也比较多。

首先,我们要知道,一套认证系统首先需要包括至少以下内容:

  • 登录/注册页面
  • 登录/注册接口
  • 认证中间件

其中,认证中间件有微软实现的Microsoft.AspNetCore.Components.Authorization和Microsoft.AspNetCore.Authorization库,无需再去单独实现(除非你的需求极为复杂);剩下两个如果你真的觉得IdentityServer4适合你的话,也可以用,但是据我了解模板什么的比较难定制,难看也得忍着,显得跟主要UI十分割裂,感官上不好。所以这里我们登录/注册使用自己编写的页面和接口,其中页面在Client项目中编写,接口在Server项目中编写。

由于每个人的具体实现不一样,这里只列出非常关键的配置,剩下的请自行思考。

Program.cs on Client

我们主要配置HttpClient和Authorization,HttpClient可以这么配置:

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

一般项目也会给你配好,如果没有就自己加上。AddHttpClient是个工厂模式的扩展,这里有个比较有意思的用法就是写一个Service层,然后通过AddHttpClient来注入HttpClient,这样可以分几个配置来使用HttpClient。例如:

builder.Services.AddHttpClient<AuthHttpService>()
    .ConfigureHttpClient(config =>
    {
        config.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
    });

这里就是将AuthHttpService注入并实例化,在razor页面就可以用注入的方式使用了:

@inject AuthHttpService AuthHttpService

如果学过Spring框架,应该也比较熟悉类似的配置,理解起来会更快些。没有接触过也可以看一下我的解释:

首先AddHttpClient根据传入的泛型确定类,然后进行实例化,在构造函数中注入准备好的HttpClient对象。与此同时,通过ConfigureHttpClient配置HttpClient的一些信息,也会更改该HttpClient对象。不过,其他HttpClient对象是互不影响的,因此每一个构建的示例都是相对独立的。至于依赖注入,建议自行了解,在Blazor/.Net Core这里,依赖注入只在构造方法完成(也就是想要注入啥对象,往构造方法的参数写就完事了,前提是该类型注册了DI注入,这个我们后面再提)。

然后还有一个小技巧,如果想要对HttpClient的Request/Response“做些手脚”,例如在Header加个Authorization,就可以通过AddHttpMessageHandler方法来加一个DelegatingHandler,以此达到类似于“请求/响应中间件”的效果,与axios那边的原理差不多。由于我的实现比较简单,就大概分享一下给大家,可以照葫芦画瓢做一下。在Program.cs中加入:

builder.Services.AddTransient<IdentityHandler>(); // 这里是将IdentityHandler在DI中注册了,如果不注册的话,AddHttpMessageHandler用不了

builder.Services.AddHttpClient<UserHttpService>()
    .ConfigureHttpClient(config =>
    {
        config.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
    })
    .AddHttpMessageHandler<IdentityHandler>();

IdentityHandler的实现如下:

using Blazored.LocalStorage;
using System.Net.Http.Headers;

namespace Cytus2ScoreManager.Client.Utils
{
    public class IdentityHandler : DelegatingHandler
    {
        private readonly ILocalStorageService _localStorageService;
        private readonly ILogger<IdentityHandler> _logger;

        public IdentityHandler(ILocalStorageService localStorageService, ILogger<IdentityHandler> logger)
        {
            _localStorageService = localStorageService;
            _logger = logger;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            HttpResponseMessage httpResponseMessage;
            try
            {
                string accessToken = await _localStorageService.GetItemAsStringAsync("auth-token");
                if (string.IsNullOrEmpty(accessToken))
                {
                    throw new Exception($"Access token is missing for the request {request.RequestUri}");
                }
                request.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

                httpResponseMessage = await base.SendAsync(request, cancellationToken);
                httpResponseMessage.EnsureSuccessStatusCode();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to run http query {RequestUri}", request.RequestUri);
                throw;
            }
            return httpResponseMessage;
        }
    }
}

这里实现了读取localstorage中auth-token的值并加入到Header的Authorization中,如果需要更多定制也可以自己加。Localstorage的库怎么用,自己搜Blazored.LocalStorage。

最后在Program.cs中,加入以下代码来实现认证功能。

builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();

 

发表评论