Asp.Net Core 3.1 學習4、Web Api 中基于JWT的token驗證及Swagger使用
1、初始JWT
1.1、JWT原理
JWT(JSON Web Token)是目前最流行的跨域身份驗證解決方案,他的優勢就在于伺服器不用存token便于分布式開發,給APP提供資料用于前後端分離的項目。登入産生的 token的項目完全可以獨立與其他項目。當使用者通路登入接口的時候會傳回一個token,然後通路其他需要登入的接口都會帶上這個token,背景進行驗證如果token是有效的我們就認為使用者是正常登入的,然後我們可以從token中取出來一些攜帶的資訊進行操作。當然這些攜帶的資訊都可以通過其他額外的字段進行傳遞,但是用token傳遞的話,不用其他額外加其他字段了。
JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的使用者身份資訊,以便于從資源伺服器擷取資源,也可以增加一些額外的其它業務邏輯所必須的聲明資訊,該token也可直接被用于認證,也可被加密。
1.2、JWT結構
JWT是由三段資訊構成的,将這三段資訊文本用.連結一起就構成了Jwt字元串。就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbklEIjoiYWRtaW4iLCJuYmYiOjE1ODc4OTE2OTMsImV4cCI6MTU4NzkyNzY5MywiaXNzIjoiV1lZIiwiYXVkIjoiRXZlcnlUZXN0T25lIn0.-snenNVHrrKq9obN8FzKe0t99ok6FUm5pHv-P_eYc30
第一部分我們稱它為頭部(header):聲明類型,這裡是jwt;聲明加密的算法 通常直接使用 HMAC SHA256
{
'typ': 'JWT',
'alg': 'HS256'
}
第二部分我們稱其為載荷(payload, 類似于飛機上承載的物品):
iss:Token釋出者
exp:過期時間 分鐘
sub:主題
aud:Token接受者
nbf:在此之前不可用
iat:釋出時間
jti:JWT ID用于辨別該JWT
除以上預設字段外,我們還可以自定義私有字段,如下例:
"sub": "1234567890",
"name": "wyy",
"admin": true
第三部分是簽證(signature):這個部分需要base64加密後的header和base64加密後的payload使用.連接配接組成的字元串,然後通過header中聲明的加密方式進行加鹽secret組合加密,然後就構成了jwt的第三部分。
2、生成Token
2.1、建立項目
在VS2019中建立一個Core Api程式 Core選3.1 然後在項目上添加一個Jwt檔案夾幫助類,建立接口ITokenHelper,類:TokenHelper繼承ITokenHelper,類JWTConfig,類TnToken
JWTConfig:用來儲存讀取jwt相關配置
///
/// 配置token生成資訊
/// </summary>
public class JWTConfig
{
/// <summary>
/// Token釋出者
/// </summary>
public string Issuer { get; set; }
/// <summary>
/// oken接受者
/// </summary>
public string Audience { get; set; }
/// <summary>
/// 秘鑰
/// </summary>
public string IssuerSigningKey { get; set; }
/// <summary>
/// 過期時間
/// </summary>
public int AccessTokenExpiresMinutes { get; set; }
}
TnToken:存放Token 跟過期時間的類
/// 存放Token 跟過期時間的類
/// </summary>
public class TnToken
{
/// <summary>
/// token
/// </summary>
public string TokenStr { get; set; }
/// <summary>
/// 過期時間
/// </summary>
public DateTime Expires { get; set; }
}
ITokenHelper接口:token工具類的接口,友善使用依賴注入,很簡單提供兩個常用的方法
/// token工具類的接口,友善使用依賴注入,很簡單提供兩個常用的方法
/// </summary>
public interface ITokenHelper
{
/// <summary>
/// 根據一個對象通過反射提供負載生成token
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="user"></param>
/// <returns></returns>
TnToken CreateToken<T>(T user) where T : class;
/// <summary>
/// 根據鍵值對提供負載生成token
/// </summary>
/// <param name="keyValuePairs"></param>
/// <returns></returns>
TnToken CreateToken(Dictionary<string, string> keyValuePairs);
}
TokenHelper:實作類
/// Token生成類
/// </summary>
public class TokenHelper : ITokenHelper
{
private readonly IOptions<JWTConfig> _options;
public TokenHelper(IOptions<JWTConfig> options)
{
_options = options;
}
/// <summary>
/// 根據一個對象通過反射提供負載生成token
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="user"></param>
/// <returns></returns>
public TnToken CreateToken<T>(T user) where T : class
{
//攜帶的負載部分,類似一個鍵值對
List<Claim> claims = new List<Claim>();
//這裡我們用反射把model資料提供給它
foreach (var item in user.GetType().GetProperties())
{
object obj = item.GetValue(user);
string value = "";
if (obj != null)
value = obj.ToString();
claims.Add(new Claim(item.Name, value));
}
//建立token
return CreateToken(claims);
}
/// <summary>
/// 根據鍵值對提供負載生成token
/// </summary>
/// <param name="keyValuePairs"></param>
/// <returns></returns>
public TnToken CreateToken(Dictionary<string, string> keyValuePairs)
{
//攜帶的負載部分,類似一個鍵值對
List<Claim> claims = new List<Claim>();
//這裡我們通過鍵值對把資料提供給它
foreach (var item in keyValuePairs)
{
claims.Add(new Claim(item.Key, item.Value));
}
//建立token
return CreateTokenString(claims);
}
/// <summary>
/// 生成token
/// </summary>
/// <param name="claims">List的 Claim對象</param>
/// <returns></returns>
private TnToken CreateTokenString(List<Claim> claims)
{
var now = DateTime.Now;
var expires = now.Add(TimeSpan.FromMinutes(_options.Value.AccessTokenExpiresMinutes));
var token = new JwtSecurityToken(
issuer: _options.Value.Issuer,//Token釋出者
audience: _options.Value.Audience,//Token接受者
claims: claims,//攜帶的負載
notBefore: now,//目前時間token生成時間
expires: expires,//過期時間
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256));
return new TnToken { TokenStr = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires };
}
}
2.2、在Startup中去配置jwt相關:
ConfigureServices中:
region jwt配置
services.AddTransient<ITokenHelper, TokenHelper>();
//讀取配置檔案配置的jwt相關配置
services.Configure<JWTConfig>(Configuration.GetSection("JWTConfig"));
//啟用JWT
services.AddAuthentication(Options =>
{
Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).
AddJwtBearer();#endregion
JwtBearerDefaults.AuthenticationScheme與AddJwtBearer();下載下傳兩個依賴即可。或者NuGet安裝
appsettings中簡單配置一下jwt相關的資訊:
"JWTConfig": {
"Issuer": "WYY", //Token釋出者
"Audience": "EveryTestOne", //Token接受者
"IssuerSigningKey": "WYY&YL889455200Sily", //秘鑰可以建構伺服器認可的token;簽名秘鑰長度最少16
"AccessTokenExpiresMinutes": "600" //過期時間 分鐘
},
Configure中去啟用驗證中間件:
//啟用認證中間件 要寫在授權UseAuthorization()的前面
app.UseAuthentication();
2.3、一個簡單的登入擷取token
在Controllers檔案夾裡面建立一個api 名字LoginTest
[EnableCors("AllowCors")]
[Route("api/[controller]/[action]")]
[ApiController]
public class LoginTestController : ControllerBase
{
private readonly ITokenHelper tokenHelper = null;
/// <summary>
/// 構造函數
/// </summary>
/// <param name="_tokenHelper"></param>
public LoginTestController(ITokenHelper _tokenHelper)
{
tokenHelper = _tokenHelper;
}
/// <summary>
/// 登入測試
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
[HttpPost]
public ReturnModel Login([FromBody]UserDto user)
{
var ret = new ReturnModel();
try
{
if (string.IsNullOrWhiteSpace(user.LoginID) || string.IsNullOrWhiteSpace(user.Password))
{
ret.Code = 201;
ret.Msg = "使用者名密碼不能為空";
return ret;
}
//登入操作 我就沒寫了 || 假設登入成功
if (1 == 1)
{
Dictionary<string, string> keyValuePairs = new Dictionary<string, string>
{
{ "loginID", user.LoginID }
};
ret.Code = 200;
ret.Msg = "登入成功";
ret.TnToken= tokenHelper.CreateToken(keyValuePairs);
}
}
catch(Exception ex)
{
ret.Code = 500;
ret.Msg = "登入失敗:"+ex.Message;
}
return ret;
}
}
UserDto接收類
/// 登入類Dto
/// </summary>
public class UserDto
{
/// <summary>
/// 使用者名
/// </summary>
public string LoginID { get; set; }
/// <summary>
/// 密碼
/// </summary>
public string Password { get; set; }
}
ReturnModel 隻是我自己封裝的一個統一的接口傳回格式标準
/// 傳回類
/// </summary>
public class ReturnModel
{
/// <summary>
/// 傳回碼
/// </summary>
public int Code { get; set; }
/// <summary>
/// 消息
/// </summary>
public string Msg { get; set; }
/// <summary>
/// 資料
/// </summary>
public object Data { get; set; }
/// <summary>
/// Token資訊
/// </summary>
public TnToken TnToken { get; set; }
}
跨域上篇文章說了這裡就不提了
2.4、前端擷取token
我是用傳統的MVC的一個啟動頁面
Token:
有效期:
<input type="button" value="擷取Token" onclick="getToken()" /><br /><br /><br />
把Api啟動起來 MVC也啟動起來試試看
在JWT管網解碼
3、驗證前端傳遞的token
現在說說怎麼來驗證前台傳遞的jwt,其實很簡單,最主要的就是驗證token的有效性和是否過期。在接口ITokenHelper中添加驗證的兩個方法 。TokenHelper中實作
ITokenHelper中添加
/// Token驗證
/// </summary>
/// <param name="encodeJwt">token</param>
/// <param name="validatePayLoad">自定義各類驗證; 是否包含那種申明,或者申明的值</param>
/// <returns></returns>
bool ValiToken(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad = null);
/// <summary>
/// 帶傳回狀态的Token驗證
/// </summary>
/// <param name="encodeJwt">token</param>
/// <param name="validatePayLoad">自定義各類驗證; 是否包含那種申明,或者申明的值</param>
/// <param name="action"></param>
/// <returns></returns>
TokenType ValiTokenState(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad, Action<Dictionary<string, string>> action);
TokenHelper中添加
/// 驗證身份 驗證簽名的有效性
/// </summary>
/// <param name="encodeJwt"></param>
/// <param name="validatePayLoad">自定義各類驗證; 是否包含那種申明,或者申明的值, </param>
public bool ValiToken(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad = null)
{
var success = true;
var jwtArr = encodeJwt.Split('.');
if (jwtArr.Length < 3)//資料格式都不對直接pass
{
return false;
}
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(_options.Value.IssuerSigningKey));
//驗證簽名是否正确(把使用者傳遞的簽名部分取出來和伺服器生成的簽名比對即可)
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()));
//不需要自定義驗證不傳或者傳遞null即可
if (validatePayLoad == null)
return true;
//再其次 進行自定義的驗證
success = success && validatePayLoad(payLoad);
return success;
}
/// <summary>
/// 時間轉換
/// </summary>
/// <param name="date"></param>
/// <returns></returns>
private long ToUnixEpochDate(DateTime date)
{
return (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
}
/// <summary>
///
/// </summary>
/// <param name="encodeJwt"></param>
/// <param name="validatePayLoad"></param>
/// <param name="action"></param>
/// <returns></returns>
public TokenType ValiTokenState(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad, Action<Dictionary<string, string>> action)
{
var jwtArr = encodeJwt.Split('.');
if (jwtArr.Length < 3)//資料格式都不對直接pass
{
return TokenType.Fail;
}
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(_options.Value.IssuerSigningKey));
//驗證簽名是否正确(把使用者傳遞的簽名部分取出來和伺服器生成的簽名比對即可)
if (!string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1]))))))
{
return TokenType.Fail;
}
//其次驗證是否在有效期内(必須驗證)
var now = ToUnixEpochDate(DateTime.UtcNow);
if (!(now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString())))
{
return TokenType.Expired;
}
//不需要自定義驗證不傳或者傳遞null即可
if (validatePayLoad == null)
{
action(payLoad);
return TokenType.Ok;
}
//再其次 進行自定義的驗證
if (!validatePayLoad(payLoad))
{
return TokenType.Fail;
}
//可能需要擷取jwt摘要裡邊的資料,封裝一下友善使用
action(payLoad);
return TokenType.Ok;
}
其中TokenType是傳回類型成功失敗
public enum TokenType
{
Ok,
Fail,
Expired
}
在api LoginTest中新增兩個驗證的方法
/// 驗證Token
/// </summary>
/// <param name="tokenStr">token</param>
/// <returns></returns>
[HttpGet]
public ReturnModel ValiToken(string tokenStr)
{
var ret = new ReturnModel
{
TnToken = new TnToken()
};
bool isvilidate = tokenHelper.ValiToken(tokenStr);
if(isvilidate)
{
ret.Code = 200;
ret.Msg = "Token驗證成功";
ret.TnToken.TokenStr = tokenStr;
}
else
{
ret.Code = 500;
ret.Msg = "Token驗證失敗";
ret.TnToken.TokenStr = tokenStr;
}
return ret;
}
/// <summary>
/// 驗證Token 帶傳回狀态
/// </summary>
/// <param name="tokenStr"></param>
/// <returns></returns>
[HttpGet]
public ReturnModel ValiTokenState(string tokenStr)
{
var ret = new ReturnModel
{
TnToken = new TnToken()
};
string loginID = "";
TokenType tokenType = tokenHelper.ValiTokenState(tokenStr, a => a["iss"] == "WYY" && a["aud"] == "EveryTestOne", action => { loginID = action["loginID"]; });
if (tokenType == TokenType.Fail)
{
ret.Code = 202;
ret.Msg = "token驗證失敗";
return ret;
}
if (tokenType == TokenType.Expired)
{
ret.Code = 205;
ret.Msg = "token已經過期";
return ret;
}
//..............其他邏輯
var data = new List<Dictionary<string, string>>();
var bb = new Dictionary<string, string>
{
{ "Wyy", "123456" }
};
data.Add(bb);
ret.Code = 200;
ret.Msg = "通路成功!";
ret.Data =data ;
return ret;
}
上面一個簡單的驗證和支援自定義驗證的就寫好了。下面帶有狀态的是讓我們清楚的知道是什麼狀态請求登入的時候 或者請求資料的時候,是token過期還是說token沒有擷取到等等。
ValiTokenState第三個參數我還更了一個系統委托,是這樣想的,處理可以驗證token,還可以順便取一個想要的資料,當然其實這樣把相關邏輯混到一起也增加代碼的耦合性,當時可以提高一點效率不用在重新解析一次資料,當然這個資料也可以通前台傳遞過來,是以怎麼用還是看實際情況,這裡隻是封裝一下提供這樣一個方法,用的時候也可以用。
其前端請求代碼
$.ajax({
type: "post",
url: "https://localhost:44331/api/LoginTest/ValiToken?tokenStr="+ $("#tokenValue").val(),
dataType: "json",
async: true,
data: { token: $("#tokenValue").val() },
contentType: 'application/json',
success: function (data) {
console.log(data);
},
error: function (data) {
console.log("錯誤" + data);
}
});
4、Api中過濾器實作通用token驗證
項目上建立一個檔案夾Filter,在檔案夾Filter裡建立一個過濾器TokenFilter
namespace JWTToken.Filter
public class TokenFilter : Attribute, IActionFilter
{
private ITokenHelper tokenHelper;
public TokenFilter(ITokenHelper _tokenHelper) //通過依賴注入得到資料通路層執行個體
{
tokenHelper = _tokenHelper;
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
ReturnModel ret = new ReturnModel();
//擷取token
object tokenobj = context.ActionArguments["token"];
if (tokenobj == null)
{
ret.Code = 201;
ret.Msg = "token不能為空";
context.Result = new JsonResult(ret);
return;
}
string token = tokenobj.ToString();
string userId = "";
//驗證jwt,同時取出來jwt裡邊的使用者ID
TokenType tokenType = tokenHelper.ValiTokenState(token, a => a["iss"] == "WYY" && a["aud"] == "EveryTestOne", action => { userId = action["userId"]; });
if (tokenType == TokenType.Fail)
{
ret.Code = 202;
ret.Msg = "token驗證失敗";
context.Result = new JsonResult(ret);
return;
}
if (tokenType == TokenType.Expired)
{
ret.Code = 205;
ret.Msg = "token已經過期";
context.Result = new JsonResult(ret);
}
if (!string.IsNullOrEmpty(userId))
{
//給控制器傳遞參數(需要什麼參數其實可以做成可以配置的,在過濾器裡邊加字段即可)
//context.ActionArguments.Add("userId", Convert.ToInt32(userId));
}
}
}
context.ActionArguments。這是前段請求的時候位址欄帶上的參數 token=xxx;這種類型的,不是請求的參數 不然會報錯;
把過濾器在startup中注入一下:
services.AddScoped();
需要驗證token的地方,直接加上這個過濾器即可
前台試試 請求上圖的GetList
<input type="button" value="擷取Token" onclick="getToken()" /><br /><br /><br />
現擷取token 指派在隐藏框裡在請求
5、在Api中使用Swagger
5.1項目中添加Swagger的相關包
5.2ConfigureServices、Configure 中添加
region Swagger
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "測試接口文檔",
Description = "測試接口"
});
// 為 Swagger 設定xml文檔注釋路徑
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
c.DocInclusionPredicate((docName, description) => true);
//添加對控制器的标簽(描述)
c.DocumentFilter<ApplyTagDescriptions>();//顯示類名
c.CustomSchemaIds(type => type.FullName);// 可以解決相同類名會報錯的問題
//c.OperationFilter<AuthTokenHeaderParameter>();
});
#endregion
app.UseSwagger(c =>
{
c.RouteTemplate = "swagger/{documentName}/swagger.json";
});
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Web App v1");
c.RoutePrefix = "doc";//設定根節點通路
//c.DocExpansion(DocExpansion.None);//折疊
c.DefaultModelsExpandDepth(-1);//不顯示Schemas
});
5.3、項目屬性修改
5.4、添加接口類的注釋
看效果
6、總結
JWT個人的了解就是api配置檔案的IssuerSigningKey作為秘鑰來加密的,用戶端登入後擷取到token 位址欄請求傳到後端 後端通過解碼擷取到IssuerSigningKey是否跟背景解析出來的一直來比對。後端可以解除安裝鍋爐器裡面來接收這個token來驗證進而限制能不能通路Api。前端可以自己封裝一個請求把token穿進去的參數就可以避免每次輸入Token,前端可以Session?
原文位址
https://www.cnblogs.com/w5942066/p/12781397.html