設計安全的API-JWT與OAuthor2

来源:https://www.cnblogs.com/taotaozhuanyong/archive/2019/11/20/11900588.html
-Advertisement-
Play Games

最近新開發一個需要給App使用的API項目。開發API肯定會想到JASON Web Token(JWT)和OAuthor2(之前一篇隨筆記錄過OAuthor2)。 JWT和OAuthor2的比較 要像比較JWT和OAuthor2,首先要明白一點就是,這是兩個完全不同的東西,沒有可比性。 JWT是一種 ...


最近新開發一個需要給App使用的API項目。開發API肯定會想到JASON Web Token(JWT)和OAuthor2(之前一篇隨筆記錄過OAuthor2)。

JWT和OAuthor2的比較

  要像比較JWT和OAuthor2,首先要明白一點就是,這是兩個完全不同的東西,沒有可比性。

  JWT是一種認證協議

    官網:http://jwt.io

    JWT提供了一種用於發佈介入靈擺(Access Token),並對發佈的簽名介入令牌進行驗證的方法。令牌(Token)本身包含了一系列聲明,應用程式可以根據這些聲明限制用戶對資源的訪問。

    在新開發的API中,我選擇的是使用JWT,稍後會簡單介紹其在.net core中的使用。

  OAuthor2是一種授權框架

    OAuthor2是一種授權框架,提供了一套詳細的授權機制(指導)。用戶或應用可以通過公開的或私有的設置,授權第三方應用訪問特定資源。

  既然JWT和OAuthor2沒有可比性,為什麼還要把這兩個放在一起說呢?實際中,會有很多人拿JWT和OAuthor2作比較,或者分不清楚。很多情況下,在討論OAuthor2的實現時,會把JSON Web Token作為一種認證機制使用。這也是為什麼他們會經常一起出現。

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是什麼?

  官網:http://oauth.net/2/

  相反,OAuthor2不是一個標準協議,而是一個安全的授權框架,它詳細描述了系統中不同角色、用戶、服務前端應用(比如API),以及客戶端(比如網站或移動APP)之間怎麼實現相互認證。

OAuthor2的基本概念,可以去閱讀之前的一片隨筆。點擊此處

使用HTTPS保護用戶密碼

  在進一步討論OAuthor2和JWT的實現之前,有必要說一下,兩種方案都需要SSL安全保護,也就是對要傳輸的數據進行加密編碼。安全地傳輸用戶提供的私密信息,在任何一個安全的系統里都是必要的。否則任何人都可以通過侵入私人wifi,在用戶登錄的時候竊取用戶的用戶名和密碼等信息。

JWT和OAuthor2應該如何選擇

  在做選擇之前,參考一下下邊提到的幾點。

  1、時間投入

    OAuthor2是一個安全框架,描述了在各種不同場景下,多個應用之間的授權問題。有海量的資料需要學習,要完全理解需要花費大量時間。甚至對於一些有經驗的開發工程師來說,也會需要大概一個月的時間來深入理解OAuth2。 這是個很大的時間投入。相反,JWT是一個相對輕量級的概念。可能花一天時間深入學習一下標準規範,就可以很容易地開始具體實施。

  2、出現錯誤的風險

    OAuth2不像JWT一樣是一個嚴格的標準協議,因此在實施過程中更容易出錯。儘管有很多現有的庫,但是每個庫的成熟度也不盡相同,同樣很容易引入各種錯誤。在常用的庫中也很容易發現一些安全漏洞。當然,如果有相當成熟、強大的開發團隊來持續OAuth2實施和維護,可以一定成都上避免這些風險。

  3、社交登錄的好處

    在很多情況下,使用用戶在大型社交網站的已有賬戶來認證會方便。如果期望你的用戶可以直接使用Facebook或者Gmail之類的賬戶,使用現有的庫會方便得多。

JWT的使用場景

無狀態的分散式API

  JWT的主要優勢在於使用無狀態、可擴展的方式處理應用中的用戶會話。服務端可以通過內嵌的聲明信息,很容易地獲取用戶的會話信息,而不需要去訪問用戶或會話的資料庫。在一個分散式的面向服務的框架中,這一點非常有用。但是,如果系統中需要使用黑名單實現長期有效的token刷新機制,這種無狀態的優勢就不明顯了。

優勢:

  1、快速開發

  2、不需要cookie

  3、JSON在移動端的廣泛應用

  4、不依賴與社交登錄

  5、相對簡單的概念理解

限制

  1、token有長度限制

  2、token不能撤銷

  3、需要token有失效時間限制(exp)

