JSON Web Token 实践 | 青训营笔记

121 阅读5分钟

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

JWT Token Example

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的特点:

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  2. JWT 不加密的情况下,不能将秘密数据写入 JWT。
  3. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

Ref:

JSON Web Token 入门教程