《進擊吧!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-ToDohtml

做者:陳超超
Ant Design Blazor 項目貢獻者,擁有十多年從業經驗,長期基於.Net 技術棧進行架構與開發產品的工做,現就任於正泰集團。
郵箱:timchen@live.com
歡迎各位讀者有任何問題聯繫我,咱們共同進步。前端

個人的 ToDo 應用基本功能已經完成,可是本身的待辦固然只有本身知道,因此咱們此次給咱們的應用增長一些安全方面的功能。git

Blazor 身份驗證與受權

身份驗證

Blazor Server 應用和 Blazor WebAssembly 應用的安全方案有所不一樣。程序員

  • Blazor WebAssembly

Blazor WebAssembly 應用在客戶端上運行。 因爲用戶可繞過客戶端檢查,由於用戶可修改全部客戶端代碼, 所以受權僅用於肯定要顯示的 UI 選項,全部客戶端應用程序技術都是如此。github

  • Blazor Server

Blazor Server 應用經過使用 SignalR 建立的實時鏈接運行。 創建鏈接後,將處理基於 SignalR 的應用的身份驗證。 可基於 cookie 或一些其餘持有者令牌進行身份驗證。後端

受權

AuthorizeView 組件根據用戶是否得到受權來選擇性地顯示 UI 內容。 若是隻須要爲用戶顯示數據,而不須要在過程邏輯中使用用戶的標識,那麼此方法頗有用。api

<AuthorizeView>
  <Authorized>
    <!--驗證經過顯示-->
  </Authorized>
  <NotAuthorized>
    <!--驗證不經過顯示-->
  </NotAuthorized>
</AuthorizeView>

Blazor 中使用 Token

在 Blazor WebAssembly 模式下, 由於應用都在客戶端運行,因此使用 Token 做爲身份認證的方式是一個比較好的選擇。
基本的使用時序圖以下安全

sequenceDiagram 前端 ->> 服務端: 登陸請求 服務端 ->> 服務端:驗證身份 服務端 ->> 服務端:建立Token 服務端 -->> 前端: 返回Token 前端 ->> 服務端: 業務請求<br/>包含Token 服務端 ->> 服務端:驗證Token 服務端 -->> 前端: 成功

對於安全要求不高的應用採用這個方法簡單、易維護,徹底沒有問題。服務器

可是 Token 自己在安全性上存在如下兩個風險:cookie

  1. Token 沒法註銷,因此能夠在 Token 有效期內發送的非法請求,服務端無能爲力。
  2. Token 經過 AES 加密存儲在客戶端,理論上能夠進行離線破解,破解後就能任意僞造 Token。

所以遇到安全要求很是高的應用時,咱們須要認證服務進行 Token 的有效性驗證

sequenceDiagram 前端 ->> 認證服務: 登陸請求 認證服務 ->> 認證服務:驗證身份 認證服務 ->> 認證服務:建立Token 認證服務 -->> 前端: 返回Token 前端 ->> 服務端: 業務請求<br/>包含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

相關文章
相關標籤/搜索