【GoWeb开发】基于Cookie、Session和基于JWT Token的认证模式介绍

用户认证


HTTP是一个无状态的协议,一次请求结束后,下次在发送服务器就不知道这个请求是谁发来的了(同一个IP不代表同一个用户),在Web应用中,用户的认证和鉴权是非常重要的一环,实践中有多种可用方案,并且各有千秋。

Cookie- Session 认证模式

在Web应用发展的初期,大部分采用基于Cookie-Session的会话管理方式,逻辑如下。

  • 客户端使用用户名、密码进行认证

  • 服务端验证用户名、密码正确后生成并存储Session,将SessionID通过Cookie返回给客户端

  • 客户端访问需要认证的接口时在Cookie 中携带SessionlD

  • 服务端通过SessionID查找Session并进行鉴权,返回给客户端需要的数据

img

基于Session的方式存在多种问题。

  • 服务端需要存储Session,并且由于Session需要经常快速查找,通常存储在内存或内存数据库中,同时在线用户较多时需要占用大量的服务器资源。

  • 当需要扩展时,创建Session的服务器可能不是验证Session的服务器,所以还需要将所有Session单独存储并共享。

  • 由于客户端使用Cookie存储SessionlD,在跨域场景下需要进行兼容性处理,同时这种方式也难以防范CSRF攻击。


Token认证模式

鉴于基于Session 的会话管理方式存在上述多个缺点,基于Token 的无状态会话管理方式诞生了,所谓无状态,就是服务端可以不再存储信息,甚至是不再存储Session,逻辑如下。

  • 客户端使用用户名、密码进行认证

  • 服务端验证用户名、密码正确后生成Token返回给客户端

  • 客户端保存Token,访问需要认证的接口时在URL参数或HTTP Header中加入Token

  • 服务端通过解码Token进行授权,返回给客户端需要的数据

img


JWT介绍

JWT是JSON Web Token 的缩写,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC7519)。JWT本身没有定义任何技术实现,它只是定义了一种基于Token的会话管理的规则,涵盖Token需要包含的标准内容和Token 的生成过程,特别适用于分布式站点的单点登录(SSO)场景。

一个JWT Token 就像这样:

eyJhbGcioiJIUzI1NiIsInR5cCI6IkpxVCJ9
.eyJ1c2VyX2lkIjoyODAxODcyNzQ4ODMyMZU4NSwiZXhwIjoxNTkONTQwMjkxLCJpc3MiOiJibHVlYmVsbCJ9
.lk_ZrAtYGCeZhK3iupHxP1kgjBTzQTVTtX0izYFx9wU

它是由.分隔的三部分组成,这三部分依次是:

  • 头部(Header)

  • 负载(Payload)

  • 签名(Signature)

头部和负载以JSON形式存在,这就是JWT中的JSON,三部分的内容都分别单独经过了Base64编码,以.拼接成一个JWT Token。

img

Header

JWT的Header中存储了所使用的加密算法和Token类型。

{
    
    
	"alg": "HS256",
  "TYP": "jwt"
}

Payload

Payload表示负载(将Token当做是一个载体,表示Token里面装的是什么),也是一个JSON对象,JWT规定了7个官方字段供选用,

iss (issuer)︰签发人
exp ( expiration time):过期时间
sub ( subject)︰主题
aud (audience)︰受众
nbf (Not Before):生效时间
iat ( Issued At)︰签发时间
jti(JwT ID)∶编号

除了官方字段,开发者也可以自己指定字段和内容,例如下面的内容。

{
    
    
  "sub" : "1234567890",
  "name " : "John Doe" ,
  "admin " : true
}

注意,JWT默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个JSON对象也要使用Base64URL算法转成字符串。

Signature

Signature部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

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

JWT优缺点

JWT拥有基于Token的会话管理方式所拥有的一切优势,不依赖Cookie,使得其可以防止CSRF攻击,也能在禁用Cookie的浏览器环境中正常运行。

而JWT的最大优势是服务端不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储Session所需要引入的Redis等组件,降低了系统架构复杂度。但这也是JWT最大的劣势,由于有效期存储在Token 中,JWTToken一旦签发,就会在有效期内一直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的JWT Token,如果需要禁用用户,单纯使用JWT就无法做到了。

基于jwt实现认证实践

前面讲的Token,都是Access Token,也就是访问资源接口时所需要的Token,还有另外一种

