欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

设计安全的API-JWT与OAuthor2

程序员文章站 2023-11-18 13:02:16
最近新开发一个需要给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";
}

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

至于这些类型的字段,可以自行在appsettings.json中去赋值。

"authentication": {
  "jwtbearer": {
    "issuer": "bingle",
    "audience": "bingle",
    "issuersigningkey": "bingle_c421aaee0d114eaaacvd",
    "accesstokenexpiresminutes": "14400",

    "refreshtokenaudience": "refreshtokenaudience",
    "refreshtokenexpiresminutes": "43200" //60*24*30
  }
},

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

 }

还要在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标签,是因为登录并不需要进行身份验证。当需要授权才能访问的接口,不需要加上这个标签。