《进击吧!Blazor!》系列入门教程 第一章 6.安全

《进击吧!Blazor!》是本人与张善友老师合作的Blazor零基础入门教程视频,此教程能让一个从未接触过Blazor的程序员掌握开发Blazor应用的能力。
视频地址:https://space.bilibili.com/483888821/channel/detail?cid=151273
Blazor WebAssembly 是单页应用 (SPA) 框架,用于使用 .NET 生成交互式客户端 Web 应用,采用 C# 代替 JavaScript 来编写前端代码
本系列文章因篇幅有限,省略了部分代码,完整示例代码:https://github.com/TimChen44/Blazor-ToDo

作者:陈超超
Ant Design Blazor 项目贡献者,拥有十多年从业经验,长期基于.Net 技术栈进行架构与开发产品的工作,现就职于正泰集团。
邮箱:timchen@live.com
欢迎各位读者有任何问题联系我,我们共同进步。

我的的 ToDo 应用基本功能已经完成,但是自己的待办当然只有自己知道,所以我们这次给我们的应用增加一些安全方面的功能。

Blazor 身份验证与授权

身份验证

Blazor Server 应用和 Blazor WebAssembly 应用的安全方案有所不同。

  • Blazor WebAssembly

Blazor WebAssembly 应用在客户端上运行。 由于用户可绕过客户端检查,因为用户可修改所有客户端代码, 因此授权仅用于确定要显示的 UI 选项,所有客户端应用程序技术都是如此。

  • Blazor Server

Blazor Server 应用通过使用 SignalR 创建的实时连接运行。 建立连接后,将处理基于 SignalR 的应用的身份验证。 可基于 cookie 或一些其他持有者令牌进行身份验证。

授权

AuthorizeView 组件根据用户是否获得授权来选择性地显示 UI 内容。 如果只需要为用户显示数据,而不需要在过程逻辑中使用用户的标识,那么此方法很有用。

<AuthorizeView>
  <Authorized>
    <!--验证通过显示-->
  </Authorized>
  <NotAuthorized>
    <!--验证不通过显示-->
  </NotAuthorized>
</AuthorizeView>

Blazor 中使用 Token

在 Blazor WebAssembly 模式下, 因为应用都在客户端运行,所以使用 Token 作为身份认证的方式是一个比较好的选择。
基本的使用时序图如下

前端 服务端 登录请求 验证身份 创建Token 返回Token 业务请求 包含Token 验证Token 成功 前端 服务端

对于安全要求不高的应用采用这个方法简单、易维护,完全没有问题。

但是 Token 本身在安全性上存在以下两个风险:

  1. Token 无法注销,所以可以在 Token 有效期内发送的非法请求,服务端无能为力。
  2. Token 通过 AES 加密存储在客户端,理论上可以进行离线破解,破解后就能任意伪造 Token。

因此遇到安全要求非常高的应用时,我们需要认证服务进行 Token 的有效性验证

前端 认证服务 服务端 登录请求 验证身份 创建Token 返回Token 业务请求 包含Token 请求验证Token 验证Token Token有效 成功 前端 认证服务 服务端

改造 ToDo

接着我们对之前的 ToDo 项目进行改造,让他支持登录功能。

ToDo.Shared

先把前后端交互所需的 Dto 创建了

public class LoginDto
{
    public string UserName { get; set; }
    public string Password { get; set; }
}
public class UserDto
{
    public string Name { get; set; }
    public string Token { get; set; }
}

ToDo.Server

先改造服务端,添加必要引用,编写身份认证代码等

添加引用

  • Microsoft.AspNetCore.Authentication.JwtBearer

Startup.cs

添加 JwtBearer 配置

public void ConfigureServices(IServiceCollection services)
{
	//......
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,//是否验证Issuer
                ValidateAudience = true,//是否验证Audience
                ValidateLifetime = true,//是否验证失效时间
                ValidateIssuerSigningKey = true,//是否验证SecurityKey
                ValidAudience = "guetClient",//Audience
                ValidIssuer = "guetServer",//Issuer,这两项和签发jwt的设置一致
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("123456789012345678901234567890123456789"))//拿到SecurityKey
            };
        });
}

此处定义了 Token 的密钥,规则等,实际项目时可以将这些信息放到配置中。

AuthController.cs

行政验证控制器,用于验证用户身份,创建 Token 等。

