互联网服务离不开用户认证。一般流程是下面这样:
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token可直接被用于认证,也可被加密。
服务器认证以后,生成一个经过签名的 JSON 对象,发回给用户,服务器则不用保存任何 session 数据了。从而把服务器变成无状态的,易于实现扩展。
以下是两个JWT的应用场景:
扁平化形式的JWT是由通过 .
分隔的三部分组成,他们分别是:
Header
Payload
Signature
所以,一个JWT看起来通常是如下的形式:
xxxxx.yyyyy.zzzzz
头部由两部分组成:
一个Header的例子:
{
"alg": "HS256",
"typ": "JWT"
}
随后,以上JSON对象会通过 Base64Url
编码为JWT的第一部分。
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
标准中注册的声明:
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
一个Payload的例子:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
随后,Payload会通过 Base64Url
编码为JWT的第二部分。
创建签名需要用到编码后的 Header、编码后的 Payload、秘钥、Header中指定的算法。
如果你想使用HMAC SHA256算法,签名将通过如下方式生成:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
如果你想把以上概念付诸实践,可以通过 https://jwt.io/ 提供的工具来玩一玩 JWT 。如下图所示:
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。
Authorization: Bearer <token>
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
Go语言版本:
package util
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
)
var ErrVerifyFailed = fmt.Errorf("verify failed")
//https://godoc.org/github.com/dgrijalva/jwt-go#example-New--Hmac
func CreateToken(claims jwt.MapClaims, privateKey []byte) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
block, _ := pem.Decode(privateKey)
if block == nil {
return "", errors.New("private key error")
}
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", err
}
return token.SignedString(priv)
}
//https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac
func VerifyToken(tokenString string, publicKey []byte) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
if token.Header["alg"] != "RS512" {
return nil, fmt.Errorf("unexpected siging alg: %v", token.Header["alg"])
}
block, _ := pem.Decode(publicKey)
if block == nil {
return nil, ErrVerifyFailed
}
pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, ErrVerifyFailed
}
pub := pubInterface.(*rsa.PublicKey)
return pub, nil
})
if err != nil {
return nil, ErrVerifyFailed
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
}
return nil, ErrVerifyFailed
}
参考来源: