回顧:認證方案之初步認識JWTjavascript
在現代Web應用程序中,即分爲前端與後端兩大部分。當前先後端的趨勢日益劇增,前端設備(手機、平板、電腦、及其餘設備)層出不窮。所以,爲了方便知足前端設備與後端進行通信,就必須有一種統一的機制。因此致使API架構的流行。而RESTful API這個API設計思想理論也就成爲目前互聯網應用程序比較歡迎的一套方式。html
這種API架構思想的引入,所以,咱們就須要考慮用一種標準的,通用的,無狀態的,與語言無關的身份認證方式來實現API接口的認證。前端
HTTP提供了一套標準的身份驗證框架:服務端能夠用來針對客戶端的請求發送質詢(challenge),客戶端根據質詢提供應答身份驗證憑證。java
質詢與應答的工做流程以下:服務端向客戶端返回401(Unauthorized,未受權)狀態碼,並在WWW-Authenticate頭中添加如何進行驗證的信息,其中至少包含有一種質詢方式。而後客戶端能夠在請求中添加Authorization頭進行驗證,其Value爲身份驗證的憑證信息。git
在本文中,將要介紹的是以Jwt Bearer方式進行認證。
github
本文要介紹的Bearer驗證也屬於HTTP協議標準驗證,它隨着OAuth協議而開始流行,詳細定義見: RFC 6570。web
+--------+ +---------------+ | |--(A)- Authorization Request ->| Resource | | | | Owner | | |<-(B)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(C)-- Authorization Grant -->| Authorization | | Client | | Server | | |<-(D)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(E)----- Access Token ------>| Resource | | | | Server | | |<-(F)--- Protected Resource ---| | +--------+ +---------------+
A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession).json
所以Bearer認證的核心是Token,Bearer驗證中的憑證稱爲BEARER_TOKEN
,或者是access_token
,它的頒發和驗證徹底由咱們本身的應用程序來控制,而不依賴於系統和Web服務器,Bearer驗證的標準請求方式以下:c#
Authorization: Bearer [BEARER_TOKEN]
那麼使用Bearer驗證有什麼好處呢?後端
302
到登陸頁面,這在非瀏覽器狀況下很難處理,而Bearer驗證則返回的是標準的401 challenge
。上面介紹的Bearer認證,其核心即是BEARER_TOKEN,那麼,如何確保Token的安全是重中之重。一種是經過HTTPS的方式,另外一種是經過對Token進行加密編碼簽名,而最流行的Token編碼簽名方式即是:JSON WEB TOKEN。
Json web token (Jwt), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準(RFC 7519)。該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登陸(SSO)場景。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也能夠增長一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
JWT是由.
分割的以下三部分組成:
Header.Payload.Signature
還記得以前說個的一篇認證方案之初步認識JWT嗎?沒有的,能夠看看,對JWT的特色和基本原理介紹,能夠進一步的瞭解。
學習了以前的文章後,咱們能夠發現使用JWT的好處在於通用性、緊湊性和可拓展性。
在這裏,咱們用微軟給咱們提供的JwtBearer認證方式,實現認證服務註冊 。
引入nuget包:Microsoft.AspNetCore.Authentication.JwtBearer
註冊服務,將服務添加到容器中,
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); var Issurer = "JWTBearer.Auth"; //發行人 var Audience = "api.auth"; //受衆人 var secretCredentials = "q2xiARx$4x3TKqBJ"; //密鑰 //配置認證服務 services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(o=>{ o.TokenValidationParameters = new TokenValidationParameters { //是否驗證發行人 ValidateIssuer = true, ValidIssuer = Issurer,//發行人 //是否驗證受衆人 ValidateAudience = true, ValidAudience = Audience,//受衆人 //是否驗證密鑰 ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretCredentials)), ValidateLifetime = true, //驗證生命週期 RequireExpirationTime = true, //過時時間 }; }); }
注意說明:
一. TokenValidationParameters的參數默認值: 1. ValidateAudience = true, ----- 若是設置爲false,則不驗證Audience受衆人 2. ValidateIssuer = true , ----- 若是設置爲false,則不驗證Issuer發佈人,但建議不建議這樣設置 3. ValidateIssuerSigningKey = false, 4. ValidateLifetime = true, ----- 是否驗證Token有效期,使用當前時間與Token的Claims中的NotBefore和Expires對比 5. RequireExpirationTime = true, ----- 是否要求Token的Claims中必須包含Expires 6. ClockSkew = TimeSpan.FromSeconds(300), ----- 容許服務器時間偏移量300秒,即咱們配置的過時時間加上這個容許偏移的時間值,纔是真正過時的時間(過時時間 +偏移值)你也能夠設置爲0,ClockSkew = TimeSpan.Zero
調用方法,配置Http請求管道:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); //1.先開啓認證 app.UseAuthentication(); //2.再開啓受權 app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
在JwtBearerOptions
的配置中,一般IssuerSigningKey(簽名祕鑰)
, ValidIssuer(Token頒發機構)
, ValidAudience(頒發給誰)
三個參數是必須的,後二者用於與TokenClaims中的Issuer
和Audience
進行對比,不一致則驗證失敗。
建立一個須要受權保護的資源控制器,這裏咱們用創建API生成項目自帶的控制器,WeatherForecastController.cs, 在控制器上使用Authorize
便可
[ApiController] [Route("[controller]")] [Authorize] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController(ILogger<WeatherForecastController> logger) { _logger = logger; } [HttpGet] public IEnumerable<WeatherForecast> Get() { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); } }
由於微軟爲咱們內置了JwtBearer驗證,可是沒有提供Token的發放,因此這裏咱們要實現生成Token的方法
引入Nugets包:System.IdentityModel.Tokens.Jwt
這裏咱們根據IdentityModel.Tokens.Jwt文檔給咱們提供的幫助類,提供了方法WriteToken建立Token,根據參數SecurityToken,能夠實例化,JwtSecurityToken,指定可選參數的類。
/// <summary> /// Initializes a new instance of the <see cref="JwtSecurityToken"/> class specifying optional parameters. /// </summary> /// <param name="issuer">If this value is not null, a { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' if present.</param> /// <param name="audience">If this value is not null, a { aud, 'audience' } claim will be added, appending to any 'aud' claims in 'claims' if present.</param> /// <param name="claims">If this value is not null then for each <see cref="Claim"/> a { 'Claim.Type', 'Claim.Value' } is added. If duplicate claims are found then a { 'Claim.Type', List<object> } will be created to contain the duplicate values.</param> /// <param name="expires">If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' if present.</param> /// <param name="notBefore">If notbefore.HasValue a { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' if present.</param> /// <param name="signingCredentials">The <see cref="SigningCredentials"/> that will be used to sign the <see cref="JwtSecurityToken"/>. See <see cref="JwtHeader(SigningCredentials)"/> for details pertaining to the Header Parameter(s).</param> /// <exception cref="ArgumentException">If 'expires' <= 'notbefore'.</exception> public JwtSecurityToken(string issuer = null, string audience = null, IEnumerable<Claim> claims = null, DateTime? notBefore = null, DateTime? expires = null, SigningCredentials signingCredentials = null) { if (expires.HasValue && notBefore.HasValue) { if (notBefore >= expires) throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12401, expires.Value, notBefore.Value))); } Payload = new JwtPayload(issuer, audience, claims, notBefore, expires); Header = new JwtHeader(signingCredentials); RawSignature = string.Empty; }
這樣,咱們能夠根據參數指定內容:
1. string iss = "JWTBearer.Auth"; // 定義發行人 2. string aud = "api.auth"; //定義受衆人audience 3. IEnumerable<Claim> claims = new Claim[] { new Claim(JwtClaimTypes.Id,"1"), new Claim(JwtClaimTypes.Name,"i3yuan"), };//定義許多種的聲明Claim,信息存儲部分,Claims的實體通常包含用戶和一些元數據 4. var nbf = DateTime.UtcNow; //notBefore 生效時間 5. var Exp = DateTime.UtcNow.AddSeconds(1000); //expires 過時時間 6. string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的長度必須 大於等於 16個字符 var secret = Encoding.UTF8.GetBytes(sign); var key = new SymmetricSecurityKey(secret); var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
好了,經過以上填充參數內容,進行傳參賦值獲得,完整代碼以下:
新增AuthController.cs控制器:
[HttpGet] public IActionResult GetToken() { try { //定義發行人issuer string iss = "JWTBearer.Auth"; //定義受衆人audience string aud = "api.auth"; //定義許多種的聲明Claim,信息存儲部分,Claims的實體通常包含用戶和一些元數據 IEnumerable<Claim> claims = new Claim[] { new Claim(JwtClaimTypes.Id,"1"), new Claim(JwtClaimTypes.Name,"i3yuan"), }; //notBefore 生效時間 // long nbf =new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds(); var nbf = DateTime.UtcNow; //expires //過時時間 // long Exp = new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds(); var Exp = DateTime.UtcNow.AddSeconds(1000); //signingCredentials 簽名憑證 string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的長度必須 大於等於 16個字符 var secret = Encoding.UTF8.GetBytes(sign); var key = new SymmetricSecurityKey(secret); var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var jwt = new JwtSecurityToken(issuer: iss, audience: aud, claims:claims,notBefore:nbf,expires:Exp, signingCredentials: signcreds); var JwtHander = new JwtSecurityTokenHandler(); var token = JwtHander.WriteToken(jwt); return Ok(new { access_token = token, token_type = "Bearer", }); } catch (Exception ex) { throw; } }
注意: 1.SecurityKey 的長度必須 大於等於 16個字符,不然生成會報錯。(可經過在線隨機生成密鑰)
訪問獲取Token方法,獲取獲得access_token:
再訪問,受權資源接口,能夠發現,再沒有添加請求頭token值的狀況下,返回了401沒有權限。
此次,在請求頭經過Authorization加上以前獲取的token值後,再次進行訪問,發現已經能夠獲取訪問資源控制器,並返回對應的數據。
在HTTP標準驗證方案中,咱們比較熟悉的是"Basic"和"Digest",前者將用戶名密碼使用BASE64編碼後做爲驗證憑證,後者是Basic的升級版,更加安全,由於Basic是明文傳輸密碼信息,而Digest是加密後傳輸。
Basic認證是一種較爲簡單的HTTP認證方式,客戶端經過明文(Base64編碼格式)傳輸用戶名和密碼到服務端進行認證,一般須要配合HTTPS來保證信息傳輸的安全。
客戶端請求須要帶Authorization請求頭,值爲「Basic xxx」,xxx爲「用戶名:密碼」進行Base64編碼後生成的值。 若客戶端是瀏覽器,則瀏覽器會提供一個輸入用戶名和密碼的對話框,用戶輸入用戶名和密碼後,瀏覽器會保存用戶名和密碼,用於構造Authorization值。當關閉瀏覽器後,用戶名和密碼將再也不保存。
憑證爲「YWxhzGRpbjpvcGVuc2VzYWl1」,是經過將「用戶名:密碼」格式的字符串通過的Base64編碼獲得的。而Base64不屬於加密範疇,能夠被逆向解碼,等同於明文,所以Basic傳輸認證信息是不安全的。
Basic基礎認證圖示:
缺陷彙總
1.用戶名和密碼明文(Base64)傳輸,須要配合HTTPS來保證信息傳輸的安全。
2.即便密碼被強加密,第三方仍可經過加密後的用戶名和密碼進行重放攻擊。
3.沒有提供任何針對代理和中間節點的防禦措施。
4.假冒服務器很容易騙過認證,誘導用戶輸入用戶名和密碼。
Digest認證是爲了修復基本認證協議的嚴重缺陷而設計的,秉承「毫不經過明文在網絡發送密碼」的原則,經過「密碼摘要」進行認證,大大提升了安全性。
Digest認證步驟以下:
第一步:客戶端訪問Http資源服務器。因爲須要Digest認證,服務器返回了兩個重要字段nonce(隨機數)和realm。
第二步:客戶端構造Authorization請求頭,值包含username、realm、nouce、uri和response的字段信息。其中,realm和nouce就是第一步返回的值。nouce只能被服務端使用一次。uri(digest-uri)即Request-URI的值,但考慮到經代理轉發後Request-URI的值可能被修改、所以實現會複製一份副本保存在uri內。response也可叫作Request-digest,存放通過MD5運算後的密碼字符串,造成響應碼。
第三步:服務器驗證包含Authorization值的請求,若驗證經過則可訪問資源。
Digest認證能夠防止密碼泄露和請求重放,但沒辦法防假冒。因此安全級別較低。
Digest和Basic認證同樣,每次都會發送Authorization請求頭,也就至關於從新構造此值。因此二者易用性都較差。
Digest認證圖示:
notBefore
和expires
來驗證),當access_token
過時後,能夠在用戶無感知的狀況下,使用refresh_token
從新獲取access_token
,但這就不屬於Bearer認證的範疇了,可是咱們能夠經過另外一種方式經過IdentityServer的方式來實現,在後續中會對IdentityServer進行詳細講解。