1. 程式人生 > WINDOWS開發 >NET Core API 框架實現介面的JWT授權驗證

NET Core API 框架實現介面的JWT授權驗證

原始碼已上傳Github:https://github.com/WangRui321/RayPI_V2.0

一、根

根據維基百科定義,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作為系統授權驗證的優缺點,這裡就不細說了,感興趣的可以自己去查閱相關資料。

1.1 在授權驗證系統中,JWT是怎麼工作的呢?

如果將JWT運用到Web Api的授權驗證中,那麼它的工作原理是這樣的:

1)客戶端向授權服務系統發起請求,申請獲取“令牌”。

2)授權服務根據使用者身份,生成一張專屬“令牌”,並將該“令牌”以JWT規範返回給客戶端

3)客戶端將獲取到的“令牌”放到http請求的headers中後,向主服務系統發起請求。主服務系統收到請求後會從headers中獲取“令牌”,並從“令牌”中解析出該使用者的身份許可權,然後做出相應的處理(同意或拒絕返回資源)

可以看出,JWT授權服務是可以脫離我們的主服務系統而作為一個獨立系統存在的。

1.2 令牌是什麼?JWT就是令牌嗎?

前面說了其實把JWT理解為一種規範更為貼切,但是往往大家把根據JWT規則生成的加密字串也叫作JWT,還有人直接稱呼JWT為令牌。本文為了闡述方便,特此做了一些區分:

1.2.1 JWT:

本文所說的JWT皆指的是JWT規範

1.2.2 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試著解析出前兩部分的內容。

1.2.3 令牌:

本文的“令牌”指的是用於http傳輸headers中用於驗證授權的JSON資料,它是key和value兩部分組成,在本文中,key為“Authorization”,value為“Bearer ”,其中value除了JWT字串外,還在前面添加了“Bearer ”字串,這裡可以把它理解為大家約約定俗成的規定即可,沒有實際的作用。例如:

{ "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJDbGllbnQiLCJqdGkiOiIwZTRjYzVkNC0yMmIzLTQwYzUtOTBjMy0wOTk0MjFjNWRjMjkiLCJpYXQiOiIyMDE4LzcvMyAyOjE3OjQ5IiwiZXhwIjoxNTMwNjI3NDY5LCJpc3MiOiJSYXlQSSJ9.98pAaDVhNwVfiSHQVeXKhYE2ML6WK_f9rYC-iwyQEpU" }

整體的思路明白了,下面實戰起來就不會亂了。

二、 道

搭建完的專案架構應該是這樣的:

這裡有三塊工作區域:

一個是RayPI.Token層,該層主要負責“令牌”的生成和儲存。

還有一個是在主專案下面的AuthHelper的TokenAuth,該類為一箇中間件,它被註冊到客服端和介面之間,在客戶端發起http請求時,這個http請求會先被傳輸到TokenAuth類中,然後該類經過一系列驗證和操作(包括了JWT驗證),決定是否對給http請求進行授權,然後將請求傳遞給下一個中介軟體。

最後一個是系統的系統類Startup.cs,我們將在這裡面註冊中介軟體,新增Authorization服務等操作。

2.1 搭建RayPI.Token 層

2.1.1 Model

在RayPI.Token層中新建一個Model資料夾,在該資料夾下新建一個TokenModel類,類的定義如下:

namespace RayPI.Token.Model

{

///

/// 令牌類

///

public class TokenModel

{

public TokenModel()

{

this.Uid = 0;

}

///

/// 使用者Id

///

public long Uid { get; set; }

///

/// 使用者名稱

///

public string Uname { get; set; }

///

/// 手機

///

public string Phone { get; set; }

///

/// 頭像

///

public string Icon { get; set; }

///

/// 暱稱

///

public string UNickname { get; set; }

///

/// 身份

///

public string Sub { get; set; }

}

}

該類用於儲存客戶端的一些基本資訊,後面我們需要將它存入到系統快取中。

2.1.2 快取類

新建一個RayPIMemoryCache類,該類是一個系統擴充套件類,用於整合我們常用的對MemoryCache的操作,程式碼如下:

using System;

using Microsoft.Extensions.Caching.Memory;

namespace RayPI.Token

