什么是 JWT ?
JWT 全称 JSON Web Token,是一种用于在网络应用之间安全传递信息的开发标准,它定义了一种紧凑且独立的方式,用于 将信息作为 JSON 对象 在各方之间安全地传输。
其中,信息是可以被验证并信任的,因为它是经过数字签名的,通常可以使用密钥(对应使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对 JWT 进行签名。
JWT一般用于身份验证和非敏感数据的传递,其设计目的是不需要服务器端存储状态,安全地传输非敏感信息给受信任的实体。
这里强调“非敏感数据”是因为常用的JWT传递的信息并没有被JWT加密,任何人截取到JWT后都能读取到其中的内容,JWT侧重通过签名保证信息的可信,但信息的加密需要自己完成。
什么是 JWT结构?
JWT 由三个部分组成,用
.
分隔,它们是:- Header
- Payload
- Signature
因此,JWT 通常以
xxxxx.yyyyy.zzzzz
的形式出现。Header
Header 通常由两部分组成:令牌类型(即 JWT)和正在使用的签名算法(如 HMAC SHA256 或 RSA)。
{ "alg": "HS256", "typ": "JWT" }
然后将此 JSON 编码为 Base64Url,形成 JWT 的第一部分。
Payload
第二部分是 Payload,通常包含若干 claims,或者称声明,它们是关于实体(例如用户)和其他数据的陈述,通常分为:已注册、公共和私有声明。
- 已注册声明:一组预定义的声明,这些声明不是必须的但推荐使用,提供一组有用的、可互操作的声明。其中一些是:iss(发行者)、exp(过期时间)、sub(主题)、aud(受众)等。
请注意,声明名称只有三个字符长,因为 JWT 旨在紧凑。
Registered claims: These are a set of predefined claims which are not mandatory but recommended, to provide a set of useful, interoperable claims. Some of them are: iss(issuer), exp(expiration time), sub(subject), aud(audience), and others.
Notice that the claim names are only three characters long as JWT is meant to be compact.
- 公开声明:这些可以由使用 JWT 的人随意定义。但为了避免冲突,应在 IANA JSON Web 令牌注册表中定义它们,或将其定义为包含抗冲突命名空间的 URI。
Public claims: These can be defined at will by those using JWTs. But to avoid collisions they should be defined in the IANA JSON Web Token Registry or be defined as a URI that contains a collision resistant namespace.
- 私人声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公开声明。
Private claims: These are the custom claims created to share information between parties that agree on using them and are neither registered or public claims.
{ "sub": "1234567890", "name": "John Doe", "admin": true } // Payload 示例
请注意,信息虽然可以防止篡改,但任何人都可以读取。除非 JWT 已加密,否则不要将机密信息放在 JWT 的有效负载或标头元素中。
然后将此 JSON 编码为 Base64Url,形成 JWT 的第二部分。
Signature
要创建签名部分,必须获取编码后的 Header、Payload,Header中指定的算法,以及最重要的:secret,才能进行签名。
例如,如果要使用 HMAC SHA256 算法(HS256),将按以下方式创建签名:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于验证消息在此过程中未更改,并且,对于使用私钥签名的令牌,它还可以验证 JWT 的发送者是否是它所说的人。
最终输出是三个由
.
分隔的 Base64-URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递,同时与基于 XML 的标准(如 SAML)相比更紧凑。JWT 是如何工作的?
JWT 获取:在身份验证中,当用户使用其凭据成功登录时,将返回 JWT。由于 JWT 是凭据,因此必须非常小心以防止出现安全问题。通常令牌应该妥善保管,保留时间应该有限制,不应将敏感会话数据存储在浏览器存储中。
JWT 使用:每当用户想要访问受保护的路由或资源时,都应发送 JWT,通常在
Authorization
标头中使用 Bearer 架构。标头的内容应如下所示:Authorization: Bearer <token>
在
Authorization
标头中发送,则跨域资源共享 (CORS) 不会成为问题,因为它不使用 cookie。JWT 验证:在某些具体的情况下,如无状态授权机制,服务器中受保护的路由将检查
Authorization
标头中是否存在有效的 JWT,如果存在,则允许用户访问受保护的资源。如果 JWT 包含必要的数据,还能减少对某些操作的数据库进行查询的需要,尽管情况可能并非总是如此。请注意,如果通过 HTTP 标头发送 JWT 令牌,则应尽量防止它们变得太大。某些服务器不接受超过 8 KB 的标头。如果尝试在 JWT 令牌中嵌入太多信息(例如包含所有用户权限),则可能需要替代解决方案,例如 Auth0 细粒度授权。
下图显示了如何获取 JWT 并用于访问 API 或资源:
- The application or client requests authorization to the authorization server. This is performed through one of the different authorization flows. For example, a typical OpenID Connect compliant web application will go through the
/oauth/authorize
endpoint using the authorization code flow.
- When the authorization is granted, the authorization server returns an access token to the application.
- The application uses the access token to access a protected resource (like an API).
请注意,使用签名令牌时,令牌中包含的所有信息都会公开给用户或其他方,即使他们无法更改它。这意味着您不应将机密信息放入令牌中。
JWT 基本应用
身份认证
这是最常见的应用,用户在通过一些方式登录成功后会得到服务端签发的 JTW,它本身保存了一些用户的基本信息(ID、权限等),后续的请求都要带上这些非敏感的基本信息,可供服务端免去查询数据库的步骤,因为经过签名验证,这些信息是可信的。
sequenceDiagram participant 浏览器 participant 服务器 浏览器->>服务器: [1]Post /users/login 包含用户名密码 服务器->>服务器: [2]登陆成功 用 secret 生成 JWT Note over 服务器 : 签名的加密方式:<br/>对称加密、非对称加密 服务器->>浏览器: [3]返回 JWT 浏览器->>服务器: [4]后续的请求头包含 JWT 服务器->>服务器: [5]验证 JWT 签名,并从中获取用户信息 服务器->>浏览器: [6]验证通过,返回请求内容
TODO 扩展单点登录
密码重置电子邮件验证
用户请求重置密码时,请求用已绑定的电子邮件地址进行验证,服务器会生成包含用户信息的 JWT 并组成一个链接发往用户的邮箱,用户点击链接后就可以重置密码或完成电子邮件验证。
与其他认证机制的对比及缺点
除了 JWT,常用的还有基于 Cookie、 API Key 的认证机制
ㅤ | JWT | Cookie | API Key |
是否有状态 | 有,可包含用户基本信息 | 无,只是用户标识 | 无,只是用户标识 |
应用场景 | 前后端之间、后端之间 | 前后端之间 | 一般用在后端之间 |
认证对象 | 主要针对用户 | 针对用户 | 主要针对系统、应用 |
可撤销(revoke) | 不方便 | 方便 | 方便 |
生产方式 | 认证过程中动态生成 | 认证过程中动态生成 | 一般预先分配 |
由于 JWT 在签发后不再受到服务端直接管理(如撤销),在登录场景中需要注意注意:
- 服务端的登出不是真正的登出,用户仍然可以拥有 JWT 进行请求,因此需要一些额外手段对 JWT 的有效性进行管理,如用 Redis 对 JWT 进行缓存和生效时间限制,用于验证每个请求的 JWT 是否与缓存一致,登出清除该条缓存。
- 服务端修改了一些基本信息后 JWT 无法及时自动同步,如记录了用户权限信息的 JWT 在降低权限的修改后仍可能继续使用的问题,同样需要引入其他机制解决。
- JWT 泄露后无法被服务端及时发觉,同样需要引入其他机制解决,如将 JWT 和 Cookie 结合,将 JWT 放入 Cookie 中简化客户端开发,同时降低 JWT 存放不合理导致的安全风险。
如果使用了上面的一些方式维护 JWT,那么其实背离了 JWT 的设计初衷:使服务端不需要维护每个对话的状态,及服务端无状态的目的。这个分歧的关键是 JWT 是自包含的,有状态的,服务端不应该再存储已经签发的 JWT,用 JWT 的系统一般是完全信任 JWT token, 否则就没必要用 JWT,但实际应用中需要考虑到需求的变动带来的技术方案上的调整。