最近新開發一個須要給App使用的API項目。開發API確定會想到JASON Web Token(JWT)和OAuthor2(以前一篇隨筆記錄過OAuthor2)。html
JWT和OAuthor2的比較前端
要像比較JWT和OAuthor2,首先要明白一點就是,這是兩個徹底不一樣的東西,沒有可比性。算法
JWT是一種認證協議數據庫
官網:http://jwt.iojson
JWT提供了一種用於發佈介入靈擺(Access Token),並對發佈的簽名介入令牌進行驗證的方法。令牌(Token)自己包含了一系列聲明,應用程序能夠根據這些聲明限制用戶對資源的訪問。瀏覽器
在新開發的API中,我選擇的是使用JWT,稍後會簡單介紹其在.net core中的使用。安全
OAuthor2是一種受權框架服務器
OAuthor2是一種受權框架,提供了一套詳細的受權機制(指導)。用戶或應用能夠經過公開的或私有的設置,受權第三方應用訪問特定資源。cookie
既然JWT和OAuthor2沒有可比性,爲何還要把這兩個放在一塊兒說呢?實際中,會有不少人拿JWT和OAuthor2做比較,或者分不清楚。不少狀況下,在討論OAuthor2的實現時,會把JSON Web Token做爲一種認證機制使用。這也是爲何他們會常常一塊兒出現。app
JSON Web Token(JWT)
JWT是一種安全標準。基本思路就是用戶提供用戶名和密碼給認證服務器,服務器驗證用戶提交的信息的合法性,若是認證成功,會產生並返回一個Token(令牌),用戶可使用這個token訪問服務器上受保護的資源。
一個token的例子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoibGl1dGFvIiwicm9sZSI6InNob3BVc2VycyIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InNob3BVc2VycyIsImFjdCI6IjEiLCJuYmYiOjE1NzQyNTAyMTgsImV4cCI6MTU3NTExNDIxOCwiaXNzIjoiWXVZdWUiLCJhdWQiOiJZdVl1ZSJ9.t39iwO-r_YgX5-7XyIV-by2duHfThqTQayI595VtqF
一個token包含三個部分:
header.claims.signature
爲了安全的在url中使用,全部部分都base64 URL-safe進行編碼處理。
Header頭部分
頭部分簡單聲明瞭類型(JWT)以及產生簽名所使用的的算法。
{ "alg" : "AES256", "typ" : "JWT" }
Claims聲明
聲明部分是整個token的核心,表示要發送的用戶詳細信息。遊學狀況下,咱們和有可能要在一個服務器上實現認證,而後訪問另外一臺服務器上的資源,或者,經過單獨的接口來生成token,token被保存在應用程序客戶端(好比瀏覽器)使用。
一個簡單的聲明(claim)的例子:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
Signature簽名
簽名的目的是爲了保證上邊兩部分信息不被篡改。若是嘗試使用Bas64對解碼後的token進行修改,簽名信息就會失效。通常使用一個私鑰(private key)經過特定算法對Header和Claims進行混淆產生簽名信息,因此只有原始的token才能於簽名信息匹配。
這裏有一個重要的實現細節。只有獲取了私鑰的應用程序(好比服務器端應用)才能徹底認證token包含聲明信息的合法性。因此,永遠不要把私鑰信息放在客戶端(好比瀏覽器)。
OAuthor2是什麼?
相反,OAuthor2不是一個標準協議,而是一個安全的受權框架,它詳細描述了系統中不一樣角色、用戶、服務前端應用(好比API),以及客戶端(好比網站或移動APP)之間怎麼實現相互認證。
OAuthor2的基本概念,能夠去閱讀以前的一片隨筆。點擊此處
使用HTTPS保護用戶密碼
在進一步討論OAuthor2和JWT的實現以前,有必要說一下,兩種方案都須要SSL安全保護,也就是對要傳輸的數據進行加密編碼。安全地傳輸用戶提供的私密信息,在任何一個安全的系統裏都是必要的。不然任何人均可以經過侵入私人wifi,在用戶登陸的時候竊取用戶的用戶名和密碼等信息。
JWT和OAuthor2應該如何選擇
在作選擇以前,參考一下下邊提到的幾點。
一、時間投入
OAuthor2是一個安全框架,描述了在各類不一樣場景下,多個應用之間的受權問題。有海量的資料須要學習,要徹底理解須要花費大量時間。甚至對於一些有經驗的開發工程師來講,也會須要大概一個月的時間來深刻理解OAuth2。 這是個很大的時間投入。相反,JWT是一個相對輕量級的概念。可能花一天時間深刻學習一下標準規範,就能夠很容易地開始具體實施。
二、出現錯誤的風險
OAuth2不像JWT同樣是一個嚴格的標準協議,所以在實施過程當中更容易出錯。儘管有不少現有的庫,可是每一個庫的成熟度也不盡相同,一樣很容易引入各類錯誤。在經常使用的庫中也很容易發現一些安全漏洞。固然,若是有至關成熟、強大的開發團隊來持續OAuth2實施和維護,能夠必定成都上避免這些風險。
三、社交登陸的好處
在不少狀況下,使用用戶在大型社交網站的已有帳戶來認證會方便。若是指望你的用戶能夠直接使用Facebook或者Gmail之類的帳戶,使用現有的庫會方便得多。
JWT的使用場景
無狀態的分佈式API
JWT的主要優點在於使用無狀態、可擴展的方式處理應用中的用戶會話。服務端能夠經過內嵌的聲明信息,很容易地獲取用戶的會話信息,而不須要去訪問用戶或會話的數據庫。在一個分佈式的面向服務的框架中,這一點很是有用。可是,若是系統中須要使用黑名單實現長期有效的token刷新機制,這種無狀態的優點就不明顯了。
優點:
一、快速開發
二、不須要cookie
三、JSON在移動端的普遍應用
四、不依賴與社交登陸
五、相對簡單的概念理解
限制
一、token有長度限制
二、token不能撤銷
三、須要token有失效時間限制(exp)
OAuthor2使用場景
外包認證服務器
上邊已經討論過,若是不介意API的使用依賴於外部的第三方認證提供者,你能夠簡單地把認證工做留給認證服務商去作。也就是常見的,去認證服務商(好比facebook)那裏註冊你的應用,而後設置須要訪問的用戶信息,好比電子郵箱、姓名等。當用戶訪問站點的註冊頁面時,會看到鏈接到第三方提供商的入口。用戶點擊之後被重定向到對應的認證服務商網站,得到用戶的受權後就能夠訪問到須要的信息,而後重定向回來。
優點:
一、快速開發
二、實施代碼量小
三、維護工做減小
大型企業解決方案
若是設計的API要被不一樣的App使用,而且每一個App使用的方式也不同,使用OAuth2是個不錯的選擇。考慮到工做量,可能須要單獨的團隊,針對各類應用開發完善、靈活的安全策略。固然須要的工做量也比較大!
優點
一、靈活的實現方式
二、能夠和JWT同時使用
三、能夠針對不一樣的應用擴展
簡單介紹下在.net core的項目中是如何使用JWT的。
首先,咱們的服務是基於組件化的,固然須要先把身份認證的服務註冊進來。在Startup類中的ConfigureServices()方法中:
services.AddSingleton<ITokenHelper, TokenHelper>();
// configure strongly typed settings objects
var jwtConfigSection = Configuration.GetSection("Authentication:JwtBearer");
services.Configure<JWTConfig>(jwtConfigSection);
// configure jwt authentication
var jwtConfig = jwtConfigSection.Get<JWTConfig>();
services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddCookie(AdminUserAccountConst.AdminUserCookie, options => { options.Cookie.Name = AdminUserAccountConst.AdminUserCookieName; options.Cookie.HttpOnly = true; options.LoginPath = AdminUserAccountConst.AdminUserLoginPath; options.AccessDeniedPath = AdminUserAccountConst.AdminUserLoginPath; }).AddJwtBearer(AdminUserAccountConst.AdminUserJwt, o => { o.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.Name, RoleClaimType = JwtClaimTypes.Role, ValidateLifetime = false, ValidIssuer = Configuration["Authentication:JwtBearer:Issuer"], ValidAudience = Configuration["Authentication:JwtBearer:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["Authentication:JwtBearer:SecurityKey"])) }; o.ForwardChallenge = AdminUserAccountConst.AdminUserCookie; });
下面是上面所須要用到一些自定義類型:
AdminUserAccountConst
public class AdminUserAccountConst { public const string AdminUserCookie = "AdminUserCookies"; public const string AdminUserCookieName = "AdminUserCookieName"; public const string AdminUserLoginPath = "/account/login"; public const string AdminUserJwt = "AdminUserJwt"; public const string AdminUserRole = "adminuser"; }
JWTConfig
public class JWTConfig { public string Issuer { get; set; } public string Audience { get; set; } public string IssuerSigningKey { get; set; } public int AccessTokenExpiresMinutes { get; set; } public string RefreshTokenAudience { get; set; } public int RefreshTokenExpiresMinutes { get; set; } }
至於這些類型的字段,能夠自行在appsettings.json中去賦值。
"Authentication": { "JwtBearer": { "Issuer": "Bingle", "Audience": "Bingle", "IssuerSigningKey": "Bingle_C421AAEE0D114EAAACVD", "AccessTokenExpiresMinutes": "14400", "RefreshTokenAudience": "RefreshTokenAudience", "RefreshTokenExpiresMinutes": "43200" //60*24*30 } },
ITokenHelper與TokenHepler
public interface ITokenHelper { ComplexToken CreateToken(User user); ComplexToken CreateToken(Claim[] claims); (Result result, string userCode) ConfirmRefreshToken(string refreshToken); } public class TokenHelper : ITokenHelper { private readonly IOptions<JWTConfig> _options; public TokenHelper(IOptions<JWTConfig> options) { _options = options; } public ComplexToken CreateToken(User user) { Claim[] claims = new Claim[] { new Claim(JwtClaimTypes.Id, user.UserCode), new Claim(JwtClaimTypes.Name, user.UserName), new Claim(JwtClaimTypes.Role, user.UserRole.GetExtendDescription()), new Claim(ClaimTypes.Role, user.UserRole.GetExtendDescription()), new Claim(JwtClaimTypes.Actor, user.PartyId) }; return CreateToken(claims); } public ComplexToken CreateToken(Claim[] claims) { return new ComplexToken { AccessToken = CreateToken(claims, TokenType.AccessToken), RefreshToken = CreateToken(new Claim[]{claims.First(x=>x.Type == JwtClaimTypes.Id)}, TokenType.RefreshToken) }; } /// <summary> /// 用於建立AccessToken和RefreshToken。 /// 這裏AccessToken和RefreshToken只是過時時間不一樣,【實際項目】中兩者的claims內容可能會不一樣。 /// 由於RefreshToken只是用於刷新AccessToken,其內容能夠簡單一些。 /// 而AccessToken可能會附加一些其餘的Claim。 /// </summary> /// <param name="claims"></param> /// <param name="tokenType"></param> /// <returns></returns> private Token CreateToken(Claim[] claims, TokenType tokenType) { var now = DateTime.Now; var expires = now.Add(TimeSpan.FromMinutes(tokenType.Equals(TokenType.AccessToken) ? _options.Value.AccessTokenExpiresMinutes : _options.Value.RefreshTokenExpiresMinutes)); var token = new JwtSecurityToken( issuer: _options.Value.Issuer, audience: tokenType.Equals(TokenType.AccessToken) ? _options.Value.Audience : _options.Value.RefreshTokenAudience, claims: claims, notBefore: now, expires: expires, signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256)); return new Token { TokenContent = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires }; } public (Result result, string userCode) ConfirmRefreshToken(string refreshToken) { var tokenHandler = new JwtSecurityTokenHandler(); if (!tokenHandler.CanReadToken(refreshToken)) return (Result.FromCode(ResultCode.InvalidToken, "RefreshToken不正確"), null); var jwtSecurityToken = tokenHandler.ReadJwtToken(refreshToken); if (jwtSecurityToken.Issuer != _options.Value.Issuer || !jwtSecurityToken.Audiences.Contains(_options.Value.RefreshTokenAudience)) return (Result.FromCode(ResultCode.InvalidToken, "RefreshToken不正確"), null); if (jwtSecurityToken.ValidTo < DateTime.Now) return (Result.FromCode(ResultCode.InvalidToken, "RefreshToken已通過期了"), null); return (Result.Ok(), jwtSecurityToken.Claims.First(x => x.Type == JwtClaimTypes.Id).Value); } }
還要在Configure方法中使用中間件:
app.UseAuthentication();
首先,定義一個API的基類,後面的API繼承此基類就能夠了
[Route("[controller]/[action]")] [ApiController] [Authorize( AuthenticationSchemes = AdminUserAccountConst.AdminUserCookie, Roles = AdminUserAccountConst.AdminUserRole)] public class BasicAdminController : ControllerBase { }
如今新建一個用戶登陸和退出的APIController繼承與上面那個基類就能夠了。這裏簡化 了代碼
[HttpPost] [AllowAnonymous] [ProducesResponseType(typeof(Result<TokenResultDto>), 200)] public JsonResult Login([FromBody]LoginDto model) { var user = new User();//這裏須要去數據庫中進行校驗 if (user == null) return Json(new {IsSuccess=false,Msg="參數錯誤"}); var result = _tokenHelper.CreateToken(new User { UserCode = user.UserCode, UserName = user.UserName, Telphone = user.Telphone, PartyId = user.ShopCode, UserRole = UserRoleEnum.user, }); user.RefreshToken = result.RefreshToken.TokenContent; return Json(new TokenResultDto { AccessToken = result.AccessToken.TokenContent, Expires = result.AccessToken.Expires, RefreshToken = result.RefreshToken.TokenContent, }); }
這裏使用AllowAnonymous標籤,是由於登陸並不須要進行身份驗證。當須要受權才能訪問的接口,不須要加上這個標籤。