深入了解JWT | 青训营

995 阅读6分钟

1 授权认证

1.1 什么是授权认证

在一般的应用中,都有游客模式和登录模式两种,游客模式可以简单的有一些功能,如果想要使用全部的服务,就需要注册或者登录了。而授权认证,就是服务器对用户进行认证,以此判断该用户是否已经登录。

实现授权的方式一般有cookie、session、token。

1.2 Cookie

cookie主要是用来弥补HTTP的无状态,客户端发起请求,服务端给出响应,但是服务端再次发起请求的时候,就不知道辨别客户端是谁了,所以产生了cookie

cookie是服务器发送到用户浏览器并保存在本地的数据,它会在浏览器下次像同一个服务器发送请求的时候被携带上并发送到服务器上,以此来判断每一次请求的用户。

每一个cookie都会绑定单一的域名,无法在别的域名下获取使用,同域名下不同端口也是可以共享使用的,所以说cookie是不可跨域的

1.3 Session

session也是基于cookie实现的,session存储在服务器端,sessionID会被存储到客户端的cookie中,session认证的流程如下:

image.png

  • 用户第一次请求服务器的时候会创建对应的session,在响应的时候,将session的标识sessionID返回给浏览器

  • 浏览器接收到响应,将sessionID存储在cookie中,并记录sessionId属于哪一个域名

  • 用户再次访问服务器的时候,会将cookie中的sessionID也一同带给发送到服务端。服务器查找对应的session信息,根据session信息判断是否登录或者登录是否过期,然后执行后续的操作

1.4 Token

Token可以看作访问接口所需要的资源凭证。凭借token可以访问到有限制的支援,不携带token也可以访问到部分资源。

token身份验证的流程如下:


image.png

  • 用户使用账号密码请求登录,服务端接收到请求,会签发一个token返回给用户
  • 用户收到token以后将token存储起来
  • 客户端需要访问有限制(需要登录才能访问)的资源的时候,就将token放到请求的header中一起发送给服务端
  • 服务端收到了请求,先去验证token(是否正确、是否过期),然后再处理具体的响应

基于token的认证方式比session认证方式更节约服务器资源,并且对移动端和分布式更加友好,其中的有点如下:

  • 支持跨域访问: cookie是无法跨域的,而token没有用到cookie,所以跨域不会存在信息丢失的问题
  • 无状态: token不需要再服务端存储session信息,因为toen自身包含了所有登录的用户信息
  • 更使用于移动端: 对于非浏览器平台,cookie不被支持,所以采用token作为认证方式会简化开发

2 JWT简介

​ JSON Web Token (JWT)是一种开放标准 (RFC7519),它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。此信息可以验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSAECDSA 的公钥/私钥对进行签名。

​ 简单的来说,token就是一个字符串,将用户的信息保存到一个json字符串中,然后进行编码得到JWT token,并且这个token带有签名信息,可以校验是信息是否被篡改,所以可以用在各方之间安全的将信息作为Json对象传输。

3 JWT结构

JWT由三部分组成(下图所示三种颜色):表头(header)、有效载荷(payload)和签名(Signature)组成在传输的时候将三部分用 . 进行连接

image.png

3.1 标头Header

JWT的第一部分,描述的是JWT元数据的Json对象:

  • alg:签名使用的算法,默认是HMAC SHA256(写为HS256)
  • typ:表示令牌的类型,JWT统一写为JWT

最后使用Base64 URL算法将上述的Json对象转换成字符串进行保存

{
  "alg": "HS256",
  "typ": "JWT"
}

3.2 Payload有效载荷

​ 有效载荷部分是JWT的主题内容,也是一个对象,包换需要传递的数据。JWT指定七个默认字段进行选择(以下是golang中Jwt结构体定义):

  • Audience:接收 JWT 的一方(可选)。
  • ExpiresAt:JWT 的过期时间(Unix 时间戳,可选)。
  • Id:JWT 的唯一标识符,通常用于防止 JWT 重放攻击(可选)。
  • IssuedAt:JWT 的签发时间(Unix 时间戳,可选)。
  • Issuer:JWT 的签发者(可选)。
  • NotBefore:在此之前 JWT 不可用(Unix 时间戳,可选)。
  • Subject:JWT 的主题,即该 JWT 所代表的用户(可选)。
type StandardClaims struct {
	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"`
}

除去以上默认字段外,还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,比如截图中的user_id.

注意:默认情况JWT是未加密的,只是采用了base64算法,所以不要将隐私信息保存再JWT中

3.3 Signature签名

JWT的第三部分是签名。首先要指定一个secret,这个secret只保存再服务器中,不能被别人知道。签名部分需要base64URL加密后的header和base64加密后的payload使用 . 连接成字符串,然后通过header中声明的加密算法,进行加盐secret组合加密,然后得出一个签名哈希,这就是Signature,而且无法反向解密。

计算出签名哈希后,JWT头,有效载荷和签名哈希三个部分组成一个字符串,每一个部分使用 . 分隔开,构成了JWT对象

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
) secret base64 encoded

注意:

  • header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据
  • signature使用了不可逆的加密算法,无法解码出原文,用于校验token是否被篡改。服务端获取header中的加密算法后,利用该算法加上secret(存储在服务器)对haeder、payload进行机密,比对加密后的数据和哭护短发送过来的数据是否一致。

4 golang实现JWT

4.1 JWT编码

type Claims struct {
	UserId int64 `json:"user_id"`
	jwt.StandardClaims  // 七个默认字段可供选择
}

func GenerateToken(userId int64) (string, error) {
   now := time.Now()
   // 创建一个自己的声明
   claims := Claims{
      UserId: userId,   // 自己定义的私有字段
      StandardClaims: jwt.StandardClaims{
         ExpiresAt: now.Add(TokenExpireDuration).Unix(), // 过期时间: 当前时间 + 过期时间
         Issuer:    "admin",                             // 签发人
      },
   }
   // 创建签名对象,指定签名算法和payload对象
   tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
   // 通过加盐secret加密,得到token对象
   token, err := tokenClaims.SignedString(Secret)   

   return token, err
}

4.2 JWT解码

func ParseToken(token string) (*Claims, error) {
    // 通过secret验证signature
	tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return Secret, nil
	})
	if err != nil {
		return nil, err
	}
	
    // 解析paylaod对象
	if tokenClaims != nil {
		if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
			return claims, nil
		}
	}
	return nil, errors.New("invalid token")
}