JSON Web Token 是目前最流行的跨域认证解决方案。
跨域认证的问题
一般流程
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
但是扩展性不好,对于服务器集群,或是跨域的服务导向架构,就要求 session数据共享, 每台服务器都能读取session.
如果 淘宝和天猫是同一家公司的关联服务,需要登录淘宝,天猫也自动登录,如何实现?
一种是 session数据持久化,写入数据库或别的持久层,服务收到请求后,都向持久层请求数据。 但是工程量大,另外持久层挂了,就会单点失败。
另一种就是服务器不保存 Session 数据,数据都保存在客户端,每次请求都发回服务器。JWT就是。
JWT原理
服务器认证后,生成一个JSON对象,发回给用户。像:
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}
以后,用户和服务端通信时,都要发回这个JSON对象,服务器完全只靠这个认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
服务器不保存任何session数据,变成无状态,毕竟容易实现扩展。
JWT的数据结构
JWT分为三个部分,用.分隔。三部分依次如下:
- Header (头部)
- Payload (负载)
- Signature (签名)
Header.Payload.Signature
Header
Header 部分是一个JSON对象,描述JWT的元数据
{
"alg": "HS256",
"typ": "JWT"
}
alg表示签名算法,默认是 HMAC SHA256;typ表示这个token 的 Type,JWT令牌统一写 JWT
最后,将上面的JSON对象使用Base64URL转成字符串。
Payload
这部分也是JSON对象,用来存放实际需要传递的数据。JWT规定了7个官方字段
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
还可以在这部分定义私有字段,如:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意,JWT默认不加密,任何人都可以读取,不要存放秘密信息在这个部分。
同上,这个JSON对象也要转成字符串。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
需要指定一个密钥(secret),只有服务器知道。然后,使用Header里面指定的签名算法(默认是 HMAC SHA256),按下面的公式生成签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名后,把三个部分拼接成一个字符串,用.分隔,返回给用户。
Base64 URL
Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。
JWT项目实践:
我们在这次的项目也是使用了JWT作为一个加密和解密的工具,采用的库是 "github.com/golang-jwt/jwt/v4".
我们分为两个Methods,一个是接收登录时发过来的token进行校验,然后是获取Token中的用户信息,如user_id
代码实现如下:
接收token进行校验
func ParseToken(tokenKey, tokenString string) (*jwt.Token, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(tokenKey), nil
})
if err != nil {
log.Errorf("Server failed to convert Token, err :", err.Error())
return nil, err
}
if token.Valid {
return token, nil
}
return nil, errors.New("invalid JWT token")
}
获取token中的数据
func GetTokenData(token *jwt.Token) (map[string]any, error) {
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.New("failed to extract claims from JWT token")
}
_, ok = claims["user_id"]
if !ok {
return nil, errors.New("the token does not carry critical data")
}
return claims, nil
}
这两个简单的算法,让我们团队可以解决关于token的鉴定问题,保证了登录时的安全,防止数据篡改导致的风险。
JWT的特点:
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
- JWT 不加密的情况下,不能将秘密数据写入 JWT。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
- JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。