网络身份认证

Posted by Hsz on May 25, 2021

网络身份认证

网络令牌技术很早就被应用在通信安全领域,通常是作为身份认证的工具.

TLS已经可以解决通信安全问题,但它的问题在于太重了

  1. 不带客户端验证的TLS无法确认客户端的合法性
  2. 带客户端验证的TLS使用非常不便

通常我们都是使用的不带客户端验证的TLS,然后再通过其他方式验证客户端的合法性.

JWT就是用于解决客户端合法性验证的一项技术.

身份认证问题

身份认证问题解决的是什么问题呢?简单说就是证明”你是你”这样一个问题.

在业务上客户和客户所拥有的权限往往是不一样的,有的用户只能访问A资源,有的可以访问所有资源,最典型的就是一般用户和管理员用户之间的差别.

如果没有身份认证,那么我们就没有办法区分不同用户应该放开的权限.

所有的身份认证问题我们可以抽象成有3方4个对象,我们可以按下面的例子来理解这个问题:

古装剧里经常会有这样一个场景,下人做了件什么事情老爷非常开心,然后就对他说:”干的好,去库房领赏吧!”.考虑下接下来下人该怎么才能领到赏呢?

这里有三方四个对象:

  1. 老爷(权限给予方)
  2. 库房(控制权限的资源)
  3. 库房看守(权限验证方)
  4. 领赏的下人(待验证权限方)

实际上上面问题就是一个最典型的身份认证问题,我们借着分析上面问题的解决方法来看看身份认证问题的解决思路

常见的身份认证问题解决方案

上面问题解决思路可以分为两类:

  1. 让老爷写个纸条记上领赏人的名字和领赏金额,然后把纸条给库房看守,领赏的下人去领赏时报名字,库房看守看看名字在不在老爷给的纸条上决定给不给赏钱
  2. 让老爷写个纸条记上领赏人的名字和领赏金额,然后拿信封装好,火漆封好交给下人,下人带着这封信去找看守.看守问清领赏下人的名字和赏金(1),确认好火漆没有开封过(2),并且打开信封后确认里面写的和下人说的一致(3),然后给赏钱.

这两个思路就是最常见的身份认证问题的解决思路,扩展到网络身份领域,就是最常见的两种技术:

  1. session技术.对应第一种思路.用户登陆后的访问权限信息都保存在服务器上(一般放在redis里),用户登录后会得到一个session,这个session是一定期限内的访问信息的key.每次用户访问的时候都会带上这个session,服务端则会拿这个session去查询访问信息以确定是否可以通过身份认证.

  2. JWT技术.对应第二种,用户登陆后会得到一个有明文访问权限信息以及密文验证信息的token,用户每次带着这个token访问资源,服务端则会验证这个token中明文信息和密文信息是否一致以确保未被篡改.确保未被篡改后直接拿这个明文信息处理权限问题.

两种技术的比较

两种技术的比较大致可以看下面的列表

方案 安全性 扩展性 可控性 维护成本 传输成本
session
JWT

他们两者的区别其实主要来自于存储的位置.

  • session存在服务端,
    • 因为数据实际上存在服务端,所以一般很难被外部篡改,因此安全性更高些
    • 因为要存所以一定是有状态的,而我们知道有状态的东西扩展性都强不起来
    • 由于数据就在服务端,各种服务可以很轻易的改变其对应的数据所以可控性强
    • 维护数据需要有额外的有状态服务,比如redis实例,而一旦规模大了维护成本将会很高
    • 因为每次只传一个id,所以传输成本低
  • JWT存在客户端
    • 因为数据存在客户端,相对更容易被外部篡改,虽然有认证的token,但如果知道使用的算法和密钥的话(尤其是纯基于hash的算法)token是可以被破解冒用的,同时由于有明文部分,所以即便不破解也可以得到传输的信息.
    • jwt是无状态自解释的所以扩展性非常好,完全可以水平无限扩展.
    • jwt令牌一旦发出去就没办法修改了,因此可控性不好
    • 由于不需要额外的有状态组件,所以维护成本几乎没有
    • 由于明文密文信息都在token中而每次传输都会带着token,所以传输成本高

总体而言个人更加偏向使用jwt,毕竟没啥维护成本,但在以下场景应该使用session

  1. 需要保存的数据量很大的情况
  2. 需要可以随时改变数据的情况
  3. 对安全性要求高的情况

session技术基本没啥可讲的,各家有个家的实现,基本都是基于redis强大的并发能力缓存信息.接下来的部分我们讲JWT

JWT技术

JWT(JSON Web Token)遵循rfc7519规范,总结如下:

结构

