NET Core API 框架實現介面的JWT授權驗證
來源:在7樓
cnblogs.com/RayWang/p/9255093.html
源碼已上傳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 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;
}
}
}
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("SystemType").Build());
options.AddPolicy("Client", policy => policy.RequireClaim("ClientType").Build());
options.AddPolicy("Admin", 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中添加了我們剛才保存的」令牌「。
看完本文有收穫?請轉發分享給更多人
關注「DotNet」,提升.Net技能
TAG:DotNet |