[ApiController]
[Route("api/[controller]/[action]")]
public class AuthController : ControllerBase
{
    //登录
    [HttpPost]
    public UserDto Login(LoginDto dto)
    {
        //模拟获得Token
        var jwtToken = GetToken(dto.UserName);

        return new() { Name = dto.UserName, Token = jwtToken };
    }

    //获得用户,当页面客户端页面刷新时调用以获得用户信息
    [HttpGet]
    public UserDto GetUser()
    {
        if (User.Identity.IsAuthenticated)//如果Token有效
        {
            var name = User.Claims.First(x => x.Type == ClaimTypes.Name).Value;//从Token中拿出用户ID
            //模拟获得Token
            var jwtToken = GetToken(name);
            return new UserDto() { Name = name, Token = jwtToken };
        }
        else
        {
            return new UserDto() { Name = null, Token = null };
        }
    }

    public string GetToken(string name)
    {
        //此处加入账号密码验证代码

        var claims = new Claim[]
        {
            new Claim(ClaimTypes.Name,name),
            new Claim(ClaimTypes.Role,"Admin"),
        };

        var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("123456789012345678901234567890123456789"));
        var expires = DateTime.Now.AddDays(30);
        var token = new JwtSecurityToken(
            issuer: "guetServer",
            audience: "guetClient",
            claims: claims,
            notBefore: DateTime.Now,
            expires: expires,
            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

ToDo.Client

改造客户端,让客户端支持身份认证

添加引用

  • Microsoft.AspNetCore.Components.Authorization

AuthenticationStateProvider

AuthenticationStateProviderAuthorizeView 组件和 CascadingAuthenticationState 组件用于获取身份验证状态的基础服务。
通常不直接使用 AuthenticationStateProvider,直接使用主要缺点是,如果基础身份验证状态数据发生更改,不会自动通知组件。其次是项目中总会有一些自定义的认证逻辑。
所以我们通常写一个类继承他,并重写一些我们自己的逻辑。

//AuthProvider.cs
public class AuthProvider : AuthenticationStateProvider
{
    private readonly HttpClient HttpClient;
    public string UserName { get; set; }

    public AuthProvider(HttpClient httpClient)
    {
        HttpClient = httpClient;
    }

    public async override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        //这里获得用户登录状态
        var result = await HttpClient.GetFromJsonAsync<UserDto>($"api/Auth/GetUser");

        if (result?.Name == null)
        {
            MarkUserAsLoggedOut();
            return new AuthenticationState(new ClaimsPrincipal());
        }
        else
        {
            var claims = new List<Claim>();
            claims.Add(new Claim(ClaimTypes.Name, result.Name));
            var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "apiauth"));
            return new AuthenticationState(authenticatedUser);
        }
    }

    /// <summary>
    /// 标记授权
    /// </summary>
    /// <param name="loginModel"></param>
    /// <returns></returns>
    public void MarkUserAsAuthenticated(UserDto userDto)
    {
        HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", userDto.Token);
        UserName = userDto.Name;

        //此处应该根据服务器的返回的内容进行配置本地策略,作为演示,默认添加了“Admin”
        var claims = new List<Claim>();
        claims.Add(new Claim(ClaimTypes.Name, userDto.Name));
        claims.Add(new Claim("Admin", "Admin"));

        var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "apiauth"));
        var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
        NotifyAuthenticationStateChanged(authState);

        //慈湖可以可以将Token存储在本地存储中,实现页面刷新无需登录
    }

    /// <summary>
    /// 标记注销
    /// </summary>
    public void MarkUserAsLoggedOut()
    {
        HttpClient.DefaultRequestHeaders.Authorization = null;
        UserName = null;

        var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
        var authState = Task.FromResult(new AuthenticationState(anonymousUser));
        NotifyAuthenticationStateChanged(authState);
    }
}

NotifyAuthenticationStateChanged方法会通知身份验证状态数据(例如 AuthorizeView)使用者使用新数据重新呈现。
HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", userDto.Token);将 HTTP 请求头中加入 Token,这样之后所有的请求都会带上 Token。

Program中注入AuthProvider服务,以便于其他地方使用

//Program.cs
builder.Services.AddScoped<AuthenticationStateProvider, AuthProvider>();

Program中配置支持的策略

builder.Services.AddAuthorizationCore(option =>
{
    option.AddPolicy("Admin", policy => policy.RequireClaim("Admin"));
});

