原文:https://chrissainty.com/securing-your-blazor-apps-configuring-role-based-authorization-with-client-side-blazor/

什么是基于角色的授权?

当涉及ASP.NET Core授权时,我们有两种选择,基于角色和基于策略(也有基于声明的,但那只是基于策略的一种特殊类型)。

基于角色的授权最初是在ASP.NET(ASP.NET Core之前)中引入,这是一种限制对资源访问的声明性方法。

开发人员可以指定用户必须是其成员的特定角色的名称,以便访问特定的资源。一般是使用[Authorize]属性指定一个角色或角色列表[Authorize(Roles="Admin")]。用户可以是单个角色的成员,也可以是多个角色的成员。

如何创建和管理角色取决于所使用的备份存储。到目前为止我们一直使用ASP.NET Core Identity,我们将继续使用它来管理和存储我们的角色。

本文章代码将基于前一篇文章基础上搭建。

设置ASP.NET Core Identity角色

我们需要添加角色服务到我们的应用中。我们需要更新Startup类中的ConfigureService方法。

            services.AddDefaultIdentity<IdentityUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

IdentityRole是ASP.NET Core Identity提供的默认角色类型。如果它无法满足你的需求,你可以提供其他的角色类型。

接下来我们将为数据库添加一些角色数据-添加一个用户和管理员角色。为此,我们将重载ApplicationDbContext中的方法OnModelCreating

    public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions options) : base(options) {
} protected override void OnModelCreating(ModelBuilder builder) {
base.OnModelCreating(builder); builder.Entity<IdentityRole>().HasData(new IdentityRole { Name = "User", NormalizedName = "USER", Id = Guid.NewGuid().ToString(), ConcurrencyStamp = Guid.NewGuid().ToString() });
builder.Entity<IdentityRole>().HasData(new IdentityRole { Name = "Admin", NormalizedName = "ADMIN", Id = Guid.NewGuid().ToString(), ConcurrencyStamp = Guid.NewGuid().ToString() });
}
}

完成之后,我们需要生成迁移,然后将其应用到数据库。

    Add-Migration SeedRoles
Update-Database

为角色分配用户

现在我们已经有一些可用的角色了,我们现在来更新账户控制器(Accounts controller)创建用户的动作。

在新增用户时候为其分配User角色。如果新用户的电子邮件以admin开头,则为其分配UserAdmin角色组。

        [HttpPost]
public async Task<IActionResult> Post([FromBody]RegisterModel model) {
var newUser = new IdentityUser { UserName = model.Email, Email = model.Email }; var result = await _userManager.CreateAsync(newUser, model.Password); if (!result.Succeeded) {
var errors = result.Errors.Select(x => x.Description); return BadRequest(new RegisterResult { Successful = false, Errors = errors }); } //为所有的新用户分配User角色
await _userManager.AddToRoleAsync(newUser, "User"); //如果电子邮件以'admin'开头则分配Admin角色
if (newUser.Email.StartsWith("admin")) {
await _userManager.AddToRoleAsync(newUser, "Admin");
} return Ok(new RegisterResult { Successful = true });
}

现在我们在用户注册时为其分配了角色,但我们需要将这些信息传递给Blazor。我们需要更新JSON Web Token中的声明来处理这个需求。

将角色声明添加到JWT

现在我们来更新登录控制器(Login controller)中的Login方法。先以下用于生成声明的代码。

     var claims = new[]
{
  new Claim(ClaimTypes.Name, login.Email)
};

并使用以下代码替换。

            var user = await _signInManager.UserManager.FindByEmailAsync(login.Email);
var roles = await _signInManager.UserManager.GetRolesAsync(user); var claims= new List<Claim>(); claims.Add(new Claim(ClaimTypes.Name, login.Email)); foreach (var role in roles) {
claims.Add(new Claim(ClaimTypes.Role, role));
}

我们通过UserManager获取当前用户并获取用户拥有的角色。之前是将用户电子邮件添加到Name声明,现在如果用户拥有角色,我们则循环将角色添加到Role声明中。

关于角色声明有一点比较很重要问题。如果一个用户拥有两个角色,那么这两个角色声明会被添加到JWT中。

http://schemas.microsoft.com/ws/2008/06/identity/claims/role - "User"
http://schemas.microsoft.com/ws/2008/06/identity/claims/role - "Admin"

然后事实上并非如此,而是两个角色合并为一个数组。

http://schemas.microsoft.com/ws/2008/06/identity/claims/role - ["User", "Admin"]

关于这一点很重要,在Blazor客户端处理角色时需要注意。

在Blazor客户端使用角色

