系列目录html
1.1 建立git
1.2 完善github
二. 搭建项目总体架构web
三. 集成轻量级ORM框架——SqlSugarajax
3.1 搭建环境算法
3.2 实战篇:利用SqlSugar快速实现CRUDjson
3.3 生成实体类api
七. 受权认证进阶篇
源码已上传上传GitHub:https://github.com/WangRui321/RayPI
该篇是第四篇“实战!带你半小时实现接口的受权认证”的进阶篇。
先说一下以前的版本:
以前在第四篇的时候曾经试着集成过一次JWT受权认证,当时搭的初版是JWT自己的受权认证机制,可是为了实现“令牌”的滑动过时效果,结果最后改为了使用缓存机制。
因此写到最后发现,其实就是变相的Session认证机制,由于发放“令牌”的时候彻底能够不用JWT,直接生成一个GUID也是能够的。
后来想了一下,这样为了实现令牌滑动过时而破坏了受权认证的独立性,感受得不偿失。因而就决定”进阶“下,在受权认证模块去掉缓存机制,只使用JWT自己的验证机制。
另外,此次还添加了一些关于对身份验证的优化。以前一个接口只能标明容许一种身份的用户访问,修改后能够实现一个接口同时容许多个身份访问(好比同时容许客户端和后台管理员两种身份的令牌访问)。
BTW,为了完整性考虑,下面有部份内容和以前第四篇相同,有须要的能够跳着看。
根据维基百科定义,JWT(读做 [/dʒɒt/]),即JSON Web Tokens,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)规范。 JWT一般由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。它是一种用于双方之间传递安全信息的表述性声明规范。 JWT做为一个开放的标准(RFC 7519),定义了一种简洁的、自包含的方法,从而使通讯双方实现以JSON对象的形式安全的传递信息。
以上是JWT的官方解释,能够看出JWT并非一种只能权限验证的工具,而是一种标准化的数据传输规范。因此,只要是在系统之间须要传输简短但却须要必定安全等级的数据时,均可以使用JWT规范来传输。规范是不因平台而受限制的,这也是JWT作为受权验证能够跨平台的缘由。
若是理解仍是有困难的话,咱们能够拿JWT和JSON类比:
JSON是一种轻量级的数据交换格式,是一种数据层次结构规范。它并非只用来给接口传递数据的工具,只要有层级结构的数据均可以使用JSON来存储和表示。固然,JSON也是跨平台的,不论是Win仍是Linux,.NET仍是Java,均可以使用它做为数据传输形式。
该篇的主要目的是实战,因此关于JWT自己的优势,以及使用JWT做为系统受权验证的优缺点,这里就不细说了,感兴趣的能够本身去查阅相关资料。
若是将JWT运用到Web Api的受权验证中,那么它的工做原理是这样的:
1)客户端向受权服务系统发起请求,申请获取“令牌”。
2)受权服务根据用户身份,生成一张专属“令牌”,并将该“令牌”以JWT规范返回给客户端
3)客户端将获取到的“令牌”放到http请求的headers中后,向主服务系统发起请求。主服务系统收到请求后会从headers中获取“令牌”,并从“令牌”中解析出该用户的身份权限,而后作出相应的处理(赞成或拒绝返回资源)
能够看出,JWT受权服务是能够脱离咱们的主服务系统而做为一个独立系统存在的。
前面说了其实把JWT理解为一种规范更为贴切,可是每每你们把根据JWT规则生成的加密字符串也叫做JWT,还有人直接称呼JWT为令牌。本文为了阐述方便,特此作了一些区分:
本文所说的JWT皆指的是JWT规范
本文所说的“JWT字符串”是指经过JWT规则加密后生成的字符串,它由三部分组成:Header(头部)、Payload(数据)、Signature(签名),将这三部分由‘.’链接而组成的一长串加密字符串就成为JWT字符串。
1)Header
由且只由两个数据组成,一个是“alg”(加密规范)指定了该JWT字符串的加密规则,另外一个是“typ”(JWT字符串类型)。例如:
{ "alg": "HS256", "typ": "JWT" }
将这组JSON格式的数据经过Base64Url格式编码后,生成的字符串就是咱们JWT字符串的第一个部分。
2)Payload
由一组数据组成,它负责传递数据,咱们能够添加一些已注册声明,好比“iss”(JWT字符串的颁发人名称)、“exp”(该JWT字符串的过时时间)、“sub”(身份)、“aud”(受众),除了这些,咱们还可根据须要添加自定义的须要传输的数据,通常是发起请求的用户的信息。例如:
{ “iss”:"RayPI", "sub": "Client", "name": "张三", "uid": 1 }
将该JSON格式的数据经过Base64Url格式编码后,生成的字符串就是咱们JWT字符串的第二部分。
3)Signature
数字签名,由4个因素所同时决定:编码后的header字符串,编码后的payload字符串,以前在头部声明的加密算法,咱们自定义的一个秘钥字符串(secret)。例如:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
因此签名能够安全地验证一个JWT的合法性(有没有被篡改过)。
最后,给一个实际生成后的JWT字符串的完整样例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJDbGllbnQiLCJqdGkiOiIwZTRjYzVkNC0yMmIzLTQwYzUtOTBjMy0wOTk0MjFjNWRjMjkiLCJpYXQiOiIyMDE4LzcvMyAyOjE3OjQ5IiwiZXhwIjoxNTMwNjI3NDY5LCJpc3MiOiJSYXlQSSJ9.98pAaDVhNwVfiSHQVeXKhYE2ML6WK_f9rYC-iwyQEpU
咱们能够拿着这个JWT字符串到https://jwt.io/#debugger试着解析出前两部分的内容。
本文的“令牌”指的是用于http传输headers中用于验证受权的JSON数据,它是key和value两部分组成,在本文中,key为“Authorization”,value为“Bearer {JWT字符串}”,其中value除了JWT字符串外,还在前面添加了“Bearer ”字符串,这里能够把它理解为你们约约定俗成的规定便可,没有实际的做用。例如:
{ "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJDbGllbnQiLCJqdGkiOiIwZTRjYzVkNC0yMmIzLTQwYzUtOTBjMy0wOTk0MjFjNWRjMjkiLCJpYXQiOiIyMDE4LzcvMyAyOjE3OjQ5IiwiZXhwIjoxNTMwNjI3NDY5LCJpc3MiOiJSYXlQSSJ9.98pAaDVhNwVfiSHQVeXKhYE2ML6WK_f9rYC-iwyQEpU" }
先问一个问题:认证和受权是一回事吗?
答案显然是否认的。由于你们都喜欢把这两个词放在一块儿说,因此很容易就混淆了他们含义。在.NET Core中,认证的单词是“Authentication”,而受权的单词是“Authorization”。
认证,验证身份的意思。即验证当前请求的用户是否为合法用户(放在当前场景下,就是验证用户携带的令牌是否为一个合法令牌);
受权,给用户颁发权限的意思。即给验证经过的用户授予相应的权限(放在当前场景下,就是根据令牌中解析出的用户身份,赋予该http请求,该http请求使用该身份就能够访问对应的接口)
因此,咱们下面要实现的整体思路是:
在每一个接口上都标明该接口容许什么样的身份访问(好比“Client”表明客户端,“Admin”表明后台管理员)。
在用户登陆成功后,咱们将该用户的身份(是Client仍是Admin)等信息生成JWT规范的令牌返回。客户端将返回的令牌存储好(通常是存在Cookie中),之后每次调用接口都要将该令牌携带上。
服务端收到请求后,提取令牌,先进行认证,若是不合法(好比被篡改),将驳回请求。若是认证经过,则从令牌中提取身份,进行受权操做,将该身份赋予http请求,放行请求。
请求进到接口后会和接口标明的容许访问身份进行匹配,若是该接口容许该身份访问,则返回相应请求资源,若是不容许,则驳回请求。
思路明白了,下面实战起来就不会乱了。
搭建完的项目架构应该是这样的:
这里有三块工做区域:
1)JwtHelper是一个Jwt帮助类,里面有两个函数,一个函数帮助生成Jwt字符串并返回,一个帮助从Jwt字符串逆向解析出数据。
2)JwtAuthorizationFilter.cs是一个受权中间件
3)Startup中添加了认证服务和受权服务
using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; // using RayPI.Model.ConfigModel; namespace RayPI.Helper { public class JwtHelper { /// <summary> /// 颁发JWT字符串 /// </summary> /// <param name="tokenModel"></param> /// <returns></returns> public static string IssueJWT(TokenModel tokenModel) { var dateTime = DateTime.UtcNow; var claims = new Claim[] { new Claim(JwtRegisteredClaimNames.Jti,tokenModel.Uid.ToString()),//用户Id new Claim("Role", tokenModel.Role),//身份 new Claim("Project", tokenModel.Project),//身份 new Claim(JwtRegisteredClaimNames.Iat,dateTime.ToString(),ClaimValueTypes.Integer64) }; //秘钥 var jwtConfig = new JwtAuthConfigModel(); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.JWTSecretKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); //过时时间 double exp = 0; switch (tokenModel.TokenType) { case "Web": exp = jwtConfig.WebExp; break; case "App": exp = jwtConfig.AppExp; break; case "MiniProgram": exp = jwtConfig.MiniProgramExp; break; case "Other": exp = jwtConfig.OtherExp; break; } var jwt = new JwtSecurityToken( issuer: "RayPI", claims: claims, //声明集合 expires: dateTime.AddHours(exp), signingCredentials: creds); var jwtHandler = new JwtSecurityTokenHandler(); var encodedJwt = jwtHandler.WriteToken(jwt); return encodedJwt; } /// <summary> /// 解析 /// </summary> /// <param name="jwtStr"></param> /// <returns></returns> public static TokenModel SerializeJWT(string jwtStr) { var jwtHandler = new JwtSecurityTokenHandler(); JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr); object role = new object(); ; object project = new object(); try { jwtToken.Payload.TryGetValue("Role", out role); jwtToken.Payload.TryGetValue("Project", out project); } catch (Exception e) { Console.WriteLine(e); throw; } var tm = new TokenModel { Uid = long.Parse(jwtToken.Id), Role = role.ToString(), Project = project.ToString() }; return tm; } } /// <summary> /// 令牌 /// </summary> public class TokenModel { /// <summary> /// 用户Id /// </summary> public long Uid { get; set; } /// <summary> /// 身份 /// </summary> public string Role { get; set; } /// <summary> /// 项目名称 /// </summary> public string Project { get; set; } /// <summary> /// 令牌类型 /// </summary> public string TokenType { get; set; } } }
其中JwtAuthConfigModel是一个存储配置文件的类,里面读取了配置文件中的jwt秘钥和几个过时时间。这里就不放了,能够在代码里直接写死。
using Microsoft.AspNetCore.Http; using RayPI.Bussiness; using RayPI.Helper; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; namespace RayPI.AuthHelp { /// <summary> /// 受权中间件 /// </summary> public class JwtAuthorizationFilter { private readonly RequestDelegate _next; /// <summary> /// /// </summary> /// <param name="next"></param> public JwtAuthorizationFilter(RequestDelegate next) { _next = next; } /// <summary> /// /// </summary> /// <param name="httpContext"></param> /// <returns></returns> public Task Invoke(HttpContext httpContext) { //检测是否包含'Authorization'请求头,若是不包含则直接放行 if (!httpContext.Request.Headers.ContainsKey("Authorization")) { return _next(httpContext); } var tokenHeader = httpContext.Request.Headers["Authorization"]; tokenHeader = tokenHeader.ToString().Substring("Bearer ".Length).Trim(); TokenModel tm = JwtHelper.SerializeJWT(tokenHeader); //BaseBLL.TokenModel = tm;//将tokenModel存入baseBll //受权 var claimList = new List<Claim>(); var claim = new Claim(ClaimTypes.Role, tm.Role); claimList.Add(claim); var identity = new ClaimsIdentity(claimList); var principal = new ClaimsPrincipal(identity); httpContext.User = principal; return _next(httpContext); } } }
完整代码:
using System.Collections.Generic; using System.IO; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.PlatformAbstractions; using Swashbuckle.AspNetCore.Swagger; using RayPI.Model.ConfigModel; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; using RayPI.AuthHelp; namespace RayPI { /// <summary> /// /// </summary> public class Startup { /// <summary> /// /// </summary> /// <param name="env"></param> public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); this.Configuration = builder.Build(); BaseConfigModel.SetBaseConfig(Configuration); } /// <summary> /// /// </summary> public IConfiguration Configuration { get; } /// <summary> /// This method gets called by the runtime. Use this method to add services to the container. /// </summary> /// <param name="services"></param> public void ConfigureServices(IServiceCollection services) { services.AddMvc().AddJsonOptions(options => { options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";//设置时间格式 }); #region Swagger services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v1.1.0", Title = "Ray WebAPI", Description = "框架集合", TermsOfService = "None", Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "RayWang", Email = "2271272653@qq.com", Url = "http://www.cnblogs.com/RayWang" } }); //添加注释服务 var basePath = PlatformServices.Default.Application.ApplicationBasePath; var apiXmlPath = Path.Combine(basePath, "APIHelp.xml"); var entityXmlPath = Path.Combine(basePath, "EntityHelp.xml"); c.IncludeXmlComments(apiXmlPath, true);//控制器层注释(true表示显示控制器注释) c.IncludeXmlComments(entityXmlPath); //添加控制器注释 //c.DocumentFilter<SwaggerDocTag>(); //添加header验证信息 //c.OperationFilter<SwaggerHeader>(); var security = new Dictionary<string, IEnumerable<string>> { { "Bearer", new string[] { } }, }; c.AddSecurityRequirement(security);//添加一个必须的全局安全信息,和AddSecurityDefinition方法指定的方案名称要一致,这里是Bearer。 c.AddSecurityDefinition("Bearer", new ApiKeyScheme { Description = "JWT受权(数据将在请求头中进行传输) 参数结构: \"Authorization: Bearer {token}\"", Name = "Authorization",//jwt默认的参数名称 In = "header",//jwt默认存放Authorization信息的位置(请求头中) Type = "apiKey" }); }); #endregion #region 认证 services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { JwtAuthConfigModel jwtConfig=new JwtAuthConfigModel(); o.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = "RayPI", ValidAudience = "wr", IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.JWTSecretKey)), /***********************************TokenValidationParameters的参数默认值***********************************/ RequireSignedTokens = true, // SaveSigninToken = false, // ValidateActor = false, // 将下面两个参数设置为false,能够不验证Issuer和Audience,可是不建议这样作。 ValidateAudience = false, ValidateIssuer = true, ValidateIssuerSigningKey = true, // 是否要求Token的Claims中必须包含 Expires RequireExpirationTime = true, // 容许的服务器时间偏移量 // ClockSkew = TimeSpan.FromSeconds(300), // 是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比 ValidateLifetime = true }; }); #endregion #region 受权 services.AddAuthorization(options => { options.AddPolicy("RequireClient", policy => policy.RequireRole("Client").Build()); options.AddPolicy("RequireAdmin", policy => policy.RequireRole("Admin").Build()); options.AddPolicy("RequireAdminOrClient", policy => policy.RequireRole("Admin,Client").Build()); }); #endregion #region CORS services.AddCors(c => { c.AddPolicy("Any", policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); c.AddPolicy("Limit", policy => { policy .WithOrigins("localhost:8083") .WithMethods("get", "post", "put", "delete") //.WithHeaders("Authorization"); .AllowAnyHeader(); }); }); #endregion } /// <summary> /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. /// </summary> /// <param name="app"></param> /// <param name="env"></param> public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } #region Swagger app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiHelp V1"); }); #endregion //认证 app.UseAuthentication(); //受权 app.UseMiddleware<JwtAuthorizationFilter>(); app.UseMvc(); app.UseStaticFiles();//用于访问wwwroot下的文件 } } }
这里有一个坑,不太了解依赖注入和中间件的人很容易踩到(其实就是我本身了)
在Startup.cs的Configure函数中,里面每一个app.UseXXXXX();是有必定顺序。能够理解为,这里添加中间件的顺序就是客户端发起http请求时所通过的顺序。
以前我由于把“app.UseMvc();”写到了认证受权上面去了,结果致使怎么Debug都找不到问题。。。
因此必定要先app.UseAuthentication()认证,而后app.UseMiddleware<JwtAuthorizationFilter>()受权,最后再app.UseMvc()
搭建完成以后,下面就是测试了。
选择一个测试控制器,在其头上标注[Authorize]属性
(Policy和Roles两种写法是一个意思,可是必需要是Startup中已经申明的,不然Swagger会直接报错)
F5运行,在swagger ui中调用一个须要受权验证的接口(根据Id获取学生信息)
输入1,先不进行任何受权认证的操做,直接点击Excute尝试调用,返回结果以下:
状态码500,还返回了一大段html代码,咱们能够将接口的完整地址输入到浏览器地址栏进行访问,就能够看到这段html代码的页面了:
能够看到接口返回了一个错误页,缘由就是中间件在http请求的头部(headers)中没有找到“Authorization"字段里的”令牌“。
如今,咱们先调用获取JWT接口(实际项目中不该该有该接口,分发令牌的功能应该集成到登录功能中,可是这里为了简单直观,我将分发令牌的功能直接写成了接口,以供测试),输入相应的客户端信息,Excute:
接口会生成”令牌“,返回JWT字符串:
咱们要复制这串JWT字符串,而后将其添加到http请求的Headers中去。测试方法有两个:
1)能够新建一个html页面,模拟前端写个ajax调用接口,在ajax添加headers字段,以下:
$.ajax({ url: "http://localhost:3608/api/Admin/Student/1", type: ”get“, dataType: "json", //data: {},
async: false,
//手动高亮 headers: { "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJBZG1pbiIsImp0aSI6IjhjMDEwMzI2LTE4M2MtNGQ5ZC1iMDFjLWFjM2EzNTIzODYxOCIsImlhdCI6IjIwMTgvNy8yIDE1OjAzOjQ4IiwiZXhwIjoxNTMwNTg3MDI4LCJpc3MiOiJSYXlQSSJ9.1Bb7hwoDD12n8ymcQsu79Xm-GDq14GERhS9b-1l1kmg" }, success: function (d) { alert(JSON.stringify(d)); }, error: function (d) { alert(JSON.stringify(d)) } });
2)若是你的swagger像我同样,集成了添加Authrize头部功能,那么能够点击这个按钮进行添加(若是你的swagger看不到这个按钮,能够参考我以前的章节【从零开始搭建本身的.NET Core Api框架】(一)建立项目并集成swagger:1.2 完善,对swagger进行相关的设置)
这里除了JWT字符串外,前面还须要手动写入“Bearer ”(有一个空格)字符串。点击Authorize保存"令牌"。
再次调用刚才的”根据id获取学生信息“接口,发现获取成功:
能够看到swagger向http请求的headers中添加了咱们刚才保存的”令牌“。
参考内容:
https://jwt.io/
https://www.cnblogs.com/webenh/p/9039322.html