webapi中使用token驗證(JWT驗證)
阿新 • • 發佈:2017-12-31
後端 erro filters missing 參考 做的 方法調用 reading minute
本文介紹如何在webapi中使用JWT驗證
準備
安裝JWT安裝包 System.IdentityModel.Tokens.Jwt 你的前端api登錄請求的方法,參考 axios.get("api/token?username=cuong&password=1").then(function (res) { // 返回一個token /* token示例如下 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Inllamlhd2VpIiwibmJmIjoxNTE0NjQyNTA0LCJleHAiOjE1MTQ2NDk3MDQsImlhdCI6MTUxNDY0MjUwNH0.ur97ZRviC_sfeFgDOHgaRpDePcYED6qmlfOvauPt9EA"
創建TokenHelper類
在項目跟目錄下創建一個TokenHelper.cs類,代碼如下 using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.IdentityModel.Tokens; namespace TokenTest { public class TokenHelper { /// <summary> /// Use the below code to generate symmetric Secret Key /// var hmac = new HMACSHA256(); /// var key = Convert.ToBase64String(hmac.Key); /// </summary> private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw=="; public static string GenerateToken(string username, int expireMinutes = 120) { // 此方法用來生成 Token var symmetricKey = Convert.FromBase64String(Secret); // 生成二進制字節數組 var tokenHandler = new JwtSecurityTokenHandler(); // 創建一個JwtSecurityTokenHandler類用來生成Token var now = DateTime.UtcNow; // 獲取當前時間 var tokenDescriptor = new SecurityTokenDescriptor // 創建一個 Token 的原始對象 { Subject = new ClaimsIdentity(new[] // Token的身份證,類似一個人可以有身份證,戶口本 { new Claim(ClaimTypes.Name, username) // 可以創建多個 }), Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)), // Token 有效期 SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(symmetricKey), SecurityAlgorithms.HmacSha256) // 生成一個Token證書,第一個參數是根據預先的二進制字節數組生成一個安全秘鑰,說白了就是密碼,第二個參數是編碼方式 }; var stoken = tokenHandler.CreateToken(tokenDescriptor); // 生成一個編碼後的token對象實例 var token = tokenHandler.WriteToken(stoken); // 生成token字符串,給前端使用 return token; } public static ClaimsPrincipal GetPrincipal(string token) { // 此方法用解碼字符串token,並返回秘鑰的信息對象 try { var tokenHandler = new JwtSecurityTokenHandler(); // 創建一個JwtSecurityTokenHandler類,用來後續操作 var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken; // 將字符串token解碼成token對象 if (jwtToken == null) return null; var symmetricKey = Convert.FromBase64String(Secret); // 生成編碼對應的字節數組 var validationParameters = new TokenValidationParameters() // 生成驗證token的參數 { RequireExpirationTime = true, // token是否包含有效期 ValidateIssuer = false, // 驗證秘鑰發行人,如果要驗證在這裏指定發行人字符串即可 ValidateAudience = false, // 驗證秘鑰的接受人,如果要驗證在這裏提供接收人字符串即可 IssuerSigningKey = new SymmetricSecurityKey(symmetricKey) // 生成token時的安全秘鑰 }; SecurityToken securityToken; // 接受解碼後的token對象 var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken); return principal; // 返回秘鑰的主體對象,包含秘鑰的所有相關信息 } catch (Exception ex) { return null; } } } }
創建過濾器類
當前端發送一個請求,需要接收並處理token 在當前項目下創建一個名為Filter的文件夾 創建一個AuthenticationAttribute類,代碼如下 using System; using System.Collections.Generic; using System.Security.Claims; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using System.Web.Http.Filters; namespace TokenTest.Filter { // IAuthenticationFilter用來自定義一個webapi控制器方法屬性 public class AuthenticationAttribute : Attribute, IAuthenticationFilter { public bool AllowMultiple => false; public string Realm { get; set; } public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken) { // 當api發送請求,自動調用這個方法 var request = context.Request; // 獲取請求的請求體 var authorization = request.Headers.Authorization; // 獲取請求的token對象 if (authorization == null || authorization.Scheme != "Bearer") return; if(string.IsNullOrEmpty(authorization.Parameter)) { // 給ErrorResult賦值需要一個類實現了IHttpActionResult接口 // 此類聲明在AuthenticationFailureResult.cs文件中,此文件用來處理錯誤信息。 context.ErrorResult = new AuthenticationFailureResult("Missing Jwt Token", request); return; } var token = authorization.Parameter; // 獲取token字符串 var principal = await AuthenticateJwtToken(token); // 調用此方法,根據token生成對應的"身份證持有人" if(principal == null) { context.ErrorResult = new AuthenticationFailureResult("Invalid token", request); } else { context.Principal = principal; // 設置身份驗證的主體 } // 此法調用完畢後,會調用ChallengeAsync方法,從而來完成WWW-Authenticate驗證 } private Task<IPrincipal> AuthenticateJwtToken(string token) { string userName; if(ValidateToken(token, out userName)) { // 這裏就是驗證成功後要做的邏輯,也就是處理WWW-Authenticate驗證 var info = new List<Claim> { new Claim(ClaimTypes.Name, userName) }; // 根據驗證token後獲取的用戶名重新在建一個聲明,你個可以在這裏創建多個聲明 // 作者註: claims就像你身份證上面的信息,一個Claim就是一條信息,將這些信息放在ClaimsIdentity就構成身份證了 var infos = new ClaimsIdentity(info, "Jwt"); // 將上面的身份證放在ClaimsPrincipal裏面,相當於把身份證給持有人 IPrincipal user = new ClaimsPrincipal(infos); return Task.FromResult(user); } return Task.FromResult<IPrincipal>(null); } private bool ValidateToken(string token, out string userName) { userName = null; var simplePrinciple = TokenHelper.GetPrincipal(token); // 調用自定義的GetPrincipal獲取Token的信息對象 var identity = simplePrinciple?.Identity as ClaimsIdentity; // 獲取主聲明標識 if (identity == null) return false; if (!identity.IsAuthenticated) return false; var userNameClaim = identity.FindFirst(ClaimTypes.Name); // 獲取聲明類型是ClaimTypes.Name的第一個聲明 userName = userNameClaim?.Value; // 獲取聲明的名字,也就是用戶名 if (string.IsNullOrEmpty(userName)) return false; return true; // 到這裏token本身的驗證工作已經完成了,因為用戶名可以解碼出來 // 後續要驗證的就是瀏覽器的 WWW-Authenticate /* 什麽是WWW-Authenticate驗證??? WWW-Authenticate是早期的一種驗證方式,很容易被破解,瀏覽器發送請求給後端,後端服務器會解析傳過來的Header驗證 如果沒有類似於本文格式的token,那麽會發送WWW-Authenticate: Basic realm= "." 到前端瀏覽器,並返回401 */ } public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken) { // 此方法在AuthenticateAsync方法調用完成之後自動調用 ChallengeAsync(context); return Task.FromResult(0); } private void ChallengeAsync(HttpAuthenticationChallengeContext context) { string parameter = null; if (!string.IsNullOrEmpty(Realm)) { parameter = "realm=\"" + Realm + "\""; } // token的parameter部分已經通過jwt驗證成功,這裏只需要驗證scheme即可 context.ChallengeWith("Bearer", parameter); // 這個自定義擴展方法定義在HttpAuthenticationChallengeContextExtensions.cs文件中 // 主要用來驗證token的Schema是不是Bearer } } } 創建AuthenticationFailureResult類,代碼如下 using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Web.Http; namespace TokenTest.Filter { // 此類比較簡單不做過多註釋 public class AuthenticationFailureResult : IHttpActionResult { public string _FailureReason { get; } public HttpRequestMessage _Request { get; } public AuthenticationFailureResult(string FailureReason, HttpRequestMessage request) { _FailureReason = FailureReason; _Request = request; } HttpResponseMessage HandleResponseMessage() { HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized) { RequestMessage = _Request, ReasonPhrase = _FailureReason }; return response; } public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { return Task.FromResult(HandleResponseMessage()); } } } 創建HttpAuthenticationChallengeContextExtensions類,寫的context的擴展方法,代碼如下 using System; using System.Net.Http.Headers; using System.Web.Http.Filters; namespace TokenTest.Filter { public static class HttpAuthenticationChallengeContextExtensions { public static void ChallengeWith(this HttpAuthenticationChallengeContext context, string scheme) { ChallengeWith(context, new AuthenticationHeaderValue(scheme)); } private static void ChallengeWith(HttpAuthenticationChallengeContext context, AuthenticationHeaderValue challenge) { if(context == null) { throw new ArgumentNullException(nameof(context)); } context.Result = new AddChallengeOnUnauthorizedResult(challenge, context.Result); } public static void ChallengeWith(this HttpAuthenticationChallengeContext context, string scheme, string parameter) { // 第二個參數的作用是根據傳進來的scheme也就是"Bearer"和parameter這裏為null,創建一個驗證頭,和前端傳過來的token是一樣的 ChallengeWith(context, new AuthenticationHeaderValue(scheme, parameter)); } } } 創建AddChallengeOnUnauthorizedResult類,代碼如下 using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using System.Web.Http; namespace TokenTest.Filter { public class AddChallengeOnUnauthorizedResult: IHttpActionResult { public AuthenticationHeaderValue _Challenge { get; } public IHttpActionResult _InnerResult { get; } public AddChallengeOnUnauthorizedResult(AuthenticationHeaderValue challenge, IHttpActionResult innerResult) { _Challenge = challenge; _InnerResult = innerResult; } public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { // 這裏講schemee也就是"Bearer"生成後的response返回給瀏覽器去做判斷,如果瀏覽器請求的Authenticate中含有含有名為"Bearer"的scheme會返回200狀態碼否則返回401狀態碼 HttpResponseMessage response = await _InnerResult.ExecuteAsync(cancellationToken); if(response.StatusCode == HttpStatusCode.Unauthorized) { // 如果這裏不成立,但是我們之前做的驗證都是成功的,這是不對的,可能出現意外情況啥的 // 這時我們手動添加一個名為"Bearer"的sheme,讓請求走通 // 到此,完畢。 if (response.Headers.WwwAuthenticate.All(h => h.Scheme != _Challenge.Scheme)) { response.Headers.WwwAuthenticate.Add(_Challenge); } } return response; } } }
配置
在你的WebApiConfig.cs文件中添加 config.Filters.Add(new AuthorizeAttribute()); // 開啟全局驗證服務
代碼使用
創建一個webapi的控制器 測試,用戶登錄 [Route("yejiawei/haha")] [HttpGet] [AllowAnonymous] // 這個屬性是必須的,表示這個類是不需要token驗證的 public string Get(string username, string password) { if (CheckUser(username, password)) { return TokenHelper.GenerateToken(username); } throw new HttpResponseException(HttpStatusCode.Unauthorized); } public bool CheckUser(string username, string password) { // 在這裏你可以在數據庫中查看用戶名是否存在 return true; } 測試,訪問後端api數據 [Route("yejiawei/haha")] [HttpGet] [Authentication] // 此方法驗證的token需要調用Authentication屬性方法 public string Get() { return "value"; } 到此一切搞定。
webapi中使用token驗證(JWT驗證)