Token,Refresh Token,通常情况下,Refresh Token的有效期会比较长,而Access Token 的有效期比较短,当Access Token由于过期而失效时,使用Refresh Token就可以获取到新的Access Token如果Refresh Token也失效了,用户就只能重新登录了。

在JWT的实践中,引入Refresh Token,将会话管理流程改进如下。

  • 客户端使用用户名密码进行认证

  • 服务端生成有效时间较短的Access Token(例如10分钟),和有效时间较长的RefreshToken(例如7天)

  • 客户端访问需要认证的接口时,携带Access Token

  • 如果Access Token没有过期,服务端鉴权后返回给客户端需要的数据

  • 如果携带Access Token访问需要认证的接口时鉴权失败(例如返回401错误),则客户端使用Refresh Token向刷新接口申请新的Access Token

  • 如果Refresh Token没有过期,服务端向客户端下发新的Access Token客户端使用新的Access Token访问需要认证的接口

img

后端需要对外提供一个刷新Token的接口,前端需要实现一个当Access Token过期时自动请求刷新Token接口获

取新Access Token的拦载器。

gin框架使用jwt

jwt-go库的基本使用详见:在gin框架中使用JWT

package jwt

import (
	"errors"
	"time"

	"github.com/dgrijalva/jwt-go"
)

// MyClaims 自定义声明结构体并内嵌jwt.StandardClaims
// jwt包自带的jwt.StandardClaims只包含了官方字段
// 我们这里需要额外记录一个UserID字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type MyClaims struct {
    
    
	UserID uint64 `json:"user_id"`
	Username string `json:"username"`
	jwt.StandardClaims
}
//定义Secret
var mySecret = []byte("夏天夏天悄悄过去")

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

//定义JWT的过期时间
const TokenExpireDuration = time.Hour * 2

/**
 * @Author huchao
 * @Description //TODO 生成JWT
 * @Date 9:42 2022/2/11
 **/
// GenToken 生成access token 和 refresh token
func GenToken(userID uint64,username string) (aToken, rToken string, err error) {
    
    
	// 创建一个我们自己的声明
	c := MyClaims{
    
    
		userID, // 自定义字段
		"username",	// 自定义字段
		jwt.StandardClaims{
    
    	// JWT规定的7个官方字段
			ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
			Issuer:    "bluebell",                                 // 签发人
		},
	}
	// 加密并获得完整的编码后的字符串token
	aToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, c).SignedString(mySecret)

	// refresh token 不需要存任何自定义数据
	rToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
    
    
		ExpiresAt: time.Now().Add(time.Second * 30).Unix(), // 过期时间
		Issuer:    "bluebell",                              // 签发人
	}).SignedString(mySecret)
	// 使用指定的secret签名并获得完整的编码后的字符串token
	return
}
//GenToken 生成 Token
func GenToken2(userID uint64, username string) (Token string, err error) {
    
    
	// 创建一个我们自己的声明
	c := MyClaims{
    
    
		userID, 			// 自定义字段
		"username",	// 自定义字段
		jwt.StandardClaims{
    
    	// JWT规定的7个官方字段
			ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
			Issuer:    "bluebell",                                 // 签发人
		},
	}
	// 加密并获得完整的编码后的字符串token
	Token, err = jwt.NewWithClaims(jwt.SigningMethodHS256, c).SignedString(mySecret)

	// refresh token 不需要存任何自定义数据
	//rToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
    
    
	//	ExpiresAt: time.Now().Add(time.Second * 30).Unix(), // 过期时间
	//	Issuer:    "bluebell",                              // 签发人
	//}).SignedString(mySecret)	// 使用指定的secret签名并获得完整的编码后的字符串token
	return
}

/**
 * @Author huchao
 * @Description //TODO 解析JWT
 * @Date 9:43 2022/2/11
 **/
func ParseToken(tokenString string) (claims *MyClaims, err error) {
    
    
	// 解析token
	var token *jwt.Token
	claims = new(MyClaims)
	token, err = jwt.ParseWithClaims(tokenString, claims, keyFunc)
	if err != nil {
    
    
		return
	}
	if !token.Valid {
    
     // 校验token
		err = errors.New("invalid token")
	}
	return
}

