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相關配置
/// <summary> /// 配置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 跟過期時間的類
/// <summary> /// 存放Token 跟過期時間的類 /// </summary> public class TnToken { /// <summary> /// token /// </summary> public string TokenStr { get; set; } /// <summary> /// 過期時間 /// </summary> public DateTime Expires { get; set; } }
ITokenHelper接口:token工具類的接口,方便使用依賴注入,很簡單提供兩個常用的方法
/// <summary> /// 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:實現類
/// <summary> /// 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接收類
/// <summary> /// 登錄類Dto /// </summary> public class UserDto { /// <summary> /// 用戶名 /// </summary> public string LoginID { get; set; } /// <summary> /// 密碼 /// </summary> public string Password { get; set; } }
ReturnModel 只是我自己封裝的一個統一的接口返回格式標准
/// <summary> /// 返回類 /// </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的一個啟動頁面
<input type="hidden" id="tokenValue" name="tokenValue" value="" /> <br /><br /><br /> <span>Token:</span><div id="txtval"></div><br /> <span>有效期:</span><div id="txtvalTime"></div><br /> <div> <input type="button" value="獲取Token" onclick="getToken()" /><br /><br /><br /> </div> <script src="~/Scripts/jquery-3.3.1.js"></script> <script type="text/javascript"> //獲取token function getToken() { var data = JSON.stringify({ LoginID: "admin", Password: "admin888" }); $.ajax({ type: "post", url: "https://localhost:44331/api/LoginTest/Login", dataType: "json", async: true, data: data, contentType: 'application/json', success: function (data) { console.log(data); $("#txtval").html(data.tnToken.tokenStr); $("#txtvalTime").html(new Date(data.tnToken.expires).Format("yyyy-MM-dd hh:mm")); $("#tokenValue").val(data.tnToken.tokenStr); }, error: function (data) { console.log("錯誤" + data); } }); } Date.prototype.Format = function (fmt) { //author: zhengsh 2016-9-5 var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "h+": this.getHours(), //小時 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "q+": Math.floor((this.getMonth() + 3) / 3), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; } </script>
把Api啟動起來 MVC也啟動起來試試看
在JWT管網解碼
3、驗證前端傳遞的token
現在說說怎么來驗證前台傳遞的jwt,其實很簡單,最主要的就是驗證token的有效性和是否過期。在接口ITokenHelper中添加驗證的兩個方法 。TokenHelper中實現
ITokenHelper中添加
/// <summary> /// 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> /// 驗證身份 驗證簽名的有效性 /// </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中新增兩個驗證的方法
/// <summary> /// 驗證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"];//前端地址欄參數傳參
//object tokenobj = context.HttpContext.Request.Headers["token"].ToString();//前端寫在header里面獲取的
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<TokenFilter>();
需要驗證token的地方,直接加上這個過濾器即可
前台試試 請求上圖的GetList
<input type="hidden" id="tokenValue" name="tokenValue" value="" /> <br /><br /><br /> <span>Token:</span><div id="txtval"></div><br /> <span>有效期:</span><div id="txtvalTime"></div><br /> <div> <input type="button" value="獲取Token" onclick="getToken()" /><br /><br /><br /> </div> <input type="button" value="獲取List" onclick="getList()" /><br /> <script src="~/Scripts/jquery-3.3.1.js"></script> <script type="text/javascript"> //獲取token function getToken() { var data = JSON.stringify({ LoginID: "admin", Password: "admin888" }); $.ajax({ type: "post", url: "https://localhost:44331/api/LoginTest/Login", dataType: "json", async: true, data: data, contentType: 'application/json', success: function (data) { console.log(data); $("#txtval").html(data.tnToken.tokenStr); $("#txtvalTime").html(new Date(data.tnToken.expires).Format("yyyy-MM-dd hh:mm")); $("#tokenValue").val(data.tnToken.tokenStr); }, error: function (data) { console.log("錯誤" + data); } }); } //獲取list function getList() { var data = JSON.stringify(); $.ajax({ type: "post", url: "https://localhost:44331/api/Home/GetList?token="+ $("#tokenValue").val(), dataType: "json", async: true, data: { token: $("#tokenValue").val() }, contentType: 'application/json', success: function (data) { console.log(data); $("#txtval").html(JSON.stringify(data)); }, error: function (data) { console.log("錯誤" + data); } }); } Date.prototype.Format = function (fmt) { //author: zhengsh 2016-9-5 var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "h+": this.getHours(), //小時 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "q+": Math.floor((this.getMonth() + 3) / 3), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; } </script>
現獲取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?
下了班寫的倉促了 哈哈。歡迎補充。