Token

token 是一串字符串,通常因为作为鉴权凭据,最常用的使用场景是 API 鉴权

API鉴权

而API鉴权主要有以下几种方式:

  • Cookie+Session
  • HTTP Basic
  • Http Digest
  • Token

Token相比Cookie+Session的优点主要用以下两点:

防范CSRF应用

这个原理不多做介绍,构成这个攻击的原因,就在于 Cookie + Session 的鉴权方式中,鉴权数据(Cookie 中的 Session_id)是由浏览器自动携带发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。而 token 是通过客户端本身逻辑作为动态参数加到请求中的,token 也不会轻易泄露出去,因此 token 在 CSRF 防御方面存在天然优势

适合移动应用

移动端上不支持 cookie,而 token 只要客户端能够进行存储就能够使用,因此 token 在移动端上也具有优势

Token的种类

  • 自定义Token:开发者根据业务逻辑自定义的 token
  • JWT:JSON Web Token,定义在 RFC 7519 中的一种 token 规范
  • Oauth2.0:定义在 RFC 6750 中的一种授权规范,但这其实并不是一种 token,只是其中也有用到 token

JWT组成

JWT 全称 JSON Web Tokens ,是一种规范化的 token。可以理解为对 token 这一技术提出一套规范,是在 RFC 7519 中提出的

JWT由三部分组成,头部、载荷与签名,中间用.分隔,例如:xxxx.yyyy.zzzz

我们使用demo使用章节中登录的token作为解读

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTIwMTMwMTMsImlhdCI6MTYxMjAxMjcxMywiaWQiOjEsInVzZXJuYW1lIjoiYWRtaW4ifQ.avsmOHGyHWIovihtoW7bjuQbXs81v1nKiZ9iYMZYwOs

头部(Header)

头部通常由两部分组成:令牌的类型(即 JWT)和正在使用的签名算法(如HS256 RSA等)

{"alg":"HS256","typ":"JWT"}经过base64编码后得到eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

$ echo -n '{"alg":"HS256","typ":"JWT"}'| base64
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

$ echo -n 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'| base64 -d
{"alg":"HS256","typ":"JWT"}


载荷(Payload)

载荷中放置了 token 的一些基本信息,以帮助接受它的服务器来理解这个 token。同时还可以包含一些自定义的信息,用户信息交换

载荷的属性也分三类:

  • 预定义(Registered)
  • 公有(public)
  • 私有(private)

预定义

{
  "sub": "1",
  "iss": "http://localhost:8000/auth/login",
  "iat": 1451888119,
  "exp": 1454516119,
  "nbf": 1451888119,
  "jti": "37c107e4609ddbcc9c096ea5ee76c667",
  "aud": "dev"
}

这里面的前 7 个字段都是由官方所定义的,也就是预定义(Registered claims)的,并不都是必需的

  • iss (issuer):签发人
  • sub (subject):主题
  • aud (audience):受众
  • exp (expiration time):过期时间
  • nbf (Not Before):生效时间,在此之前是无效的
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

在gf-jwt中,仅使用了expiat

公有载荷

在使用 JWT 时可以额外定义的载荷。为了避免冲突,应该使用 IANA JSON Web Token Registry 中定义好的,或者给额外载荷加上类似命名空间的唯一标识

私有载荷

在信息交互的双方之间约定好的,既不是预定义载荷也不是公有载荷的一类载荷。这一类载荷可能会发生冲突,所以应该谨慎使用

而在demo中,私有载荷为{"id": 1, "username": "admin"}

将预定义,公有载荷,私有载荷组合起来{"exp":1612013013,"iat":1612012713,"id":1,"username":"admin"}进行base64编码得到eyJleHAiOjE2MTIwMTMwMTMsImlhdCI6MTYxMjAxMjcxMywiaWQiOjEsInVzZXJuYW1lIjoiYWRtaW4ifQ

// ==经过strings.TrimRight处理不显示
$ echo -n '{"exp":1612013013,"iat":1612012713,"id":1,"username":"admin"}' | base64
eyJleHAiOjE2MTIwMTMwMTMsImlhdCI6MTYxMjAxMjcxMywiaWQiOjEsInVzZXJuYW1lIjoiYWRtaW4ifQ==

$ echo -n 'eyJleHAiOjE2MTIwMTMwMTMsImlhdCI6MTYxMjAxMjcxMywiaWQiOjEsInVzZXJuYW1lIjoiYWRtaW4ifQ' | base64 -d
{"exp":1612013013,"iat":1612012713,"id":1,"username":"admin"}

签名(Signature)

签名时需要用到前面编码过的两个字符串,如果以 HS256 加密为例,就如下:

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

secret就是配置说明中的Key,因此密钥需要妥善保管,不能泄露

加密后再进行 base64url 编码最后得到的字符串就是 token 的第三部分 zzzzz

组合便可以得到 token:xxxxx.yyyyy.zzzzz

func Hs256(message string, secret string) string {
	hasher := hmac.New(sha256.New, []byte(secret))
 	hasher.Write([]byte(message))
	sign := strings.TrimRight(base64.URLEncoding.EncodeToString(hasher.Sum(nil)), "=")
	return sign
}

// main.go
func main() {
	sign := Hs256("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTIwMTMwMTMsImlhdCI6MTYxMjAxMjcxMywiaWQiOjEsInVzZXJuYW1lIjoiYWRtaW4ifQ", "secret key")
	fmt.Println(sign)
	// avsmOHGyHWIovihtoW7bjuQbXs81v1nKiZ9iYMZYwOs
}

签名的作用:保证 JWT 没有被篡改过,原理如下:

HMAC 算法是不可逆算法,类似 MD5 和 hash ,但多一个密钥,密钥(即上面的 secret)由服务端持有,客户端把 token 发给服务端后,服务端可以把其中的头部和载荷再加上事先共享的 secret 再进行一次 HMAC 加密,得到的结果和 token 的第三段进行对比,如果一样则表明数据没有被篡改。


  • No labels