最近新開(kāi)發(fā)一個(gè)需要給App使用的API項(xiàng)目。開(kāi)發(fā)API肯定會(huì)想到JASON Web Token(JWT)和OAuthor2(之前一篇隨筆記錄過(guò)OAuthor2)。
JWT和OAuthor2的比較
要像比較JWT和OAuthor2,首先要明白一點(diǎn)就是,這是兩個(gè)完全不同的東西,沒(méi)有可比性。
JWT是一種認(rèn)證協(xié)議
官網(wǎng):http://jwt.io
JWT提供了一種用于發(fā)布介入靈擺(Access Token),并對(duì)發(fā)布的簽名介入令牌進(jìn)行驗(yàn)證的方法。令牌(Token)本身包含了一系列聲明,應(yīng)用程序可以根據(jù)這些聲明限制用戶對(duì)資源的訪問(wèn)。
在新開(kāi)發(fā)的API中,我選擇的是使用JWT,稍后會(huì)簡(jiǎn)單介紹其在.net core中的使用。
OAuthor2是一種授權(quán)框架
OAuthor2是一種授權(quán)框架,提供了一套詳細(xì)的授權(quán)機(jī)制(指導(dǎo))。用戶或應(yīng)用可以通過(guò)公開(kāi)的或私有的設(shè)置,授權(quán)第三方應(yīng)用訪問(wèn)特定資源。
既然JWT和OAuthor2沒(méi)有可比性,為什么還要把這兩個(gè)放在一起說(shuō)呢?實(shí)際中,會(huì)有很多人拿JWT和OAuthor2作比較,或者分不清楚。很多情況下,在討論OAuthor2的實(shí)現(xiàn)時(shí),會(huì)把JSON Web Token作為一種認(rèn)證機(jī)制使用。這也是為什么他們會(huì)經(jīng)常一起出現(xiàn)。
JSON Web Token(JWT)
JWT是一種安全標(biāo)準(zhǔn)?;舅悸肪褪怯脩籼峁┯脩裘兔艽a給認(rèn)證服務(wù)器,服務(wù)器驗(yàn)證用戶提交的信息的合法性,如果認(rèn)證成功,會(huì)產(chǎn)生并返回一個(gè)Token(令牌),用戶可以使用這個(gè)token訪問(wèn)服務(wù)器上受保護(hù)的資源。
一個(gè)token的例子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoibGl1dGFvIiwicm9sZSI6InNob3BVc2VycyIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InNob3BVc2VycyIsImFjdCI6IjEiLCJuYmYiOjE1NzQyNTAyMTgsImV4cCI6MTU3NTExNDIxOCwiaXNzIjoiWXVZdWUiLCJhdWQiOiJZdVl1ZSJ9.t39iwO-r_YgX5-7XyIV-by2duHfThqTQayI595VtqF
一個(gè)token包含三個(gè)部分:
header.claims.signature
為了安全的在url中使用,所有部分都base64 URL-safe進(jìn)行編碼處理。
Header頭部分
頭部分簡(jiǎn)單聲明了類型(JWT)以及產(chǎn)生簽名所使用的的算法。
{ "alg" : "AES256", "typ" : "JWT"}
Claims聲明
聲明部分是整個(gè)token的核心,表示要發(fā)送的用戶詳細(xì)信息。游學(xué)情況下,我們和有可能要在一個(gè)服務(wù)器上實(shí)現(xiàn)認(rèn)證,然后訪問(wèn)另一臺(tái)服務(wù)器上的資源,或者,通過(guò)單獨(dú)的接口來(lái)生成token,token被保存在應(yīng)用程序客戶端(比如瀏覽器)使用。
一個(gè)簡(jiǎn)單的聲明(claim)的例子:
{ "sub": "1234567890", "name": "John Doe", "admin": true}
Signature簽名
簽名的目的是為了保證上邊兩部分信息不被篡改。如果嘗試使用Bas64對(duì)解碼后的token進(jìn)行修改,簽名信息就會(huì)失效。一般使用一個(gè)私鑰(private key)通過(guò)特定算法對(duì)Header和Claims進(jìn)行混淆產(chǎn)生簽名信息,所以只有原始的token才能于簽名信息匹配。
這里有一個(gè)重要的實(shí)現(xiàn)細(xì)節(jié)。只有獲取了私鑰的應(yīng)用程序(比如服務(wù)器端應(yīng)用)才能完全認(rèn)證token包含聲明信息的合法性。所以,永遠(yuǎn)不要把私鑰信息放在客戶端(比如瀏覽器)。
OAuthor2是什么?
官網(wǎng):http://oauth.net/2/
相反,OAuthor2不是一個(gè)標(biāo)準(zhǔn)協(xié)議,而是一個(gè)安全的授權(quán)框架,它詳細(xì)描述了系統(tǒng)中不同角色、用戶、服務(wù)前端應(yīng)用(比如API),以及客戶端(比如網(wǎng)站或移動(dòng)APP)之間怎么實(shí)現(xiàn)相互認(rèn)證。
OAuthor2的基本概念,可以去閱讀之前的一片隨筆。點(diǎn)擊此處
使用HTTPS保護(hù)用戶密碼
在進(jìn)一步討論OAuthor2和JWT的實(shí)現(xiàn)之前,有必要說(shuō)一下,兩種方案都需要SSL安全保護(hù),也就是對(duì)要傳輸?shù)臄?shù)據(jù)進(jìn)行加密編碼。安全地傳輸用戶提供的私密信息,在任何一個(gè)安全的系統(tǒng)里都是必要的。否則任何人都可以通過(guò)侵入私人wifi,在用戶登錄的時(shí)候竊取用戶的用戶名和密碼等信息。
JWT和OAuthor2應(yīng)該如何選擇
在做選擇之前,參考一下下邊提到的幾點(diǎn)。
1、時(shí)間投入
OAuthor2是一個(gè)安全框架,描述了在各種不同場(chǎng)景下,多個(gè)應(yīng)用之間的授權(quán)問(wèn)題。有海量的資料需要學(xué)習(xí),要完全理解需要花費(fèi)大量時(shí)間。甚至對(duì)于一些有經(jīng)驗(yàn)的開(kāi)發(fā)工程師來(lái)說(shuō),也會(huì)需要大概一個(gè)月的時(shí)間來(lái)深入理解OAuth2。 這是個(gè)很大的時(shí)間投入。相反,JWT是一個(gè)相對(duì)輕量級(jí)的概念。可能花一天時(shí)間深入學(xué)習(xí)一下標(biāo)準(zhǔn)規(guī)范,就可以很容易地開(kāi)始具體實(shí)施。
2、出現(xiàn)錯(cuò)誤的風(fēng)險(xiǎn)
OAuth2不像JWT一樣是一個(gè)嚴(yán)格的標(biāo)準(zhǔn)協(xié)議,因此在實(shí)施過(guò)程中更容易出錯(cuò)。盡管有很多現(xiàn)有的庫(kù),但是每個(gè)庫(kù)的成熟度也不盡相同,同樣很容易引入各種錯(cuò)誤。在常用的庫(kù)中也很容易發(fā)現(xiàn)一些安全漏洞。當(dāng)然,如果有相當(dāng)成熟、強(qiáng)大的開(kāi)發(fā)團(tuán)隊(duì)來(lái)持續(xù)OAuth2實(shí)施和維護(hù),可以一定成都上避免這些風(fēng)險(xiǎn)。
3、社交登錄的好處
在很多情況下,使用用戶在大型社交網(wǎng)站的已有賬戶來(lái)認(rèn)證會(huì)方便。如果期望你的用戶可以直接使用Facebook或者Gmail之類的賬戶,使用現(xiàn)有的庫(kù)會(huì)方便得多。
JWT的使用場(chǎng)景
無(wú)狀態(tài)的分布式API
JWT的主要優(yōu)勢(shì)在于使用無(wú)狀態(tài)、可擴(kuò)展的方式處理應(yīng)用中的用戶會(huì)話。服務(wù)端可以通過(guò)內(nèi)嵌的聲明信息,很容易地獲取用戶的會(huì)話信息,而不需要去訪問(wèn)用戶或會(huì)話的數(shù)據(jù)庫(kù)。在一個(gè)分布式的面向服務(wù)的框架中,這一點(diǎn)非常有用。但是,如果系統(tǒng)中需要使用黑名單實(shí)現(xiàn)長(zhǎng)期有效的token刷新機(jī)制,這種無(wú)狀態(tài)的優(yōu)勢(shì)就不明顯了。
優(yōu)勢(shì):
1、快速開(kāi)發(fā)
2、不需要cookie
3、JSON在移動(dòng)端的廣泛應(yīng)用
4、不依賴與社交登錄
5、相對(duì)簡(jiǎn)單的概念理解
限制
1、token有長(zhǎng)度限制
2、token不能撤銷
3、需要token有失效時(shí)間限制(exp)
OAuthor2使用場(chǎng)景
外包認(rèn)證服務(wù)器
上邊已經(jīng)討論過(guò),如果不介意API的使用依賴于外部的第三方認(rèn)證提供者,你可以簡(jiǎn)單地把認(rèn)證工作留給認(rèn)證服務(wù)商去做。也就是常見(jiàn)的,去認(rèn)證服務(wù)商(比如facebook)那里注冊(cè)你的應(yīng)用,然后設(shè)置需要訪問(wèn)的用戶信息,比如電子郵箱、姓名等。當(dāng)用戶訪問(wèn)站點(diǎn)的注冊(cè)頁(yè)面時(shí),會(huì)看到連接到第三方提供商的入口。用戶點(diǎn)擊以后被重定向到對(duì)應(yīng)的認(rèn)證服務(wù)商網(wǎng)站,獲得用戶的授權(quán)后就可以訪問(wèn)到需要的信息,然后重定向回來(lái)。
優(yōu)勢(shì):
1、快速開(kāi)發(fā)
2、實(shí)施代碼量小
3、維護(hù)工作減少
大型企業(yè)解決方案
如果設(shè)計(jì)的API要被不同的App使用,并且每個(gè)App使用的方式也不一樣,使用OAuth2是個(gè)不錯(cuò)的選擇??紤]到工作量,可能需要單獨(dú)的團(tuán)隊(duì),針對(duì)各種應(yīng)用開(kāi)發(fā)完善、靈活的安全策略。當(dāng)然需要的工作量也比較大!
優(yōu)勢(shì)
1、靈活的實(shí)現(xiàn)方式
2、可以和JWT同時(shí)使用
3、可以針對(duì)不同的應(yīng)用擴(kuò)展
簡(jiǎn)單介紹下在.net core的項(xiàng)目中是如何使用JWT的。
首先,我們的服務(wù)是基于組件化的,當(dāng)然需要先把身份認(rèn)證的服務(wù)注冊(cè)進(jìn)來(lái)。在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> /// 用于創(chuàng)建AccessToken和RefreshToken。 /// 這里AccessToken和RefreshToken只是過(guò)期時(shí)間不同,【實(shí)際項(xiàng)目】中二者的claims內(nèi)容可能會(huì)不同。 /// 因?yàn)镽efreshToken只是用于刷新AccessToken,其內(nèi)容可以簡(jiǎn)單一些。 /// 而AccessToken可能會(huì)附加一些其他的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已經(jīng)過(guò)期了"), null); return (Result.Ok(), jwtSecurityToken.Claims.First(x => x.Type == JwtClaimTypes.Id).Value); } }
還要在Configure方法中使用中間件:
app.UseAuthentication();
首先,定義一個(gè)API的基類,后面的API繼承此基類就可以了
[Route("[controller]/[action]")][ApiController][Authorize( AuthenticationSchemes = AdminUserAccountConst.AdminUserCookie, Roles = AdminUserAccountConst.AdminUserRole)]public class BasicAdminController : ControllerBase{}
現(xiàn)在新建一個(gè)用戶登錄和退出的APIController繼承與上面那個(gè)基類就可以了。這里簡(jiǎn)化 了代碼
[HttpPost][AllowAnonymous][ProducesResponseType(typeof(Result<TokenResultDto>), 200)]public JsonResult Login([FromBody]LoginDto model){ var user = new User();//這里需要去數(shù)據(jù)庫(kù)中進(jìn)行校驗(yàn) if (user == null) return Json(new {IsSuccess=false,Msg="參數(shù)錯(cuò)誤"}); 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標(biāo)簽,是因?yàn)榈卿洸⒉恍枰M(jìn)行身份驗(yàn)證。當(dāng)需要授權(quán)才能訪問(wèn)的接口,不需要加上這個(gè)標(biāo)簽。
聯(lián)系客服