JWT 没那么神秘,用它换掉 Session + Cookie 认证

本项目代码已开源,具体见fullstack-blog

数据库初始化脚本:关注公众号程序员白彬,回复关键词“博客数据库脚本”,即可获取。

背景引入

在《前端轻松拿捏!最简全栈登录认证和权限设计!》一文中,我们掌握了如何基于 Session + Cookie 实现一个基本的登录认证功能,这是一个经得起时间考验的方案。

基于 Session + Cookie 的认证,在用户登录认证时,简单做法是将 Session ID 作为 Token,接着将这个 Token 放到 Cookie 中,后续客户端发送请求时,服务端就可以从 Cookie 中取出 Token,验证用户身份。

实现上,我们可以将这个 Token 直接作为 user 表中的一个字段,在用户登录后直接写入数据库,后续验证都是根据 Session ID 查 user 表验明身份。

总的来说,Session + Cookie 的方案相当于是对每个 Token 都做了登记,每个 Token 都有对应的内存或存储系统来维护,有时还会考虑将 Token 写入到数据库中,这就是一种有状态的认证机制。当我们验证 Token 时,就需要从 Cookie 中取出口令,再去校验这个 Token 的有效性。

Session + Cookie 实现登录认证的方案,它易于实现、安全性较强,但是由于它的实现机制,也会受到一些制约。

比如 Cookie 的一些问题:

  • Cookie 受同源策略限制,在跨域应用场景下使用会遇到一些困难。
  • 依赖于客户端对 Cookie 的支持,如果用户禁用 Cookie,这套体系将无法正常工作。
  • Cookie 还有有名的 CSRF 钓鱼链接问题,不过现在浏览器对 Cookie 也有了一些安全措施应对。

Session 也会有一些问题存在:

  • 每个用户的 Session 数据都需要存储在服务器上,当用户量大时会占用大量服务器内存,增加服务器负载。当然很多 Session 的库实现也支持外部存储,比如提供 Redis 连接,但是都对内存提出了更高要求。
  • 分布式场景下,要考虑引入较为复杂的分布式 Session 方案,可扩展性一般。

那么有没有备选的认证方案呢?我们不妨来看一看JWT

认识 JWT

image-20240715172203552

JWT 全名是 JSON Web Tokens。

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

JWT 有一个很重要的特点是 self-contained,也就是说它不依赖于外部系统就能正常工作,生成 Token 和验签的过程都基于自身的算法完成。

JWT格式解释

JWT 的格式是这样的,也就是由头部、载荷、签名三部分组成:

Header.Payload.Signature

其中头部包含标准的两个字段:

  • typ: 其实是 type 的简写,代表 Token 的类型,一般固定为JWT
  • alg: 采用的签名算法,通常有HMAC SHA256, RSA
{
  "alg": "HS256",
  "typ": "JWT"
}

HMAC SHA256 采用对称密钥,RSA 则采用非对称加密,这也说明了 JWT 并不强制采用对称加密或者非对称加密。

在实际使用场景中,RSA 适合分布式系统,可以在认证服务中使用私钥签名和下发 JWT,其他各个微服务可以使用公钥验签,这既保证了签发 Token 的安全性,也避免了各个微服务对认证服务的过度调用。

头部 json 进行Base64Url编码后,作为Header.Payload.Signature中的 Header 部分。

而 Payload 则是由三类声明组成,分别是 Registered claims, Public claims, Private claims。

其中 Registered claims 是可选的标准化的,具体参考RFC7519#Registered Claim Names

Public claims 是公开的一些字段,建议大家使用时尽量往这上面凑,满足通用性和 interoperable。

Private claims 就是完全自定义的,意思就是在上面两大类 claims 中实在是找不到适合自己业务的,你就自己起个名字自己用。

整理好 Payload 字段后,也是进行Base64Url编码。

Signature 就很好理解了,签名嘛,假设采用 HMACSHA256 算法,就可以参考这个公式:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

相当于把前面两个 Base64Url 编码的内容用英文点号连接起来,使用密钥进行签名,签名的结果再进行Base64Url编码。

这样一来,我们就得到了三个Base64Url编码的结果,把这三个结果用点号连接起来就是一个标准的 JWT。下方就是一个示例。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9qZWN0IjoiZnVsbHN0YWNrLWJsb2ciLCJhdXRob3IiOiLnmb3lvawiLCJzdWIiOiJkZW1vIiwiaWF0IjoxNzIxMDM0ODMyLCJuYmYiOjE3MjEwMzQ4MzIsImV4cCI6MTcyMTEyMTIzMn0.g1rUb1rWBtV6mzbUtFxk4dBRfLFYQmwHP_eQtKyVfIw

寻找合适的库实现

理解了上面的数据结构后,就可以找具体的语言库来应用了,这个大家可以在 JWT 官方推荐的 Libraries 中找到,并且各个语言的实现都有。

image-20240715172050893

代码实现改造

接下来就是代码的改造了,主要考虑两处,一个是认证下发 Token 的地方,一个是验证 Token 的地方。

下发 Token

在本博客项目中,认证下发 Token 也就是在登录的实现里。

image-20240715173144715

在使用 Session 认证时,我们的思路是:验证了用户名和密码后,就将 Session ID 更新到 user 表中的 token 字段,同时把这个 token 设置到 Cookie 中。

而采用 JWT 时,我们的做法是:验证了用户名和密码后,采用jwt.sign签发 Token,并将 Token 返回给前端。

前端则将登录接口返回的 token 存起来,后续接口请求中在Authorization头部中携带 token。

验证 Token

权限验证我们是放在 BaseController 实现的,并且我们仅会对 authMap 中标识过的接口进行权限验证。

image-20240715174034070

对于需要验证权限的接口请求,在使用 Session 认证时,我们是判断请求 Cookie 中是否携带了 token,如果没有,直接返回“未授权”,如果携带了 token,再去查 user 表验证身份。

改为 JWT 后,我们的做法是:取出请求中的Authorization头部字段,进行 JWT 验签,如果通过,继续验证角色是否符合。

注意事项

以上实现仅供参考,如用于业务项目生产环境使用,应该还做更多考虑。

JWT 不应该存储经常变化的信息,假设你在一个管理系统中使用 JWT,通常来说,用户的权限信息可能随时会变化,我们不应该把权限信息放在 JWT 中。

如果你实在需要在 JWT 中放置一些可能会变化的信息,那么也不能放关键信息,并且尽量让 JWT 的有效期短一点。

JWT 也不应该存储敏感信息,你不应该把一些密码之类的信息放在 JWT 中。

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值