jwt和paseto比较
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 on 2022-09-01 22:40 myworldworld 阅读(805) 评论(0) 收藏 举报