今天的文章介绍一种适用于restful+json的API认证方法,这个方法是基于jwt,并且加入了一些从oauth2.0借鉴的改良。
首先要明白,认证和鉴权是不同的。认证是判定用户的合法性,鉴权是判定用户的权限级别是否可执行后续操作。这里所讲的仅含认证。认证有几种方法:
这是http协议中所带带基本认证,是一种简单为上的认证方式。原理是在每个请求的header中添加用户名和密码的字符串(格式为“username:password”,用base64编码)。
这种方式相当于将“用户名:密码”绑定为一个开放式证书,这会有几个问题:①每次请求都需要用户名密码,如果此连接未使用SSL/TLS,或加密被破解,用户名密码基本就暴露了;②无法注销用户的登录状态;③证书不会过期,除非修改密码。
总体来说,这种方法的特点就是,简单但不安全。
将认证的结果存在客户端的cookie中,通过检查cookie中的身份信息来作为认证结果。这种方式的特点是便捷,且只需要一次认证,多次可用;也可以注销登录状态和设置过期时间;甚至也有办法(比如设置httpOnly)来避免XSS攻击。但它的缺点十分明显,使用cookie那便是有状态的服务了。
①声明类型,这里是jwt
②声明加密的算法 通常直接使用 HMAC SHA256,一种常见的头部是这样的:
再将其进行base64编码。
payload是放置实际有效使用信息的地方。JWT定义了几种内容,包括:
①标准中注册的声明,如签发者,接收者,有效时间(exp),时间戳(iat,issued at)等;为官方建议但非必须;
②公共声明;
③私有声明;
一个常见的payload是这样的:
事实上,payload中的内容是自由的,按照自己开发的需要加入。 Ps.有个小问题。使用itsdangerous包的TimedJSONWebSignatureSerializer进行token序列生成的结果,exp是在头部里的。这里似乎违背了jwt的协议规则。
存储了序列化的secreate key和salt key。这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密 方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
目标场景是一个前后端分离的后端系统,用于运维工作,虽在内网使用,也有一定的保密性要求。
①API为restful+json的无状态接口,要求认证也是相同模式
②可横向扩展
③较低数据库压力
④证书可注销
⑤证书可自动延期
⑥选择JWT。
这里使用python模块itsdangerous,这个模块能做很多编码工作,其中一个是实现JWS的token序列。genTokenSeq 这个函数用于生成token。其中使用的是TimedJSONWebSignatureSerializer进行序列的生成,这里secret_key密 钥、salt盐值从配置文件中读取,当然也可以直接写死在这里。expires_in是超时时间间隔,这个间隔以秒记,可以直接在这里设置,我选择将其设 为方法的形参(因为这个函数也用在了解决下提到的问题2)。
使用这个Serializer可以帮我们处理好header、signature的问题。我们只需要用s.dumps将payload的内容写进来。这里我准备在每个token中写入三个值:用户id、用户角色id和当前时间(‘iat’是JWT标准注册声明中的一项)。
假设我所写入的信息是
采用以上的方法所生成的token为
它是由“header.payload.signature”构成的。
解析需要使用到同样的serializer,配置一样的secret key和salt,使用loads方法来解析token。itsdangerous提供了各种异常处理类,用起来也很方便,如果是SignatureExpired,则可以直接返回过期;如果是BadSignature,则代表了所有其他签名错误的情况,于是又分为:
①能读取到payload:那么这个消息是一个内容被篡改、消息体加密过程正确的消息,secret key和salt很可能泄露了;
②不能读取到payload: 消息体直接被篡改,secret key和salt应该仍然安全。
以上内容写成一个函数,用于验证用户token。如果实现在python flask,可以考虑将此函数改为一个decorator修饰漆,将修饰器@到所有需要验证token的方法前面,则代码可以更加优雅。
检查和判定的机制如下:
1、使用加密的类,再用来解密(用上之前的密钥和盐值),得到结果存入data;
2、如果捕获到SignatureExpired异常,则代表根据token中的expired设置,token已经超时失效,返回‘token expired’;
3、如果是其他BadSignature异常,又要分为:
4、如果payload还完整,则解析payload,如果捕获BadData异常,则代表token已经被篡改,返回‘token tampered’;
5、如果payload不完整,直接返回‘badSignature of token’;
6、如果以上异常都不对,那只能返回未知异常‘wrong token with unknown reason’;
7、最后,如果data能正常解析,则将payload中的数据取出来,验证payload中是否有合法信息(这里是user_id和 user_role键值的json数据),如果数据不合法,则返回‘illegal payload inside’。一旦出现这种情况,则代表密钥和盐值泄露的可能性很大。
上述的方法可以做到基本的JWT认证,但在实际开发过程中还有其他问题:
token在生成之后,是靠expire使其过期失效的。签发之后的token,是无法收回修改的,因此涉及token的有效期的更改是个难题,它体现在以下两个问题:

问题1.用户登出

问题2.token自动延期
如何解决更改token有效期的问题,网上看到很多讨论,主要集中在以下内容:

JWT是一次性认证完毕加载信息到token里的,token的信息内含过期信息。过期时间过长则被重放攻击的风险太大,而过期时间太短则请求端体验太差(动不动就要重新登录)

把token存进库里,很自然能想到的是把每个token存库,设置一个valid字段,一旦注销了就valid=0;设置有效期字段,想要延期 就增加有效期时间。openstack keystone就是这么做的。这个做法虽方便,但对数据库的压力较大,甚至在访问量较大,签发token较多的情况下,是对数据库的一个挑战。况且这也 有悖于JWT的初衷。

为了使用户不需要经常重新登录,客户端将用户名密码保存起来(cookie),然后使用用户名密码验证,但那还得考虑防御CSRF攻击的问题。

一个为access token,用于用户后续的各个请求中携带的认证信息

另一个是refresh token,为access token过期后,用于申请一个新的access token。
由此可以给两类不同token设置不同的有效期,例如给access token仅1小时的有效时间,而refresh token则可以是一个月。api的登出通过access token的过期来实现(前端则可直接抛弃此token实现登出),在refresh token的存续期内,访问api时可执refresh token申请新的access token(前端可存此refresh token,access token过其实进行更新,达到自动延期的效果)。refresh token不可再延期,过期需重新使用用户名密码登录。
这种方式的理念在于,将证书分为三种级别:

access token 短期证书,用于最终鉴权

refresh token 较长期的证书,用于产生短期证书,不可直接用于服务请求

用户名密码 几乎永久的证书,用于产生长期证书和短期证书,不可直接用于服务请求
通过这种方式,使证书功效和证书时效结合考虑。
ps.前面提到创建token的时候将expire_in(jwt的推荐字段,超时时间间隔)作为函数的形参,是为了将此函数用于生成access token和refresh token,而两者的expire_in时间是不同的。
我们做了一个JWT的认证模块:
(access token在以下代码中为'token',refresh token在代码中为'rftoken')

首次认证
client --用户名密码---> server
client <--token、rftoken-- server

access token存续期内的请求
client --请求(携带token)--> server
client <---结果----server

access token超时
client ---请求(携带token)--> server
client <----msg:token expired-- server

重新申请access token
client -请求新token(携带rftoken)-> server
client <-----新token----- server

rftoken token超时
client <----msg:rftoken expired--- server
如果设计一个针对此认证的前端,需要:

存储access token、refresh token

访问时携带access token,自动检查access token超时,超时则使用refresh token更新access token;状态延期用户无感知

用户登出直接抛弃access token与refresh token
原文发布时间为:2017-04-10
本文作者:茶客furu声