系列目錄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_V2.0
2018.08.29更新進階篇:【從零開始搭建本身的.NET Core Api框架】(七)受權認證進階篇
根據維基百科定義,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" }
總體的思路明白了,下面實戰起來就不會亂了。
搭建完的項目架構應該是這樣的:
這裏有三塊工做區域:
一個是RayPI.Token層,該層主要負責「令牌」的生成和存儲。
還有一個是在主項目下面的AuthHelper的TokenAuth,該類爲一箇中間件,它被註冊到客服端和接口之間,在客戶端發起http請求時,這個http請求會先被傳輸到TokenAuth類中,而後該類通過一系列驗證和操做(包括了JWT驗證),決定是否對給http請求進行受權,而後將請求傳遞給下一個中間件。
最後一個是系統的系統類Startup.cs,咱們將在這裏面註冊中間件,添加Authorization服務等操做。
在RayPI.Token層中新建一個Model文件夾,在該文件夾下新建一個TokenModel類,類的定義以下:
namespace RayPI.Token.Model { /// <summary>
/// 令牌類 /// </summary>
public class TokenModel { public TokenModel() { this.Uid = 0; } /// <summary>
/// 用戶Id /// </summary>
public long Uid { get; set; } /// <summary>
/// 用戶名 /// </summary>
public string Uname { get; set; } /// <summary>
/// 手機 /// </summary>
public string Phone { get; set; } /// <summary>
/// 頭像 /// </summary>
public string Icon { get; set; } /// <summary>
/// 暱稱 /// </summary>
public string UNickname { get; set; } /// <summary>
/// 身份 /// </summary>
public string Sub { get; set; } } }
該類用於存儲客戶端的一些基本信息,後面咱們須要將它存入到系統緩存中。
新建一個RayPIMemoryCache類,該類是一個系統擴展類,用於集成咱們經常使用的對MemoryCache的操做,代碼以下:
using System; using Microsoft.Extensions.Caching.Memory; namespace RayPI.Token { public class RayPIMemoryCache { public static MemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); /// <summary>
/// 驗證緩存項是否存在 /// </summary>
/// <param name="key">緩存Key</param>
/// <returns></returns>
public static bool Exists(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } object cached; return _cache.TryGetValue(key, out cached); } /// <summary>
/// 獲取緩存 /// </summary>
/// <param name="key">緩存Key</param>
/// <returns></returns>
public static object Get(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return _cache.Get(key); } /// <summary>
/// 添加緩存 /// </summary>
/// <param name="key">緩存Key</param>
/// <param name="value">緩存Value</param>
/// <param name="expiresSliding">滑動過時時長(若是在過時時間內有操做,則以當前時間點延長過時時間)</param>
/// <param name="expiressAbsoulte">絕對過時時長</param>
/// <returns></returns>
public static bool AddMemoryCache(string key, object value, TimeSpan expiresSliding, TimeSpan expiressAbsoulte) { if (key == null) { throw new ArgumentNullException(nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } _cache.Set(key, value, new MemoryCacheEntryOptions() .SetSlidingExpiration(expiresSliding) .SetAbsoluteExpiration(expiressAbsoulte) ); return Exists(key); } } }
該類只有一個方法叫IssueJWT,咱們將tokenModel傳遞給這個函數,它會根據tokenModel生成JWT字符串,而後將JWT字符串做爲key、tokenModel做爲value存入系統緩存中中。
using Microsoft.IdentityModel.Tokens; using RayPI.Token.Model; using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace RayPI.Token { /// <summary>
/// 令牌類 /// </summary>
public class RayPIToken { public RayPIToken() { } /// <summary>
/// 獲取JWT字符串並存入緩存 /// </summary>
/// <param name="tm"></param>
/// <param name="expireSliding"></param>
/// <param name="expireAbsoulte"></param>
/// <returns></returns>
public static string IssueJWT(TokenModel tokenModel, TimeSpan expiresSliding, TimeSpan expiresAbsoulte) { DateTime UTC = DateTime.UtcNow; Claim[] claims = new Claim[] { new Claim(JwtRegisteredClaimNames.Sub,tokenModel.Sub),//Subject,
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),//JWT ID,JWT的惟一標識
new Claim(JwtRegisteredClaimNames.Iat, UTC.ToString(), ClaimValueTypes.Integer64),//Issued At,JWT頒發的時間,採用標準unix時間,用於驗證過時
}; JwtSecurityToken jwt = new JwtSecurityToken( issuer: "RayPI",//jwt簽發者,非必須
audience: tokenModel.Uname,//jwt的接收該方,非必須
claims: claims,//聲明集合
expires: UTC.AddHours(12),//指定token的生命週期,unix時間戳格式,非必須
signingCredentials: new Microsoft.IdentityModel.Tokens .SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes("RayPI's Secret Key")), SecurityAlgorithms.HmacSha256));//使用私鑰進行簽名加密
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);//生成最後的JWT字符串
RayPIMemoryCache.AddMemoryCache(encodedJwt, tokenModel, expiresSliding, expiresAbsoulte);//將JWT字符串和tokenModel做爲key和value存入緩存
return encodedJwt; } } }
在主項目中添加文件夾AuthHelp,在文件夾下添加TokenAuth類。
該類後面咱們會把它註冊爲中間件,用於驗證並受權客戶端發來的http請求。代碼以下:
using Microsoft.AspNetCore.Http; using RayPI.Token; using RayPI.Token.Model; using System; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; namespace RayPI.AuthHelper { /// <summary>
/// Token驗證受權中間件 /// </summary>
public class TokenAuth { /// <summary>
/// http委託 /// </summary>
private readonly RequestDelegate _next; /// <summary>
/// 構造函數 /// </summary>
/// <param name="next"></param>
public TokenAuth(RequestDelegate next) { _next = next; } /// <summary>
/// 驗證受權 /// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public Task Invoke(HttpContext httpContext) { var headers = httpContext.Request.Headers; //檢測是否包含'Authorization'請求頭,若是不包含返回context進行下一個中間件,用於訪問不須要認證的API
if (!headers.ContainsKey("Authorization")) { return _next(httpContext); } var tokenStr = headers["Authorization"]; try { string jwtStr = tokenStr.ToString().Substring("Bearer ".Length).Trim(); //驗證緩存中是否存在該jwt字符串
if (!RayPIMemoryCache.Exists(jwtStr)) { return httpContext.Response.WriteAsync("非法請求"); } TokenModel tm = ((TokenModel)RayPIMemoryCache.Get(jwtStr)); //提取tokenModel中的Sub屬性進行authorize認證
List<Claim> lc = new List<Claim>(); Claim c = new Claim(tm.Sub+"Type", tm.Sub); lc.Add(c); ClaimsIdentity identity = new ClaimsIdentity(lc); ClaimsPrincipal principal = new ClaimsPrincipal(identity); httpContext.User = principal; return _next(httpContext); } catch (Exception) { return httpContext.Response.WriteAsync("token驗證異常"); } } } }
在ConfigureServices,咱們須要註冊兩個服務項
1)緩存
services.AddSingleton<IMemoryCache>(factory => { var cache = new MemoryCache(new MemoryCacheOptions()); return cache; });
2)認證
services.AddAuthorization(options => { options.AddPolicy("System", policy => policy.RequireClaim("SystemType").Build()); options.AddPolicy("Client", policy => policy.RequireClaim("ClientType").Build()); options.AddPolicy("Admin", policy => policy.RequireClaim("AdminType").Build()); });
這裏放了三個身份,System、Client和Admin,後面若是須要能夠再添加。
在Configure中,須要將以前的TokenAuth類註冊爲中間件
app.UseMiddleware<TokenAuth>();
完整的Startup.cs代碼是這樣的:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using RayPI.SwaggerHelp; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.PlatformAbstractions; using RayPI.AuthHelper; using RayPI.Token; using Swashbuckle.AspNetCore.Swagger; using Microsoft.AspNetCore.StaticFiles; namespace RayPI { /// <summary>
///
/// </summary>
public class Startup { /// <summary>
///
/// </summary>
/// <param name="configuration"></param>
public Startup(IConfiguration configuration) { Configuration = 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.Configure<Dictionary<string, string>>(Configuration.GetSection("Mime")); 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 xmlPath = Path.Combine(basePath, "APIHelp.xml"); c.IncludeXmlComments(xmlPath); //添加對控制器的標籤(描述)
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 Token services.AddSingleton<IMemoryCache>(factory => { var cache = new MemoryCache(new MemoryCacheOptions()); return cache; }); services.AddAuthorization(options => { options.AddPolicy("System", policy => policy.RequireClaim("SystemType").Build()); options.AddPolicy("Client", policy => policy.RequireClaim("ClientType").Build()); options.AddPolicy("Admin", policy => policy.RequireClaim("AdminType").Build()); }); #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
#region TokenAuth app.UseMiddleware<TokenAuth>(); #endregion app.UseMvc(); app.UseStaticFiles();//用於訪問wwwroot下的文件
} } }
這裏有一個坑,不太瞭解依賴注入和中間件的人很容易踩到(其實就是我本身了)
在Startup.cs的Configure函數中,裏面每一個app.UseXXXXX();是有必定順序。能夠理解爲,這裏添加中間件的順序就是客戶端發起http請求時所通過的順序。
以前我由於把「app.UseMvc();」寫到了「app.UseMiddleware<TokenAuth>();」上面去了,結果致使怎麼Debug都找不到問題。。。
搭建完成以後,下面就是測試了。
選擇一個測試控制器,在其頭上標註[Authorize]屬性
而後在TokenAuth的Invoke函數下添加一個斷點,在咱們調用接口發起http請求後,應該會先命中這個斷點,在處理了受權驗證以後纔會進入咱們的接口中。
F5運行,在swagger ui中調用一個須要受權驗證的接口(根據Id獲取學生信息)
輸入1,先不進行任何受權認證的操做,直接點擊Excute嘗試調用,系統命中Invoke函數下的斷點,放行,返回結果以下:
狀態碼500,還返回了一大段html代碼,咱們能夠將接口的完整地址輸入到瀏覽器地址欄進行訪問,就能夠看到這段html代碼的頁面了:
能夠看到接口返回了一個錯誤頁,緣由就是由於該接口加了受權驗證以後,中間件TokenAuth會在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