golang之jwt的token登录

什么是 JSON Web Token?

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 方式安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用** HMAC 算法)或使用 RSA  ECDSA 的公钥/私钥对对 JWT 进行签名**。

直白的讲 jwt 就是一种用户认证(区别于 session、cookie)的解决方案。

出现的背景

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

这里我们先了解一下 session,cookie。

session

熟悉 session 运行机制的同学都知道,用户的 session 数据以 file 或缓存(redis、memcached)等方式存储在服务器端,客户端浏览器 cookie 中只保存 sessionid。服务器端 session 属于集中存储,数量不大的情况下,没什么问题,当用户数据逐渐增多到一程度,就会给服务端管理和维护带来大的负担。

session 有两个弊端:

1、无法实现跨域。

2、由于 session 数据属于集中管理里,量大的时候服务器性能是个问题。

优点:

1、session 存在服务端,数据相对比较安全。

2、session 集中管理也有好处,就是用户登录、注销服务端可控。

cookie

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

cookie 避免 session 集中管理的问题,但也存在弊端:

1、跨域问题。

2、数据存储在浏览器端,数据容易被窃取及被 csrf 攻击,安全性差。

优点:

1、相对于 session 简单,不用服务端维护用户认证信息。

2、数据持久性。

jwt

jwt 通过 json 传输,php、java、golang 等很多语言支持,通用性比较好,不存在跨域问题。传输数据通过数据签名相对比较安全。客户端与服务端通过 jwt 交互,服务端通过解密 token 信息,来实现用户认证。不需要服务端集中维护 token 信息,便于扩展。当然 jwt 也有其缺点。

缺点:

1、用户无法主动登出,只要 token 在有效期内就有效。这里可以考虑 redis 设置同 token 有效期一直的黑名单解决此问题。

2、token 过了有效期,无法续签问题。可以考虑通过判断旧的 token 什么时候到期,过期的时候刷新 token 续签接口产生新 token 代替旧 token。

jwt 设置有效期

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

通常服务端设置两个 token

  • Access Token:添加到 HTTP 请求的 header 中,进行用户认证,请求接口资源。 一般该值有效期比较短, 例如:10min
  • refresh token:用于当 Access Token 过期后,客户端传递 refresh token 刷新 Access Token 续期接口,获取新的 Access Token 和 refresh token。其有效期比 Access Token 有效期长。例如:7d

jwt 构成:

  • Header:TOKEN 的类型,就是 JWT,签名的算法,如 HMAC SHA256、HS384
  • Payload:载荷又称为 Claim,携带的信息,比如用户名、过期时间等,一般叫做 Claim
  • Signature:签名,是由 header、payload 和你自己维护的一个 secret 经过加密得来的

jwt 使用

这里推荐个使用比较多的开源项目、[github.com/dgrijalva/jwt-go](),更多文档。

示例:

package main

import (
    "fmt"
    "github.com/dgrijalva/jwt-go"
    "time"
)
const (
    SECRETKEY = "243223ffslsfsldfl412fdsfsdf"//私钥
)
//自定义 Claims
type CustomClaims struct {
    UserId int64
    jwt.StandardClaims
}
func main() {
    //生成 token
    maxAge:=60*60*24
    customClaims :=&CustomClaims{
        UserId: 11,//用户 id
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(time.Duration(maxAge)*time.Second).Unix(), // 过期时间,必须设置
            Issuer:"jerry",   // 非必须,也可以填充用户名,
        },
    }
    //采用 HMAC SHA256 加密算法
    token:=jwt.NewWithClaims(jwt.SigningMethodHS256, customClaims)
    tokenString,err:= token.SignedString([]byte(SECRETKEY))
    if err!=nil {
        fmt.Println(err)
    }
    fmt.Printf("token: %v\n", tokenString)

    //解析 token
    ret,err :=ParseToken(tokenString)
    if err!=nil {
        fmt.Println(err)
    }
    fmt.Printf("userinfo: %v\n", ret)
}

//解析 token
func ParseToken(tokenString string)(*CustomClaims,error)  {
    token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(SECRETKEY), nil
    })
    if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
        return claims,nil
    } else {
        return nil,err
    }
}

 

Claims:

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"`

