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

作者:陈超超
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 通过 AES 加密存储在客户端,理论上可以进行离线破解,破解后就能任意伪造 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 };
                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());
                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) {
          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>
    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);
                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
    

    运行查看效果

    更多关于安全

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

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

    更多关于Blazor学习资料:

    [aka.ms/LearnBlazor…

    ](link.zhihu.com/?target=htt…)

  • 私信