网络身份认证
网络令牌技术很早就被应用在通信安全领域,通常是作为身份认证的工具.
TLS已经可以解决通信安全问题,但它的问题在于太重了
- 不带客户端验证的TLS无法确认客户端的合法性
- 带客户端验证的TLS使用非常不便
通常我们都是使用的不带客户端验证的TLS,然后再通过其他方式验证客户端的合法性.
JWT就是用于解决客户端合法性验证的一项技术.
身份认证问题
身份认证问题解决的是什么问题呢?简单说就是证明”你是你”这样一个问题.
在业务上客户和客户所拥有的权限往往是不一样的,有的用户只能访问A资源,有的可以访问所有资源,最典型的就是一般用户和管理员用户之间的差别.
如果没有身份认证,那么我们就没有办法区分不同用户应该放开的权限.
所有的身份认证问题我们可以抽象成有3方4个对象,我们可以按下面的例子来理解这个问题:
古装剧里经常会有这样一个场景,下人做了件什么事情老爷非常开心,然后就对他说:”干的好,去库房领赏吧!”.考虑下接下来下人该怎么才能领到赏呢?
这里有三方四个对象:
- 老爷(权限给予方)
- 库房(控制权限的资源)
- 库房看守(权限验证方)
- 领赏的下人(待验证权限方)
实际上上面问题就是一个最典型的身份认证问题,我们借着分析上面问题的解决方法来看看身份认证问题的解决思路
常见的身份认证问题解决方案
上面问题解决思路可以分为两类:
- 让老爷写个纸条记上领赏人的名字和领赏金额,然后把纸条给库房看守,领赏的下人去领赏时报名字,库房看守看看名字在不在老爷给的纸条上决定给不给赏钱
- 让老爷写个纸条记上领赏人的名字和领赏金额,然后拿信封装好,火漆封好交给下人,下人带着这封信去找看守.看守问清领赏下人的名字和赏金(1),确认好火漆没有开封过(2),并且打开信封后确认里面写的和下人说的一致(3),然后给赏钱.
这两个思路就是最常见的身份认证问题的解决思路,扩展到网络身份领域,就是最常见的两种技术:
-
session
技术.对应第一种思路.用户登陆后的访问权限信息都保存在服务器上(一般放在redis里),用户登录后会得到一个session
,这个session
是一定期限内的访问信息的key.每次用户访问的时候都会带上这个session
,服务端则会拿这个session
去查询访问信息以确定是否可以通过身份认证. -
JWT
技术.对应第二种,用户登陆后会得到一个有明文访问权限信息以及密文验证信息的token,用户每次带着这个token访问资源,服务端则会验证这个token中明文信息和密文信息是否一致以确保未被篡改.确保未被篡改后直接拿这个明文信息处理权限问题.
两种技术的比较
两种技术的比较大致可以看下面的列表
方案 | 安全性 | 扩展性 | 可控性 | 维护成本 | 传输成本 |
---|---|---|---|---|---|
session |
高 | 低 | 高 | 高 | 低 |
JWT |
低 | 高 | 低 | 低 | 高 |
他们两者的区别其实主要来自于存储的位置.
session
存在服务端,- 因为数据实际上存在服务端,所以一般很难被外部篡改,因此安全性更高些
- 因为要存所以一定是有状态的,而我们知道有状态的东西扩展性都强不起来
- 由于数据就在服务端,各种服务可以很轻易的改变其对应的数据所以可控性强
- 维护数据需要有额外的有状态服务,比如redis实例,而一旦规模大了维护成本将会很高
- 因为每次只传一个id,所以传输成本低
JWT
存在客户端- 因为数据存在客户端,相对更容易被外部篡改,虽然有认证的token,但如果知道使用的算法和密钥的话(尤其是纯基于hash的算法)token是可以被破解冒用的,同时由于有明文部分,所以即便不破解也可以得到传输的信息.
jwt
是无状态自解释的所以扩展性非常好,完全可以水平无限扩展.jwt
令牌一旦发出去就没办法修改了,因此可控性不好- 由于不需要额外的有状态组件,所以维护成本几乎没有
- 由于明文密文信息都在token中而每次传输都会带着token,所以传输成本高
总体而言个人更加偏向使用jwt,毕竟没啥维护成本,但在以下场景应该使用session
- 需要保存的数据量很大的情况
- 需要可以随时改变数据的情况
- 对安全性要求高的情况
session
技术基本没啥可讲的,各家有个家的实现,基本都是基于redis强大的并发能力缓存信息.接下来的部分我们讲JWT
JWT技术
JWT(JSON Web Token)遵循rfc7519规范,总结如下:
结构
可以分为3部分,每个部分用.
分隔,按顺序如下:
-
Header
用于声明元信息.内容为Base64URL
算法序列化过的json字符串,json字符串的主要内容是typ
字段和alg
字段{ "alg": "HS256", "typ": "JWT" }
-
Payload
,用于声明要传输保存的数据,内容为Base64URL
算法序列化过的json字符串,json字符串的中有如下几个字段是规范规定好的字段字段 类型 用途 iss
string
声明权限给予方(签发人) exp
int
声明过期时间(时间戳) sub
string
声明待验证权限方(授予人) aud
array[string]
声明授予权限的资源 nbf
int
声明生效时间(时间戳) iat
int
声明签发时间(时间戳) jti
string
声明JWT编号 其他字段则可以用户自定义.
-
Signature
,对上面Header
和Payload
的签名,这个签名需要使用上面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和这个黑名单做比较,在黑名单里就禁止其访问.用这种方式好处是
- 黑名单数据规模小,可以将维护成本控制在一个相对较小的范围内.
- 一定程度上增加了jwt数据的可控性.
长期JWT结合自动刷新
上面的思路缺点是不可避免的引入了有状态服务,虽然成本不高但确实增加了复杂度,如果实在不希望引入有状态服务,可以考虑另一个思路:
- 登录后设置长期JWT(比如过期时间为7天)
- 访问时检查签名时间和当前时间的差值,如果大于一个数(比如1小时)则创建一个新的jwt发给客户端让它替掉之前的jwt
这个思路的优点是可以每次更新jwt时顺便就把信息更新了,但缺点也是一样明显–每次替掉之前的jwt后其实之前的jwt还都可以使用.
长短期JWT结合自动刷新
上面思路可以进一步扩展,我们设置
-
一个短期JWT–
access_token
(比如30分钟)用于认证操作.这个jwt中需要包含验权的全部有用信息 -
[可选]一个长期JWT–
refresh_token
(比如7天)用于控制可以自动刷新认证JWT的时间范围从而增强体验,这个jwt中只需要包含过期时间和用户id信息,可以放在Header中的Refresh-Token
字段,为了防止恶意用户在知道所有服务签发的jwt都使用同一个算法同一个密码时拿到一个refresh_token
后与其他负载不同但用户信息相同的key进行组合,一个比较简单的办法是将access_token
的jti
设置为refresh_token
的jti
,这样两者一比对就可以知道是否是一组了,当然更好的方式是不同的业务签发jwt使用不同的密码.
这样我们的认证流程如下:
- 如果
access_token
没有过期,则正常验权 - 如果
access_token
过期了查看有没有refresh_token
- 如果没有
refresh_token
则要求用户重新登录 - 如果有
refresh_token
且过期了也要求用户重新登录 - 如果有
refresh_token
但未过期但用户信息和access_token
不一致则要求用户重新登录 - 如果有
refresh_token
但未过期且用户信息和access_token
一致则重新分发一个access_token
给用户替换原有access_token
,且当作这次访问验权正常.