1. 什么是JWT

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON方式安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。
直白的讲jwt就是一种用户认证(区别于session、cookie)的解决方案,主要思想为口令(token)验证

众所周知,在jwt出现之前,我们已经有session、cookie来解决用户登录等认证问题,为什么还要jwt呢?
这里我们先了解一下session,cookie。

1.2 简说cookie

cookie也是一种解决网站用户认证的实现方式,用户登录时,服务器会发送包含登录凭据的Cookie到用户浏览器客户端,浏览器会将 Cookie 的key/value 保存用户本地(内存或硬盘),用户再访问网站,浏览器会发送cookie信息到服务器端,服务器端接收cookie并解析来维护用户的登录状态。
关键信息为,用户的登录信息保存在客户端而非服务端

  • 缺点
    • 跨域问题
    • 数据存储在浏览器端,数据容易被窃取及被csrf攻击,安全性差
  • 优点

    • 相对于session简单,不用服务端维护用户认证信息
    • 数据持久性

      1.3 简说session

      用户的session数据以file或缓存(redis、memcached)等方式存储在服务器端,客户端浏览器cookie中只保存sessionid。服务器端session属于集中存储,数量不大的情况下,没什么问题,当用户数据逐渐增多到一程度,就会给服务端管理和维护带来大的负担
      关键信息为,用户的登录信息保存在服务端而非客户端
  • 缺点

    • 无法实现跨域
    • 由于session数据属于集中管理里,量大的时候服务器性能是个问题
  • 优点

    • session存在服务端,数据相对比较安全
    • session集中管理也有好处,就是用户登录、注销服务端可控

      1.4 简说jwt

      jwt通过json传输,php、java、golang等很多语言支持,通用性比较好,不存在跨域问题。传输数据通过数据签名相对比较安全。客户端与服务端通过jwt交互,服务端通过解密token信息,来实现用户认证。不需要服务端集中维护token信息,便于扩展。当然jwt也有其缺点。
      也即,jwt相当于把 cookie 和 session 的机制作了个调和,不单独推责于服务端或客户端,而是两者交互作用,客户端传输信息,服务端进行验证
  • 但还是有一些缺点的:

    • 用户无法主动登出,只要token在有效期内就有效。这里可以考虑redis设置同token有效期一直的黑名单解决此问题。
    • token过了有效期,无法续签问题。可以考虑通过判断旧的token什么时候到期,过期的时候刷新token续签接口产生新token代替旧token

2. 什么是鉴权

在网站中,有些页面是登录后的用户才能访问的,由于http是无状态的协议,我们无法确认用户的状态(如是否登录)。这时候浏览器在访问这些页面时,需要额外传输一些用户的账户信息给后台,让后台知道该用户是否登录、是哪个用户在访问。


3. 细说cookie

cookie是浏览器实现的技术,在浏览器中可以存储用户是否登录的凭证,每次请求都会将该凭证发送给服务器。
cookie实现鉴权步骤:

  • 用户登录成功后,后端向浏览器设置一个cookie:username=lisi
  • 每次请求,浏览器会自动把该cookie发送给服务端
  • 服务端处理请求时,从cookie中取出username,就知道是哪个用户了
  • 如果没过期,则鉴权通过,过期了,则重定向到登录页

【在go中使用cookie:】
image.png
但是这样做,风险很大,黑客很容易知道cookie中传递的内容,即用户的真实账户信息


  1. 细说session

    4.1 session原理

    为了解决cookie的安全问题,基于cookie,衍生了session技术。session技术将用户的信息存储在了服务端,保证了安全,其实现步骤为
  • 服务端设置cookie时,不再存储username,而是存储一个随机生成的字符串,比如32位的uuid,服务端额外存储一个uuid与用户名的映射
  • 用户再次请求时,会自动把cookie中的uuid带入给服务器
  • 服务器使用uuid进行鉴权

    一般上述的 uuid 在 cookie 中存储的键都是 sid(session_id),也就是常说的session方案,服务端此时需要额外开辟空间存储sid与用户真实信息的对应映射

    4.2 session实现

    如果要手动实现session,需要注意以下方面:

  • 全局session管理器:

  • 保证sessionid 的全局唯一性
  • 为每个客户关联一个session
  • session 过期处理
  • session 的存储(可以存储到内存、文件、数据库等)

关于 session 数据(sid与真实用户的映射)的存储,可以存放在服务端的一个文件中,比如该 session 第三方库:https://github.com/gorilla/sessions

【下面展示一下,用go的原生语句实现session的示例:】
cpy的,不用掌握,看看就行

  1. package main
  2. import(
  3. "fmt"
  4. "net/http"
  5. "github.com/gorilla/sessions"
  6. )
  7. // 利用cookie方式创建session,秘钥为 mykey
  8. var store = sessions.NewCookieStore([]byte("mykey"))
  9. func setSession(w http.ResponseWriter, r *http.Request){
  10. session, _ := store.Get(r, "sid")
  11. session.Values["username"] = "张三"
  12. session.Save(r, w)
  13. }
  14. func profile(w http.ResponseWriter, r *http.Request){
  15. session, _ := store.Get(r, "sid")
  16. if session.Values["username"] == nil {
  17. fmt.Fprintf(w, `未登录,请前往 localhost:8080/setSession`)
  18. return
  19. }
  20. fmt.Fprintf(w, `已登录,用户是:%s`, session.Values["username"])
  21. return
  22. }
  23. func main() {
  24. // 访问隐私页面
  25. http.HandleFunc("/profile", profile)
  26. // 设置session
  27. http.HandleFunc("/setSession", setSession)
  28. server := http.Server{
  29. Addr: ":8080",
  30. }
  31. server.ListenAndServe()
  32. }