登录界面

添加Login.razor组件,代码如下

<div style="margin:100px">
  <Spin Spinning="isLoading">
    @if (model != null) {
    <form
      OnFinish="OnSave"
      Model="@model"
      LabelCol="new ColLayoutParam() {Span = 6 }"
    >
      <FormItem Label="用户名">
        <input @bind-Value="context.UserName" />
      </FormItem>
      <FormItem Label="密码">
        <input @bind-Value="context.Password" type="password" />
      </FormItem>
      <FormItem WrapperColOffset="6">
        <button type="@ButtonType.Primary" HtmlType="submit">登录</button>
      </FormItem>
    </form>
    }
  </Spin>
</div>
public partial class Login
{
    [Inject] public HttpClient Http { get; set; }
    [Inject] public MessageService MsgSvr { get; set; }
    [Inject] public AuthenticationStateProvider AuthProvider { get; set; }

    LoginDto model = new LoginDto();
    bool isLoading;

    async void OnLogin()
    {
        isLoading = true;

        var httpResponse = await Http.PostAsJsonAsync<LoginDto>($"api/Auth/Login", model);
        UserDto result = await httpResponse.Content.ReadFromJsonAsync<UserDto>();

        if (string.IsNullOrWhiteSpace(result?.Token) == false )
        {
            MsgSvr.Success($"登录成功");
            ((AuthProvider)AuthProvider).MarkUserAsAuthenticated(result);
        }
        else
        {
            MsgSvr.Error($"用户名或密码错误");
        }
        isLoading = false;
       InvokeAsync( StateHasChanged);
    }
}

登录界面代码很简单,就是向api/Auth/Login请求,根据返回的结果判断是否登入成功。
((AuthProvider)AuthProvider).MarkUserAsAuthenticated(result);标记身份认证状态已经修改。

修改布局

修改MainLayout.razor文件

<CascadingAuthenticationState>
  <AuthorizeView>
    <Authorized>
      <Layout>
        <Sider Style="overflow: auto;height: 100vh;position: fixed;left: 0;">
          <div class="logo">进击吧!Blazor!</div>
          <menu Theme="MenuTheme.Dark" Mode="@MenuMode.Inline">
            <menuitem RouterLink="/"> 主页 </menuitem>
            <menuitem RouterLink="/today" RouterMatch="NavLinkMatch.Prefix">
              我的一天
            </menuitem>
            <menuitem RouterLink="/star" RouterMatch="NavLinkMatch.Prefix">
              重要任务
            </menuitem>
            <menuitem RouterLink="/search" RouterMatch="NavLinkMatch.Prefix">
              全部
            </menuitem>
          </menu>
        </Sider>
        <Layout Class="site-layout"> @Body </Layout>
      </Layout>
    </Authorized>
    <NotAuthorized>
      <ToDo.Client.Pages.Login></ToDo.Client.Pages.Login>
    </NotAuthorized>
  </AuthorizeView>
</CascadingAuthenticationState>

当授权通过后显示<AuthorizeView><Authorized>的菜单及主页,反之显示<NotAuthorized>Login组件内容。
当需要根据权限显示不同内容,可以使用<AuthorizeView>Policy属性实现,具体是在AuthenticationStateProvider中通过配置策略,比如示例中claims.Add(new Claim("Admin", "Admin"));就添加了Admin策略,在页面上只需<AuthorizeView Policy="Admin">就可以控制只有Admin策略的账户显示其内容了。
CascadingAuthenticationState级联身份状态,它采用了 Balzor 组件中级联机制,这样我们可以在任意层级的组件中使用AuthorizeView来控制 UI 了
AuthorizeView 组件根据用户是否获得授权来选择性地显示 UI 内容。
Authorized组件中的内容只有在获得授权时显示。
NotAuthorized组件中的内容只有在未经授权时显示。

修改_Imports.razor文件,添加必要的引用

@using Microsoft.AspNetCore.Components.Authorization

运行查看效果
在这里插入图片描述

更多关于安全

安全是一个很大的话题,这个章节只是介绍了其最简单的实现方式,还有更多内容推荐阅读官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/?view=aspnetcore-5.0

次回预告

我们通过几张图表,将我们 ToDo 应用中任务情况做个完美统计。

学习资料

更多关于Blazor学习资料:https://aka.ms/LearnBlazor

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值