本文目录
- 一、JWT
- Header
- Payload
- Signature
- 二、代码解析
- 刷新Token
- 鉴权中间件
JWT_3">一、JWT
JWT是Json Web Token的缩写。JWT本身是没有定义任何技术实现,只是定义了一种基于Token的会话管理规则,涵盖Token需要包含的标准内容和Token生成过程,特别适用于分布式站点的单点登录SSO场景。
JWT长这样,三个部分分别是头部、负载、签名。
头部和负载以Json的形式存在,就是JWT中的Json,三部分的内容分别经过了Base64编码,然后拼接成一个JWT token。
Header
头部存储了所使用的加密算法还有Token类型。比如
{ "alg":"HS2546","TYP":"jwt" }
.
Payload
将Token当成是一个载体,表示Token里面装了什么,也是一个json对象。JWT规定了7个官方字段给开发者使用。
除了官方字段,开发者也可以指定字段和内容,比如下面的。
JWT默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个不分,这个JSON对象也要使用Base64URL算法转成字符串。
Signature
Signature是对前两个部分的签名,防止篡改。
首先需要指定一个秘钥,这个密钥只有服务器才知道,不能泄露给用户。然后使用Header里面指定的签名算法(默认SHA 256),按照下面的公式产生签名。
token function">HMACSHA256token punctuation">(token function">base64UrlEncodetoken punctuation">(headertoken punctuation">)token operator">+token string">"."token operator">+token function">base64UrlEncodetoken punctuation">(payloadtoken punctuation">)token punctuation">,secrettoken punctuation">)
JWT拥有基于Token的会话管理方式所拥有的一切优势,不依赖Cookie,使得其可以防止CSRF攻击,也能在禁用Cookie的浏览器环境中正常运行。
1、jwt基于json,非常方便解析;
2、可以在令牌中自定义丰富的内容,易扩展;
3、通过非对称加密算法及数字签名技术。
JWT的最大优势是服务端不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储Session所需要引入的Redis等组件,降低了系统架构复杂度。
但这也是JWT最大的劣势,由于有效期存储在Token 中,JWT Token一旦签发,就会在有效期内一直可用(所以一般设置的时间会比较短),无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的Token,如果需要禁用用户,单纯使用JWT就无法做到了。
二、代码解析
token keyword">type MyClaims token keyword">struct token punctuation">{UserID token builtin">uint64 token string">`json:"user_id"`Username token builtin">string token string">`json:"username"`jwttoken punctuation">.StandardClaims
token punctuation">}
定义一个自定义的 JWT 声明结构体 MyClaims,用于扩展标准的 JWT 声明以满足特定需求。jwt.StandardClaims 是 JWT 包自带的结构体,包含了官方定义的标准字段(如 Issuer、ExpiresAt 等)。由于标准声明中没有包含用户 ID 和用户名等自定义信息,因此通过内嵌 jwt.StandardClaims 并添加额外字段(如 UserID 和 Username),可以将这些自定义信息嵌入到 JWT 中。这样,生成的 JWT 令牌不仅包含标准的 JWT 信息,还能携带用户相关的自定义数据,方便在后续的认证和授权过程中使用。
token keyword">var mySecret token operator">= token punctuation">[token punctuation">]token function">bytetoken punctuation">(token string">"Golinie"token punctuation">)token keyword">func token function">keyFunctoken punctuation">(token boolean">_ token operator">*jwttoken punctuation">.Tokentoken punctuation">) token punctuation">(i token keyword">interfacetoken punctuation">{token punctuation">}token punctuation">, err token builtin">errortoken punctuation">) token punctuation">{token keyword">return mySecrettoken punctuation">, token boolean">nil
token punctuation">}
参数 _ *jwt.Token 是一个占位符,表示这个函数接收一个 JWT 令牌对象,但在这个实现中并没有使用它(因此用 _ 忽略)。
函数返回两个值:
i interface{}:返回密钥,这里返回的是全局变量 mySecret。
i interface{}:表示函数返回的第一个值是一个空接口类型。空接口可以存储任何类型的值,因此可以返回任何类型的数据。
err error:返回可能发生的错误。在这个实现中,没有错误发生,因此返回 nil。
token comment">// GenToken 生成access token 和 refresh token
token keyword">func token function">GenTokentoken punctuation">(userID token builtin">uint64token punctuation">, username token builtin">stringtoken punctuation">) token punctuation">(aTokentoken punctuation">, rToken token builtin">stringtoken punctuation">, err token builtin">errortoken punctuation">) token punctuation">{token comment">// 创建一个我们自己的声明c token operator">:= MyClaimstoken punctuation">{userIDtoken punctuation">, token comment">// 自定义字段token string">"username"token punctuation">, token comment">// 自定义字段jwttoken punctuation">.StandardClaimstoken punctuation">{ token comment">// JWT规定的7个官方字段ExpiresAttoken punctuation">: timetoken punctuation">.token function">Nowtoken punctuation">(token punctuation">)token punctuation">.token function">Addtoken punctuation">(timetoken punctuation">.token function">Durationtoken punctuation">(vipertoken punctuation">.token function">GetInttoken punctuation">(token string">"auth.jwt_expire"token punctuation">)token punctuation">) token operator">* timetoken punctuation">.Hourtoken punctuation">)token punctuation">.token function">Unixtoken punctuation">(token punctuation">)token punctuation">, token comment">// 过期时间Issuertoken punctuation">: token string">"bluebell"token punctuation">, token comment">// 签发人token punctuation">}token punctuation">,token punctuation">}token comment">// 加密并获得完整的编码后的字符串tokenaTokentoken punctuation">, err token operator">= jwttoken punctuation">.token function">NewWithClaimstoken punctuation">(jwttoken punctuation">.SigningMethodHS256token punctuation">, ctoken punctuation">)token punctuation">.token function">SignedStringtoken punctuation">(mySecrettoken punctuation">)token comment">// refresh token 不需要存任何自定义数据rTokentoken punctuation">, err token operator">= jwttoken punctuation">.token function">NewWithClaimstoken punctuation">(jwttoken punctuation">.SigningMethodHS256token punctuation">, jwttoken punctuation">.StandardClaimstoken punctuation">{ExpiresAttoken punctuation">: timetoken punctuation">.token function">Nowtoken punctuation">(token punctuation">)token punctuation">.token function">Addtoken punctuation">(timetoken punctuation">.Second token operator">* token number">30token punctuation">)token punctuation">.token function">Unixtoken punctuation">(token punctuation">)token punctuation">, token comment">// 过期时间Issuertoken punctuation">: token string">"bluebell"token punctuation">, token comment">// 签发人token punctuation">}token punctuation">)token punctuation">.token function">SignedStringtoken punctuation">(mySecrettoken punctuation">)token comment">// 使用指定的secret签名并获得完整的编码后的字符串tokentoken keyword">return
token punctuation">}
代码首先定义了一个自定义的 JWT 声明结构体 MyClaims
,它包含用户 ID 和用户名等自定义字段,以及 JWT 标准声明字段(如过期时间和签发人)。这些自定义字段允许在生成的 Access Token
中携带用户相关信息,方便后续的验证和授权。
通过 jwt.NewWithClaims
方法,结合自定义声明 MyClaims
和 HS256
签名方法,生成 Access Token
。Access Token
的过期时间通过配置文件动态获取(viper.GetInt(“auth.jwt_expire”)),并设置为从当前时间起的若干小时后过期。最终,使用全局密钥 mySecret
对 Access Token
进行签名并编码为字符串。
Refresh Token
的生成逻辑类似,但不需要携带自定义数据,仅使用 JWT 标准声明字段。用于在 Access Token
过期时刷新新的 Access Token
。同样使用 mySecret
进行签名。
函数返回生成的 Access Token
和 Refresh Token
,以及可能发生的错误。这两个令牌将被发送给客户端,客户端在后续请求中使用 Access Token
访问资源,当 Access Token
过期时,使用 Refresh Token
获取新的 Access Token
。
Access Token 是用户访问受保护资源的凭证,通常有效期较短(如几分钟到几小时)。这种设计减少了令牌被泄露后可能造成的损害,因为即使令牌被攻击者获取,它很快就会失效。用户在每次请求受保护资源时都需要携带 Access Token,因此它需要频繁地被验证和使用。
Refresh Token 的有效期通常较长(如几天到几周),用于在 Access Token 过期时刷新一个新的 Access Token。这样可以避免用户频繁重新登录,提升用户体验。Refresh Token 通常存储在客户端的更安全位置(如 HTTPOnly Cookie 或本地存储),并且不会直接用于访问受保护资源,因此即使被泄露,攻击者也无法直接利用它访问系统。
Access Token 和 Refresh Token 可以独立管理。例如,服务器可以在检测到异常行为时吊销 Refresh Token,而不会影响当前正在使用的 Access Token。
而 Refresh Token 虽然有效期长,但通常不会直接用于访问资源,且可以被服务器端更严格地管理(如限制使用次数、IP 绑定等)。
接下来看看解析Token和刷新Token的代码。
先看看解析Token:定义了一个名为 ParseToken
的函数,用于解析和验证 JWT 令牌。它接收一个字符串形式的 JWT 令牌 tokenString,并尝试将其解析为自定义的 MyClaims 声明结构体。函数通过调用 jwt.ParseWithClaims
方法,结合自定义的密钥函数 keyFunc 来解析令牌,并将解析后的声明存储在 claims 中。如果解析过程中出现错误,函数会返回错误;如果解析成功但令牌无效,函数会返回一个自定义错误 “invalid token”。
该函数的作用是确保传入的 JWT 令牌是有效的,并提取其中的声明信息(如用户 ID 和用户名)。通过这种方式,可以在后续的业务逻辑中安全地使用这些声明信息,例如验证用户身份或授权访问特定资源。
刷新Token
一图胜千言,看图说话。
所以说后端需要对外提供一个刷新Token的接口,前端需要实现一个当Access Toekn过期时自动请求刷新Token的接口获取新Access Token的拦截器。
首先尝试解析 Refresh Token。如果解析失败(例如 Refresh Token 无效或已过期),函数直接返回错误。
尝试解析 Access Token,并提取其中的声明信息(MyClaims)。如果解析失败,会捕获错误并尝试将其转换为 jwt.ValidationError,以便进一步处理。
如果 Access Token 的错误类型是 jwt.ValidationErrorExpired(即 Access Token 已过期),并且 Refresh Token 仍然有效(前面已经验证过),则调用 GenToken 函数生成新的 Access Token 和 Refresh Token。(返回一个token也可以,看情况自己定义)。
鉴权中间件
当涉及权限的某些接口,就需要Token验证了,比如说只有登录了之后才能进行评论。
也就是通过了鉴权,才能访问下面的接口。
为一组路由应用 JWT 认证中间件,定义多个受保护的 HTTP 接口。通过调用 middlewares.JWTAuthMiddleware()
,所有定义在该中间件下的路由都将要求客户端提供有效的 JWT 令牌才能访问,从而确保这些接口的安全性。这些路由涵盖了创建帖子、投票、评论等操作,以及一些数据查询接口,例如获取帖子详情、评论列表等。
来看看基于JWT的认证中间件的实现。
这里我们可以使用post工具来模拟。
上面的代码首先从 HTTP 请求头的 Authorization 字段中获取令牌,检查令牌是否存在且格式是否正确(假设以“Bearer”开头)。如果令牌缺失或格式错误,中间件会返回错误响应并终止请求处理。如果令牌格式正确,它会调用 jwt.ParseToken 函数解析 JWT 令牌,并验证其有效性。如果解析失败,中间件会返回错误并终止请求;如果解析成功,它会将解析出的用户 ID 存储到 Gin 的上下文 c 中,供后续的处理函数使用,然后继续执行后续的请求处理流程。
这里对用户的ID进行了封装,也就是获取当前用户的ID,方便后续的操作。
getCurrentUserID
函数通过从上下文中获取用户 ID,并进行类型断言,确保了获取到的用户 ID 是有效的。如果用户未登录或上下文中没有用户 ID,函数会返回一个明确的错误 ErrorUserNotLogin,这有助于在业务逻辑中快速识别和处理未登录的情况。