asp.net core 集成JWT(一)

【什麼是JWT】

  JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案。html

  JWT的官網地址:https://jwt.io/前端

  通俗地來說,JWT是能表明用戶身份的令牌,可使用JWT令牌在api接口中校驗用戶的身份以確認用戶是否有訪問api的權限。git

  JWT中包含了身份認證必須的參數以及用戶自定義的參數,JWT可使用祕密(使用HMAC算法)或使用RSAECDSA的公鑰/私鑰對進行簽名github

【何時應該使用JSON Web令牌?】

  1. 受權:這是使用JWT的最多見方案。一旦用戶登陸,每一個後續請求將包括JWT,容許用戶訪問該令牌容許的路由,服務和資源。Single Sign On是一種如今普遍使用JWT的功能,由於它的開銷很小,而且可以在不一樣的域中輕鬆使用。web

  2. 信息交換:JSON Web令牌是在各方之間安全傳輸信息的好方法。由於JWT能夠簽名 - 例如,使用公鑰/私鑰對 - 您能夠肯定發件人是他們所說的人。此外,因爲使用標頭和有效負載計算簽名,您還能夠驗證內容是否未被篡改。算法

【JWT有什麼優點?】

  咱們先看咱們傳統的身份校驗方式

  1. 用戶向服務器發送用戶名和密碼。
  2. 服務器驗證經過後,在當前對話(session)裏面保存相關數據,好比用戶角色、登陸時間等等。
  3. 服務器向用戶返回一個 session_id,寫入用戶的 Cookie。
  4. 用戶隨後的每一次請求,都會經過 Cookie,將 session_id 傳回服務器。
  5. 服務器收到 session_id,找到前期保存的數據,由此得知用戶的身份。

  這種模式的問題在於,擴展性(scaling)很差。單機固然沒有問題,若是是服務器集羣,或者是跨域的服務導向架構,就要求 session 數據共享,每臺服務器都可以讀取 session。若是session存儲的節點掛了,那麼整個服務都會癱瘓,體驗至關很差,風險也很高。數據庫

  相比之下,JWT的實現方式是將用戶信息存儲在客戶端,服務端不進行保存。每次請求都把令牌帶上以校驗用戶登陸狀態,這樣服務就變成了無狀態的,服務器集羣也很好擴展。api

【JWT令牌結構】

  在緊湊的形式中,JSON Web Tokens由dot(.分隔的三個部分組成,它們是:跨域

  • Header 頭
  • Payload 有效載荷
  • Signature 簽名

  所以,JWT一般以下所示:安全

  xxxxx.yyyyy.zzzzz

  1.Header 頭

  標頭一般由兩部分組成:令牌的類型,即JWT,以及正在使用的簽名算法,例如HMAC SHA256或RSA。

  例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

  而後,這個JSON被編碼Base64Url,造成JWT的第一部分。

  2.Payload 有效載荷

  Payload 部分也是一個 JSON 對象,用來存放實際須要傳遞的數據。JWT 規定了7個官方字段,供選用。

  • iss (issuer):簽發人

  • exp (expiration time):過時時間

  • sub (subject):主題

  • aud (audience):受衆

  • nbf (Not Before):生效時間

  • iat (Issued At):簽發時間

  • jti (JWT ID):編號

  除了官方字段,你還能夠在這個部分定義私有字段,下面就是一個例子。例如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

  注意,JWT 默認是不加密的,任何人均可以讀到,因此不要把祕密信息放在這個部分。這個 JSON 對象也要使用 Base64URL 算法轉成字符串。

  3.Signature 簽名

  Signature 部分是對前兩部分的簽名,防止數據篡改。

  首先,須要指定一個密鑰(secret)。這個密鑰只有服務器才知道,不能泄露給用戶。而後,使用 Header 裏面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

  簽名用於驗證消息在此過程當中未被更改,而且,在使用私鑰簽名的令牌的狀況下,它還能夠驗證JWT的發件人是不是它所聲稱的人。  

  把他們三個所有放在一塊兒

  輸出是三個由點分隔的Base64-URL字符串,能夠在HTML和HTTP環境中輕鬆傳遞,而與基於XML的標準(如SAML)相比更加緊湊。

  下面顯示了一個JWT,它具備先前的頭和​​有效負載編碼,並使用機密簽名。 

  

  若是您想使用JWT並將這些概念付諸實踐,您可使用jwt.io Debugger來解碼,驗證和生成JWT。

   

【JSON Web令牌如何工做?】

  在身份驗證中,當用戶使用其憑據成功登陸時,將返回JSON Web令牌。因爲令牌是憑證,所以必須很是當心以防止出現安全問題。通常狀況下,您不該該將令牌保留的時間超過要求。

  每當用戶想要訪問受保護的路由或資源時,用戶代理應該使用承載模式發送JWT,一般在Authorization標頭中標題的內容應以下所示:

  Authorization: Bearer <token>

  在某些狀況下,這能夠是無狀態受權機制。服務器的受保護路由將檢查Authorization標頭中的有效JWT ,若是存在,則容許用戶訪問受保護資源。若是JWT包含必要的數據,則能夠減小查詢數據庫以進行某些操做的須要,儘管可能並不是老是如此。

  若是在標Authorization頭中發送令牌,則跨域資源共享(CORS)將不會成爲問題,由於它不使用cookie。

  下圖顯示瞭如何獲取JWT並用於訪問API或資源:

  

  1. 應用程序向受權服務器請求受權
  2. 校驗用戶身份,校驗成功,返回token
  3. 應用程序使用訪問令牌訪問受保護的資源

【ASP.Net Core 集成JWT】

  前面咱們介紹了JWT的原理,下面咱們在asp.net core實際項目中集成JWT。

  首先咱們新建一個Demo asp.net core 空web項目

  

  添加數據訪問模擬api,ValuesController

  其中api/value1是能夠直接訪問的,api/value2添加了權限校驗特性標籤 [Authorize]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Demo.Jwt.Controllers
{
    [ApiController]
    public class ValuesController : ControllerBase
    {
        [HttpGet]
        [Route("api/value1")]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value1" };
        }

        [HttpGet]
        [Route("api/value2")]
        [Authorize]
        public ActionResult<IEnumerable<string>> Get2()
        {
            return new string[] { "value2", "value2" };
        }
    }
}

  添加模擬登錄,生成Token的api,AuthController

  這裏模擬一下登錄校驗,只驗證了用戶密碼不爲空即經過校驗,真實環境完善校驗用戶和密碼的邏輯。

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;

namespace Demo.Jwt.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        [AllowAnonymous]
        [HttpGet]
        public IActionResult Get(string userName, string pwd)
        {
            if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(pwd))
            {
                var claims = new[]
                {
                    new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
                    new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"),
                    new Claim(ClaimTypes.Name, userName)
                };
                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey));
                var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                var token = new JwtSecurityToken(
                    issuer: Const.Domain,
                    audience: Const.Domain,
                    claims: claims,
                    expires: DateTime.Now.AddMinutes(30),
                    signingCredentials: creds);

                return Ok(new
                {
                    token = new JwtSecurityTokenHandler().WriteToken(token)
                });
            }
            else
            {
                return BadRequest(new { message = "username or password is incorrect." });
            }
        }
    }
}

  Startup添加JWT驗證的相關配置

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Text;


namespace Demo.Jwt
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            //添加jwt驗證:
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options => {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,//是否驗證Issuer
                        ValidateAudience = true,//是否驗證Audience
                        ValidateLifetime = true,//是否驗證失效時間
                        ClockSkew = TimeSpan.FromSeconds(30),
                        ValidateIssuerSigningKey = true,//是否驗證SecurityKey
                        ValidAudience = Const.Domain,//Audience
                        ValidIssuer = Const.Domain,//Issuer,這兩項和前面簽發jwt的設置一致
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey
                    };
                });

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            ///添加jwt驗證
            app.UseAuthentication();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

  最後把代碼裏面用到的一些相關常量也粘貼過來,Const.cs

namespace Demo.Jwt
{
    public class Const
    {
        /// <summary>
        /// 這裏爲了演示,寫死一個密鑰。實際生產環境能夠從配置文件讀取,這個是用網上工具隨便生成的一個密鑰
        /// </summary>
        public const string SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o1+SitIeJSWtLJU8/Wz2m7gStexajkeD+Lka6DSTy8gt9UwfgVQo6uKjVLG5Ex7PiGOODVqAEghBuS7JzIYU5RvI543nNDAPfnJsas96mSA7L/mD7RTE2drj6hf3oZjJpMPZUQI/B1Qjb5H3K3PNwIDAQAB";
        public const string Domain = "http://localhost:5000";
    }
}

  到這裏,已是咱們項目的全部代碼了。

  若是須要完整的項目代碼,Github地址:https://github.com/sevenTiny/Demo.Jwt

