三分钟学会.NET Core Jwt 策略授权认证
一.前言
大家好我又回来了,前几天讲过一个关于jwt的身份验证最简单的案例,但是功能还是不够强大,不适用于真正的项目,是的,在真正面对复杂而又苛刻的客户中,我们会不知所措,就现在需要将认证授权这一块也变的复杂而又实用起来,那在专业术语中就叫做自定义策略的api认证,本次案例运行在.net core 3.0中,最后我们将在swagger中进行浏览,来尝试项目是否正常,对于.net core 2.x 版本,这篇文章有些代码不适用,但我会在文中说明。
二.在.net core中尝试
我们都知道jwt是为了认证,微软给我们提供了进城打鬼子的城门,那就是 authorizationhandle。
我们首先要实现它,并且我们还可以根据依赖注入的 authorizationhandlercontext 来获取上下文,就这样我们就更可以做一些权限的手脚
public class policyhandler : authorizationhandler<policyrequirement> { protected override task handlerequirementasync(authorizationhandlercontext context, policyrequirement requirement) { var http = (context.resource as microsoft.aspnetcore.routing.routeendpoint); var questurl = "/"+http.routepattern.rawtext; //赋值用户权限 var userpermissions = requirement.userpermissions; //是否经过验证 var isauthenticated = context.user.identity.isauthenticated; if (isauthenticated) { if (userpermissions.any(u=>u.url == questurl)) { //用户名 var username = context.user.claims.singleordefault(s => s.type == claimtypes.nameidentifier).value; if (userpermissions.any(w => w.username == username)) { context.succeed(requirement); } } } return task.completedtask; } }
首先,我们重写了 handlerequirementasync 方法,如果你看过aspnetcore的源码你一定知道,它是jwt身份认证的开端,也就是说你重写了它,原来那一套就不会走了,我们观察一下源码,我贴在下面,可以看到这就是一个最基本的授权,通过 context.succeed(requirement 完成了最后的认证动作!
public class denyanonymousauthorizationrequirement : authorizationhandler<denyanonymousauthorizationrequirement>, iauthorizationrequirement { /// <summary> /// makes a decision if authorization is allowed based on a specific requirement. /// </summary> /// <param name="context">the authorization context.</param> /// <param name="requirement">the requirement to evaluate.</param> protected override task handlerequirementasync(authorizationhandlercontext context, denyanonymousauthorizationrequirement requirement) { var user = context.user; var userisanonymous = user?.identity == null || !user.identities.any(i => i.isauthenticated); if (!userisanonymous) { context.succeed(requirement); } return task.completedtask; } }
那么 succeed 是一个什么呢?它是一个在 authorizationhandlercontext的定义动作,包括fail() ,也是如此,当然具体实现我们不在细谈,其内部还是挺复杂的,不过我们需要的是 denyanonymousauthorizationrequirement 被当作了抽象的一部分。
public abstract class authorizationhandler<trequirement> : iauthorizationhandler where trequirement : iauthorizationrequirement {}
好吧,言归正传(看源码挺刺激的),我们刚刚在 policyhandler实现了自定义认证策略,上面还说到了两个方法。现在我们在项目中配置并启动它,并且我在代码中也是用了swagger用于后面的演示。
在 addjwtbearer中我们添加了jwt验证包括了验证参数以及几个事件处理,这个很基本,不在解释。不过在swagger中添加jwt的一些功能是在 addsecuritydefinition 中写入的。
public void configureservices(iservicecollection services) { //添加策略鉴权模式 services.addauthorization(options => { options.addpolicy("permission", policy => policy.requirements.add(new policyrequirement())); }) .addauthentication(s => { //添加jwt scheme s.defaultauthenticatescheme = jwtbearerdefaults.authenticationscheme; s.defaultscheme = jwtbearerdefaults.authenticationscheme; s.defaultchallengescheme = jwtbearerdefaults.authenticationscheme; }) //添加jwt验证: .addjwtbearer(options => { options.tokenvalidationparameters = new tokenvalidationparameters { validatelifetime = true,//是否验证失效时间 clockskew = timespan.fromseconds(30), validateaudience = true,//是否验证audience //validaudience = const.getvalidudience(),//audience //这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了 audiencevalidator = (m, n, z) => { return m != null && m.firstordefault().equals(const.validaudience); }, validateissuer = true,//是否验证issuer validissuer = const.domain,//issuer,这两项和前面签发jwt的设置一致 validateissuersigningkey = true,//是否验证securitykey issuersigningkey = new symmetricsecuritykey(encoding.utf8.getbytes(const.securitykey))//拿到securitykey }; options.events = new jwtbearerevents { onauthenticationfailed = context => { //token expired if (context.exception.gettype() == typeof(securitytokenexpiredexception)) { context.response.headers.add("token-expired", "true"); } return task.completedtask; } }; }); services.addswaggergen(c => { c.swaggerdoc("v1", new openapiinfo { version = "v1", title = "haozi jwt", description = "基于.net core 3.0 的jwt 身份验证", contact = new openapicontact { name = "zaranet", email = "zaranet@163.com", url = new uri("http://cnblogs.com/zaranet"), }, }); c.addsecuritydefinition("bearer", new openapisecurityscheme() { description = "在下框中输入请求头中需要添加jwt授权token:bearer token", name = "authorization", in = parameterlocation.header, type = securityschemetype.apikey, bearerformat = "jwt", scheme = "bearer" }); c.addsecurityrequirement(new openapisecurityrequirement { { new openapisecurityscheme { reference = new openapireference { type = referencetype.securityscheme, id = "bearer" } }, new string[] { } } }); }); //认证服务 services.addsingleton<iauthorizationhandler, policyhandler>(); services.addcontrollers(); }
在以上代码中,我们通过鉴权模式添加了认证规则,一个名叫 policyrequirement 的类,它实现了 iauthorizationrequirement 接口,其中我们需要定义一些规则,通过构造函数我们可以添加我们要识别的权限规则。那个username就是 attribute 。
public class policyrequirement : iauthorizationrequirement {/// <summary> /// user rights collection /// </summary> public list<userpermission> userpermissions { get; private set; } /// <summary> /// no permission action /// </summary> public string deniedaction { get; set; } /// <summary> /// structure /// </summary> public policyrequirement() { //jump to this route without permission deniedaction = new pathstring("/api/nopermission"); //route configuration that users have access to, of course you can read it from the database, you can also put it in redis for persistence userpermissions = new list<userpermission> { new userpermission { url="/api/value3", username="admin"}, }; } } public class userpermission { public string username { get; set; } public string url { get; set; } }
随后我们应当启动我们的服务,在.net core 3.0 中身份验证的中间件位置需要在路由和端点配置的中间。
public void configure(iapplicationbuilder app, iwebhostenvironment env) { if (env.isdevelopment()) { app.usedeveloperexceptionpage(); } app.useswagger(); app.useswaggerui(c => { c.swaggerendpoint("/swagger/v1/swagger.json", "my api v1"); }); app.userouting(); app.useauthentication(); app.useauthorization(); app.useendpoints(endpoints => { endpoints.mapcontrollers(); }); }
我们通常会有一个获取token的api,用于让jwt通过 jwtsecuritytokenhandler().writetoken(token) 用于生成我们的token,虽然jwt是没有状态的,但你应该也明白,如果你的jwt生成了随后你重启了你的网站,你的jwt会失效,这个是因为你的密钥进行了改变,如果你的密钥一直写死,那么这个jwt将不会再过期,这个还是有安全风险的,这个我不在这里解释,gettoken定义如下:
[apicontroller] public class authcontroller : controllerbase { [allowanonymous] [httpget] [route("api/nopermission")] public iactionresult nopermission() { return forbid("no permission!"); } /// <summary> /// login /// </summary> [allowanonymous] [httpget] [route("api/auth")] public iactionresult get(string username, string pwd) { if (checkaccount(username, pwd, out string role)) { const.validaudience = username + pwd + datetime.now.tostring(); // push the user’s name into a claim, so we can identify the user later on. //这里可以随意加入自定义的参数,key可以自己随便起 var claims = new[] { new claim(jwtregisteredclaimnames.nbf,$"{new datetimeoffset(datetime.now).tounixtimeseconds()}") , new claim (jwtregisteredclaimnames.exp,$"{new datetimeoffset(datetime.now.addminutes(30)).tounixtimeseconds()}"), new claim(claimtypes.nameidentifier, username), new claim("role", role) }; //sign the token using a secret key.this secret will be shared between your api and anything that needs to check that the token is legit. var key = new symmetricsecuritykey(encoding.utf8.getbytes(const.securitykey)); var creds = new signingcredentials(key, securityalgorithms.hmacsha256); //.net core’s jwtsecuritytoken class takes on the heavy lifting and actually creates the token. var token = new jwtsecuritytoken( issuer: const.domain, //颁发者 audience: const.validaudience,//过期时间 expires: datetime.now.addminutes(30),// 签名证书 signingcredentials: creds, //自定义参数 claims: claims ); return ok(new { token = new jwtsecuritytokenhandler().writetoken(token) }); } else { return badrequest(new { message = "username or password is incorrect." }); } } /// <summary> /// 模拟登陆校验 /// </summary> private bool checkaccount(string username, string pwd, out string role) { role = "user"; if (string.isnullorempty(username)) return false; if (username.equals("admin")) role = "admin"; return true; }
可能比较特别的是 allowanonymous ,这个看我文章的同学可能头一次见,其实怎么说好呢,这个可无可有,没有硬性的要求,我看到好几个知名博主加上了,我也加上了~...最后我们创建了几个资源控制器,它们是受保护的。
在你添加策略权限的时候例如政策名称是xxx,那么在对应的api表头就应该是xxx,随后到了 policyhandler我们解析了 claims 处理了它是否有权限。
// get api/values1 [httpget] [route("api/value1")] public actionresult<ienumerable<string>> get() { return new string[] { "value1", "value1" }; } // get api/values2 /** * 该接口用authorize特性做了权限校验,如果没有通过权限校验,则http返回状态码为401 */ [httpget] [route("api/value2")] [authorize] public actionresult<ienumerable<string>> get2() { var auth = httpcontext.authenticateasync().result.principal.claims; var username = auth.firstordefault(t => t.type.equals(claimtypes.nameidentifier))?.value; return new string[] { "这个接口登陆过的都能访问", $"username={username}" }; } /** * 这个接口必须用admin **/ [httpget] [route("api/value3")] [authorize("permission")] public actionresult<ienumerable<string>> get3() { //这是获取自定义参数的方法 var auth = httpcontext.authenticateasync().result.principal.claims; var username = auth.firstordefault(t => t.type.equals(claimtypes.nameidentifier))?.value; var role = auth.firstordefault(t => t.type.equals("role"))?.value; return new string[] { "这个接口有管理员权限才可以访问", $"username={username}", $"role={role}" }; }
三.效果图
四.栗子源代码和以往版本
看到很多前辈彩的坑,原来的 (context.resource as microsoft.aspnetcore.routing.routeendpoint); 实际上在.net core 3.0 已经不能用了,原因是.net core 3.0 启用 endpointrouting 后,权限filter不再添加到 actiondescriptor ,而将权限直接作为中间件运行,同时所有filter都会添加到 endpoint.metadata ,如果在.net core 2.1 & 2.2 版本中你通常handler可以这么写:
public class policyhandler : authorizationhandler<policyrequirement> { protected override task handlerequirementasync(authorizationhandlercontext context, policyrequirement requirement) { //赋值用户权限 var userpermissions = requirement.userpermissions; //从authorizationhandlercontext转成httpcontext,以便取出表求信息 var httpcontext = (context.resource as microsoft.aspnetcore.mvc.filters.authorizationfiltercontext).httpcontext; //请求url var questurl = httpcontext.request.path.value.toupperinvariant(); //是否经过验证 var isauthenticated = httpcontext.user.identity.isauthenticated; if (isauthenticated) { if (userpermissions.groupby(g => g.url).any(w => w.key.toupperinvariant() == questurl)) { //用户名 var username = httpcontext.user.claims.singleordefault(s => s.type == claimtypes.nameidentifier).value; if (userpermissions.any(w => w.username == username && w.url.toupperinvariant() == questurl)) { context.succeed(requirement); } else { //无权限跳转到拒绝页面 httpcontext.response.redirect(requirement.deniedaction); } } else context.succeed(requirement); } return task.completedtask; } }
该案例源代码在我的github上:https://github.com/zaranetcore/aspnetcore_jsonwebtoken/tree/master/jwt_policy_demo 谢谢大家
推荐阅读
-
[转]三分钟学会.NET Core Jwt 策略授权认证
-
【.NET Core项目实战-统一认证平台】第九章 授权篇-使用Dapper持久化IdentityServer4
-
.Net Core官方JWT授权验证的全过程
-
ASP.NET Core 3.0 一个 jwt 的轻量角色/用户、单个API控制的授权认证库
-
【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程
-
【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇
-
ASP.NET Core 2.0利用Jwt实现授权认证
-
.Net Core Cookie-Based认证与授权
-
【.NET Core项目实战-统一认证平台】第六章 网关篇-自定义客户端授权
-
【.NET Core项目实战-统一认证平台】第十三章 授权篇-如何强制有效令牌过期