ASP.NET Core按用戶等級授權(quán)的方法
驗(yàn)證和授權(quán)是兩個獨(dú)立但又存在聯(lián)系的過程。驗(yàn)證是檢查訪問者的合法性,授權(quán)是校驗(yàn)訪問者有沒有權(quán)限查看資源。它們之間的聯(lián)系——先驗(yàn)證再授權(quán)。
貫穿這兩過程的是叫 Claim 的東東,可以叫它“聲明”。沒什么神秘的,就是由兩個字符串組成的對象,一曰 type,一曰 value。type 和 value 有著映射關(guān)系,類似字典結(jié)構(gòu)的 key 和 value。Claim 用來收集用戶相關(guān)信息,比如
UserName = admin Age = 105 Birth = 1990,4,12 Address = 火星街130號
ClaimTypes 靜態(tài)類定義了一些標(biāo)準(zhǔn)的 type 值。如用戶名Name,國家Country,手機(jī)號MobilePhone,家庭電話HomePhone 等等。你也可以自己定義一個,反正就是個字符串。
另外,還有一個ClaimValueTypes 輔助類,也是一組字符串,用于描述 value 的類型。如Integer、HexBinary、String、DnsName 等。其實(shí)所有 value 都是用字符串表示的,ValueTypes 只是基于內(nèi)容本身的含義而定義的分類,在查找和分析 Claim 時有輔助作用。比如,值是 “00:15:30”,可以認(rèn)為其 ValueType 是 Time,這樣在分析這些數(shù)據(jù)時可以方便一些。
一般,代碼會在 Sign in 前收集這些用戶信息。作用是為后面的授權(quán)做準(zhǔn)備。授權(quán)時會對這些用戶信息進(jìn)行綜合評估,以決定該用戶是否有能力訪問某些資源。
回到本文主題。本文的重點(diǎn)是說授權(quán),老周的想法是根據(jù)用戶的等級來授權(quán)。比如,用戶A的等級是2,如果某個URL要求4級以上的用戶才能訪問,那么A就無權(quán)訪問了。
為了簡單,老周就不建數(shù)據(jù)庫這么復(fù)雜的東西了,直接寫個類就好了。
public class User
{
public string? UserName { get; set; }
public string? Password { get; set; }
/// <summary>
/// 用戶等級,1-5
/// </summary>
public int Level { get; set; } = 1;
}上面類中,Level 屬性表示的是用戶等級。然后,用下面的代碼來產(chǎn)生一些用戶數(shù)據(jù)。
public static class UserDatas
{
internal static readonly IEnumerable<User> UserList = new User[]
{
new(){UserName="admin", Password="123456", Level=5},
new(){UserName="kitty", Password="112211", Level=3},
new(){UserName="bob",Password="215215", Level=2},
new(){UserName="billy", Password="886600", Level=1}
};
// 獲取所有用戶
public static IEnumerable<User> GetUsers() => UserList;
// 根據(jù)用戶名和密碼校對后返回的用戶實(shí)體
public static User? CheckUser(string username, string passwd)
{
return UserList.FirstOrDefault(u => u.UserName!.Equals(username, StringComparison.OrdinalIgnoreCase) && u.Password == passwd);
}
}這樣的功能,對于咱們今天要說的內(nèi)容,已經(jīng)夠用了。
關(guān)于驗(yàn)證,這里不是重點(diǎn)。所以老周用最簡單的方案——Cookie。
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt =>
{
opt.LoginPath = "/UserLog";
opt.LogoutPath = "/Logout";
opt.AccessDeniedPath = "/Denied";
opt.Cookie.Name = "ck_auth_ent";
opt.ReturnUrlParameter = "backUrl";
});這個驗(yàn)證方案是結(jié)合 Session 和 Cookie 來完成的,也是Web身份驗(yàn)證的經(jīng)典方案了。上述代碼中我配置了一些選項(xiàng):
LoginPath——當(dāng) SessionID 和 Cookie 驗(yàn)證不成功時,自動轉(zhuǎn)到些路徑,要求用戶登錄。
LogoutPath——退出登錄(注銷)時的路徑。
AccessDeniedPath——訪問被拒絕后轉(zhuǎn)到的路徑。
ReturnUrlParameter——回調(diào)URL,就是驗(yàn)證失敗后會轉(zhuǎn)到登錄URL,然后會在URL參數(shù)中加一個回調(diào)URL。這個選項(xiàng)就是配置這個參數(shù)的名稱的。比如這里我配置為backUrl。假如我要訪問/home,但是,驗(yàn)證失敗,跳轉(zhuǎn)到 /UserLog 登錄,這時候會在URL后面加上 /UserLog?backUrl=/home。如果登錄成功且驗(yàn)證也成功了,就會跳轉(zhuǎn)回 backUrl指定的路徑(/home)。
這里要注意的是,我們不能把要求輸入用戶名和密碼作為驗(yàn)證過程。驗(yàn)證由內(nèi)置的CookieAuthenticationHandler 類去處理,它只驗(yàn)證 Session 和 Cookie 中的數(shù)據(jù)是否匹配,而不是檢查用戶名/密碼對不對。你想想,如果把檢查用戶名和密碼作為驗(yàn)證過程,那豈不是每次都要讓用戶去輸入一次?說不定每訪問一個URL都要驗(yàn)證一次的,那用戶不累死?所以,輸入用戶名/密碼登錄只在 LoginPath 選項(xiàng)中配置,只在必要時輸入一次,然后配合 session 和 cookie 把狀態(tài)記錄下來,下次再訪問,只驗(yàn)證此狀態(tài)即可,不用再輸入了。
LogoutPath 和AccessDeniedPath 我就不弄太復(fù)雜了,直接這樣就完事。
app.MapGet("/Denied", () => "訪問被拒絕");
app.MapGet("/Logout", async (HttpContext context) =>
{
await context.SignOutAsync();
});對于 LoginPath,我用一個 Razor Pages 來處理。
@page
@using MyApp
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@using System.Security.Claims
@addTagHelper *,Microsoft.AspNetCore.Mvc.TagHelpers
<form method="post">
<style>
label{
display:inline-block;
min-width:100px;
}
</style>
<div>
<label for="userName">用戶名:</label>
<input type="text" name="userName" />
</div>
<div>
<label for="passWord">密碼:</label>
<input type="password" name="passWord" />
</div>
<div>
<button type="submit">登入</button>
</div>
</form>
@functions{
//[IgnoreAntiforgeryToken]
public async void OnPost(string userName, string passWord)
{
var u = UserDatas.CheckUser(userName, passWord);
if(u != null)
{
Claim[] cs = new Claim[]
{
new Claim(ClaimTypes.Name, u.UserName!),
new Claim("level", u.Level.ToString()) //注意這里,收集重要情報
};
ClaimsIdentity id = new(cs, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal p = new(id);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, p);
//HttpContext.Response.Redirect("/");
}
}
}
其他的各位可以不關(guān)注,重點(diǎn)是 OnPost 方法,首先用剛才寫的UserDatas.CheckUser 靜態(tài)方法來驗(yàn)證用戶名和密碼(這個是要我們自己寫代碼來完成的,CookieAuthenticationHandler 可不負(fù)責(zé)這個)。用戶名和密碼正確后,咱們就要收集信息了。收集啥呢?這個要根據(jù)你稍后在授權(quán)時要用到什么來決定的。就拿今天的主題來講,我們需要知道用戶等級,所以要收集 Level 屬性的值。這里 ClaimType 我直接用“level”,Value 就是 Level 屬性的值。
收集完用戶信息后,要匯總到ClaimsPrincipal 對象中,隨后調(diào)用HttpContext.SignInAsync 擴(kuò)展方法,會觸發(fā)CookieAuthenticationHandler 去保存狀態(tài),因?yàn)樗鼘?shí)現(xiàn)了IAuthenticationSignInHandler 接口,從而帶有SignInAsync 方法。
var ticket = new AuthenticationTicket(signInContext.Principal!, signInContext.Properties, signInContext.Scheme.Name);
// 保存 Session
if (Options.SessionStore != null)
{
if (_sessionKey != null)
{
// Renew the ticket in cases of multiple requests see: https://github.com/dotnet/aspnetcore/issues/22135
await Options.SessionStore.RenewAsync(_sessionKey, ticket, Context, Context.RequestAborted);
}
else
{
_sessionKey = await Options.SessionStore.StoreAsync(ticket, Context, Context.RequestAborted);
}
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.ClaimsIssuer));
ticket = new AuthenticationTicket(principal, null, Scheme.Name);
}
// 生成加密后的 Cookie 值
var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
// 追加 Cookie 到響應(yīng)消息中
Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name!,
cookieValue,
signInContext.CookieOptions);
……----------------------------------------------------------------------------------------
好了,上面的都是周邊工作,下面我們來干正事。
授權(quán)大體上分為兩種模式:
1、基于角色授權(quán)。即“你是誰就給你相應(yīng)的權(quán)限”。你是狼人嗎?你是預(yù)言家嗎?你是女巫嗎?你是好人嗎?是狼人就賦予你殺人的權(quán)限。
2、基于策略。老周覺得這個靈活性高一點(diǎn)(純個人看法)。一個策略需要一定數(shù)量的約束條件,是否賦予用戶權(quán)限就看他能否滿足這些約束條件了。約束實(shí)現(xiàn)IAuthorizationRequirement 接口。這個接口未包含任何成員,因此你可以自由發(fā)揮了。
這只不過是按用途來劃分的,若從類型本質(zhì)上看,就是一堆IAuthorizationRequirement 組合起來提供給了AuthorizationHandlerContext,AuthorizationHandlerContext 再通過一堆IAuthorizationHandler 來處理。最后由IAuthorizationEvaluator 去總結(jié)授權(quán)的結(jié)果。
這里咱們需要的約束條件是用戶等級,所以,咱們實(shí)現(xiàn)一個LevelAuthorizationRequirement。
public class LevelAuthorizationRequirement : IAuthorizationRequirement
{
public int Level { get; private set; }
public LevelAuthorizationRequirement(int lv)
{
Level = lv;
}
}授權(quán)處理有兩個接口:
1、IAuthorizationHandler:處理過程,一個授權(quán)請求可以執(zhí)行多個IAuthorizationHandler。一般用于授權(quán)過程中的某個階段(或針對某個約束條件)。一個授權(quán)請求可以由多IAuthorizationHandler 參與處理。
2、IAuthorizationEvaluator:綜合評估是否決定授權(quán)。評估一般在各種IAuthorizationHandler 之后進(jìn)行收尾工作。所以只執(zhí)行一次就可以了,用于總結(jié)整個授權(quán)過程的情況得出最終結(jié)論(放權(quán)還是不放權(quán))。
ASP.NET Core 內(nèi)置了DefaultAuthorizationEvaluator,這是默認(rèn)實(shí)現(xiàn),如無特殊需求,我們不會重新實(shí)現(xiàn)。
public class DefaultAuthorizationEvaluator : IAuthorizationEvaluator
{
public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
=> context.HasSucceeded
? AuthorizationResult.Success()
: AuthorizationResult.Failed(context.HasFailed
? AuthorizationFailure.Failed(context.FailureReasons)
: AuthorizationFailure.Failed(context.PendingRequirements));
}所以,咱們的代碼可以選擇實(shí)現(xiàn)一個抽象類:AuthorizationHandler<TRequirement>,其中,TRequirement 需要實(shí)現(xiàn)IAuthorizationRequirement 接口。這個抽象類已經(jīng)滿足咱們的需求了。
public class LevelAuthorizationHandler : AuthorizationHandler<LevelAuthorizationRequirement>
{
// 策略名稱,寫成常量方便使用
public const string POLICY_NAME = "Level";
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LevelAuthorizationRequirement requirement)
{
// 查找聲明
Claim? clm = context.User.Claims.FirstOrDefault(c => c.Type == "level");
if(clm != null)
{
// 讀出用戶等級
int lv = int.Parse(clm.Value);
// 看看用戶等級是否滿足條件
if(lv >= requirement.Level)
{
// 滿足,標(biāo)記此階段允許授權(quán)
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}在授權(quán)請求啟動時,AuthorizationHandlerContext (上下文)對象會把所有IAuthorizationRequirement 對象添加到一個哈希表中(HashSet<T>),表示一大串正等著授權(quán)處理的約束條件。
當(dāng)我們調(diào)用 Succeed 方法時,會把已滿足要求的IAuthorizationRequirement 傳遞給方法參數(shù)。在 Success 方法內(nèi)部會從哈希表中刪除此IAuthorizationRequirement,以表示該條件已滿足了,不必再證。
public virtual void Succeed(IAuthorizationRequirement requirement)
{
_succeedCalled = true;
_pendingRequirements.Remove(requirement);
}記得要在服務(wù)容器中注冊,否則咱們寫的 Handler 是不起作用的。
builder.Services.AddSingleton<IAuthorizationHandler, LevelAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, LevelAuthorizationHandler>();
builder.Services.AddAuthorizationBuilder().AddPolicy(LevelAuthorizationHandler.POLICY_NAME, pb =>
{
pb.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
pb.AddRequirements(new LevelAuthorizationRequirement(3));
});策略的名稱我們前面以常量的方式定義了,記得否?
public const string POLICY_NAME = "Level";
AddAuthenticationSchemes 是把此授權(quán)策略與一個驗(yàn)證方案關(guān)聯(lián),當(dāng)進(jìn)行鑒權(quán)時順便做一次驗(yàn)證。上述代碼我們關(guān)聯(lián) Cookie 驗(yàn)證即可,這個在文章前面已經(jīng)設(shè)置了。AddRequirements 方法添加我們自定義的約束條件,這里我設(shè)置的用戶等級是 3 —— 用戶等級要 >= 3 才允許訪問。
下面寫個 MVC 控制器來檢驗(yàn)一下是否能正確授權(quán)。
public class HomeController : Controller
{
[HttpGet("/")]
[Authorize(Policy = LevelAuthorizationHandler.POLICY_NAME)]
public IActionResult Index()
{
return View();
}
}這里咱們用基于策略的授權(quán)方式,所以[Authorize]特性要指定策略名稱。
好,運(yùn)行。本來是訪問根目錄 / 的,但由于驗(yàn)證不通過,自動跳到登錄頁了。

注意URL上的 backUrl 參數(shù):?backUrl=/。本來要訪問 / 的,所以登錄后再跳回 / 。我們選一個用戶等級為 5 的登錄。

由于用戶等級為 5,是 >=3 的存在,所以授權(quán)通過。

現(xiàn)在,把名為 ck_auth_ent 的Cookie刪除。

這個 ck_auth_ent 是在代碼中配置的,還記得嗎?
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt =>
{
opt.LoginPath = "/UserLog";
opt.LogoutPath = "/Logout";
opt.AccessDeniedPath = "/Denied";
opt.Cookie.Name = "ck_auth_ent";
opt.ReturnUrlParameter = "backUrl";
});現(xiàn)在咱們找個用戶等級低于 3 的登錄。

登錄后被拒絕訪問。

到此為止,好像、貌似、似乎已大功告成了。但是,老周又發(fā)現(xiàn)問題了:如果我一個控制器內(nèi)或不同控制器之間有的操作方法要讓用戶等級 3 以上的用戶訪問,有些操作方法只要等級在 2 以上的用戶就可以訪問。這該咋整呢?有大伙伴可以會說了,那就多弄幾個策略,每個策略代表一個等級。
builder.Services.AddAuthorizationBuilder()
.AddPolicy("Level3", pb =>
{
pb.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
pb.AddRequirements(new LevelAuthorizationRequirement(3));
})
.AddPolicy("Level5", pb =>
{
pb.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
pb.AddRequirements(new LevelAuthorizationRequirement(5));
});是的,這樣確實(shí)是可行的。不過不夠動態(tài),要是我弄個策略從 Level1 到 Level10 呢,豈不要寫十個?
官方有個用 Age 生成授權(quán)策略的示例讓老周獲得了靈感——是的,咱們就是要動態(tài)生成授權(quán)策略。需要用到一個接口:IAuthorizationPolicyProvider。這個接口可以根據(jù)策略名稱返回授權(quán)策略,所以,咱們可以拿它做文章。
public class LevelAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
private readonly AuthorizationOptions _options;
public LevelAuthorizationPolicyProvider(IOptions<AuthorizationOptions> opt)
{
_options = opt.Value;
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
{
return Task.FromResult(_options.DefaultPolicy);
}
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
{
return Task.FromResult(_options.FallbackPolicy);
}
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if(policyName.StartsWith(LevelAuthorizationHandler.POLICY_NAME,StringComparison.OrdinalIgnoreCase))
{
// 比如,策略名 Level4,得到等級4
// 提取名稱最后的數(shù)字
int prefixLen = LevelAuthorizationHandler.POLICY_NAME.Length;
if(int.TryParse(policyName.Substring(prefixLen), out int level))
{
// 動態(tài)生成策略
AuthorizationPolicyBuilder plcyBd = new AuthorizationPolicyBuilder();
plcyBd.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
plcyBd.AddRequirements(new LevelAuthorizationRequirement(level));
// Build 方法生成策略
return Task.FromResult(plcyBd.Build())!;
}
}
// 未處理,交由選項(xiàng)類去返回默認(rèn)的策略
return Task.FromResult(_options.GetPolicy(policyName));
}
}
這樣可以根據(jù)給定的策略名稱,生成與用戶等級相關(guān)的配置。例如,名稱“Level3”,就是等級3;“Level5”就是等級5。
于是,在配置服務(wù)容器時,我們不再需要AddAuthorizationBuilder 一大段代碼了,直接把LevelAuthorizationPolicyProvider 注冊一下就行了。
builder.Services.AddSingleton<IAuthorizationHandler, LevelAuthorizationHandler>(); builder.Services.AddTransient<IAuthorizationPolicyProvider, LevelAuthorizationPolicyProvider>(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt => ……
然后,在MVC控制器上咱們就可以666地玩了。
public class HomeController : Controller
{
[HttpGet("/")]
[Authorize(Policy = $"{LevelAuthorizationHandler.POLICY_NAME}3")]
public IActionResult Index()
{
return View();
}
[HttpGet("/music")]
[Authorize(Policy = $"{LevelAuthorizationHandler.POLICY_NAME}2")]
public IActionResult Foo()
=> Content("2星級用戶擾民音樂俱樂部");
[HttpGet("/movie")]
[Authorize(Policy = $"{LevelAuthorizationHandler.POLICY_NAME}5")]
public IActionResult Movies()
=> Content("5星級鬼畜影院");
}這樣一來,配置不同等級的授權(quán)就方便多了。
到此這篇關(guān)于ASP.NET Core按用戶等級授權(quán)的文章就介紹到這了,更多相關(guān)ASP.NET Core授權(quán)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
asp.net結(jié)合Ajax驗(yàn)證用戶名是否存在的代碼
關(guān)于Ajax的操作簡單總結(jié),結(jié)合Ajax驗(yàn)證用戶名是否存在的代碼2010-06-06
ASP.NET?MVC5實(shí)現(xiàn)文件上傳與地址變化處理(5)
這篇文章主要介紹了ASP.NET?MVC5實(shí)現(xiàn)文件上傳與地址變化處理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2015-09-09
asp.net JavaScript插件 JavaScript Function Outliner
一個JavaScript Function Outliner插件 第四版本 支持內(nèi)嵌javascript,且可以對javascript進(jìn)行壓縮2008-07-07
asp.net基于windows服務(wù)實(shí)現(xiàn)定時發(fā)送郵件的方法
這篇文章主要介紹了asp.net基于windows服務(wù)實(shí)現(xiàn)定時發(fā)送郵件的方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了asp.net調(diào)用Windows系統(tǒng)服務(wù)的相關(guān)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11
運(yùn)用.net core中實(shí)例講解RabbitMQ
RabbitMQ是實(shí)現(xiàn)了高級消息隊(duì)列協(xié)議(AMQP)的開源消息代理軟件(亦稱面向消息的中間件),本文詳細(xì)講解了RabbitMQ以及運(yùn)用.net core中實(shí)例講解其6中模式,感興趣的小伙伴一起來學(xué)習(xí)吧2021-09-09
Gridview利用DataFormatString屬性設(shè)置數(shù)據(jù)格式的方法
這篇文章主要介紹了Gridview利用DataFormatString屬性設(shè)置數(shù)據(jù)格式的方法,涉及Gridview屬性操作及數(shù)據(jù)格式相關(guān)技巧,需要的朋友可以參考下2016-03-03
CheckBoxList兩列并排編譯為表格顯示具體實(shí)現(xiàn)
CheckBoxList兩列并排的顯示效果相比大家都有見到過吧,下面是具體的實(shí)現(xiàn)代碼,感興趣的朋友可以參考下哈2013-05-05

