jwt 的应用场景
单点登录(SSO)
JWT(JSON Web Tokens)在单点登录(SSO)系统中的应用可以更通用地描述为:
在传统的基于会话(session)或 cookie 的认证系统中,如果用户访问两个不同的域名,比如 A.cn 和 B.cn,他们会分别接收到两个不同的 cookie。这意味着,用户无法使用 A.cn 的 cookie 来通过 B.cn 的认证,因为每个域名都有自己独立的认证机制和会话存储。
然而,在使用 JWT 的场景中,认证过程变得更加统一和简便。JWT 为用户提供了一个统一的 token,这个 token 在不同的域名间是可共享和可验证的。换句话说,用户只需要获得一次认证,即可访问所有接入单点登录系统的应用,而无需针对每个应用分别登录。
即使你选择使用 cookie 或 session 来存储这个 JWT,这也不会影响其作为单点登录解决方案的有效性。JWT 本身是一个独立的、可在不同服务间共享的认证凭证,它包含了用户身份验证的所有必要信息,并且可以被多个不同的服务器独立验证,从而实现了跨域名的单点登录功能。
是什么
JWT是一种用于在认证之后传输信息的开放标准。它可以用于跨域信息交换和取代Session会话管理。
一个标准的 jwt 由三个对象组成:
- Header:头部
- Payload:负载
- Signature:签名
先知道这三个东西,接下来会逐一进行解释。
我们从最简单的 Payload 开始讲。
Payload
这是一个 payload 的 demo
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
很显然,这个部分就是用于我们要在 jwt 中存储的信息,它没有什么很特别的地方,它的功能就是用于存储信息。
需要注意的是,由于 jwt 默认是不加密的,因此请勿在 jwt 中存放私密信息。
JWT 规定了7个官方字段,供选用。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
当然你也可以设置自己的字段,在一开始的 demo 中就设置了两个自定义字段。
payload 部分只是一个 JSON 对象,用于存储所需信息。如果有必要可以采用其他格式,但建议使用JSON因为:
JSON 是比较通用的轻量级数据交换格式,易于解析。
jwt 其实是不易阅读的(指不能一步到位),给你们看个 jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
就问你能看懂不?
因为这玩意是经过 base64 编码的,要变成这样能阅读的格式还需要再解码一次。
Header:
{ "alg": "HS256", "typ": "JWT" }
Payload:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
Signature:
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
使用 JSON 可以使 JWT 本身也是自描述的,便于调试和理解。
JSON 库在各种语言和平台上广泛支持。
JSON 格式化的 Payload 易于在 URL 中传输(JWT 可以在 URL 的 Hash 部分或者 POST 请求体中传输,包括但不限于 URL。)。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
把用户信息存储到本地其实是一种比较危险的操作,如果你使用的系统后端校验比较少,那么对 jwt 的修改可能就被直接通过,从而实现提权。举个栗子:上面 demo 中有个字段admin
普通用户通过手动修改此字段为 true
,同时后端的校验比较简陋,那么会发生什么不用我说了吧。
为了防止 jwt 被修改,需要对其进行加密。
Signature部分是JWT中用于验证和保护消息完整性的部分。它是经过加密后的校验数据,通过重新计算提交上的数据和 signature 是否符合来判断 jwt 是否被修改。
这是一个运算后的 signature
6iGp-vOgdXhc3HcFDmvrk2G_HJQZ-wGku02eTvhKOYs
它是由下面四部分进行运算得到的:
- 加密算法(alg): 如HMAC SHA256或RSA 。
- Header
- payload
- 密钥(secret key):该密钥必须始终保密。
运算过程(demo)
HMACSHA256(
base64UrlEncode(Header) + "." +
base64UrlEncode(Payload),
"secretkey" )
= 6iGp-vOgdXhc3HcFDmvrk2G_HJQZ-wGku02eTvhKOYs
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
。
缺点
- 有效载荷易于解析:JWT 的有效载荷(Payload)部分是以 Base64 编码字符串的形式进行传输,这意味着任何人都可以解码它并读取其内容。所以不要放置敏感数据在 Payload 中。
- 签名易于解析:JWT 的签名也是以 Base64 编码字符串的形式进行传输,这意味着如果使用对称加密算法(如 HS256),任何人都可以解码签名部分并破解密钥。所以对称加密算法只适用于测试环境,生产环境建议使用非对称加密算法(如 RS256)。
- 无法撤销:一旦 JWT 发布,它就不能在过期时间之前被撤销,这是由于 JWT 的无状态性质造成的。所以一些需要撤销功能的场景不适合使用 JWT,需要使用会话来实现。
- 加密开销较大:由于 JWT 需要加密和签名,这需要计算资源,这可能会带来一定的性能损耗。当然,这也要根据具体使用场景来定,对性能要求不高的应用影响不大。
快速上手
此处以 go 作为演示语言来实现 jwt
安装
go get github.com/golang-jwt/jwt
此版本是现在社区维护的一个较为好用的 jwt 库
前身是:
github.com/dgrijalva/jwt-go
接下来我们就按照学习 jwt 的过程来构建一个基本的 jwt。
demo
package main
import (
"fmt"
"github.com/golang-jwt/jwt"
)
func main() {
//1. 设定一份密钥,需要好好存储
key := []byte("(*≧︶≦))( ̄▽ ̄* )ゞ")
//2. 设置 payload ,此包中是 claims
//推荐使用 NewWithClaims 其
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"name": "test1",
"role": "admin",
})
//也可以直接使用官方给的默认 claims 进行快速解析
//token:=jwt.NewWithClaims(jwt.SigningMethodHS256,jwt.StandardClaims{
// Audience: "",
// ExpiresAt: 0,
// Id: "",
// IssuedAt: 0,
// Issuer: "",
// NotBefore: 0,
// Subject: "",
//})
//根据需求可以自行添加 header
//token.Header["test"] = "test"
s, _ := token.SignedString(key)
fmt.Println(s)
}