// RefreshToken 刷新AccessToken
func RefreshToken(aToken, rToken string) (newAToken, newRToken string, err error) {
    
    
	// refresh token无效直接返回
	if _, err = jwt.Parse(rToken, keyFunc); err != nil {
    
    
		return
	}

	// 从旧access token中解析出claims数据	解析出payload负载信息
	var claims MyClaims
	_, err = jwt.ParseWithClaims(aToken, &claims, keyFunc)
	v, _ := err.(*jwt.ValidationError)

	// 当access token是过期错误 并且 refresh token没有过期时就创建一个新的access token
	if v.Errors == jwt.ValidationErrorExpired {
    
    
		return GenToken(claims.UserID,claims.Username)
	}
	return
}

鉴权中间件开发

const (
	ContextUserIDKey = "userID"
)

var (
	ErrorUserNotLogin = errors.New("当前用户未登录")
)

// JWTAuthMiddleware 基于JWT的认证中间件
func JWTAuthMiddleware() func(c *gin.Context) {
    
    
	return func(c *gin.Context) {
    
    
		// 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI
		// 这里假设Token放在Header的Authorization中,并使用Bearer开头
		// 这里的具体实现方式要依据你的实际业务情况决定
		authHeader := c.Request.Header.Get("Authorization")
		if authHeader == "" {
    
    
			controller.ResponseErrorWithMsg(c, controller.CodeInvalidToken, "请求头缺少Auth Token")
			c.Abort()
			return
		}
		// 按空格分割
		parts := strings.SplitN(authHeader, " ", 2)
		if !(len(parts) == 2 && parts[0] == "Bearer") {
    
    
			controller.ResponseErrorWithMsg(c, controller.CodeInvalidToken, "Token格式不对")
			c.Abort()
			return
		}
		// parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
		mc, err := jwt.ParseToken(parts[1])
		if err != nil {
    
    
			fmt.Println(err)
			controller.ResponseError(c, controller.CodeInvalidToken)
			c.Abort()
			return
		}
		// 将当前请求的userID信息保存到请求的上下文c上
		c.Set(.ContextUserIDKey, mc.UserID)
		c.Next() // 后续的处理函数可以用过c.Get(ContextUserIDKey)来获取当前请求的用户信息
	}
}

生成access token和refresh token

// GenToken 生成access token 和 refresh token 
func GenToken(userID uint64, username string) (Token string, err error) {
    
    
	// 创建一个我们自己的声明
	c := MyClaims{
    
    
		userID, 			// 自定义字段
		"username",	// 自定义字段
		jwt.StandardClaims{
    
    	// JWT规定的7个官方字段
			ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
			Issuer:    "bluebell",                                 // 签发人
		},
	}
	// 加密并获得完整的编码后的字符串token
	Token, err = jwt.NewWithClaims(jwt.SigningMethodHS256, c).SignedString(mySecret)

	// refresh token 不需要存任何自定义数据
	//rToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
    
    
	//	ExpiresAt: time.Now().Add(time.Second * 30).Unix(), // 过期时间
	//	Issuer:    "bluebell",                              // 签发人
	//}).SignedString(mySecret)	// 使用指定的secret签名并获得完整的编码后的字符串token
	return
}

解析access token

// 解析JWT
func ParseToken(tokenString string) (claims *MyClaims, err error) {
    
    
	// 解析token
	var token *jwt.Token
	claims = new(MyClaims)
	token, err = jwt.ParseWithClaims(tokenString, claims, keyFunc)
	if err != nil {
    
    
		return
	}
	if !token.Valid {
    
     // 校验token
		err = errors.New("invalid token")
	}
	return
}

refresh token

// RefreshToken 刷新AccessToken
func RefreshToken(aToken, rToken string) (newAToken, newRToken string, err error) {
    
    
	// refresh token无效直接返回
	if _, err = jwt.Parse(rToken, keyFunc); err != nil {
    
    
		return
	}

	// 从旧access token中解析出claims数据
	var claims MyClaims
	_, err = jwt.ParseWithClaims(aToken, &claims, keyFunc)
	v, _ := err.(*jwt.ValidationError)

	// 当access token是过期错误 并且 refresh token没有过期时就创建一个新的access token
	if v.Errors == jwt.ValidationErrorExpired {
    
    
		return GenToken(claims.UserID)
	}
	return
}

相关参考链接

  • https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

猜你喜欢

转载自blog.csdn.net/qq_45696377/article/details/122891184