【JWT測試】

   咱們找一個趁手的工具,好比fiddler,而後把咱們的web站點運行起來

  首先調用無權限的接口:http://localhost:5000/api/value1

  

  

  正確地返回了數據,那麼接下來咱們測試JWT的流程

  1. 無權限

  首先咱們什麼都不加調用接口:http://localhost:5000/api/value2

  

  

  返回了狀態碼401,也就是未經受權:訪問因爲憑據無效被拒絕。 說明JWT校驗生效了,咱們的接口收到了保護。

  2.獲取Token

  調用模擬登錄受權接口:http://localhost:5000/api/Auth?userName=zhangsan&pwd=123

  這裏的用戶密碼是隨便寫的,由於咱們模擬登錄只是校驗了下非空,所以寫什麼都能經過

  

  成功獲得了響應

  

  

  而後咱們獲得了一個xxx.yyy.zzz 格式的 token 值。咱們把token複製出來

  3.在剛纔401的接口請求HEADER中添加JWT的參數,把咱們的token加上去

  再次調用咱們的模擬數據接口,可是此次咱們加了一個HEADER:http://localhost:5000/api/value2

  

  把內容粘出來

User-Agent: Fiddler
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzQ1MDIxIiwiZXhwIjoxNTYwMzQ2ODIxLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.x7Slk4ho1hZc8sR8_McVTB6VEYLz_v-5eaHvXtIDS-o

  這裏須要注意 Bearer 後面是有一個空格的,而後就是咱們上一步獲取到的token

  

  

  嗯,沒有401了,成功返回了數據

  4.JWT的Token過時

  咱們且倒一杯開水,坐等30分鐘(咱們代碼中設置的過時時間),而後再次調用數據接口:http://localhost:5000/api/value2

  

  

  又變成了401,咱們看下詳細的返回數據

  

  這裏有標註,錯誤描述 token過時,說明咱們設置的token過時時間生效了

  5.JWT添加自定義的參數(好比帶上用戶信息)

  假如咱們想在認證經過的時候,直接從jwt的token中獲取到登錄的用戶名,該怎麼操做呢?

  首先在咱們的獲取token 的api接口裏面添加一個Claim節點,key能夠隨便給,也可使用已經提供好的一些預置Key,value是咱們登錄的userName(僅做爲演示)

  

  而後在咱們的模擬數據接口獲取自定義參數

  

  這裏使用HttpContext的受權擴展方法,拿到認證的信息,咱們來看下結果

  

  

  請求成功返回,而且也拿到了咱們一開始寫入的userName

【評論區的一些問題】

  1.token過時了怎麼辦?

  token過時了說明登錄信息已通過期,須要從新登錄,跳轉到登陸頁從新登錄獲取新的token。(固然自動刷新token除外)

  2.如何交換新的token

  若是要保證token長期有效,能夠前端在過時前調用登錄接口刷新token。或者使用SignalR輪詢,按期刷新token。

  3.如何強制token失效?

  咱們有個ValidAudience(接收人),能夠利用這個標準參數,登錄時候生成一個GUID,在數據庫/Redis/xxx存一份,而後驗證接口的時候再把這個值拿出來去一塊兒校驗。若是值變了校驗就失敗了,固然,從新登錄就會刷新這個值,因此只要從新登錄,舊的token也就失效了。

  4.如何應用到集羣模式

  當前Demo裏面,咱們驗證jwt的全部參數都是Const常量寫死的,可是在真實生產環境都是能夠走統一的配置中心,因此集羣場景下,一個token能夠在多個服務上被驗證經過,由於校驗token正確的密鑰和相關參數都是從配置中心獲取的。

【結束】

  到這裏,咱們JWT的簡介以及asp.net core 集成JWT已經完美完成,固然了這只是一個demo,在實際的應用中須要補充和完善的地方還有不少。

  這一篇文章中評論區的一些疑問我放在了下一篇文章逐一解決,有興趣的朋友請移步下文:asp.net core 集成JWT(二)token的強制失效,基於策略模式細化api權限

  若是想要完整項目源碼的,能夠參考地址:https://github.com/sevenTiny/Demo.Jwt

  若是有幸能幫助到你,高擡貴手點個star吧~

相關文章
相關標籤/搜索