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的认证模式介绍的主要内容,如果未能解决你的问题,请参考以下文章