JWT认证的解析和实际使用

这周在个人项目上完成了登录注册,其中便涉及到一些认证问题,接触到了jwt,全称JSON Web Token。现在很多项目都在使用这个作为session的替代。那么我们先来看看他的组成

组成

JSON Web Token(JWT)是一种开放标准(RFC 7519),定义了一种紧凑且自包含的方式,用于以JSON对象的形式在各方之间安全地传输信息。该信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

总结出以下几点:以JSON格式传输、传输中包含一些隐私信息、可以公钥或私钥进行签名

JSON Web Token由三个由点(.)分隔的部分组成,分别是:

  1. 标头(Header)
  2. 有效负载(Payload)
  3. 签名(Signature)

因此,JWT通常看起来像这样:

xxx.yyyyy.zzzz

接下来进行具体分析讲解

标头(Header): 标头通常包含两部分:令牌的类型(JWT)和使用的签名算法。例如:

json 复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}

然后,此JSON进行Base64Url编码,形成JWT的第一部分。

有效负载(Payload): 有效负载包含声明(CLaim)。声明是关于实体(通常是用户)和附加数据的陈述。有三种类型的声明:注册、公共和私有声明。

  1. 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都应该是不重复的,避免重复发放
  1. Public claims

这个,可以想成是传递的栏位必须是跟上面Registered claims栏位不能冲突,然后可以向官方申请定义公开声明,会进行审核等步骤,实务上在开发上是不太会用这部分的。

  1. 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包含的信息

用处

  1. 授权: 这是使用JWT的最常见场景。一旦用户登录,每个后续请求都将包含JWT,允许用户访问具有该令牌允许的路由、服务和资源。由于JWT具有小的开销并且易于在不同域之间使用,单点登录(Single Sign On)是一种广泛使用JWT的功能。
  2. 信息交换: 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()
	}
}

之后直接用于框架中即可

相关推荐
许野平1 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
齐 飞3 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
童先生3 小时前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go
LunarCod3 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。4 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man4 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*4 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu5 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s5 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子5 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算