aud: 接收 jwt 的一方
exp: jwt 的过期时间,这个过期时间必须要大于签发时间
jti: jwt 的唯一身份标识,主要用来作为一次性 token, 从而回避重放攻击。
iat: jwt 的签发时间
iss: jwt 签发者
nbf: 定义在什么时间之前,该 jwt 都是不可用的。就是这条 token 信息生效时间。这个值可以不设置,但是设定后,一定要大于当前 Unix UTC, 否则 token 将会延迟生效。
sub: jwt 所面向的用户

 

 

示例:

package main

import (
    "fmt"
    "github.com/dgrijalva/jwt-go"
    "time"
)
const (
    SECRETKEY = "243223ffslsfsldfl412fdsfsdf"//私钥
)
//自定义 Claims
type CustomClaims struct {
    UserId int64
    jwt.StandardClaims
}
func main() {
    //生成 token
    maxAge:=60*60*24
    // Create the Claims
    //claims := &jwt.StandardClaims{
    //    //    ExpiresAt: time.Now().Add(time.Duration(maxAge)*time.Second).Unix(), // 过期时间,必须设置,
    //    //    Issuer:    "jerry",// 非必须,也可以填充用户名,
    //    //}

    //或者用下面自定义 claim
    claims := jwt.MapClaims{
        "id":       11,
        "name":       "jerry",
        "exp": time.Now().Add(time.Duration(maxAge)*time.Second).Unix(), // 过期时间,必须设置,
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString([]byte(SECRETKEY))
    if err!=nil {
        fmt.Println(err)
    }
    fmt.Printf("token: %v\n", tokenString)

    //解析 token
    ret,err :=ParseToken(tokenString)
    if err!=nil {
        fmt.Println(err)
    }
    fmt.Printf("userinfo: %v\n", ret)
}

//解析 token
func ParseToken(tokenString string)(jwt.MapClaims,error)  {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Don't forget to validate the alg is what you expect:
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }

        // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
        return []byte(SECRETKEY), nil
    })
    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        return claims,nil
    } else {
        return nil,err
    }
}

 

前面已经说明了,Token在有效期内可以一直访问资源,就产生了使用refresh_token来刷新access_token, 其中将access_token设置有效期时间段一些, refresh_token会设置的时间长一些, 如果access_token被窃取,只可以在较短的时间段内进行资源的访问,再次使用refresh_token去获取access_token的时候, 可以通过对refresh_token的控制来控制access_token的获取. 就此引入refresh_token:

  • 客户端使用用户名密码进行认证
  • 服务端生成有效时间较短的 Access Token(例如 10 分钟),和有效时间较长的 Refresh Token(例如 7 天)
  • 客户端访问需要认证的接口时,携带 Access Token
  • 如果 Access Token 没有过期,服务端鉴权后返回给客户端需要的数据
  • 如果携带 Access Token 访问需要认证的接口时鉴权失败(例如返回 401 错误),则客户端使用 Refresh Token 向刷新接口申请新的 Access Token
  • 如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token
  • 客户端使用新的 Access Token 访问需要认证的接口

 

 

将生成的 Refresh Token 以及过期时间存储在服务端的数据库中,由于 Refresh Token 不会在客户端请求业务接口时验证,只有在申请新的 Access Token 时才会验证,所以将 Refresh Token 存储在数据库中,不会对业务接口的响应时间造成影响,也不需要像 Session 一样一直保持在内存中以应对大量的请求。

上述的架构,提供了服务端禁用用户 Token 的方式,当用户需要登出或禁用用户时,只需要将服务端的 Refresh Token 禁用或删除,用户就会在 Access Token 过期后,由于无法获取到新的 Access Token 而再也无法访问需要认证的接口。这样的方式虽然会有一定的窗口期(取决于 Access Token 的失效时间),但是结合用户登出时客户端删除 Access Token 的操作,基本上可以适应常规情况下对用户认证鉴权的精度要求。

 

 

 

 

小结:

  • 服务端生成的 jwt 返回客户端可以存到 cookie 也可以存到 localStorage 中(相比 cookie 容量大),存在 cookie 中需加上 HttpOnly 的标记,可以防止 XSS) 攻击。
  • 尽量用 https 带证书网址访问。
  • session 和 jwt 没有绝对好与不好,各有其擅长的应用环境,请根据实际情况选择。

 

 

 

 

 

 

 

posted @ 2022-08-15 20:01  X-Wolf  阅读(1095)  评论(0编辑  收藏  举报