这周在个人项目上完成了登录注册,其中便涉及到一些认证问题,接触到了jwt,全称JSON Web Token。现在很多项目都在使用这个作为session的替代。那么我们先来看看他的组成
组成
JSON Web Token(JWT)是一种开放标准(RFC 7519),定义了一种紧凑且自包含的方式,用于以JSON对象的形式在各方之间安全地传输信息。该信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
总结出以下几点:以JSON格式传输、传输中包含一些隐私信息、可以公钥或私钥进行签名
JSON Web Token由三个由点(.)分隔的部分组成,分别是:
- 标头(Header)
- 有效负载(Payload)
- 签名(Signature)
因此,JWT通常看起来像这样:
xxx.yyyyy.zzzz
接下来进行具体分析讲解
标头(Header): 标头通常包含两部分:令牌的类型(JWT)和使用的签名算法。例如:
json
{
"alg": "HS256",
"typ": "JWT"
}
然后,此JSON进行Base64Url编码,形成JWT的第一部分。
有效负载(Payload): 有效负载包含声明(CLaim)。声明是关于实体(通常是用户)和附加数据的陈述。有三种类型的声明:注册、公共和私有声明。
- Registered claims
可以想成是标准公认的一些讯息,可以选择性放,例如:
- iss(Issuer):JWT签发者
- exp(Expiration Time):JWT的过期时间,过期时间必须大于签发JWT时间
- sub(Subject):JWT所面向的用户
- aud(Audience):接收JWT的一方
- nbf(Not Before):也就是定义拟发放JWT之后,的某段时间点前该JWT仍旧是不可用的
- iat(Issued At):JWT签发时间
- jti(JWT id):JWT的身份标识,每个JWT的id都应该是不重复的,避免重复发放
- Public claims
这个,可以想成是传递的栏位必须是跟上面Registered claims栏位不能冲突,然后可以向官方申请定义公开声明,会进行审核等步骤,实务上在开发上是不太会用这部分的。
- Private claims
这个就是发放JWT伺服器可以自定义的栏位的部分,例如实务上会放User Account、User Name、User Role等不敏感的数据。
例如:
JSON
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后,有效负载进行Base64Url编码,形成JWT的第二部分。
签名(Signature): 由经过指定算法加密后的header和payload组成,以及自己提供的secret
secret是存储在服务端中的,如果客户端获得了则可以随意生成jwt
例如,如果要使用HMAC SHA256算法,签名将以以下方式创建:
scss
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
签名用于验证消息在传递过程中未被更改,并且对于使用私钥签名的令牌,它还可以验证JWT的发送方是其所声称的身份。
将所有部分组合在一起,输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递,与基于XML的标准(如SAML)相比更为紧凑。
但是由于编码方式是固定的,所以我们可以很清楚的获取到jwt中除签名以外的任何信息。我们确认信息的真伪则是通过签名。可以通过 JWT.IO 查看jwt包含的信息
用处
- 授权: 这是使用JWT的最常见场景。一旦用户登录,每个后续请求都将包含JWT,允许用户访问具有该令牌允许的路由、服务和资源。由于JWT具有小的开销并且易于在不同域之间使用,单点登录(Single Sign On)是一种广泛使用JWT的功能。
- 信息交换: JSON Web Tokens是安全传输信息的一种良好方式。由于JWT可以签名,例如使用公钥/私钥对,您可以确信发送方是其声称的身份。此外,由于签名是使用标头和有效负载计算的,您还可以验证内容是否未被篡改。
JSON Web Tokens是如何工作的?
在身份验证中,用户使用其凭据成功登录后,将返回一个JSON Web Token。由于令牌是凭据,必须小心以防止安全问题。通常情况下,不应该将令牌保存得比所需时间更长。
用户想要访问受保护的路由或资源时,用户代理应该发送JWT,通常使用Bearer模式在Authorization标头中。标头的内容应该如下所示:
Authorization: Bearer <token>
在某些情况下,这可能是一种无状态的授权机制。服务器的受保护路由将检查Authorization标头中是否有有效的JWT,如果存在,则允许用户访问受保护的资源。如果JWT包含必要的数据,则可能减少对数据库进行某些操作的需求,尽管这并非总是如此。
请注意,如果通过HTTP标头发送JWT令牌,应尽量防止它们变得过大。一些服务器不接受超过8 KB的标头。如果尝试在JWT令牌中嵌入太多信息(例如包括所有用户权限),可能需要使用其他解决方案,如Auth0 Fine-Grained Authorization。
如果令牌在Authorization标头中发送,跨源资源共享(CORS)将不是一个问题,因为它不使用cookie。
以下图表显示了如何获取JWT并将其用于访问API或资源的过程:
实际使用
首先我们定义一个声明
go
type Claims struct {
UserID string `json:"user_id"`
AppKey string `json:"app_key"`
jwt.StandardClaims
}
一共实现以下方法,分别为获取secret、生成、解析
go
type JwtPort interface {
GetJWTSecret() []byte
GenerateToken(userid string, AppKey string) (string, error)
ParseToken(tokenString string) (*Claims, error)
}
使用以下外部库
go
go get -u github.com/dgrijalva/jwt-go
具体实现:
go
// JWTAdapters is a struct that encapsulates functionality related to JWT (JSON Web Token) operations.
type JWTAdapters struct {
logger logger.LoggerPorts
}
// NewJWTAdapters is a constructor function for JWTAdapters, initializing it with the provided logger.
func NewJWTAdapters(logger logger.LoggerPorts) *JWTAdapters {
return &JWTAdapters{
logger: logger,
}
}
// GetJWTSecret returns the JWT secret key from the global configuration.
func (j JWTAdapters) GetJWTSecret() []byte {
return []byte(global.JWTSetting.Secret)
}
// GenerateToken generates a JWT token with the specified user ID and application key.
// It returns the generated token as a string and any error encountered during the process.
func (j JWTAdapters) GenerateToken(userid string, AppKey string) (string, error) {
now := time.Now()
expireTime := now.Add(7200)
claims := middleware.Claims{
UserID: util.EncodingMD5(userid),
AppKey: util.EncodingMD5(AppKey),
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireTime.Unix(),
Issuer: global.JWTSetting.Issuer,
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return tokenClaims.SignedString(j.GetJWTSecret())
}
// ParseToken parses the provided JWT token string and returns the claims if the token is valid.
// It returns an error if the token is invalid or any other error occurs during parsing.
func (j JWTAdapters) ParseToken(tokenString string) (*middleware.Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(tokenString, &middleware.Claims{}, func(token *jwt.Token) (interface{}, error) {
return j.GetJWTSecret(), nil
})
if err != nil {
return nil, err
}
if claims, ok := tokenClaims.Claims.(*middleware.Claims); tokenClaims.Valid && ok {
return claims, nil
}
// Log a message for an invalid token.
j.logger.Log(4, "INVALID TOKEN")
return nil, err
}
此处使用了自定义的logger,可直接去掉
再定义一个handler
go
func (j *JWTHandlerAdapter) JWTHandler() gin.HandlerFunc {
// get token
// token exist ? validate : abort
// next or abort
return func(c *gin.Context) {
token, exist := c.GetQuery("token")
if !exist {
token = c.GetHeader("token")
}
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"msg": jwt.ErrSignatureInvalid})
c.Abort()
return
} else {
_, err := j.jwtPorts.ParseToken(token)
if err != nil {
switch err.(*jwt.ValidationError).Errors {
case jwt.ValidationErrorExpired:
c.JSON(http.StatusOK, gin.H{"msg": jwt.ValidationErrorExpired})
default:
c.JSON(http.StatusUnauthorized, gin.H{"msg": jwt.ValidationErrorNotValidYet})
}
c.Abort()
return
}
}
c.Next()
}
}
之后直接用于框架中即可