【ASP.NET Core】按用户等级授权
验证和授权是两个独立但又存在联系的过程。验证是检查访问者的合法性,授权是校验访问者有没有权限查看资源。它们之间的联系——先验证再授权。
贯穿这两过程的是叫 Claim 的东东,可以叫它“声明”。没什么神秘的,就是由两个字符串组成的对象,一曰 type,一曰 value。type 和 value 有着映射关系,类似字典结构的 key 和 value。Claim 用来收集用户相关信息,比如
UserName = admin
Age = 105
Birth = 1990,4,12
Address = 火星街130号
ClaimTypes 静态类定义了一些标准的 type 值。如用户名 Name,国家 Country,手机号 MobilePhone,家庭电话 HomePhone 等等。你也可以自己定义一个,反正就是个字符串。
另外,还有一个 ClaimValueTypes 辅助类,也是一组字符串,用于描述 value 的类型。如 Integer、HexBinary、String、DnsName 等。其实所有 value 都是用字符串表示的,ValueTypes 只是基于内容本身的含义而定义的分类,在查找和分析 Claim 时有辅助作用。比如,值是 “00:15:30”,可以认为其 ValueType 是 Time,这样在分析这些数据时可以方便一些。
一般,代码会在 Sign in 前收集这些用户信息。作用是为后面的授权做准备。授权时会对这些用户信息进行综合评估,以决定该用户是否有能力访问某些资源。
回到本文主题。本文的重点是说授权,老周的想法是根据用户的等级来授权。比如,用户A的等级是2,如果某个URL要求4级以上的用户才能访问,那么A就无权访问了。
为了简单,老周就不建数据库这么复杂的东西了,直接写个类就好了。
public class User
{
public string? UserName { get; set; }
public string? Password { get; set; } /// <summary>
/// 用户等级,1-5
/// </summary>
public int Level { get; set; } = 1;
}
上面类中,Level 属性表示的是用户等级。然后,用下面的代码来产生一些用户数据。
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; // 根据用户名和密码校对后返回的用户实体
public static User? CheckUser(string username, string passwd)
{
return UserList.FirstOrDefault(u => u.UserName!.Equals(username, StringComparison.OrdinalIgnoreCase) && u.Password == passwd);
}
}
这样的功能,对于咱们今天要说的内容,已经够用了。
关于验证,这里不是重点。所以老周用最简单的方案——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";
});
这个验证方案是结合 Session 和 Cookie 来完成的,也是Web身份验证的经典方案了。上述代码中我配置了一些选项:
LoginPath——当 SessionID 和 Cookie 验证不成功时,自动转到些路径,要求用户登录。
LogoutPath——退出登录(注销)时的路径。
AccessDeniedPath——访问被拒绝后转到的路径。
ReturnUrlParameter——回调URL,就是验证失败后会转到登录URL,然后会在URL参数中加一个回调URL。这个选项就是配置这个参数的名称的。比如这里我配置为backUrl。假如我要访问/home,但是,验证失败,跳转到 /UserLog 登录,这时候会在URL后面加上 /UserLog?backUrl=/home。如果登录成功且验证也成功了,就会跳转回 backUrl指定的路径(/home)。
这里要注意的是,我们不能把要求输入用户名和密码作为验证过程。验证由内置的 CookieAuthenticationHandler 类去处理,它只验证 Session 和 Cookie 中的数据是否匹配,而不是检查用户名/密码对不对。你想想,如果把检查用户名和密码作为验证过程,那岂不是每次都要让用户去输入一次?说不定每访问一个URL都要验证一次的,那用户不累死?所以,输入用户名/密码登录只在 LoginPath 选项中配置,只在必要时输入一次,然后配合 session 和 cookie 把状态记录下来,下次再访问,只验证此状态即可,不用再输入了。
LogoutPath 和 AccessDeniedPath 我就不弄太复杂了,直接这样就完事。
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("/");
}
}
}
其他的各位可以不关注,重点是 OnPost 方法,首先用刚才写的 UserDatas.CheckUser 静态方法来验证用户名和密码(这个是要我们自己写代码来完成的,CookieAuthenticationHandler 可不负责这个)。用户名和密码正确后,咱们就要收集信息了。收集啥呢?这个要根据你稍后在授权时要用到什么来决定的。就拿今天的主题来讲,我们需要知道用户等级,所以要收集 Level 属性的值。这里 ClaimType 我直接用“level”,Value 就是 Level 属性的值。
收集完用户信息后,要汇总到 ClaimsPrincipal 对象中,随后调用 HttpContext.SignInAsync 扩展方法,会触发 CookieAuthenticationHandler 去保存状态,因为它实现了 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 到响应消息中
Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name!,
cookieValue,
signInContext.CookieOptions);
……
----------------------------------------------------------------------------------------
好了,上面的都是周边工作,下面我们来干正事。
授权大体上分为两种模式:
1、基于角色授权。即“你是谁就给你相应的权限”。你是狼人吗?你是预言家吗?你是女巫吗?你是好人吗?是狼人就赋予你杀人的权限。
2、基于策略。老周觉得这个灵活性高一点(纯个人看法)。一个策略需要一定数量的约束条件,是否赋予用户权限就看他能否满足这些约束条件了。约束实现 IAuthorizationRequirement 接口。这个接口未包含任何成员,因此你可以自由发挥了。
这里咱们需要的约束条件是用户等级,所以,咱们实现一个 LevelAuthorizationRequirement。
public class LevelAuthorizationRequirement : IAuthorizationRequirement
{
public int Level { get; private set; } public LevelAuthorizationRequirement(int lv)
{
Level = lv;
}
}
授权处理有两个接口:
1、IAuthorizationHandler:处理过程,一个授权请求可以执行多个 IAuthorizationHandler。一般用于授权过程中的某个阶段(或针对某个约束条件)。一个授权请求可以由多 IAuthorizationHandler 参与处理。
2、IAuthorizationEvaluator:综合评估是否决定授权。评估一般在各种 IAuthorizationHandler 之后进行收尾工作。所以只执行一次就可以了,用于总结整个授权过程的情况得出最终结论(放权还是不放权)。
ASP.NET Core 内置了 DefaultAuthorizationEvaluator,这是默认实现,如无特殊需求,我们不会重新实现。
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));
}
所以,咱们的代码可以选择实现一个抽象类:AuthorizationHandler<TRequirement>,其中,TRequirement 需要实现 IAuthorizationRequirement 接口。这个抽象类已经满足咱们的需求了。
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)
{
// 满足,标记此阶段允许授权
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
在授权请求启动时,AuthorizationHandlerContext (上下文)对象会把所有 IAuthorizationRequirement 对象添加到一个哈希表中(HashSet<T>),表示一大串正等着授权处理的约束条件。
当我们调用 Succeed 方法时,会把已满足要求的 IAuthorizationRequirement 传递给方法参数。在 Success 方法内部会从哈希表中删除此 IAuthorizationRequirement,以表示该条件已满足了,不必再证。
public virtual void Succeed(IAuthorizationRequirement requirement)
{
_succeedCalled = true;
_pendingRequirements.Remove(requirement);
}
记得要在服务容器中注册,否则咱们写的 Handler 是不起作用的。
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 是把此授权策略与一个验证方案关联,当进行鉴权时顺便做一次验证。上述代码我们关联 Cookie 验证即可,这个在文章前面已经设置了。AddRequirements 方法添加我们自定义的约束条件,这里我设置的用户等级是 3 —— 用户等级要 >= 3 才允许访问。
下面写个 MVC 控制器来检验一下是否能正确授权。
public class HomeController : Controller
{
[HttpGet("/")]
[Authorize(Policy = LevelAuthorizationHandler.POLICY_NAME)]
public IActionResult Index()
{
return View();
}
}
这里咱们用基于策略的授权方式,所以[Authorize]特性要指定策略名称。
好,运行。本来是访问根目录 / 的,但由于验证不通过,自动跳到登录页了。
注意URL上的 backUrl 参数:?backUrl=/。本来要访问 / 的,所以登录后再跳回 / 。我们选一个用户等级为 5 的登录。
由于用户等级为 5,是 >=3 的存在,所以授权通过。
现在,把名为 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";
});
现在咱们找个用户等级低于 3 的登录。
登录后被拒绝访问。
到此为止,好像、貌似、似乎已大功告成了。但是,老周又发现问题了:如果我一个控制器内或不同控制器之间有的操作方法要让用户等级 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));
});
是的,这样确实是可行的。不过不够动态,要是我弄个策略从 Level1 到 Level10 呢,岂不要写十个?
官方有个用 Age 生成授权策略的示例让老周获得了灵感——是的,咱们就是要动态生成授权策略。需要用到一个接口:IAuthorizationPolicyProvider。这个接口可以根据策略名称返回授权策略,所以,咱们可以拿它做文章。
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
// 提取名称最后的数字
int prefixLen = LevelAuthorizationHandler.POLICY_NAME.Length;
if(int.TryParse(policyName.Substring(prefixLen), out int level))
{
// 动态生成策略
AuthorizationPolicyBuilder plcyBd = new AuthorizationPolicyBuilder();
plcyBd.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
plcyBd.AddRequirements(new LevelAuthorizationRequirement(level));
// Build 方法生成策略
return Task.FromResult(plcyBd.Build())!;
}
}
// 未处理,交由选项类去返回默认的策略
return Task.FromResult(_options.GetPolicy(policyName));
}
}
这样可以根据给定的策略名称,生成与用户等级相关的配置。例如,名称“Level3”,就是等级3;“Level5”就是等级5。
于是,在配置服务容器时,我们不再需要 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星级鬼畜影院");
}
这样一来,配置不同等级的授权就方便多了。
【ASP.NET Core】按用户等级授权的更多相关文章
- asp.net core系列 49 Identity 授权(上)
一.概述 授权是指用户能够访问资源的权限,如页面数据的查看.编辑.新增.删除.导出.下载等权限.ASP.NET Core 授权提供了多种且灵活的方式,包括:Razor pages授权约定.简单授权.R ...
- asp.net core根据用户权限控制页面元素的显示
asp.net core根据用户权限控制页面元素的显示 Intro 在 web 应用中我们经常需要根据用户的不同允许用户访问不同的资源,显示不同的内容,之前做了一个 AccessControlHelp ...
- ASP.NET Core 新增用户 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core 新增用户 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 新增用户 上一章节我们实现了一个注册表单,但也留了一些东西还没完成, ...
- ASP.NET CORE API Swagger+IdentityServer4授权验证
简介 本来不想写这篇博文,但在网上找到的文章博客都没有完整配置信息,所以这里记录下. 不了解IdentityServer4的可以看看我之前写的入门博文 Swagger 官方演示地址 源码地址 配置Id ...
- ASP.NET 拼多多用户登录授权后使用code去换取access_token
一.拼多多开放平台 由于本人刚毕业进公司实习 遇到一些问题然后想通过博客来记录和分享给大家一起学习. 第一次写博客没什么经验不是写的很好 请大家多多关照 嘴下留情哈哈 谢谢! 好了 话不多说直接进入主 ...
- Asp .Net Core 2.0 登录授权以及多用户登录
用户登录是一个非常常见的应用场景 .net core 2.0 的登录方式发生了点变化,应该是属于是良性的变化,变得更方便,更容易扩展. 配置 打开项目中的Startup.cs文件,找到Configur ...
- Asp .Net Core 2.0 登录授权以及前后台多用户登录
用户登录是一个非常常见的应用场景 .net core 2.0 的登录方式发生了点变化,应该是属于是良性的变化,变得更方便,更容易扩展. 配置 打开项目中的Startup.cs文件,找到Configur ...
- asp.net core系列 51 Identity 授权(下)
1.6 基于资源的授权 前面二篇中,熟悉了五种授权方式(对于上篇讲的策略授权,还有IAuthorizationPolicyProvider的自定义授权策略提供程序没有讲,后面再补充).本篇讲的授权方式 ...
- asp.net core系列 50 Identity 授权(中)
1.5 基于策略的授权 在上篇中,已经讲到了授权访问(authorization)的四种方式.其中Razor Pages授权约定和简单授权二种方式更像是身份认证(authentication) ,因为 ...
- 用asp.net core 把用户访问记录优化到极致
菜菜呀,前几天做的用户空间,用户反映有时候比较慢呀 CEO,CTO,CFO于一身的CXO 是吗? 菜菜 我把你拉进用户反馈群,你解决一下呀 CEO,CTO,CFO于一身的CXO (完了,以后没清净时候 ...
随机推荐
- js读取excel进行批量操作
推荐这款插件 http://oss.sheetjs.com/js-xls/ 具体用法大家查看api! <!DOCTYPE html> <html> <head> & ...
- ASP.NET Core :缓存系列(四):内存缓存 MemoryCache
System.Runtime.Caching/MemoryCache ICacheEntry 接口中的属性:具体设置过期时间 可以参考:微软文档ICacheEntry 接口 缓存基本使用 (一) 绝对 ...
- 快速上手Spring项目
通过maven依赖管理导入所需Jar包 注 : spring 需要导入commons-logging进行日志记录 . 我们利用maven , 他会自动下载对应的依赖项 . <dependency ...
- SpringBoot 2.5.5整合轻量级的分布式日志标记追踪神器TLog
TLog能解决什么痛点 随着微服务盛行,很多公司都把系统按照业务边界拆成了很多微服务,在排错查日志的时候.因为业务链路贯穿着很多微服务节点,导致定位某个请求的日志以及上下游业务的日志会变得有些困难. ...
- MySQL的日志文件
本文将重点介绍MySQL的日志文件类型,并讲解其作用,并结合一定实操演示,相信跟着做下来你会对MySQL有更深的理解. 文件的概念 在开始讲MySQL日志文件之前,首先我们要明确一下文件的概念.MyS ...
- Educational Codeforces Round 138 (Rated for Div. 2) A-E
比赛链接 A 题解 知识点:贪心. 注意到 \(m\geq n\) 时,不存在某一行或列空着,于是不能移动. 而 \(m<n\) 时,一定存在,可以移动. 时间复杂度 \(O(1)\) 空间复杂 ...
- 8.uvloop
uvloop是asyncio的事件循环的替代方案,性能高于默认asyncio的事件循环的效率,相当于提升两倍,效率可以比肩Go pip3 install uvloop import asyncio ...
- 13-ORM-更新&删除
一.更改单个数据 修改单个实体的某些字段 1.查: - 通过get()得到要修改的实体对象 2.改: - 通过对象属性的=的方式修改数据 3.保存 - 通过对象.save()保存数据 二.批量 ...
- VS使用正则表达式删除程序中的空行
Ctrl+H; 需要替换的正则表达式 ^(?([^\r\n])\s)*\r?$\r?\n
- vulnhub靶场之ICA: 1
准备: 攻击机:虚拟机kali.本机win10. 靶机:ICA: 1,网段地址我这里设置的桥接,所以与本机电脑在同一网段,下载地址:https://download.vulnhub.com/ica/i ...