jwt - go
身份验证使应用程序知道向应用程序发送请求的人是谁。JSON Web 令牌(JWT)是一种允许身份验证的方法,而无需在系统本身实际存储任何有关用户的任何信息(与基于会话的身份验证相反 )。
在本文中,我们将演示基于 JWT 的身份验证的工作原理,以及如何在 Go 中构建示例应用程序以实现该示例。
如果你已经知道 JWT 的工作原理,并且只想看一下实现,则可以 跳过,或者在 Github 上查看源代码 。
JWT 格式
假设我们有一个名为的用户 user1,他们尝试登录到应用程序或网站。一旦成功,他们将收到一个看起来像这样的令牌:
1 |
|
这是一个 JWT,由三部分组成(以分隔.):
- 第一部分是标题 header(
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
)。标头指定信息,例如用于生成签名的算法(第三部分)。这部分是标准的,并且对于使用相同算法的任何 JWT 都是相同的。 - 第二部分是有效负载 payload (
eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ
),其中包含特定于应用程序的信息(在我们的示例中,这是用户名),以及有关令牌的到期和有效性的信息。 - 第三部分是签名(
2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54
)。它是通过组合和散列前两个部分以及一个秘密密钥来生成的。
现在有趣的是,标题 header 和有效负载 payload 未加密。它们只是 base64 编码的。这意味着任何人都可以通过解码来查看其内容。
例如,我们可以使用此 在线工具 对标题或有效负载进行解码。
eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ
将显示为以下内容:
1 |
|
JWT 签名如何工作
因此,如果任何人都可以读写 JWT 的标头和签名,那么实际上如何保证 JWT 是安全的?答案在于如何生成最后一部分(签名)。
假设你的应用程序想要向成功登录的用户 user1 签发 JWT。
使标头和有效负载非常简单:标头或多或少是固定的,有效负载 JSON 对象是通过设置用户 ID 和有效时间(以 Unix 毫秒为单位)来形成的。
发行令牌的应用程序还拥有一个密钥,该密钥是一个私有值,并且仅对应用程序本身是已知的。然后将标头和有效负载的 base64 表示形式与密钥组合,然后通过哈希算法计算签名值(在本例中为 HS256,如标头中所述)
如何实现算法的细节超出了本文的讨论范围,但是要注意的重要一点是,这是一种 hash 方法,这意味着我们无法破解算法并获得进行签名的密钥,因此我们秘密密钥仍然是私有的。
验证 JWT
为了验证传入的 JWT,将使用传入的 JWT 的标头和有效负载以及密钥再次生成签名。如果签名与 JWT 上的签名匹配,则认为 JWT 有效。
现在,让我们假设你是一个试图发行假令牌的黑客。你可以轻松地生成标头和有效负载,但是在不知道密钥的情况下,无法生成有效的签名。如果你尝试篡改有效 JWT 的有效负载 payload,则签名将不再匹配。
这样,JWT 可以以一种安全的方式授权用户,而无需在应用程序服务器上实际存储任何信息(除了密钥)。
GO 的实现
现在,我们已经了解了基于 JWT 的身份验证的工作原理,让我们使用 Go 来实现它。
创建 HTTP 服务器
首先让我们初始化需要使用的 HTTP 服务器路由:
1 |
|
现在,我们可以定义 Signin
和 Welcome
路由。
处理用户登录
/signin
路由将获取用户凭据并登录。为简化起见,我们在代码中将用户信息存储在 map:
1 |
|
因此,目前,我们的应用程序中只有两个有效用户: user1 和 user2。接下来,我们可以编写 SigninHTTP 处理程序。对于此示例,我们使用 dgrijalva/jwt-go 库来帮助我们创建和验证 JWT 令牌。
1 |
|
如果用户使用正确的凭据登录,则此处理程序将使用 JWT 值在客户端设置 cookie。一旦在客户端上设置了 cookie,此后它将与每个请求一起发送。现在,我们可以编写 Welcome 方法来处理用户特定的信息。
处理认证后的路由
现在,所有已登录的客户端都使用 cookie 存储用户信息,我们可以将其用于:
- 验证后续用户请求
- 获取有关发出请求的用户的信息
让我们编写 Welcome
处理方法来做到这一点:
1 |
|
双令牌续签
双令牌:刷新令牌 + 访问令牌
1、刷新令牌的低使用频率和安全性
在双令牌系统中,访问令牌和刷新令牌的角色和使用频率有明显差异:
- 访问令牌:用于访问系统资源,因此会频繁使用,每次API调用都可能需要验证访问令牌。这意味着访问令牌更频繁地在客户端和服务器之间传输,从而增加了被截获的风险。
- 刷新令牌:主要用于在访问令牌过期后获取新的访问令牌。因此,其使用频率远低于访问令牌,通常只在访问令牌失效时才使用。
安全优势
刷新令牌的这种使用模式带来了几个安全优势:
- 降低暴露风险:由于刷新令牌不需要频繁发送,其被截获的机会相对较小。这减少了被盗用的风险。
- 控制和监控:刷新令牌的使用可以更容易地被监控和控制,因为其使用情况较少且具有特定目的(仅用于令牌续签)。这使得任何异常使用模式更容易被检测到。
- 更长的生命周期管理:虽然刷新令牌具有更长的有效期,但由于其使用频率低,可以在系统检测到安全问题时及时撤销,而不像访问令牌那样经常处于活跃状态中。
为了最大程度地减少对 JWT 的滥用,通常将到期时间保持在几分钟左右。通常,客户端应用程序将在后台刷新令牌。
jwt + redis续签
JWT 实现登录认证 + Token 自动续期方案,这才是正确的使用姿势!
安全性:使用JWT作为session_id,其自带的签名验证确保了令牌的安全性,防止篡改。
状态管理:通过在Redis中存储prefix + session_id对应的用户信息,您可以轻松管理用户状态,如权限、偏好设置等。
可扩展性:Redis具有出色的读写性能,适用于大规模应用,且易于水平扩展。
灵活性:每次API访问时刷新Redis中的过期时间可以灵活地管理会话有效期,允许用户在活跃时保持登录状态。
在项目code-comment-analyzer中,我的做法:
- jwt中的内容,content部分含有user_id,但是不做其他设置
- redis中存储一个格式为
prefix_userid
的key(方便后续其他操作指定的token),以及jwt生成的token作为value,并且会设置过期时间
gpt评价
整体来说,你目前的方案在可行性和简单性上是没有问题的,但如果要长期维护并在更复杂的场景下使用,还需要留意以下几点:
你这样做的优点:
-
实现简单:
- JWT 中只存
user_id
,在验证时解析出user_id
即可去 Redis 查找对应记录,不需要把过多的信息放进 JWT,减少了令牌体过长的问题。 - Redis 中只存一对
prefix_userid -> token
,查找和更新都比较直接。
- JWT 中只存
-
可强制下线/过期:
- 当你需要让某个用户的 Token 立即失效时,只需删除 Redis 里对应的 Key(
prefix_userid
),即可让原先的 JWT 形同无效(因为你在校验时会比对 Redis 中存储的 token 是否一致)。
- 当你需要让某个用户的 Token 立即失效时,只需删除 Redis 里对应的 Key(
-
续期可控:
- 只要每次用户调用接口时,你都能够刷新 Redis 中
prefix_userid
这个 Key 的过期时间,就能实现“滑动过期”或“自动续期”的效果。
- 只要每次用户调用接口时,你都能够刷新 Redis 中
可能需要进一步考虑的点
一、多端登录或多 Token 管理
目前你的 Redis Key 是以 prefix_userid
作为唯一标识,这意味着 同一个用户只能存储一个 Token。这在有些应用场景中是可以的,比如你的系统只允许单端登录,或者用户必须在新设备登录时将旧设备的登录顶下去。
但如果你需要实现多设备同时登录、多 Token 并存(譬如手机端、PC端同时使用),就需要考虑以下两种做法之一:
- 在 Redis 中以 Token 自身为 Key,比如
prefix_token -> user_id
形式。这样可以同时存在多条记录,每条记录的过期时间独立管理。 - 在 Key 中带上更细分的信息,比如
prefix_{user_id}_{device_id}
,从而允许每个用户在多个设备上各自持有 Token 并独立管理过期时间。
二、Token 本身的唯一性 (jti)
在更严格的安全策略下,JWT 中可能需要包含一个 唯一 ID(jti),用来标识该 Token 实例,这样可以实现更灵活的令牌撤销机制、令牌防重放等等。
- 生成 Token 时在其 Claims 中加入
jti
(uuid 或随机字符串)。 - 在 Redis 中不仅仅存
{prefix_userid -> token}
,而是存{jti -> 用户或会话信息}
或者{jti -> token}
。 - 用户每次请求带上 Token 时,你可以解析出 jti,然后去 Redis 确认 jti 是否存在/过期,从而确定有效性。
这样做好处:
- 即便你的 JWT 在外面被复制了一份,如果你主动在 Redis 中删除对应 jti,也可以让所有复制的令牌都失效。
- 如果要实现一次性 Token(某些场景才需要),配合 jti 可以更好地做防重放。
三、Redis 存储的内容与过期策略
“jwt中的内容,content部分含有user_id,但是不做其他设置;Redis 中存储一个格式为
prefix_userid
的 key,以及 jwt 生成的 token 作为 value,并且会设置过期时间。”
如果主要是为了强制 Token 失效,那么把 token 本身放 Redis 里是可以的;如果要进一步做附加信息缓存(比如用户权限、角色信息),就可以在 Redis 里存一个更丰富的对象(Hash / JSON),这样在授权校验时可以少做一次数据库查询。
同时,你也要决定JWT 的过期时间与Redis Key 的过期时间怎么配合:
- 如果 JWT 的过期时间较短,而 Redis Key 过期时间较长,Token 有可能在 Redis 中还存在,但 JWT 已经失效;
- 如果 JWT 的过期时间比 Redis Key 过期时间更长,那么就会出现 Token 的签名和时间都还有效,但 Redis 里 Key 已经删除导致令牌无效。
通常的策略是:
- JWT 本身设置一个相对较短的过期时间(如 2 小时或 1 天);
- Redis Key 设置与 JWT 同步或更短的过期时间,并在每次用户访问时“滑动更新过期时间”,从而实现“只要活跃,就保持登录”。
- 可以通过 Refresh Token 机制去续发一个新的 Access Token(取决于你的系统安全需求,是否要区分 Access Token 和 Refresh Token)。
四、安全性权衡
JWT 的好处是自带签名,不易被篡改,但不能随意撤销(除非你实现了 Redis 或数据库层面的对照)。
既然你已经采用了 Redis 校验,那基本就把 JWT 当作一种无状态 Token + 有状态的验证结合使用了。
- 好处:可实现更灵活的会话管理,比如强制登出、滑动过期、权限变更即时生效等。
- 代价:每次鉴权需要访问 Redis,再做一次校验,牺牲了 JWT “可脱离服务端缓存”的天然特性,但在分布式下依然比存数据库效率好很多,而且安全性更高。
结论
你目前的做法在很多实际项目中是常见且可行的组合拳:
- JWT 只放最小必要信息 (如 user_id),减少令牌大小;
- Redis 存储并映射用户与 Token,实现强制失效和续期控制;
- 每次请求刷新 Redis 的过期时间,在用户活跃期间保持 Token 有效;
- 利用 JWT 的签名防篡改和 Redis 校验,既保留了某些无状态特性,又能灵活管理会话。
需要结合自己项目的需求决定是否要:
- 支持多设备多 Token;
- 在 JWT 加入
jti
并以此为 Key; - 区分 Access Token 和 Refresh Token;
- 在 Redis 里存更多的用户/权限信息;
- 决定 JWT 有效期与 Redis 过期时间的策略。
如果你的场景只需要单端登录、对 Token 的撤销/续期有明确需求,并且不会出现太多“多设备共存”的场景,那么这种方案是简洁且有效的。
若以后需求扩张,需要多端登录或者更复杂的细粒度权限管理,你就可以做相应的“Key 结构”或“Claims 结构”的调整,依然可以复用这个思路,无需推翻重来。