前言

为什么使用JWT?

cookiesession

Cookie和Session

SessionIDSessionIDSessionIDCookiecookiesessiontoken

token (header.payload.signature)

tokentokentokentoken

token 安全性

tokentokentokentoken

基于token安全性的处理 access token 和 refresh token

access tokenatokenrefresh tokenrtoken
atokenrtokenatokenrtokenrtokenclient-sercetatokenrtokenrtoken
atokenrtokenclient-sercet

客户端与服务端基于无感刷新流程图

golang实现atoken和rtoken

go get -u github.com/golang-jwt/jwt/v4

颁发token

// GenToken 颁发token access token 和 refresh token
func GenToken(UserID int64, Username string) (atoken, rtoken string, err error) {
	rc := jwt.RegisteredClaims{
		ExpiresAt: getJWTTime(ATokenExpiredDuration),
		Issuer:    TokenIssuer,
	}
	at := MyClaim{
		UserID,
		Username,
		rc,
	}
	atoken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, at).SignedString(mySecret)

	// refresh token 不需要保存任何用户信息
	rt := rc
	rt.ExpiresAt = getJWTTime(RTokenExpiredDuration)
	rtoken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, rt).SignedString(mySecret)
	return
}
atokenrtoken
func (t *Token) SignedString(key interface{}) (string, error)
SignedString creates and returns a complete, signed JWT. The token is signed using the SigningMethod specified in the token.
SignedString

校验token

// VerifyToken 验证Token
func VerifyToken(tokenID string) (*MyClaim, error) {
	var myc = new(MyClaim)
	token, err := jwt.ParseWithClaims(tokenID, myc, keyFunc)
	if err != nil {
		return nil, err
	}
	if !token.Valid {
		return nil, ErrorInvalidToken
	}

	return myc, nil
}

根据传入的token值来判断是否有错误,如果错误为无效,说明token格式不正确。然后校验token是否过期。

无感刷新token

// RefreshToken 通过 refresh token 刷新 atoken
func RefreshToken(atoken, rtoken string) (newAtoken, newRtoken string, err error) {
	// rtoken 无效直接返回
	if _, err = jwt.Parse(rtoken, keyFunc); err != nil {
		return
	}
	// 从旧access token 中解析出claims数据
	var claim MyClaim
	_, err = jwt.ParseWithClaims(atoken, &claim, keyFunc)
	// 判断错误是不是因为access token 正常过期导致的
	v, _ := err.(*jwt.ValidationError)
	if v.Errors == jwt.ValidationErrorExpired {
		return GenToken(claim.UserID, claim.Username)
	}
	return
}

注释已经写得很明白了,会根据旧的atoken和rtoken来返回新token。

完整实现代码

package main

import (
	"errors"
	"time"

	"github.com/golang-jwt/jwt/v4"
)

const (
	ATokenExpiredDuration  = 2 * time.Hour
	RTokenExpiredDuration  = 30 * 24 * time.Hour
	TokenIssuer            = ""
)

var (
	mySecret          = []byte("xxxx")
	ErrorInvalidToken = errors.New("verify Token Failed")
)

type MyClaim struct {
	UserID   int64  `json:"user_id"`
	Username string `json:"username"`
	jwt.RegisteredClaims
}

func getJWTTime(t time.Duration) *jwt.NumericDate {
	return jwt.NewNumericDate(time.Now().Add(t))
}

func keyFunc(token *jwt.Token) (interface{}, error) {
	return mySecret, nil
}

// GenToken 颁发token access token 和 refresh token
func GenToken(UserID int64, Username string) (atoken, rtoken string, err error) {
	rc := jwt.RegisteredClaims{
		ExpiresAt: getJWTTime(ATokenExpiredDuration),
		Issuer:    TokenIssuer,
	}
	at := MyClaim{
		UserID,
		Username,
		rc,
	}
	atoken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, at).SignedString(mySecret)

	// refresh token 不需要保存任何用户信息
	rt := rc
	rt.ExpiresAt = getJWTTime(RTokenExpiredDuration)
	rtoken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, rt).SignedString(mySecret)
	return
}

// VerifyToken 验证Token
func VerifyToken(tokenID string) (*MyClaim, error) {
	var myc = new(MyClaim)
	token, err := jwt.ParseWithClaims(tokenID, myc, keyFunc)
	if err != nil {
		return nil, err
	}
	if !token.Valid {
		err = ErrorInvalidToken
		return nil, err
	}

	return myc, nil
}

// RefreshToken 通过 refresh token 刷新 atoken
func RefreshToken(atoken, rtoken string) (newAtoken, newRtoken string, err error) {
	// rtoken 无效直接返回
	if _, err = jwt.Parse(rtoken, keyFunc); err != nil {
		return
	}
	// 从旧access token 中解析出claims数据
	var claim MyClaim
	_, err = jwt.ParseWithClaims(atoken, &claim, keyFunc)
	// 判断错误是不是因为access token 正常过期导致的
	v, _ := err.(*jwt.ValidationError)
	if v.Errors == jwt.ValidationErrorExpired {
		return GenToken(claim.UserID, claim.Username)
	}
	return
}

SSO(Single Sign On)单用户登录以及无感刷新token

实现思路

tokentokentokentokenrediskey-valueuserIDtokentokentoken

实战代码

// parts[1]是获取到的atoken,我们使用之前定义好的解析JWT的函数来解析它
mc, err := jwt.VerifyToken(parts[1])
if err != nil {
    // 如果解析失败,可能是因为token过期,可以进入refreshToken进行判断
   if newAtoken, newRtoken, err := jwt.RefreshToken(parts[1],rtoken); err == nil {
       // 如果无错误,就更新redis中的token
      if err = redis.SetSingleUserToken(mc.Username, newAtoken); err == nil {
          // 这里根据需求返回给前端,由前端进行处理
         c.Writer.Header().Set("newAtoken", newAtoken)
         c.Writer.Header().Set("newRtoken", newRtoken)
         // 如果无错误,请求继续
          c.Next()
      }
   }
    // 这里使用的是gin框架, 如果有错误直接阻止并返回
   c.Abort()
   return
}
// 如果解析成功,就在redis中进行判断,是否单用户登录
// 通过获取redis中的token来校验是否单用户登录
token, err := redis.GetSingleUserToken(mc.Username)
if err != nil {
   serializer.ResponseError(c, e.CodeServerBusy)
   c.Abort()
   return
}

判断过程

  1. 请求从前端传来,经过认证中间件进行校验token,如果没有问题就进行redis单用户校验。
  2. 如果有问题,可能是token过期。进行无感刷新,如果刷新成功将新token设置在header中,请求继续
  3. 如果无感刷新失败请求阻止。

小结

  • token需要保存在客户端中,由前端代码进行管理。后端只需做校验和刷新处理。
  • 如果使用双token就用无感刷新。
  • 如果使用单token就用token校验。
  • SSO单点登录限制可以通过redis实现。