GoWeb开发基于CookieSession和基于JWT Token的认证模式介绍

Posted 胡毛毛_三月

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GoWeb开发基于CookieSession和基于JWT Token的认证模式介绍相关的知识,希望对你有一定的参考价值。

目录

用户认证


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

Cookie- Session 认证模式

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

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

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

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

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

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

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

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

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


Token认证模式

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

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

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

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

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


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。

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访问需要认证的接口

后端需要对外提供一个刷新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

以上是关于GoWeb开发基于CookieSession和基于JWT Token的认证模式介绍的主要内容,如果未能解决你的问题,请参考以下文章

接口的鉴权cookiesession和token多测师

GOweb框架

系统开发系列 之web开发中cookiesession和token的使用

Gin框架快速入门

Gin框架快速入门

goweb-搭建服务