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 @ 2022-04-01 13:26  aganippe  阅读(1177)  评论(0编辑  收藏  举报