Blazor PWA 单页应用身份认证机制示例

Blazor PWA 单页应用身份认证机制示例

概述

    本项目包含三部分:统一登录站点,受保护的webapi接口站点,以及blazor前端项目。

    这个Demo是我的学习项目,是借鉴了(copy)了众多前辈代码完成的,如有不足之处还请多多指教。代码地址: https://dev.azure.com/1903268310/_git/blazor-sso-demo

blazor单页应用(SPA)的身份认证机制

  • 注:示例中,我将统一登录站点生成的jwtToken保存在了cookie中,因此身份校验时也是从cookie中取出token再校验。
 [HttpPost]
        public async Task<IActionResult> Login(string userName, string password, string returnUrl = null)
        {
            ViewData["returnUrl"] = returnUrl;
            UserInfo user = _userService.GetUser(userName, password);
            if (user != null)
            {
                HttpContext.Response.Cookies.Append("token",TokenService.GenerateToken(user));

                await HttpContext.SignInAsync(new IdentityServerUser(user.UserName), new AuthenticationProperties
                {
                    ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(3)
                });
                if (returnUrl != null)
                {
                    return Redirect(returnUrl);
                }

                return View();
            }
            else
            {
                return View();
            }
        }

AuthenticationStateProvider对象

    关于该对象的具体介绍可以去官网查看。该对象是blazor内置对象,用于为页面提供用户身份认证信息。

    其中最重要的是 Task GetAuthenticationStateAsync()方法。 该方法返回一个AuthenticationState对象,它包含用户的认证信息,你会发现该对象的构造函数接受一个ClaimsPrincipal对象作为参数,做过ASP.NET Core身份认证的同学想必对该对象并不陌生。

AuthenticationStateProvider对象可以与AuthorizeView页面组件相配合。

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/?view=aspnetcore-3.1

<AuthorizeView>
    <Authorized>
        <h5>Hello, @context.User.Identity.Name</h5> |
        <button class="btn-link" @onclick="Logout">退出</button>
    </Authorized>
    <NotAuthorized>
        <h5>Authentication Failure!</h5>
    </NotAuthorized>
</AuthorizeView>

    AuthorizeView 组件根据用户是否有权查看来选择性地显示 UI。AuthorizeView 组件支持基于角色或基于策略的授权 :

<AuthorizeView Roles="admin, superuser">
    <p>You can only see this if you're an admin or superuser.</p>
</AuthorizeView>

    通常,我们需要公开身份验证状态作为级联参数。级联参数是blazor中将父组件内的数据流转到子组件的一种方式。
MainLayout.razor:

@functions
{
    [CascadingParameter]
    Task<AuthenticationState> AuthenticationState { get; set; }
}

并且修改App.razor中代码如下:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" 
                                DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

之后,在Program.Main中添加选项和授权服务:

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

    然后,AuthorizeView会通过依赖注入的AuthenticationStateProvider对象的GetAuthenticationStateAsync()方法 获取用户当前状态,然后通过Authorized和NotAuthorized标签选择显示的内容。该组件还包含了一个 AuthenticationState 类型的 context 变量,可以使用该变量来访问有关已登录用户的信息。

自定义AuthenticationStateProvider对象

    用于获取身份认证状态的GetAuthenticationStateAsync()方法是怎样实现的呢,我们只需要创建出包含已登录用户信息的ClaimsPrincipal对象即可。我们可以设计一个返回当前登录用户信息的后台接口,也可从jwtToken中获取我们需要的内容。本项目采用的是后者,jwtToken的解析可以参考项目源码。
CustomAuthenticationStateProvider.cs

 public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var authState = new AuthenticationState(await GetUser(useCache: true));

            //通知AuthorizeView页面身份认证信息发生改变
            NotifyAuthenticationStateChanged(Task.FromResult(authState));
            return authState;
        }

        private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false)
        {
            var now = DateTimeOffset.Now;
            if (useCache && now < _userLastCheck + _userCacheRefreshInterval)
            {
                return _cachedUser;
            }

            _cachedUser = await FetchUser();
            _userLastCheck = now;

            return _cachedUser;
        }

        private async Task<ClaimsPrincipal> FetchUser()
        {
            var token = await _httpClient.GetStringAsync("/auth/token");

            if (string.IsNullOrWhiteSpace(token))
            {
                return new ClaimsPrincipal(new ClaimsIdentity());
            }

            return new ClaimsPrincipal(new ClaimsIdentity(TokenService.ParseClaims(token), "jwt"));
        }

使用HttpClientFactory

    ServiceCollection对象包含一个AddHttpClient()扩展方法,当你使用它的时候,就意味着你可以在程序中按照依赖注入惯例使用IHttpClientFactory,你可以为不同的httpClient对象指定名字(key),以及BaseAddress。
Program.cs

builder.Services.AddTransient<RequestMessageHandler>();

            builder.Services.AddTransient<IdentityHandler>();

            builder.Services.AddHttpClient("api", client => { client.BaseAddress = new Uri("http://localhost:5001"); })
                .AddHttpMessageHandler<RequestMessageHandler>()
                .AddHttpMessageHandler<IdentityHandler>();

            builder.Services.AddHttpClient("auth", client =>
                {
                    client.BaseAddress = new Uri("http://localhost:5000");
                })
                .AddHttpMessageHandler<IdentityHandler>();

    更重要的是,你可以为每一个httpClient指定DelegatingHandler,DelegatingHandler的作用类似于asp.net请求中的中间件。它可以修改请求头以及请求内容,处理响应等。

当用户身份未认证,或服务器返回401时,跳转登录页面的实现方法:

public class RequestMessageHandler : DelegatingHandler
    {
        private readonly CustomAuthenticationStateProvider _authenticationStateProvider;

        public RequestMessageHandler(CustomAuthenticationStateProvider authenticationStateProvider)
        {
            _authenticationStateProvider = authenticationStateProvider;
        }

        protected override async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
            HttpResponseMessage responseMessage;

            if (!authState.User.Identity.IsAuthenticated)
            {
                responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized);
            }
            else
            {
                responseMessage = await base.SendAsync(request, cancellationToken);
            }

            if (responseMessage.StatusCode == HttpStatusCode.Unauthorized)
            {
                _authenticationStateProvider.SignIn();
            }
           
            return responseMessage;
        }
    }

携带身份认证标识

 public class IdentityHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            //携带身份标识-Cookie
            request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

            var responseMessage = await base.SendAsync(request, cancellationToken);

            return responseMessage;
        }
    }

以上就是示例的blazor部分,统一登录站点使用的是IdentityServer4来实现,用于分发token,token的校验由接口站点完成。两者实现都较为基础,此处不再赘述。

e = await base.SendAsync(request, cancellationToken);

        return responseMessage;
    }
}
以上就是示例的blazor部分,统一登录站点使用的是IdentityServer4来实现,用于分发token,token的校验由接口站点完成。两者实现都较为基础,此处不再赘述。

猜你喜欢

转载自blog.csdn.net/qq_40404477/article/details/107237538
PWA