在企业级开发中,经常使用额外的数据库redis来存储session数据


5. 细说jwt

5.1 jwt介绍

session将数据存储在了服务端,无端造成了服务端空间浪费,可否像cookie那样将用户数据存储在客户端,而不被黑客破解到呢?
JWT是json web token缩写,它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证

/* 重点

JWT和session有所不同,session需要在服务器端生成,服务器保存session,只返回给客户端sessionid,客户端下次请求时带上sessionid即可。因session是储存在服务器中,有多台服务器时会出现一些麻烦,需要同步多台主机的信息,不然会出现在请求A服务器时能获取信息,但是请求B服务器身份信息无法通过。JWT能很好的解决这个问题,服务器端不用保存jwt,只需要保存加密用的secret,在用户登录时将jwt加密生成并发送给客户端,由客户端存储,以后客户端的请求带上,由服务器解析jwt并验证。这样服务器不用浪费空间去存储登录信息,也不用浪费时间去做同步。

*/

5.2 jwt构成

三个部分:

  • header: 告诉我们使用的算法和 token 类型
  • Payload: 载荷又称为Claim ,必须使用 sub key 来指定用户 ID, 还可以包括其他信息如 email, username 等.
  • Signature: 用来保证 JWT 的真实性. 可以使用不同算法

【header】

  1. // base64编码的字符串`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9`
  2. // 这里规定了加密算法,hash256
  3. {
  4. "alg": "HS256",
  5. "typ": "JWT"
  6. }

【payload】

  1. {
  2. "sub": "1234567890", // 这个字段一定要有
  3. "name": "John Doe",
  4. "admin": true
  5. }
  6. // base64编码的字符串`eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9`

【 signature是用 header + payload + secret组合起来加密的,公式是: 】

  1. HMACSHA256(
  2. base64UrlEncode(header) + "." +
  3. base64UrlEncode(payload),
  4. secret
  5. )

这里 secret就是自己定义的一个随机字符串,这一个过程只能发生在 server 端,会随机生成一个 hash 值,这样组合起来之后就是一个完整的 jwt 了:

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.4c9540f793ab33b13670169bdf444c1eb1c37047f18e861981e14e34587b1e04

5.3 jwt设置有效期

token可以设置有效期,加入有效期是为了增加安全性,即token被黑客截获,也只能攻击较短时间。设置有效期就会面临token续签问题,解决方案如下
通常服务端设置两个token

  • Access Token:添加到 HTTP 请求的 header 中,进行用户认证,请求接口资源。
  • refresh token:用于当 Access Token过期后,客户端传递refresh token刷新 Access Token续期接口,获取新的Access Token和refresh token。其有效期比 Access Token有效期长。

说白了,Access Token 用于用户认证,refresh token 用于续命

5.4 jwt实现

首先得调用第三方包 “github.com/dgrijalva/jwt-go”

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/dgrijalva/jwt-go"
  5. "time"
  6. )
  7. const (
  8. SECRETKEY = "243223ffslsfsldfl412fdsfsdf"//私钥
  9. )
  10. //自定义Claims
  11. type CustomClaims struct {
  12. UserId int64
  13. jwt.StandardClaims
  14. }
  15. func main() {
  16. //生成token
  17. maxAge:=60*60*24
  18. customClaims :=&CustomClaims{
  19. UserId: 11,//用户id
  20. StandardClaims: jwt.StandardClaims{
  21. ExpiresAt: time.Now().Add(time.Duration(maxAge)*time.Second).Unix(), // 过期时间,必须设置
  22. Issuer:"jerry", // 非必须,也可以填充用户名,
  23. },
  24. }
  25. //采用HMAC SHA256加密算法
  26. token:=jwt.NewWithClaims(jwt.SigningMethodHS256, customClaims)
  27. tokenString,err:= token.SignedString([]byte(SECRETKEY))
  28. if err!=nil {
  29. fmt.Println(err)
  30. }
  31. fmt.Printf("token: %v\n", tokenString)
  32. //解析token
  33. ret,err :=ParseToken(tokenString)
  34. if err!=nil {
  35. fmt.Println(err)
  36. }
  37. fmt.Printf("userinfo: %v\n", ret)
  38. }
  39. //解析token
  40. func ParseToken(tokenString string)(*CustomClaims,error) {
  41. token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
  42. if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
  43. return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
  44. }
  45. return []byte(SECRETKEY), nil
  46. })
  47. if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
  48. return claims,nil
  49. } else {
  50. return nil,err
  51. }
  52. }
  1. jwt 的使用主要分为两个部分
  • 1)给用户生成token(tokenString)
  • 2)将token(tokenString)解析成对应的用户信息

封装一下功能,即:
image.png


参考:
[1] https://www.jianshu.com/p/dec83120b1fc
[2] https://pkg.go.dev/github.com/dgrijalva/jwt-go#NewWithClaims