原文: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開頭,則為其分配User和Admin角色組,
[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('.')[1]; 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 % 4) { case 2: base64 += "=="; break; case 3: 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(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), 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)
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/96217.html
標籤:.NET Core
