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"]; 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?
下了班寫的倉促了 哈哈。歡迎補充。