設計安全的API-JWT與OAuthor2

最近新開發一個須要給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是什麼?

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

  相反,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";
}
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標籤,是由於登陸並不須要進行身份驗證。當須要受權才能訪問的接口,不須要加上這個標籤。

相關文章
相關標籤/搜索