隐藏

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

发布:2022/1/7 17:49:30作者:管理员 来源:本站 浏览次数:1111

我的的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本身在安全性上存在以下两个风险:

    Token无法注销,所以可以在Token有效期内发送的非法请求,服务端无能为力。
    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

AuthenticationStateProvider 是 AuthorizeView 组件和 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

如果想了解更多,可以直接浏览微软文档:
ASP.NET Core Blazor 身份验证和授权
次回预告

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