{

public class RayPIMemoryCache

{

public static MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());

///

/// 驗證快取項是否存在

///

///

快取Key

///

public static bool Exists(string key)

{

if (key == null)

{

throw new ArgumentNullException(nameof(key));

}

object cached;

return _cache.TryGetValue(key,out cached);

}

///

/// 獲取快取

///

///

快取Key

///

public static object Get(string key)

{

if (key == null)

{

throw new ArgumentNullException(nameof(key));

}

return _cache.Get(key);

}

///

/// 新增快取

///

///

快取Key

///

快取Value

///

滑動過期時長(如果在過期時間內有操作,則以當前時間點延長過期時間)

///

絕對過期時長

///

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);

}

}

}

2.1.3 RayPIToken類

該類只有一個方法叫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

{

///

/// 令牌類

///

public class RayPIToken

{

public RayPIToken()

{

}

///

/// 獲取JWT字串並存入快取

///

///

///

///

///

public static string IssueJWT(TokenModel tokenModel,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;

}

}

}

2.2. 搭建AuthHelp中介軟體

在主專案中新增資料夾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

{

///

/// Token驗證授權中介軟體

///

public class TokenAuth

{

///

/// http委託

///

private readonly RequestDelegate _next;

///

/// 建構函式

///

///

public TokenAuth(RequestDelegate next)

{

_next = next;

}

///

/// 驗證授權

///

///

///

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 lc = new List();

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驗證異常");

}

}

}

}

2.3 設定Startup.cs類

在ConfigureServices,我們需要註冊兩個服務項

1)快取

services.AddSingleton(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();

完整的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

{

///

///

///

public class Startup

{

///

///

///

///

public Startup(IConfiguration configuration)

{

Configuration = configuration;

}

///

///

///

public IConfiguration Configuration { get; }

///

/// This method gets called by the runtime. Use this method to add services to the container.

///

///

public void ConfigureServices(IServiceCollection services)

{

services.Configure>(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",

});

//添加註釋服務

var basePath = PlatformServices.Default.Application.ApplicationBasePath;

var xmlPath = Path.Combine(basePath,"APIHelp.xml");

c.IncludeXmlComments(xmlPath);

//新增對控制器的標籤(描述)

c.DocumentFilter();

//新增header驗證資訊

//c.OperationFilter();

var security = new Dictionary> { { "Bearer",new string[] { } },};

c.AddSecurityRequirement(security);//新增一個必須的全域性安全資訊,和AddSecurityDefinition方法指定的方案名稱要一致,這裡是Bearer。

c.AddSecurityDefinition("Bearer",new ApiKeyScheme

{

Description = "JWT授權(資料將在請求頭中進行傳輸) 引數結構: \"Authorization: Bearer \"",

Name = "Authorization",//jwt預設的引數名稱

In = "header",//jwt預設存放Authorization資訊的位置(請求頭中)

Type = "apiKey"

});

});

#endregion

#region Token

services.AddSingleton(factory =>

{

var cache = new MemoryCache(new MemoryCacheOptions());

return cache;

});

services.AddAuthorization(options =>

{

options.AddPolicy("System",policy => policy.RequireClaim("AdminType").Build());

});

#endregion

}

///

/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

///

///

///

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();

#endregion

app.UseMvc();

app.UseStaticFiles();//用於訪問wwwroot下的檔案

}

}

}

Tips:

這裡有一個坑,不太瞭解依賴注入和中介軟體的人很容易踩到(其實就是我自己了)

在Startup.cs的Configure函式中,裡面每個app.UseXXXXX();是有一定順序。可以理解為,這裡新增中介軟體的順序就是客戶端發起http請求時所經過的順序。

之前我因為把“app.UseMvc();”寫到了“app.UseMiddleware();”上面去了,結果導致怎麼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頭部功能,那麼可以點選這個按鈕進行新增。

技術分享圖片 技術分享圖片

這裡除了JWT字串外,前面還需要手動寫入“Bearer ”(有一個空格)字串。點選Authorize儲存"令牌"。

再次呼叫剛才的”根據id獲取學生資訊“介面,發現獲取成功:

技術分享圖片

可以看到swagger向http請求的headers中添加了我們剛才儲存的”令牌“。