我们将角色分配给新用户,当他们登录时,我们通过JWT返回这些角色。那么在Blazor内部要如何使用角色呢?

在这个问题上目前微软官方并未提供任何可以帮助我们处理角色的东西,所以我们必须手动处理它。

private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split('.')[];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes); keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles); if (roles != null)
{
if (roles.ToString().Trim().StartsWith("["))
{
var parsedRoles = JsonSerializer.Deserialize<string[]>(roles.ToString()); foreach (var parsedRole in parsedRoles)
{
claims.Add(new Claim(ClaimTypes.Role, parsedRole));
}
}
else
{
claims.Add(new Claim(ClaimTypes.Role, roles.ToString()));
} keyValuePairs.Remove(ClaimTypes.Role);
} claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()))); return claims;
} private byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % )
{
case : base64 += "=="; break;
case : base64 += "="; break;
}
return Convert.FromBase64String(base64);
}

上面代码对JWT进行解码、提取声明并返回声明。但我们没有涉及的是我对其进行了修改,以处理特殊情况下的角色。

如果存在角色声明,那么我们将检查第一个字符是否为[,表名它是一个JSON数组。如果找到roles声明,则解析提取角色名称,循环遍历角色名称,并将每个角色名称作为声明添加。如果roles不是一个数组,则作为单个角色声明添加。

这个方法不一定是最好的,但它确实实现了我们的目的。

我们需要更新MarkUserAsAuthenticated方法来调用ParseClaimsFromJwt

        public void MarkUserAsAuthenticated(string token) {
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser)); NotifyAuthenticationStateChanged(authState);
}

最后,我们需要更新AuthService中的Login方法,以便在调用MarkUserAsAuthenticated时传递令牌而不是电子邮件。

        public async Task<LoginResult> Login(LoginModel loginModel) {
var result = await _httpClient.PostJsonAsync<LoginResult>("api/Login", loginModel); if (result.Successful) {
await _localStorage.SetItemAsync("authToken", result.Token);
((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsAuthenticated(result.Token);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token); return result;
} return result;
}

现在,我们应该能够将基于角色的授权应用到我们的应用程序中。我们来关注下API的处理。

将基于角色的授权应用于API

WeatherForecastController上的Get方法设置为仅对Admin角色中经过身份验证的用户可访问。我们使用Authorize属性并指定用于访问它的角色。(这里在默认生成模版与原文有出入)

        [HttpGet]
[Authorize(Roles = "Admin")]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(, ).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-, ),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}

如果您创建一个属于Admin角色的新用户,并在Blazor应用程序中访问Fetch Data页面,您应该可以看到一切都按预期的加载。

但你如果创建一个普通的用户并执行相同的操作,您应该会看到页面被卡在Loading...

在Blazor中使用基于角色的授权

Blazor还可以使用Authorize属性来保护页面。这是通过使用@attribute指令来应该[Authorize]属性来实现的。您还可以使用AuthorizeView组件来限制对页面部分的访问。

在 Blazor WebAssembly 应用中,可以绕过授权检查,因为用户可以修改所有客户端代码。 所有客户端应用程序技术都是如此,其中包括 JavaScript SPA 框架或任何操作系统的本机应用程序。

始终对客户端应用程序访问的任何 API 终结点内的服务器执行授权检查。

由于预测数据只对管理员用户可用,所以我们使用Authorize属性限制对该页面的访问。

@page "/fetchdata"
@attribute [Authorize(Roles = "Admin")]

现在尝试使用管理用户登录到该页面。一切应该都正常加载。然后尝试使用普通用户登录,您应该会看到一条未经授权的消息。

我们来测试一下AuthorizeView,在主页(index.razor)添加如下代码。

<AuthorizeView Roles="User">
<p>You can only see this if you're in the User role.</p>
</AuthorizeView> <AuthorizeView Roles="Admin">
<p>You can only see this if you're in the Admin role.</p>
</AuthorizeView>

同样,使用管理员用户账户登录。您应该看到这两条消息,因为您同时拥有这两个角色权限。

如果您使用普通用户登录则只能看到第一条消息。

总结

在这篇文章中,我们了解了什么是基于角色的授权以及如何使用ASP.NET Core Identity来设置和管理角色。然后我们讨论了如何使用JSON Web Tokens将角色从API传递给客户端并处理在Blazor中的角色声明,最后在API和Blazor上实现一些基于角色的授权检查。

我只是想重申一下,您不能仅仅依赖于客户端身份验证或授权,客户端永远不能被信任。必须始终在服务器上执行身份验证和授权检查。

附上代码(Github)

