设计安全的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标签,是由于登陆并不须要进行身份验证。当须要受权才能访问的接口,不须要加上这个标签。

相关文章
相关标签/搜索