開發提供數據的WebApi服務,最重要的是數據的安全性。那么對於我們來說,如何確保數據的安全是要思考的問題。
在ASP.NET WebService服務中可以通過SoapHead驗證機制來實現,那么在ASP.NET WebApi中我們應該如何保證我們的接口的安全呢?
- 什么是JWT?
JSON Web Token(JWT)是一個開放標准,它定義了一種緊湊的,自包含的方式,用於作為JSON對象在各方之間安全地傳輸信息,該信息可以被驗證和信任,因為它是數字簽名的
簡單理解就是一個字符串
- JWT長什么樣子?
JWT是由三段信息構成的,將這三段信息文本用.鏈接一起就構成了Jwt字符串
- JWT構成
1、第一部分頭部header
header典型的由兩部分組成,token的類型("JWT")和算法名稱(比如:HMAC HS256--哈希256)
例如
Base64({
"alg":"HS256"
"typ":"JWT"
})
然后用Base64對這個JSON編碼就得到JWT的第一部分
2、第二部分載荷Payload--載荷就是存放有效信息的地方
Payload聲明有3種類型
a、register:預定義的
b、public:公有的
c、private:私有的
2.1、標准中注冊的聲明 (建議但不強制使用) :
iss: jwt簽發者
sub:jwt所面向的用戶
aud: 接收jwt的一方
exp:jwt的過期時間,這個過期時間必須要大於簽發時間
nbf: 定義在什么時間之前,該jwt都是不可用的.
iat:jwt的簽發時間
jti:jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
2.2、公共的聲明 :
公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密.
2.3、私有的聲明 :
私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息
例如:
Base64({
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
})
對payload進行Base64編碼就得到JWT的第二部分
3、第三部分簽名Signature
為了得到簽名部分,你必須有編碼過的header,編碼過的payload,一個秘鑰,簽名算法是header中指定的那個,然后對它們簽名即可,例如:HMACSHA256(base64UrlEncode(header)+ "." + base64UrlEncode(payload),secret)。
這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的簽名算法配合secret進行簽名,然后就構成了jwt的第三部分
將這三部分用.連接成一個完整的字符串,構成了最終的jwt
簽名是用於驗證消息在傳遞過程中有沒有更改,並且,對於使用私鑰簽名的token,它還可以驗證JWT的發送方是否為它所稱的發送方
- 注意
secret密鑰是保存在服務器端的,jwt的簽發生成是在服務器端做的,jwt的驗簽也是在服務器端做的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了
- 如何應用
一般是在請求頭里加入Authorization,並加上Bearer標注:
- 基於JWT實現Token簽名認證基本思路如下
基本流程上是這樣的:
1、用戶使用用戶名和密碼來請求服務器
2、服務器進行驗證用戶的信息
3、服務器通過驗證,發送給用戶一個token
4、客戶端存儲token,並在每次請求時在request header附上這個token值,服務端並不存儲token
5、服務器驗證token值,驗證成功后,則返回數據
- 實現原理圖
1、POST/user/login with username and password
2、Create a JWT with a secret
3、Returns the JWT to the Brower
4、Sends the JWT on the Authorization Header
5、Check JWT signature Get user info from the JWT
6、Sends response to the client
服務器端做兩件事情,1--簽發token值,2--驗證token值
JWT並不存儲token
- 代碼具體實現
jwt官網:https://jwt.io/
1、簽發token
1.1、ApiTokenService用於驗證用戶名和密碼通過后簽發token
public class ApiTokenServiceController : ApiController { /// <summary> /// 獲取訪問口令令牌 /// </summary> /// <returns></returns> [HttpGet] [HttpPost] public ResponseResult<LoginResponseResult> GetToken(string appId, string appSecret) { var result = ResponseResult<LoginResponseResult>.Default(); if (string.IsNullOrEmpty(appId)) { return ResponseResult<LoginResponseResult>.Faild("appid不能為空!"); } if (string.IsNullOrEmpty(appSecret)) { return ResponseResult<LoginResponseResult>.Faild("appsecret不能為空!"); } try { //1、模擬從數據庫中驗證appId和appSecret if (appId.Equals("admin") && appSecret.Equals("123456")) { //2、登錄成功后將token以jwt字符串的形式返回給客戶端。 var payload = new Dictionary<string, object> { { "iat", JwtHelper.ConvertDateTimeInt(DateTime.Now) },//token創建時間,unix時間戳。unix { "exp", JwtHelper.ConvertDateTimeInt(DateTime.Now.AddMinutes(10)) },//expire 指定token的生命周期。unix { "AppId",appId }, { "AppSecret", appSecret }, }; LoginResponseResult entity = new LoginResponseResult { AppId = appId, AppSecret = appSecret, Token = JwtHelper.GenerateJwtEncode(payload), }; result = ResponseResult<LoginResponseResult>.Success(entity, "獲取token成功!"); } else { result = ResponseResult<LoginResponseResult>.Faild("獲取token失敗!"); } } catch (System.Exception ex) { result = ResponseResult<LoginResponseResult>.Exception(ex.Message); } return result; } /// <summary> /// 刷新新的口令令牌 /// </summary> /// <returns></returns> [HttpGet] [HttpPost] public ResponseResult<LoginResponseResult> RefreshToken() { string tokenParam = WebHelper.GetHeadersParamValue("token"); var result = ResponseResult<LoginResponseResult>.Default(); if (string.IsNullOrEmpty(tokenParam)) { return ResponseResult<LoginResponseResult>.Faild("token不能為空!"); } var tokenVaule = JwtHelper.GetJwtDecode(tokenParam); if (tokenVaule == null) { var resp = Request.CreateResponse<ResponseResult>(HttpStatusCode.OK, ResponseResult.NotAuthorization("token過期或無效!"), "application/json"); throw new HttpResponseException(resp); } try { string appId = tokenVaule["AppId"].ToString(); string appSecret = tokenVaule["AppSecret"].ToString(); //2、登錄成功后將token以jwt字符串的形式返回給客戶端。 //對象初始化器 var payload = new Dictionary<string, object> { { "iat", JwtHelper.ConvertDateTimeInt(DateTime.Now) },//token創建時間,unix時間戳。unix { "exp", JwtHelper.ConvertDateTimeInt(DateTime.Now.AddMinutes(10)) },//expire 指定token的生命周期。unix { "AppId",appId }, { "AppSecret",appSecret }, }; LoginResponseResult entity = new LoginResponseResult { AppId = appId, AppSecret = appSecret, Token = JwtHelp.GenerateJwtEncode(payload), }; result = ResponseResult<LoginResponseResult>.Success(entity, "口令刷新成功!"); } catch (System.Exception ex) { result = ResponseResult<LoginResponseResult>.Exception(ex.Message); } return result; } }
1.2、響應結果實體ResponseResult
public class ResponseResult { public ResponseResult() { } /// <summary> /// 狀態 /// </summary> public int RequestStatus { get; set; } /// <summary> /// 消息內容 /// </summary> public string Msg { get; set; } public static ResponseResult Default() { var result = new ResponseResult(); result.RequestStatus = (int)ResponseResultStatus.Default; result.Msg = ""; return result; } public static ResponseResult Success(string message = "") { var result = new ResponseResult(); result.RequestStatus = (int)ResponseResultStatus.Succeed; result.Msg = message; return result; } public static ResponseResult Exception(string message) { var result = new ResponseResult(); result.RequestStatus = (int)ResponseResultStatus.Exception; result.Msg = message; return result; } public static ResponseResult Faild(string message) { var result = new ResponseResult(); result.RequestStatus = (int)ResponseResultStatus.Faild; result.Msg = message; return result; } public static ResponseResult NotAuthorization(string message) { var result = new ResponseResult(); result.RequestStatus = (int)ResponseResultStatus.NotAuthorization; result.Msg = message; return result; } } public class ResponseResult<T> : ResponseResult where T : class, new() { public ResponseResult() { this.Data = new T(); } public T Data { get; set; } public static ResponseResult<T> Default() { var result = new ResponseResult<T>(); result.Data = default(T); result.RequestStatus = (int)ResponseResultStatus.Default; result.Msg = ""; return result; } public static ResponseResult<T> Success(T t, string message = "") { var result = new ResponseResult<T>(); result.Data = t; result.RequestStatus = (int)ResponseResultStatus.Succeed; result.Msg = message; return result; } public static ResponseResult<T> Exception(string message) { var result = new ResponseResult<T>(); result.Data = default(T); result.RequestStatus = (int)ResponseResultStatus.Exception; result.Msg = message; return result; } public static ResponseResult<T> Faild(string message) { var result = new ResponseResult<T>(); result.Data = default(T); result.RequestStatus = (int)ResponseResultStatus.Faild; result.Msg = message; return result; } public static ResponseResult<T> NotAuthorization(string message) { var result = new ResponseResult<T>(); result.Data = default(T); result.RequestStatus = (int)ResponseResultStatus.NotAuthorization; result.Msg = message; return result; } } public enum ResponseResultStatus { Default = 0, Succeed = 100, Faild = 101, Exception = 102, NotAuthorization = 403 }
1.3、LoginResponseResult
public class LoginResponseResult { public string AppId { get; set; } public string AppSecret { get; set; } public string Token { get; set; } }
2、JwtHelper
通過nuget引用jwt組件,掌握JWT組件的使用,學會使用JWT組件生成token值,以及驗證token值
public class JwtHelper { /// <summary> /// jwt的secret千萬不能泄密了,只能讓服務端自己知道。jwt的secret用戶服務端token的簽發和驗證。 /// </summary> private static string secret = "davidmengdavidmeng"; /// <summary> /// 生成JwtToken /// </summary> /// <param name="payload"></param> /// <returns></returns> public static string GenerateJwtEncode(Dictionary<string, object> payload) { IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); IJsonSerializer serializer = new JsonNetSerializer(); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); var token = encoder.Encode(payload, secret); return token; } /// <summary> /// 根據jwtToken獲取payload /// </summary> /// <param name="token">jwtToken</param> /// <returns></returns> public static IDictionary<string, object> GetJwtDecode(string token) { return GetJwtDecode<Dictionary<string, object>>(token); } public static T GetJwtDecode<T>(string token) { try { IJsonSerializer serializer = new JsonNetSerializer(); IDateTimeProvider provider = new UtcDateTimeProvider(); IJwtValidator validator = new JwtValidator(serializer, provider); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); return decoder.DecodeToObject<T>(token, secret, verify: true); } catch (TokenExpiredException) { Console.WriteLine("Token has expired"); } catch (SignatureVerificationException) { Console.WriteLine("Token has invalid signature"); } return default(T); } /// <summary> /// Unix時間戳轉為C#格式時間 /// </summary> /// <param name="timeStamp">Unix時間戳格式,例如1482115779</param> /// <returns>C#格式時間</returns> public static DateTime GetTime(string timeStamp) { DateTime dtStart = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)); long lTime = long.Parse(timeStamp + "0000000"); TimeSpan toNow = new TimeSpan(lTime); return dtStart.Add(toNow); } /// <summary> /// DateTime時間格式轉換為Unix時間戳格式 /// </summary> /// <param name="time"> DateTime時間格式</param> /// <returns>Unix時間戳格式</returns> public static int ConvertDateTimeInt(System.DateTime time) { System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1)); return (int)(time - startTime).TotalSeconds; } }
3、BaseApiTokenController
讓業務接口繼承自BaseApiTokenController,而BaseApiTokenController繼承自ApiController,實現重寫Initialize方法,在里面實現用JWT組件驗證用戶的JWT Token的有效性(用戶token口令無效或者是否已經過期),這樣每一個繼承自BaseApiTokenController的業務接口都將驗證token
public class BaseApiTokenController : ApiController { protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext) { base.Initialize(controllerContext); string tokenParam = WebHelper.GetHeadersParamValue("token"); #region 1、驗證token、參數合法性 if (string.IsNullOrEmpty(tokenParam)) { var resp = Request.CreateResponse<ResponseResult>(HttpStatusCode.OK, ResponseResult.NotAuthorization("token參數不能為空!"), "application /json"); throw new HttpResponseException(resp); } #endregion #region 2、驗證用戶的JWT Token的有效性(用戶token口令無效或者是否已經過期) var tokenVaule = JwtHelper.GetJwtDecode(tokenParam); if (tokenVaule == null) { var resp = Request.CreateResponse<ResponseResult>(HttpStatusCode.OK, ResponseResult.NotAuthorization("token過期或無效!"), "application/json"); throw new HttpResponseException(resp); } #endregion } }
訂單服務OrderServiceController
public class OrderServiceController : BaseApiTokenController { [HttpGet] [HttpPost] public IEnumerable<string> GetOrder() { return new string[] { "ASP.NET WebApi 基於JWT實現Token簽名認證" }; } }
4、界面
4.1、登錄輸入正確的用戶名和密碼
4.2、數據庫驗證用戶名和密碼通過,簽發token
4.3、獲取到token后,跳轉到另外一個界面,單擊獲取訂單按鈕訪問訂單服務
請求時把jwt token,加入到header中
4.4、訂單服務通過token驗證,進入到訂單服務