Blazor应用程序基于角色的授权的更多相关文章

  1. Blazor应用程序基于策略的授权

    原文:https://chrissainty.com/securing-your-blazor-apps-configuring-policy-based-authorization-with-bla ...

  2. Asp.Net Core--基于角色的授权

    翻译如下: 当创建身份时,它可以属于一个或多个角色,例如Tracy可以属于管理员和用户角色,而Scott可以仅属于用户角色. 如何创建和管理这些角色取决于授权过程的后备存储. 角色通过ClaimsPr ...

  3. ASP.NET MVC 随想录——探索ASP.NET Identity 身份验证和基于角色的授权,中级篇

    在前一篇文章中,我介绍了ASP.NET Identity 基本API的运用并创建了若干用户账号.那么在本篇文章中,我将继续ASP.NET Identity 之旅,向您展示如何运用ASP.NET Ide ...

  4. Security » Authorization » 基于角色的授权

    Role based Authorization¶ 基于角色的授权 133 of 153 people found this helpful When an identity is created i ...

  5. Asp.net中基于Forms验证的角色验证授权

    Asp.net的身份验证有有三种,分别是"Windows | Forms | Passport",其中又以Forms验证用的最多,也最灵活. Forms 验证方式对基于用户的验证授 ...

  6. ASP.NET Identity 身份验证和基于角色的授权

    ASP.NET Identity 身份验证和基于角色的授权 阅读目录 探索身份验证与授权 使用ASP.NET Identity 身份验证 使用角色进行授权 初始化数据,Seeding 数据库 小结 在 ...

  7. ASP.NET Core 2.1中基于角色的授权

    ASP.NET Core 2.1中基于角色的授权 授权是来描述用户能够做什么的过程.例如,只允许管理员用户可以在电脑上进行软件的安装以及卸载.而非管理员用户只能使用软件而不能进行软件的安装以及卸载.它 ...

  8. ASP.NET Core 使用 JWT 自定义角色/策略授权需要实现的接口

    目录 ① 存储角色/用户所能访问的 API ② 实现 IAuthorizationRequirement 接口 ③ 实现 TokenValidationParameters ④ 生成 Token ⑤ ...

  9. java:shiro(认证,赋予角色,授权...)

    1.shiro(权限框架(认证,赋予角色,授权...)): readme.txt(运行机制): 1.从jsp的form中的action属性跳转到springmvc的Handler中(controlle ...

随机推荐

  1. JAVA 设置模块间的依赖关系

    项目目录概况 Demo01项目 Test01.java package com.sam.demo01; public class Test01 { public void ShowTest01() { ...

  2. 关于es6及以上的js编译成es5

    问题:es6及以上版本在IE浏览器上不能执行起来,但Chrome浏览器上轻松运行,解决兼容IE的问题就需要使用babel:这个可以去babel的官网去查看; 关于babel的简单使用,有两种方式: 1 ...

  3. CSTC-2017-Web-writeup

    0x01  前言 这一次的比赛web题只做出来3个,也是菜的抠脚.. 0x02 web-签到题   php弱类型 查看源码,发现是代码审计,要求用户名必须为字母,密码必须为数字,登陆页面可以用开头为0 ...

  4. Android TextView文本处理库推荐

    版权声明:本文为xing_star原创文章,转载请注明出处! 本文同步自http://javaexception.com/archives/115 Android TextView文本处理库推荐 现在 ...

  5. git设置github的远程仓库的相关操作

        git能够把github作为远程仓库,本地可以进行推送有关变更,从而多人可以进行协作开发工作.    1  ssh-keygen -t rsa -C "your-email@163. ...

  6. Mysql—日志文件系统

    MySQL中的日志包括:错误日志.通用查询日志.二进制日志.慢查询日志等等.这里主要介绍下比较常用的两个功能:通用查询日志和慢查询日志. 错误日志:记录启动.运行或停止mysqld时出现的问题.通用日 ...

  7. 最长上升子序列(LIS: Longest Increasing Subsequence)

    示例: 输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4. 从网上找的一段代码(我由java改为了C++版本),原作者 ...

  8. 12Java基础_数组定义格式/动态初始化/静态初始化

    /* Java数组 格式一: int[] array; 格式二: int array[]; 数组初始化: 为数组中的元素分配内存空间 动态初始化: int[] array=new int[数组长度] ...

  9. 深浅拷贝及 join set

    1.join s="**".join (['风清扬',"独孤求败"] )       join  把里面的东西拿出来,进行拼接 s="_". ...

  10. python,adb,分别给多个设备安装多个apk文件,os.popen(); os.system; os.path.splitext(); a.split(' \t'); readlines(); append(); os.path.join(); time.sleep();

    #encoding:utf-8import os,time#=======================查找手机设备序列号=============a='adb devices'b=os.popen ...