OAuthor2使用場景

外包認證伺服器

  上邊已經討論過,如果不介意API的使用依賴於外部的第三方認證提供者,你可以簡單地把認證工作留給認證服務商去做。也就是常見的,去認證服務商(比如facebook)那裡註冊你的應用,然後設置需要訪問的用戶信息,比如電子郵箱、姓名等。當用戶訪問站點的註冊頁面時,會看到連接到第三方提供商的入口。用戶點擊以後被重定向到對應的認證服務商網站,獲得用戶的授權後就可以訪問到需要的信息,然後重定向回來。

優勢:

  1、快速開發

  2、實施代碼量小

  3、維護工作減少

大型企業解決方案

  如果設計的API要被不同的App使用,並且每個App使用的方式也不一樣,使用OAuth2是個不錯的選擇。考慮到工作量,可能需要單獨的團隊,針對各種應用開發完善、靈活的安全策略。當然需要的工作量也比較大!

優勢

  1、靈活的實現方式

  2、可以和JWT同時使用

  3、可以針對不同的應用擴展

簡單介紹下在.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";
}
View Code

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; }
}
View Code

至於這些類型的欄位,可以自行在appsettings.json中去賦值。

"Authentication": {
  "JwtBearer": {
    "Issuer": "Bingle",
    "Audience": "Bingle",
    "IssuerSigningKey": "Bingle_C421AAEE0D114EAAACVD",
    "AccessTokenExpiresMinutes": "14400",

    "RefreshTokenAudience": "RefreshTokenAudience",
    "RefreshTokenExpiresMinutes": "43200" //60*24*30
  }
},
View Code

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);
     }

 }
View Code

還要在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標簽,是因為登錄並不需要進行身份驗證。當需要授權才能訪問的介面,不需要加上這個標簽。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Mybatis配置詳解 XML配置文件層次結構 下圖展示了mybatis config.xml的全部配置元素 properties元素 properties是一個配置屬性的元素,讓我們能在配置文件的上下文中使用它,MyBatis提供3種配置方式。 property子元素。 properties配置文 ...
  • 怎麼開通企業付款到零錢? 有的商戶號的產品中心是沒有這個功能的,不過,該功能的pid(product id)是5,只要隨便進去某一個產品,在地址欄把pid改為5。 即可進入該功能頁面,進行開通,不過要滿足條件。 用戶提現代碼: 1 //用戶微信提現 2 private function withdr ...
  • MATLAB實例:新建文件夾,保存.mat文件並保存數據到.txt文件中 作者:凱魯嘎吉 - 博客園 http://www.cnblogs.com/kailugaji/ 用MATLAB實現:指定路徑下新建文件夾,將數據保存為.mat文件存放到新建的文件夾里,並將數據寫入.txt文件中,存放到新建的文 ...
  • 任何服務對資料庫的日常操作,都離不開增刪改查。如果一次查詢的紀錄很多,那我們必須採用分頁的方式。對於一個Springboot項目,訪問和查詢MySQL資料庫,持久化框架可以使用MyBatis,分頁工具可以使用github的 PageHelper。我們來看一下PageHelper的使用方法: 1 // ...
  • 作為程式員,在日常開發中,記憶猶新的莫過於寫代碼,升級程式。升級程式包含兩部分:一是,對服務程式更新;二是,對資料庫結構更新。本篇博文主要介紹資料庫結構更新,在對資料庫升級時,不知道園友們是否有如下經歷: 1)腳本文件中建表語句未作判斷是否存在,而導致執行失敗。 2)腳本文件中修改欄位在建表語句之前 ...
  • asp.net core 自定義 Policy 替換 AllowAnonymous 的行為 Intro 最近對我們的服務進行了改造,原本內部服務在內部可以匿名調用,現在增加了限制,通過 identity server 來管理 api 和 client,網關和需要訪問api的客戶端或api服務相互調用 ...
  • 交錯數組:數組元素本身也是一個數組 1 public static void Main(string[] args) 2 { 3 4 int[][] arr = new int[5][]; 5 arr[0] = new int[8]; 6 arr[1] = new int[8]; 7 arr[2] ...
  • 1. 概述反射 通過反射可以提供類型信息,從而使得我們開發人員在運行時能夠利用這些信息構造和使用對象。 反射機制允許程式在執行過程中動態地添加各種功能。 2. Type類的介紹 是BCL(基底類別庫)聲明的一個抽象類,所有它不能被實例化 對於程式中用到的每一個類型,CLR(公共語言運行時)都會創建一 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...