什么是 JSON Web Token?
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 方式安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用** HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对对 JWT 进行签名**。
直白的讲 jwt 就是一种用户认证(区别于 session、cookie)的解决方案。
出现的背景
众所周知,在 jwt 出现之前,我们已经有 session、cookie 来解决用户登录等认证问题,为什么还要 jwt 呢?
这里我们先了解一下 session,cookie。
session
熟悉 session 运行机制的同学都知道,用户的 session 数据以 file 或缓存(redis、memcached)等方式存储在服务器端,客户端浏览器 cookie 中只保存 sessionid。服务器端 session 属于集中存储,数量不大的情况下,没什么问题,当用户数据逐渐增多到一程度,就会给服务端管理和维护带来大的负担。
session 有两个弊端:
1、无法实现跨域。
2、由于 session 数据属于集中管理里,量大的时候服务器性能是个问题。
优点:
1、session 存在服务端,数据相对比较安全。
2、session 集中管理也有好处,就是用户登录、注销服务端可控。
cookie
cookie 也是一种解决网站用户认证的实现方式,用户登录时,服务器会发送包含登录凭据的 Cookie 到用户浏览器客户端,浏览器会将 Cookie 的 key/value 保存用户本地(内存或硬盘),用户再访问网站,浏览器会发送 cookie 信息到服务器端,服务器端接收 cookie 并解析来维护用户的登录状态。
cookie 避免 session 集中管理的问题,但也存在弊端:
1、跨域问题。
2、数据存储在浏览器端,数据容易被窃取及被 csrf 攻击,安全性差。
优点:
1、相对于 session 简单,不用服务端维护用户认证信息。
2、数据持久性。
jwt
jwt 通过 json 传输,php、java、golang 等很多语言支持,通用性比较好,不存在跨域问题。传输数据通过数据签名相对比较安全。客户端与服务端通过 jwt 交互,服务端通过解密 token 信息,来实现用户认证。不需要服务端集中维护 token 信息,便于扩展。当然 jwt 也有其缺点。
缺点:
1、用户无法主动登出,只要 token 在有效期内就有效。这里可以考虑 redis 设置同 token 有效期一直的黑名单解决此问题。
2、token 过了有效期,无法续签问题。可以考虑通过判断旧的 token 什么时候到期,过期的时候刷新 token 续签接口产生新 token 代替旧 token。
jwt 设置有效期
可以设置有效期,加入有效期是为了增加安全性,即 token 被黑客截获,也只能攻击较短时间。设置有效期就会面临 token 续签问题,解决方案如下
通常服务端设置两个 token
- Access Token:添加到 HTTP 请求的 header 中,进行用户认证,请求接口资源。
- refresh token:用于当 Access Token 过期后,客户端传递 refresh token 刷新 Access Token 续期接口,获取新的 Access Token 和 refresh token。其有效期比 Access Token 有效期长。
jwt 构成:
- Header:TOKEN 的类型,就是 JWT,签名的算法,如 HMAC SHA256、HS384
- Payload:载荷又称为 Claim,携带的信息,比如用户名、过期时间等,一般叫做 Claim
- Signature:签名,是由 header、payload 和你自己维护的一个 secret 经过加密得来的
jwt 使用
这里推荐个使用比较多的开源项目、[github.com/dgrijalva/jwt-go](),更多文档。
示例:
package main import ( "fmt" "github.com/dgrijalva/jwt-go" "time" ) const ( SECRETKEY = "243223ffslsfsldfl412fdsfsdf"//私钥 ) //自定义 Claims type CustomClaims struct { UserId int64 jwt.StandardClaims } func main() { //生成 token maxAge:=60*60*24 customClaims :=&CustomClaims{ UserId: 11,//用户 id StandardClaims: jwt.StandardClaims{ ExpiresAt: time.Now().Add(time.Duration(maxAge)*time.Second).Unix(), // 过期时间,必须设置 Issuer:"jerry", // 非必须,也可以填充用户名, }, } //采用 HMAC SHA256 加密算法 token:=jwt.NewWithClaims(jwt.SigningMethodHS256, customClaims) tokenString,err:= token.SignedString([]byte(SECRETKEY)) if err!=nil { fmt.Println(err) } fmt.Printf("token: %v\n", tokenString) //解析 token ret,err :=ParseToken(tokenString) if err!=nil { fmt.Println(err) } fmt.Printf("userinfo: %v\n", ret) } //解析 token func ParseToken(tokenString string)(*CustomClaims,error) { token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return []byte(SECRETKEY), nil }) if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { return claims,nil } else { return nil,err } }
Claims:
Audience string `json:"aud,omitempty"` ExpiresAt int64 `json:"exp,omitempty"` Id string `json:"jti,omitempty"` IssuedAt int64 `json:"iat,omitempty"` Issuer string `json:"iss,omitempty"` NotBefore int64 `json:"nbf,omitempty"` Subject string `json:"sub,omitempty"` aud: 接收 jwt 的一方 exp: jwt 的过期时间,这个过期时间必须要大于签发时间 jti: jwt 的唯一身份标识,主要用来作为一次性 token, 从而回避重放攻击。 iat: jwt 的签发时间 iss: jwt 签发者 nbf: 定义在什么时间之前,该 jwt 都是不可用的。就是这条 token 信息生效时间。这个值可以不设置,但是设定后,一定要大于当前 Unix UTC, 否则 token 将会延迟生效。 sub: jwt 所面向的用户
示例:
package main import ( "fmt" "github.com/dgrijalva/jwt-go" "time" ) const ( SECRETKEY = "243223ffslsfsldfl412fdsfsdf"//私钥 ) //自定义 Claims type CustomClaims struct { UserId int64 jwt.StandardClaims } func main() { //生成 token maxAge:=60*60*24 // Create the Claims //claims := &jwt.StandardClaims{ // // ExpiresAt: time.Now().Add(time.Duration(maxAge)*time.Second).Unix(), // 过期时间,必须设置, // // Issuer: "jerry",// 非必须,也可以填充用户名, // //} //或者用下面自定义 claim claims := jwt.MapClaims{ "id": 11, "name": "jerry", "exp": time.Now().Add(time.Duration(maxAge)*time.Second).Unix(), // 过期时间,必须设置, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(SECRETKEY)) if err!=nil { fmt.Println(err) } fmt.Printf("token: %v\n", tokenString) //解析 token ret,err :=ParseToken(tokenString) if err!=nil { fmt.Println(err) } fmt.Printf("userinfo: %v\n", ret) } //解析 token func ParseToken(tokenString string)(jwt.MapClaims,error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Don't forget to validate the alg is what you expect: if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") return []byte(SECRETKEY), nil }) if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { return claims,nil } else { return nil,err } }
小结:
- 服务端生成的 jwt 返回客户端可以存到 cookie 也可以存到 localStorage 中(相比 cookie 容量大),存在 cookie 中需加上
HttpOnly
的标记,可以防止 XSS) 攻击。 - 尽量用 https 带证书网址访问。
- session 和 jwt 没有绝对好与不好,各有其擅长的应用环境,请根据实际情况选择。