可以分为3部分,每个部分用.分隔,按顺序如下:

  1. Header用于声明元信息.内容为Base64URL算法序列化过的json字符串,json字符串的主要内容是typ字段和alg字段

     {
     "alg": "HS256",
     "typ": "JWT"
     }
    
  2. Payload,用于声明要传输保存的数据,内容为Base64URL算法序列化过的json字符串,json字符串的中有如下几个字段是规范规定好的字段

    字段 类型 用途
    iss string 声明权限给予方(签发人)
    exp int 声明过期时间(时间戳)
    sub string 声明待验证权限方(授予人)
    aud array[string] 声明授予权限的资源
    nbf int 声明生效时间(时间戳)
    iat int 声明签发时间(时间戳)
    jti string 声明JWT编号

    其他字段则可以用户自定义.

  3. Signature,对上面HeaderPayload的签名,这个签名需要使用上面Header部分alg字段声明的算法实现.公式可以认为是

     Algo(base64UrlEncode(header) + "." + base64UrlEncode(payload), [secret|private_key])
    

通常使用的算法

rfc7518中给出了jwt中常用和推荐的算法,

“alg” Digital Signature or MAC Algorithm Implementation Requirements
HS256 HMAC using SHA-256 Required
HS384 HMAC using SHA-384 Optional
HS512 HMAC using SHA-512 Optional
RS256 RSASSA-PKCS1-v1_5 using SHA-256 Recommended
RS384 RSASSA-PKCS1-v1_5 using SHA-384 Optional
RS512 RSASSA-PKCS1-v1_5 using SHA-512 Optional
ES256 ECDSA using P-256 and SHA-256 Recommended+
ES384 ECDSA using P-384 and SHA-384 Optional
ES512 ECDSA using P-521 and SHA-512 Optional
PS256 RSASSA-PSS using SHA-256 and MGF1 with SHA-256 Optional
PS384 RSASSA-PSS using SHA-384 and MGF1 with SHA-384 Optional
PS512 RSASSA-PSS using SHA-512 and MGF1 with SHA-512 Optional
none No digital signature or MAC performed Optional

需要注意,none是很危险的,意味着明文,所以不要用

使用方法

通常jwt都是接口获得,得到后在客户端保存在localStorage中.此后客户端每次与服务器通信都要带上这个JWT.通常是放在HTTP请求的头信息Authorization字段里面.其形式一般是Authorization: Bearer <token>.

JWT结合黑名单

JWT技术最大的痛点其实在于可控性,一旦发出去的token想收回来就难了,因此一个方案是将要提前取消访问权的token做个hash然后存起来作为黑名单(一般还是存redis,而且要设置过期),然后每个请求的token做hash和这个黑名单做比较,在黑名单里就禁止其访问.用这种方式好处是

  1. 黑名单数据规模小,可以将维护成本控制在一个相对较小的范围内.
  2. 一定程度上增加了jwt数据的可控性.

长期JWT结合自动刷新

上面的思路缺点是不可避免的引入了有状态服务,虽然成本不高但确实增加了复杂度,如果实在不希望引入有状态服务,可以考虑另一个思路:

  1. 登录后设置长期JWT(比如过期时间为7天)
  2. 访问时检查签名时间和当前时间的差值,如果大于一个数(比如1小时)则创建一个新的jwt发给客户端让它替掉之前的jwt

这个思路的优点是可以每次更新jwt时顺便就把信息更新了,但缺点也是一样明显–每次替掉之前的jwt后其实之前的jwt还都可以使用.

长短期JWT结合自动刷新

上面思路可以进一步扩展,我们设置

  1. 一个短期JWT–access_token(比如30分钟)用于认证操作.这个jwt中需要包含验权的全部有用信息

  2. [可选]一个长期JWT–refresh_token(比如7天)用于控制可以自动刷新认证JWT的时间范围从而增强体验,这个jwt中只需要包含过期时间和用户id信息,可以放在Header中的Refresh-Token字段,为了防止恶意用户在知道所有服务签发的jwt都使用同一个算法同一个密码时拿到一个refresh_token后与其他负载不同但用户信息相同的key进行组合,一个比较简单的办法是将access_tokenjti设置为refresh_tokenjti,这样两者一比对就可以知道是否是一组了,当然更好的方式是不同的业务签发jwt使用不同的密码.

这样我们的认证流程如下:

  1. 如果access_token没有过期,则正常验权
  2. 如果access_token过期了查看有没有refresh_token
  3. 如果没有refresh_token则要求用户重新登录
  4. 如果有refresh_token且过期了也要求用户重新登录
  5. 如果有refresh_token但未过期但用户信息和access_token不一致则要求用户重新登录
  6. 如果有refresh_token但未过期且用户信息和access_token一致则重新分发一个access_token给用户替换原有access_token,且当作这次访问验权正常.