golang gin后端开发框架(四):JWT和PASETO校验中间件

1. Token-based Authentication

在这种验证机制中,用户第一次登录需要POST自己的用户名和密码,在服务器端检验用户名和密码正确之后,就可以签署一个令牌,并将其返回给客户端

在此之后,客户端就可以用这个access_token来访问服务器上的资源,服务器只会验证该令牌是否有效

同时,access_token有一定的生命周期,在这个周期内,客户端都可以通过这个token来访问服务器的资源

 

2. JWT

 JWT -- JSON Web Token

 

2.1 JWT简介

JWT是一个base64编码的字符串,主要由三部分组成:

  • header
  • payload
  • verify signature

 

其中header和payload是base64编码的,而没有加密,这意味着我们可以编码或者解码任意的payload,但是最后的蓝色部分,也就是JWT签名,保证了只有服务器有私钥来签署这个token

JWT提供了很多签名算法,可以分为以下几类:

  • 对称秘钥加密算法:适用于共享秘钥的场景,本地,典型的算法有:HS256、HS384、HS512
  • 非对称加密算法:私钥对token签名,公钥验证token,可以提供第三方服务,典型的算法有:RS256、PS256、ES256等

 

JWT的问题是什么?

(1)不安全的加密算法

  JWT给开发者提供了很多的加密算法选择,其中就包括了已知的易受攻击的算法

 (2)在header中包含了签名算法的种类

  攻击者只需要将header中的alg字段设置为none就可以绕过签名验证过程

  在知道服务器使用非对称加密算法的情况下,修改alg为一个对称加密算法

 

2.2 在golang中实现JWT

首先我们定义一个token maker的接口,在之后会使用PASETO和JWT来实现这个接口

Maker接口包括了两个方法,分别是创建token和验证token:

type Maker interface {
// CreateToken 创建一个token
CreateToken(username string, duration time.Duration) (string, error)
// VerifyToken 验证token
VerifyToken(token string) (*Payload, error)
}

  

现在定义token的payload结构体,其中应该包含一些我们需要的字段,一般意义上就是用户名、创建时间、过期时间、tokenID这几个信息:

type Payload struct {
ID uuid.UUID `json:"id" `
Username string `json:"username" `
IssuedAt time.Time `json:"issued_at" `
ExpiredAt time.Time `json:"expired_at" `
}

 

然后对外提供一个创建payload的函数:

func NewPayload(username string, duration time.Duration) (*Payload, error) {
tokenID, err := uuid.NewRandom()
if err != nil {
return nil, err
}
payload := &Payload{
ID: tokenID,
Username: username,
IssuedAt: time.Now(),
ExpiredAt: time.Now().Add(duration),
}
return payload, nil
}

  

现在我们就可以开始实现JWT token的代码了,其需要实现Maker接口定义的两个方法

func (maker *JWTMaker) CreateToken(username string, duration time.Duration) (string, error) {
payload, err := NewPayload(username, duration)
if err != nil {
return "", err
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
return jwtToken.SignedString([]byte(maker.secretKey))
}

 

值得注意的是,在jwt.NewWithClaims()方法中,我们传入payload时会报错,仔细看提示会发现jwt需要我们定义的payload结构体提供一个验证功能,就是一个 func(payload *Payload) Valid() error 签名的函数

我们就可以做一个简单的过期时间验证:

func (payload *Payload) Valid() error {
if time.Now().After(payload.ExpiredAt) {
return ErrExpiredToken
}
return nil
}

 

同样,我们再去实现验证token的方法:

func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {
keyFunc := func(token *jwt.Token) (interface{}, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, ErrInvalidToken
}
return []byte(maker.secretKey), nil
}
jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc)
if err != nil {
verr, ok := err.(*jwt.ValidationError)
if ok && errors.Is(verr.Inner, ErrExpiredToken) {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
payload, ok := jwtToken.Claims.(*Payload)
if !ok {
return nil, ErrInvalidToken
}
return payload, nil
}

 

 在 jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc) 中

keyFunc需要我们自己实现,其作用是验证header中的签名算法是否合法,防止一些琐碎的攻击

同样err在jwt包内部是被隐藏的,对于验证失败的令牌有两种情况:令牌过期或者令牌不合法

所以我们需要做一次类型断言,找出具体的错误来做返回

 

3. PASETO

PASETO -- Platform-Agnostic SEcurity TOkens

 

3.1 PASETO简介

每一个版本的PASETO都包含了强大的加密套件,选择对应的加密算法只需要选择PASETO版本即可

最多只能有两个版本同时处于活跃状态

 

 

相比于JWT,PASETO所做的改变在于:

  • 不会向用户开放所有的加密算法
  • header中不再含有alg字段,也不会有none算法
  • payload使用加密算法,而不是简单的编码

 

PASETO的令牌结构:

 

 

 

3.2 在golang中实现PASETO

PASETO的实现要比JWT简单一些,我们同样还是使用对称加密算法来实现,首先是创建token的方法:

func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, error) {
payload, err := NewPayload(username, duration)
if err != nil {
return "", err
}
return maker.paseto.Encrypt(maker.symmetricKey, payload, nil)
}

  

然后是验证token:

func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) {
payload := &Payload{}
if err := maker.paseto.Decrypt(token, maker.symmetricKey, payload, nil); err != nil {
return nil, ErrInvalidToken
}
if err := payload.Valid(); err != nil {
return nil, err
}
return payload, nil
}

  

至此我们就完成了PASETO对称加密的token

 

4. 实现token验证中间件

首先客户端需要提供登录信息,包括了用户名和密码。然后服务器创建一个token返回给客户端,用于之后的身份验证

const (
authorizationHeaderKey = "authorization"
authorizationTypeBearer = "bearer"
authorizationPayloadKey = "authorization_payload"
)
func authMiddleware(tokenMaker Maker) gin.HandlerFunc {
return func(ctx *gin.Context) {
authorizationHeader := ctx.GetHeader(authorizationHeaderKey)
if len(authorizationHeader) == 0 {
err := errors.New("authorization header is not provide")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
return
}
fields := strings.Fields(authorizationHeader)
if len(fields) < 2 {
err := errors.New("invalid authorization header format")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
return
}
authorizationType := strings.ToLower(fields[0])
if authorizationType != authorizationTypeBearer {
err := fmt.Errorf("unsupported authorization type %s", authorizationType)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
return
}
accessToken := fields[1]
payload, err := tokenMaker.VerifyToken(accessToken)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
return
}
ctx.Set(authorizationPayloadKey, payload)
ctx.Next()
}
}

  

然后我们可以将需要授权的api做一个路由组,使用这个中间件

同时我们在授权阶段可以简单的使用一个ctx.MustGet()方法来取得token中的payload,里面包含有用户名的验证信息,这样就可以保证用户只可以访问自己的相关内容

posted @   aganippe  阅读(1503)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示