既然选择了远方, 便只顾风雨兼程 __ HANS 许
JWT(JSON web Token)
ASP.NET Core 的 Middleware 实现
引言: 挺久没更新了, 之前做了 vue 的系列, 后面想做做服务端的系列, 上下衔接, 我们就讲讲 WebApi(网络应用程序接口), 接口免不了用户认证, 所以接下来我们的主题系列文章便是 "基于 ASP.NET Core 的用户认证", 分为市面上流行的 JWT(JSON WebToken)与 OAuth2(开放授权)
JWT(JSON Web Token)
什么叫 JWT
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案.
一般来说, 互联网用户认证是这样子的.
1, 用户向服务器发送用户名和密码.
2, 服务器验证通过后, 在当前对话 (session) 里面保存相关数据, 比如用户角色, 登录时间等等.
3, 服务器向用户返回一个 session_id, 写入用户的 Cookie.
4, 用户随后的每一次请求, 都会通过 Cookie, 将 session_id 传回服务器.
5, 服务器收到 session_id, 找到前期保存的数据, 由此得知用户的身份.
服务器需要保存 session, 做持久化, 这种模式没有分布式架构, 无法支持横向扩展, 如果真的要的话就必须采用分布式缓存来进行管理 Seesion. 那 JWT 相反, 它保存的是在客户端, 每次请求都将 JWT 代入服务器, 进行签名, 权限验证. JWT 由客户端请求, 服务端生成, 客户端保存, 服务端验证.
JWT 的原理与格式
原理
在上面, 我们也讲过了, 简单的来说, 我们将服务器需要验证我们的条件(账户, 密码等等), 发给服务器, 服务器认证通过, 生成一个 JSON 对象返回给我们, 例如下面. 当然, 为了防止被篡改, 所以我们会将对象加密, 再次请求服务器, 需要将 jwt 放在请求头部, 传递给服务器, 来判断权限等等.
- {
- "姓名": "张三",
- "角色": "管理员",
- "到期时间": "2018 年 7 月 1 日 0 点 0 分"
- }
格式
实际格式
- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
- eyJBIjoiQUFBQSIsIkIiOiJCQkJCIiwiQyI6IkNDQ0MiLCJ1ZXIiOiJ4dWh1YWxlIiwib3BlbmlkIjoiNTE1NjEzMTM1MTYzMjEiLCJmZiI6ImRmc2RzZGZzZGZzZHMiLCJuYmYiOjE1NTIyMTE4NjAsImV4cCI6MTU1MjIxMzY2MH0.
- 16m57YnnIcgIth25dwphQKPYuIq42BVmZV6LIBO7KDg
它是一个很长的字符串, 中间用点 (.) 分隔成三个部分. 注意, JWT 内部是没有换行的, 这里只是为了便于展示, 将它写成了几行. JWT 的三个部分依次如下.
- Header(头部)
- Payload(负载)
- Signature(签名)
简单讲下, Header 描述加密算法与 token 类型, Payload 描述的是实际需要传递的数据, 如失效时间, 签发人等等, Signature 描述的是一段对于前面两部部分的签名, 当然秘钥只有服务器才知道.
简单的介绍下 JWT, 更多的话, 可以这边文章看看. 我们着重讲下实现.
ASP.NET http://asp.net/ Core 的 Middleware 实现
创建 JWT
首先我们要先创建 token, 毕竟这个是最重要的. Core 自带 JWT 帮助类, 所以我们按照帮助类的意思来写个方法创建 token.
- publicstringCreateJsonWebToken(Dictionary<string, string> payLoad)
- {
- if (string.IsNullOrWhiteSpace(setting.SecurityKey))
- {
- throw new ArgumentNullException("JsonWebTokenSetting.securityKey",
- "securityKey 为 NULL 或空字符串. 请在 \"appsettings.json\"配置 \"JsonWebToken\"节点及其子节点 \"securityKey\"");
- }
- 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(setting.ExpiresMinute)),
- signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(setting.SecurityKey)), SecurityAlgorithms.HmacSha256));
- var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
- return encodedJwt;
- }
从方法我们看到, 我们传入的是负载这个片段, 而失效时间与秘钥我们是放在了 appsettings.JSON 来进行配置的. 使用 DI 来获取配置文件中的节点值.
编写中间件
我们都知道, 中间件是 Core 的管道模型组成部分, 所以我们在中间件做验证, 来判断每次请求用户是有有权限是有该 WebApi 或者其他 API.
中间件
- public JwtCustomerAuthorizeMiddleware(RequestDelegate next, IOptions<JsonWebTokenSetting> options, IJsonWebTokenValidate jsonWebTokenValidate, Func<Dictionary<string, string>, JsonWebTokenSetting, bool> validatePayLoad, List<string> anonymousPathList)
- {
- this._next = next;
- this._setting = options.Value;
- this._jsonWebTokenValidate = jsonWebTokenValidate;
- this._validatePayLoad = validatePayLoad;
- this._anonymousPathList = anonymousPathList;
- }
- publicasync Task Invoke(HttpContext context)
- {
- //JsonWebTokenValidate
- // 若是路径可以匿名访问, 直接跳过
- if (_anonymousPathList.Contains(context.Request.Path.Value))
- {
- // 还未验证
- await _next(context);
- return;
- }
- var result = context.Request.Headers.TryGetValue("Authorization", out StringValues authStr);
- if (!result || string.IsNullOrEmpty(authStr.ToString()))
- {
- throw new UnauthorizedAccessException("未授权, 请传递 Header 头的 Authorization 参数.");
- }
- // 进行验证与自定义验证
- result = _jsonWebTokenValidate.Validate(authStr.ToString().Substring("Bearer".Length).Trim()
- , _setting, _validatePayLoad);
- if (!result)
- {
- throw new UnauthorizedAccessException("验证失败, 请查看传递的参数是否正确或是否有权限访问该地址.");
- }
- await _next(context);
- }
从代码来看, anonymousPathList 是 URL 路径, 若是在这个 List 内的 URL, 便可直接跳过验证,
接着将 authStrtoken 代入验证函数, validatePayLoad 却是我们自代入的委托函数, 用于服务器自定义验证.
验证
验证方法, 我只是做了签名验证与时间验证. 并没有定得死死的, 让用户自由度的去进行验证.
- publicboolValidate(
- string encodeJwt, JsonWebTokenSetting setting, Func<Dictionary<string, string>, JsonWebTokenSetting, bool> validatePayLoad
- )
- {
- if (string.IsNullOrWhiteSpace(setting.SecurityKey))
- {
- throw new ArgumentNullException("JsonWebTokenSetting.securityKey",
- "securityKey 为 NULL 或空字符串. 请在 \"appsettings.json\"配置 \"JsonWebToken\"节点及其子节点 \"securityKey\"");
- }
- var success = true;
- var jwtArr = encodeJwt.Split('.');
- var header = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[0]));
- var payLoad = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[1]));
- var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(setting.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, setting);
- return success;
- }
加载中间件
使用扩展方法, 来封装中间件
- publicstatic IApplicationBuilder UseJwtCustomerAuthorize(this IApplicationBuilder App, Action<IJwtCustomerAuthorezeOption> action)
- {
- var _JwtCustomerAuthorezeOption = App.ApplicationServices.GetService<IJwtCustomerAuthorezeOption>() as JwtCustomerAuthorezeOption; //new JwtCustomerAuthorezeOption();
- action(_JwtCustomerAuthorezeOption);
- return App.UseMiddleware<JwtCustomerAuthorizeMiddleware>(_JwtCustomerAuthorezeOption.validatePayLoad, _JwtCustomerAuthorezeOption.anonymousPath);
- }
在 Startup.cs 使用
注册服务
- publicvoidConfigureServices(IServiceCollection services)
- {
- services.AddJwt(Configuration);
- }
使用中间件
- publicvoidConfigure(IApplicationBuilder App, IHostingEnvironment env)
- {
- App.UseJwtCustomerAuthorize(option =>
- {
- // 设置不会被验证的 url, 可以使用链式调用一直添加
- option.SetAnonymousPaths(new System.Collections.Generic.List<string>()
- {
- // "/",
- "/Home/Privacy",
- "/Home/CreateJsonToken"
- });
- // 自定义验证函数, playLoad 为带过来的参数字典, setting 为失效时间与秘钥
- option.SetValidateFunc((playLoad, sertting) =>
- {
- return true;
- });
- });
- }
总结下, 通过上面, 就完成了 JWT 在 ASP.NET http://xn--jwtasp-2e8in30d5jjesboy6a.net/ Core 使用中间件的方式的实现. 简单来说就是用自带方法创建 token, 验证则使用中间件的形式, 每次请求都需要进行验证当然你可以设置特殊 URL. 在下篇文章我们来讲讲使用策略模式的 JWT 实现.
来源: https://www.cnblogs.com/xuhuale/p/10507241.html