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,里面包含有用户名的验证信息,这样就可以保证用户只可以访问自己的相关内容
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理