asp net core 2.1中如何使用jwt(从原理到精通)
为什么使用 jwt
最近,移动开发的劲头越来越足,学校搞的各种比赛都需要用手机 app 来撑场面,所以,作为写后端的,很有必要改进一下以往的基于 session 的身份认证方式了,理由如下:
- 移动端经常要保持长时间(1 到 2 星期)在线,但是 session 却不好在服务端保存这么久,虽然可以持久化到数据库,但是还是挺费资源
- 移动端往往不是使用的网页技术,所以藏在 cookie 里面的 sessionid 不是很方便的传递给服务端
- 服务端暴露给客户端的接口往往是 restful 风格的,这是一种无状态的 api 风格,所以身份认证的方式最好也是无状态的才好
所以我选择了使用 jwt (json web token) 这个技术。jwt 是一种无状态的分布式的身份验证方式,与 session 相反,jwt 将用户信息存放在 token 的 payload 字段保存在客户端,通过 rsa 加密的方式,保证数据不会被篡改,验证数据有效性。
下面是一个使用 jwt 的系统的身份验证流程:
可以看出,用户的信息保存在 token 中,而 token 分布在用户的设备中,所以服务端不再需要在内存中保存用户信息了
身份认证的 token 传递时以一种相当简单的格式保存在 header 中,方便客户端对其进行操作
原理
jwt对所有语言都是通用的,只要知道秘钥,另一一种语言有可以对jwt的有效性进行判断;
jwt的组成;header部分base64转化.payload部分base64转化.使用hs256方式根据秘钥对前面两部分进行加密后再base64转化,其中使用的hs256加密是header部分指定的,也可以通过官网的查看,如下图:
原理就这么简单,那究竟用怎样使用c#来实现呢,又怎么确定它的正确性呢?,请继续
使用c#实现
我们定义一个今天方法,其中需要使用到microsoft.identitymodel.tokens.dll,asp.net core 2.1再带,如果其他版本,没有自带,需要nuget 一下这个类库
/// <summary> /// 创建jwttoken,源码自定义 /// </summary> /// <param name="payload"></param> /// <param name="header"></param> /// <returns></returns> public static string createtoken(dictionary<string, object> payload,int expiresminute, dictionary<string, object> header = null) { if (header == null) { header = new dictionary<string, object>(new list<keyvaluepair<string, object>>() { new keyvaluepair<string, object>("alg", "hs256"), new keyvaluepair<string, object>("typ", "jwt") }); } //添加jwt可用时间(应该必须要的) var now = datetime.utcnow; payload["nbf"] = tounixepochdate( now);//可用时间起始 payload["exp"] = tounixepochdate(now.add(timespan.fromminutes(expiresminute)));//可用时间结束 var encodedheader = base64urlencoder.encode(jsonconvert.serializeobject(header)); var encodedpayload = base64urlencoder.encode(jsonconvert.serializeobject(payload)); var hs256 = new hmacsha256(encoding.ascii.getbytes(securitykey)); var encodedsignature = base64urlencoder.encode(hs256.computehash(encoding.utf8.getbytes(string.concat(encodedheader, ".", encodedpayload)))); var encodedjwt = string.concat(encodedheader, ".", encodedpayload, ".", encodedsignature); return encodedjwt; } public static long tounixepochdate(datetime date) => (long)math.round((date.touniversaltime() - new datetimeoffset(1970, 1, 1, 0, 0, 0, timespan.zero)).totalseconds);
该方法很简单,只需要传入header键值对和payload键值对,然后根据原理进行base64转换和hs256加密,接下来我们来使用一个测试类对其进行测试,代码如下:
[testmethod] public void tokenvalidatetest() { dictionary<string, object> payload = new dictionary<string, object>(); payload.add("sub", "rober"); payload.add("jti", "09e572c7-62d0-4198-9cce-0915d7493806"); payload.add("nbf", null); payload.add("exp", null); payload.add("iss", "roberissuer"); payload.add("aud", "roberaudience"); payload.add("age", 30); var encodejwt = tokencontext.createtoken(payload, 30); var result = tokencontext.validate(encodejwt, (load) => { return true; }); assert.istrue(result); }
先不管后面的验证,我们先看看其中生成的encodejwt的值:eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioijyb2jlciisimp0asi6ijy0owmyyjuxlte4zgqtndezyy05yzc5lti4nwnhmdaxodu2nsisim5izii6mtu0mdyxmdy2nswizxhwijoxntqwnjeyndy1lcjpc3mioijyb2jlcklzc3vlciisimf1zci6injvymvyqxvkawvuy2uilcjhz2uiojmwfq.7is2kyhatsr5fw2gpu1jgehpzz2ulczjgcwb40lsyyw
第一部分和第二部分,并不是加密,只是base64转换,我们可以通过其他语言轻松转换回来,如下使用javascript进行转,window.atob(base64加密) window.btoa(base64解密)
var header=json.parse(window.atob('eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9'))
如下图:
我再对payloa进行转换回来, var payload=json.parse(window.atob('eyjzdwiioijyb2jlciisimp0asi6ijy0owmyyjuxlte4zgqtndezyy05yzc5lti4nwnhmdaxodu2nsisim5izii6mtu0mdyxmdy2nswizxhwijoxntqwnjeyndy1lcjpc3mioijyb2jlcklzc3vlciisimf1zci6injvymvyqxvkawvuy2uilcjhz2uiojmwfq'))
,如下图:
所以,从这里可以看出来,base64并不是属于加密,只是简单转换,因此,不能在payload中存放重要内容,比如密码等
使用aspnetcore 中自带的类生成jwt
aspnet core中自带了一个jwt帮助类,其实原理一样,对上面做了封装,丰富了一个内容,我们继续使用一个静态方法,如下
/// <summary> /// 创建jwttoken,采用微软内部方法,默认使用hs256加密,如果需要其他加密方式,请更改源码 /// 返回的结果和createtoken一样 /// </summary> /// <param name="payload"></param> /// <param name="expiresminute">有效分钟</param> /// <returns></returns> public static string createtokenbyhandler(dictionary<string, object> payload, int expiresminute) { var now = datetime.utcnow; // specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims. // you can add other claims here, if you want: var claims = new list<claim>(); foreach (var key in payload.keys) { var tempclaim = new claim(key, payload[key]?.tostring()); claims.add(tempclaim); } // create the jwt and write it to a string var jwt = new jwtsecuritytoken( issuer: null, audience: null, claims: claims, notbefore: now, expires: now.add(timespan.fromminutes(expiresminute)), signingcredentials: new signingcredentials(new symmetricsecuritykey(encoding.ascii.getbytes(securitykey)), securityalgorithms.hmacsha256)); var encodedjwt = new jwtsecuritytokenhandler().writetoken(jwt); return encodedjwt; }
它效果和上面一模一样,如果使用同样的header 、payload、秘钥,生成的jwt肯定一样,这里就不演示了,感兴趣的可以自行尝试;
aspnetcore中如何使用自定义jwt验证
上面讲了那么多,只是为了大家更好的理解如何使用jwt进行验证,那是jwt是如何进行验证的呢?,如果一个http请求过来,一般jwt携带在http请求头部的authorization中;先不看如何获取,先看看他是如何验证的,我们再定义个静态方法,如下:
/// <summary> /// 验证身份 验证签名的有效性, /// </summary> /// <param name="encodejwt"></param> /// <param name="validatepayload">自定义各类验证; 是否包含那种申明,或者申明的值, </param> /// 例如:payload["aud"]?.tostring() == "roberauddience"; /// 例如:验证是否过期 等 /// <returns></returns> public static bool validate(string encodejwt,func<dictionary<string,object>,bool> validatepayload) { var success = true; var jwtarr = encodejwt.split('.'); var header = jsonconvert.deserializeobject<dictionary<string, object>>(base64urlencoder.decode(jwtarr[0])); var payload = jsonconvert.deserializeobject<dictionary<string, object>>(base64urlencoder.decode(jwtarr[1])); var hs256 = new hmacsha256(encoding.ascii.getbytes(securitykey)); //首先验证签名是否正确(必须的) success = success && string.equals(jwtarr[2], base64urlencoder.encode(hs256.computehash(encoding.utf8.getbytes(string.concat(jwtarr[0], ".", jwtarr[1]))))); if (!success) { return success;//签名不正确直接返回 } //其次验证是否在有效期内(也应该必须) var now = tounixepochdate(datetime.utcnow); success = success && (now >= long.parse(payload["nbf"].tostring()) && now < long.parse(payload["exp"].tostring())); //再其次 进行自定义的验证 success = success && validatepayload(payload); return success; }
其中validatepayload 参数是一个自定义的验证的fun,执行该fun方法时会把解密后的payload作为参数传入进去
我们验证通过分为两部分,
第一,必须的(自认为的)
- jwt签名是否正确,请看以上代码实现
- jwt是否在可以时间内,请看以上代码实现
第二,自定义的(各复杂的,原理就是获取payload 的某个值,然后对这个值进行各种判读--等于,大于,包含,)
- 该jwt是不是进入黑名单
- aud==‘roberaudience'
我们来通过一个测试类验证
[testmethod] public void tokencustomervalidatetest() { dictionary<string, object> payload = new dictionary<string, object>(); payload.add("sub", "rober"); payload.add("jti", guid.newguid().tostring()); payload.add("nbf", null); payload.add("exp", null); payload.add("iss", "roberissuer"); payload.add("aud", "roberaudience"); payload.add("age", 30); var encodejwt = tokencontext.createtoken(payload, 30); var result = tokencontext.validate(encodejwt, (load) => { var success = true; //验证是否包含aud 并等于 roberaudience success = success&& load["aud"]?.tostring() == "roberaudience"; //验证age>20等 int.tryparse(load["age"].tostring(), out int age); assert.istrue(age > 30); //其他验证 jwt的标识 jti是否加入黑名单等 return success; }); assert.istrue(result); }
如上面,我们可以把jwt中的payload解析出来,然后进行各种复杂的想要的验证;
其实,aspnet core中的基于角色,用户、策略,自定义策略的验证就相当这里的自定义验证,一下章将详细说明和对比,这里暂时不讲解
看完上面,是不是觉得jwt很简单就,主要就两部
- 创建jwt;
- 验证jwt;
完整代码如下:
/// <summary> /// token上下文,负责token的创建和验证 /// </summary> public class tokencontext { /// <summary> /// 秘钥,可以从配置文件中获取 /// </summary> public static string securitykey = "gqdstclechengroberbojpoxoyg5mbej1xt0ufiwdvvvbrk"; /// <summary> /// 创建jwttoken,源码自定义 /// </summary> /// <param name="payload"></param> /// <param name="header"></param> /// <returns></returns> public static string createtoken(dictionary<string, object> payload,int expiresminute, dictionary<string, object> header = null) { if (header == null) { header = new dictionary<string, object>(new list<keyvaluepair<string, object>>() { new keyvaluepair<string, object>("alg", "hs256"), new keyvaluepair<string, object>("typ", "jwt") }); } //添加jwt可用时间(应该必须要的) var now = datetime.utcnow; payload["nbf"] = tounixepochdate( now);//可用时间起始 payload["exp"] = tounixepochdate(now.add(timespan.fromminutes(expiresminute)));//可用时间结束 var encodedheader = base64urlencoder.encode(jsonconvert.serializeobject(header)); var encodedpayload = base64urlencoder.encode(jsonconvert.serializeobject(payload)); var hs256 = new hmacsha256(encoding.ascii.getbytes(securitykey)); var encodedsignature = base64urlencoder.encode(hs256.computehash(encoding.utf8.getbytes(string.concat(encodedheader, ".", encodedpayload)))); var encodedjwt = string.concat(encodedheader, ".", encodedpayload, ".", encodedsignature); return encodedjwt; } /// <summary> /// 创建jwttoken,采用微软内部方法,默认使用hs256加密,如果需要其他加密方式,请更改源码 /// 返回的结果和createtoken一样 /// </summary> /// <param name="payload"></param> /// <param name="expiresminute">有效分钟</param> /// <returns></returns> public static string createtokenbyhandler(dictionary<string, object> payload, int expiresminute) { var now = datetime.utcnow; // specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims. // you can add other claims here, if you want: var claims = new list<claim>(); foreach (var key in payload.keys) { var tempclaim = new claim(key, payload[key]?.tostring()); claims.add(tempclaim); } // create the jwt and write it to a string var jwt = new jwtsecuritytoken( issuer: null, audience: null, claims: claims, notbefore: now, expires: now.add(timespan.fromminutes(expiresminute)), signingcredentials: new signingcredentials(new symmetricsecuritykey(encoding.ascii.getbytes(securitykey)), securityalgorithms.hmacsha256)); var encodedjwt = new jwtsecuritytokenhandler().writetoken(jwt); return encodedjwt; } /// <summary> /// 验证身份 验证签名的有效性, /// </summary> /// <param name="encodejwt"></param> /// <param name="validatepayload">自定义各类验证; 是否包含那种申明,或者申明的值, </param> /// 例如:payload["aud"]?.tostring() == "roberauddience"; /// 例如:验证是否过期 等 /// <returns></returns> public static bool validate(string encodejwt,func<dictionary<string,object>,bool> validatepayload) { var success = true; var jwtarr = encodejwt.split('.'); var header = jsonconvert.deserializeobject<dictionary<string, object>>(base64urlencoder.decode(jwtarr[0])); var payload = jsonconvert.deserializeobject<dictionary<string, object>>(base64urlencoder.decode(jwtarr[1])); var hs256 = new hmacsha256(encoding.ascii.getbytes(securitykey)); //首先验证签名是否正确(必须的) success = success && string.equals(jwtarr[2], base64urlencoder.encode(hs256.computehash(encoding.utf8.getbytes(string.concat(jwtarr[0], ".", jwtarr[1]))))); if (!success) { return success;//签名不正确直接返回 } //其次验证是否在有效期内(也应该必须) var now = tounixepochdate(datetime.utcnow); success = success && (now >= long.parse(payload["nbf"].tostring()) && now < long.parse(payload["exp"].tostring())); //再其次 进行自定义的验证 success = success && validatepayload(payload); return success; } /// <summary> /// 获取jwt中的payload /// </summary> /// <param name="encodejwt"></param> /// <returns></returns> public static dictionary<string ,object> getpayload(string encodejwt) { var jwtarr = encodejwt.split('.'); var payload = jsonconvert.deserializeobject<dictionary<string, object>>(base64urlencoder.decode(jwtarr[1])); return payload; } public static long tounixepochdate(datetime date) => (long)math.round((date.touniversaltime() - new datetimeoffset(1970, 1, 1, 0, 0, 0, timespan.zero)).totalseconds); }
以上就是jwt的基本内容,它确实很简单,不要被aspnet core中的各种写法给搞晕了,只要是jwt相关的验证都是基于上面这些东西
下一章节将讲述:
- 在aspnet core中,自定义jwt管道验证;
- 在aspnet core中,自定义策略验证commonauthorizehandler : authorizationhandler<commonauthorize>
- 自定义jwt逻辑验证和原生的角色,用户,策略,等进行对比
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
上一篇: VBE decoder
下一篇: 袁绍出身名门,为什么在演义中那么不堪呢?