简介:面向C#开发者与需要对API接口做安全加固的技术人员,这份资源围绕JWT标准展开,覆盖Token从生成、签名、验签到过期刷新、OAuth2.0集成等完整流程,并提供可直接落地的示例工程。资源共748个文件,压缩包约55MB,核心内容为C#源码(cs)与MVC视图页面(cshtml),同时附带所需依赖库dll、配置文件config以及项目工程文件sln/csproj,便于直接编译运行与二次修改。已有9415人学习下载,适合初中级开发者理解JWT原理并快速实现身份验证功能。从中可掌握System.IdentityModel.Tokens.Jwt库的典型用法,包括TokenValidationParameters参数配置、密钥管理与刷新策略,并了解到微服务场景下无状态认证的具体写法,是一份结合理论说明与可运行代码的实用参考资料。 JWT Token 这个东西,只要做过前后端分离项目,基本都会碰到。前阵子我接手一个老系统,用户登录成功后,后续每个请求都要拿 sessionId 去 Redis 里查用户信息,高峰期 Redis 的连接数和响应时间都压得有点难看。后来把会话方案整体切到 JWT Token,服务端彻底无状态了,登录后发一个 Token,后续请求带着它进来,验签通过就直接信任,一下轻松了不少。当然,JWT 也不是银弹,什么场景该用、生成和验证这条链路上有哪些坑、Token 怎么续签、怎么防伪造,这些点今天一次性说清楚,正好对应上你搜的那些词:jwt在线解析、token失效、token续签、jwt伪造,等等。
1. JWT到底解决了什么问题:从Session的痛点说起
1.1 Session登录的局限与JWT的定位
早期做 Web 登录,主流方案是 Session-Cookie:用户输完账号密码,服务端生成一个 sessionId 存到内存或者 Redis,再把 sessionId 写进 Cookie 返回给浏览器。浏览器下次请求带上 Cookie,服务端拿 sessionId 去查对应的会话数据。这套方案在单机时代没问题,但一旦服务拆成多个实例部署,或者做了负载均衡,就会面临一个尴尬局面:用户第一次请求落在 A 机器,登录状态存在 A,第二次请求被负载均衡转到了 B 机器,B 上没有这份 session,用户就被当成未登录了。
解决思路无非两种:一种是做 Session 粘滞,把同一个用户的请求固定转发到同一台机器,但这会破坏负载均衡的弹性;另一种是把 Session 集中放到 Redis,所有实例共享,这个方案用得很广,但 Redis 一旦抖动,所有用户的登录态都受影响,而且每次请求都要多一次网络 IO。
JWT 的思路完全反过来:不把状态存在服务端,而是把用户身份信息直接编码进 Token 里发给客户端,服务端只负责验签。签名通过就认为这个 Token 合法,里面的用户信息可以直接用,不需要再查询任何会话存储。这就是 JWT 常说的"无状态"。
1.2 JWT的结构拆解:Header、Payload、Signature
JWT 是一串由三个部分组成的字符串,用点号分隔,长得像这样:xxxxx.yyyyy.zzzzz。三个部分分别是 Header、Payload、Signature。
- Header 里放的是算法和 Token 类型,最常见的组合是
{"alg":"HS256","typ":"JWT"}。算法字段很关键,后面讲安全漏洞的时候还要专门提它。 - Payload 是载荷区,可以放签发人、过期时间、主题、自定义的业务字段。注意,Payload 只是做了 Base64URL 编码,并没有加密,任何人都可以解码看到里面的内容。网上搜一下"jwt在线解析",把 Token 粘进去就能看到明文。所以千万别把密码、手机号、身份证这类敏感信息塞进 Payload。
- Signature 是整个 Token 的防伪标识,由 Header + Payload + 密钥经过指定算法计算出来。服务端验证 Token 时,重新计算一遍签名,比对是否一致,一致就说明 Token 没被篡改过。
用生活一点的话说,JWT 就像一张盖了公章的工作证。公章代表服务端的私密密钥,证上的信息谁都能看(Payload 明文),但别人没有公章,伪造不出来。只要章是真的、内容没被改过,这张证就能用。
1.3 Cookie、Session与Token的区别
这三者的关系经常有人搞混。Session 是服务端的会话记录,Cookie 是浏览器端存储数据的机制,Token 是客户端保存的一种凭证。URL 里常见的cookie session token区别这个问题,其实就是三个不同维度的东西:Session 是"状态放在服务端",Cookie 是"状态放在浏览器"的一种载体,JWT Token 则是"状态本身被编码进凭证里"。它们不是互斥的,也经常会组合出现:Session 方案里用 Cookie 传 sessionId,JWT 方案里也可以把 Token 丢进 Cookie 里传输,也可以放请求头 Authorization 里传,看具体场景。
2. 生成Token的完整实操:选型、依赖与核心代码
2.1 技术选型:jjwt还是java-jwt
Java 生态里做 JWT 的库不少,比较主流的有 jjwt(io.jsonwebtoken)、Java JWT(com.auth0)、Nimbus JOSE JWT。我个人的习惯是选 jjwt,原因很直接:API 设计比较清爽,官方文档完整,社区活跃,支持的签名算法覆盖了 HS、RS、ES 全系列,对 Jackson 的集成也很顺,不需要自己写序列化适配。
如果项目里用的是 Spring Boot,jjwt 和 Spring Security 的搭配也比较常见。下面所有示例都以 jjwt0.11.5版本为准,这个版本是目前生产环境里用得比较多的稳定版。
2.2 生成Token的代码实现与参数说明
先引入 Maven 依赖,总共三个包:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>接口在 jjwt-api 里,真正干活的是 runtime 的 impl 和 jackson 模块。少了后面任何一个,运行阶段都会报 ClassNotFoundException 这一类的错误。
然后是核心的生成方法:
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; public class TokenGenerator { // 生产环境务必从配置中心读取,不要硬编码在代码里 private static final String SECRET = "your-256-bit-secret-key-change-me-to-a-long-random-string"; private static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)); public static String generateToken(String userId, String username, String role) { long now = System.currentTimeMillis(); long expireMills = 30 * 60 * 1000; // 30分钟过期 return Jwts.builder() .setHeaderParam("alg", "HS256") .setSubject(userId) .setIssuer("my-app") .setIssuedAt(new Date(now)) .setExpiration(new Date(now + expireMills)) .claim("username", username) .claim("role", role) .signWith(KEY, SignatureAlgorithm.HS256) .compact(); } }几个关键点说一下:
setSubject存用户主键,这是最常用的字段,后续验证完 Token 可以直接拿 userId 出来当上下文。claim是自定义字段,用户名、角色、权限点都可以往里面放。但记住前面说的,敏感信息不能放。signWith指定密钥和算法。HS256 是对称算法,生成和验证用的是同一个密钥,所以密钥本身一定要保密,长度也不能太短,否则会报WeakKeyException。测试阶段容易踩这个坑,随便写个"secret"当密钥,结果项目启动就抛异常。HS256 要求密钥至少 256 位,也就是 32 个字节。
2.3 密钥管理:HS256和RS256怎么选
HS256 和 RS256 是两种最常用的签名算法,选择时要考虑应用场景。
HS256 是对称加密,一个密钥同时用来签名和验签。优点是计算快,实现简单,适合单体应用内部服务之间的 Token 签发和验证。缺点是密钥只有一把,拿到它就能伪造任何 Token,所以密钥必须锁死在服务端,不能发给任何第三方。
RS256 是非对称加密,私钥签名,公钥验签。典型场景是第三方开放平台:你的服务用私钥给客户端签发 JWT,客户端或其他信任方用你下发的公钥验证真伪,私钥永远不需要离开你的服务。这样就算公钥泄露,也无法伪造 Token。缺点是非对称计算比对称慢,配置也更重。
具体做项目时,如果 JWT 只在自己系统内部用,HS256 是够的;如果 Token 会被外部系统消费,或者要开放给第三方校验,那就得用 RS256,同时把公钥通过接口或者 JWKS 的方式开放出去。
3. 验证Token不是简单的解析:完整校验链路
3.1 认证过滤器的位置与设计
生成 Token 只是前半段,真正考验细节的是验证链路。在 Spring Boot 项目里,最常见的做法是写一个 OncePerRequestFilter,把它挂到 Spring Security 的过滤器链上,或者直接注册到 Servlet 的过滤器链上,对所有需要鉴权的接口做统一拦截。
过滤器的逻辑大致是:
- 从请求头取
Authorization: Bearer <token>段。 - 没有 Token 就直接放行或者按匿名处理,由后面的授权逻辑决定放不放行。
- 有 Token 就进入验签流程。
- 校验通过,把用户信息放进 SecurityContext 或者 ThreadLocal,供 Controller 层使用。
- 校验失败,按异常类型返回 401 或者 403。
实际的代码我习惯写成下面这样,先取 Header,再剥掉Bearer前缀:
String header = request.getHeader("Authorization"); if (header == null || !header.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } String token = header.substring(7);3.2 过期校验、签名校验与异常分类
验证的核心代码并不复杂,难在异常处理要区分清楚。完整代码如下:
import io.jsonwebtoken.*; public class TokenValidator { private static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)); public static Claims validateToken(String token) { Jws<Claims> jws = Jwts.parserBuilder() .setSigningKey(KEY) .build() .parseClaimsJws(token); return jws.getBody(); } }parseClaimsJws内部按顺序做了几件重要的事:
- 格式校验:Token 必须是三段,否则抛 MalformedJwtException。
- 签名校验:用密钥重新算一遍签名,和 Token 里的 Signature 比对。对不上,说明 Token 被篡改或者密钥不对,抛 SignatureException。
- 过期时间校验:判断
exp是否早于当前时间,过期了抛 ExpiredJwtException。 - 时间窗校验:如果设置了
nbf(not before)字段,还会校验当前时间是否在生效时间之后。
所以捕获异常时不要一把梭,要分类型处理:
| 异常类型 | 含义 | 建议处理 |
|---|---|---|
| ExpiredJwtException | Token 过期 | 返回 401,提示登录态过期,前端跳登录页 |
| SignatureException | 签名校验失败 | 按非法请求处理,记日志,返回 401 |
| MalformedJwtException | Token 格式错误 | 按非法请求处理 |
| UnsupportedJwtException | 算法不支持等 | 按非法请求处理,排查签名算法 |
3.3 常见验证失败场景与放开白名单
我踩过的坑之一是 Redis 查询被误背锅。切换 JWT 之前,团队里有人说 JWT 无状态省事,但也有同事担心 Token 被抢了没法主动踢人。这个矛盾在验证环节要先想清楚:JWT 验证默认只在服务端本地算签名和过期时间,不发任何外部请求,所以从性能上说,验一个 Token 就是几次 HMAC 计算,毫秒级都不到。但如果你想实现"用户被禁用了 Token 立刻失效"这类需求,就还得引入黑名单或者版本号机制,在第 4 节细说。
另一个是"springboot jwt 放开swagger"这类白名单配置的场景。Swagger 或接口文档的静态资源、登录接口本身是不需要鉴权的,否则会陷入"登录接口也要 Token,但没登录哪来的 Token"的死循环。实际项目里一般维护一个 permitAll 的 URL 列表,包括/auth/login、/swagger-ui/**、/v3/api-docs/**这些路径,在过滤器里先判断请求路径是否命中白名单,命中就直接放行,不走 Token 校验。这样省事,也避免把接口文档也锁起来。
3.4 获取请求头里的Token与上下文传递
这里要提醒一个问题:很多人从request.getHeader("Authorization")取出 Token 后,只验签就完事了,不去检查 Payload 里的签发人、受众等字段。如果项目有多个客户端共用同一套签发密钥,或者 Token 可能流通到别的系统里,最好再核对一下iss、aud这些声明,避免一个服务签发的 Token 被另一个服务无脑接受。
验签通过后的 Claims 对象里,getSubject()拿到的是 userId,建议立刻放进请求上下文中。通常做法是在过滤器里往request.setAttribute("userId", userId)塞一份,然后在 Controller 里用@RequestAttribute("userId")取;配合 Spring Security 的话,就构造一个 Authentication 对象塞进 SecurityContextHolder。无论哪种做法,目的都是一样的:让后续的业务代码不需要再去解析 JWT,拿到的直接是用户身份。
4. 有效期设计与Token续签:token失效该怎么做
4.1 有效期设多长合适
Token 有效期这个参数,设置短了用户用着难受,设置长了安全风险变高。所以没有标准答案,完全看业务性质。
如果做一个普通的后台管理系统,半小时到两小时算比较常见的区间。我的一般做法是:访问 Token 定 30 分钟,管理和后台系统也差不多,稍微长一点到 2 小时也可以。如果做的是 App 或 SPA 的登录态,结合刷新机制的话,访问 Token 可以短到 15 分钟,刷新 Token 放到 7 天甚至 30 天。
核心原则是先想清楚:Token 被偷走后,你希望通过短过期来缩小攻击面,还是通过刷新机制来兼顾体验?两者不可兼得,只能取舍。
4.2 滑动续签的实现方案
"jwt实现token续签" 是很多搜索词里的高频问题。续签方案里最简单直观的是滑动续签:用户在系统的有效期内持续操作,只要每次请求时检查剩余有效期,当剩余时间低于某个阈值,就顺手签发一个新 Token 返回给前端,前端下次请求用新 Token。
配合 Spring Boot 的 Filter,实现思路大致是这样:验签通过后,取出exp字段,计算剩余时间:
long remainingMillis = expiration.getTime() - System.currentTimeMillis(); long renewThreshold = 10 * 60 * 1000L; // 剩余不足10分钟就续签 if (remainingMillis > 0 && remainingMillis < renewThreshold) { String newToken = TokenGenerator.generateToken(userId, username, role); response.setHeader("X-New-Token", newToken); }前端在请求完成后检查响应头,如果存在X-New-Token就把它替换成本地新的 Token,继续后面的请求。这种方案的好处是不用引入刷新 Token 的概念,实现成本低;缺点是 Token 在有效期内基本都是"无限续签"的状态,对安全要求极高的系统不合适。
更严谨的做法是双 Token 方案:短期的 Access Token 和长期的 Refresh Token。Access Token 过期后,前端拿 Refresh Token 去请求一个刷新接口,换取新的 Access Token。刷新接口要再校验一边 Refresh Token 的有效性,同时可以在这里做"是否还在白名单里""Refresh Token 是否已吊销"等逻辑。双 Token 的优点是控制更细,刷新的时机完全掌握在后端手里;缺点是实现量更大,前端需要处理并发请求时的 Token 刷新竞态,避免多个请求同时拿旧的 Refresh Token 换新后,先换的 Token 失效了。
4.3 主动失效:黑名单、版本号与登出场景
JWT 无状态是一把双刃剑,因为有状态的服务端会话可以被主动删除,JWT 一旦发出去,服务端没有"会话记录"可删。要解决"用户改密码了、被踢下线了、主动登出了,Token 应立即失效"这类问题,就得引入一点"半状态"手段。
最常见的做法是黑名单机制:把需要失效的 Token 的 jti(JWT ID)或者用户的 Token 版本号扔进 Redis,并设置一个和 Token 过期时间一致的 TTL。每次验签通过后,再查一次黑名单,命中了就拒绝。这个方案多了一次 Redis 查询,但换来了"主动失效"能力。对于权限敏感的系统,这个代价完全值得。
另一个思路是版本号:在用户表里存一个token_version字段,签 JWT 时把这个版本号放进 payload。验证 Token 时,从数据库查出用户的当前版本号,比对是否一致。不一致就说明 Token 是旧的。这个方案最灵活的地方在于,修改用户密码或者管理员禁用账号时,只要把版本号加一,该用户所有已签发的 Token 立即全部作废。缺点则是每次验证要查一次数据库,无状态优势打了几分折扣。
4.4 登录态过期的前端处理
"token失效"之后,前端表现不好也是很多项目体验差的根源。正常流程应该是:接口返回 401 时,前端拦截器统一捕获,清掉本地存储的 Token,跳转到登录页。但这里有个常见毛病:一个页面同时发了好几个接口请求,全部 401,跳转逻辑被触发了 N 次,路由跳转重入,页面状态混乱。
比较稳妥的做法是做一个 401 去重:定义一个布尔标志位,第一次收到 401 时查标志位,如果已经处理过就直接忽略,避免重复跳转。同时注意保留当前路由,登录成功后跳回原页面,不要每次都被踢回首页,这种体验细节很影响用户对系统的耐受度。
5. 安全边界:jwt伪造、密钥泄露与在线解析的陷阱
5.1 jwt在线解析为什么会泄露信息
网上搜"jwt在线解析",能找到一堆网站,粘 Token 进去就能看到完整的 Header 和 Payload 明文。这个功能本身没问题,问题在于很多人意识不到:所有 JWT 的 Payload 都是公开可读的,不需要什么破解能力,只是 Base64URL 解码而已。你把 Token 贴给在线工具的同时,等于把业务关键词、用户 ID、角色信息都展示给了第三方网站。
我见过一些新人在调试接口时,随手把真实环境的 Token 贴到在线解析网站,结果 payload 里的手机号、邮箱全被看见了。虽然 JWT 里的敏感字段不至于直接被利用,但这本质上是一次信息泄露。建议工具类需求用本地脚本解码,或者只在对自己维护的解析服务中调试。JWT 是明文无加密的容器,这个定位一定要刻到脑子里面。
5.2 算法混淆攻击与jwt伪造的手段
"jwt伪造"和"jwt破解"是安全测试人员最爱聊的两个话题。先澄清一个常见误解:JWT 本身很难被"破解",因为签名的安全性建立在密钥上,HS256 用足够长的随机密钥时,暴力破解的代价是天文数字。真正的漏洞通常出在使用方配合上,而不是算法本身。
最经典的攻击是算法混淆。有些库在验证 Token 时,会读取 Header 里的alg字段来决定验证方式。攻击者把alg改成none,去掉签名部分,一些没做防护的库居然会直接通过校验。或者把alg从RS256改成HS256,让服务端用公钥当对称密钥去验签,攻击者只要能拿到公钥,就能自己算出合法的 HS256 签名。防住这类攻击其实很简单,就一条原则:验证时明确指定允许的算法,绝不能依赖 Token Header 里的算法名。用 jjwt 时可以通过parserBuilder().setSigningKey(key)固定密钥,并且不要引入不可信的解析方式,从库层面就把这条路堵死。
把alg改成none的伪造方式,在 jjwt 这种主流库里基本已经免疫了,但如果你在用的旧版库或者自研实现,一定要检查是否有这个洞。
RSA公钥误用为HMAC密钥的典型防范
再看一个细节:如果你的服务用 RS256,对外暴露了 JWKS 或公钥接口,当有人试图把 Token 的算法改成 HS256 提交过来时,你的服务可能拿公钥字符串当作 HS256 的密钥做验签。攻击者知道公钥内容,完全可以伪装成合法签名。要做到防范,校验时明确只接受 RS256,拒绝其他算法,或者签出来的 Token 上加一个固定的 Header 值,解析时先核对再往下走。这属于老生常谈,但每次线上排查伪造 Token 的案例,总结下来还是这类低级问题最致命。
5.3 前端存储与会话风险控制
Token 存哪里也是个值得展开的话题。很多 SPA 项目图省事,直接把 Token 放 localStorage,刷新页面不丢,取用也方便。但 localStorage 的访问权限是任何同源 JavaScript 都能读的,一旦页面被注入了一段恶意脚本,Token 就像把钥匙挂在大门口一样被人顺手摸走。相比之下,把 Token 放进 HttpOnly 的 Cookie 里,JavaScript 完全读不到,只能靠浏览器自动携带,XSS 攻击拿它没办法。代价是无法用请求头方式传递,对跨域场景要配置SameSite和 CORS 策略,调试时也要多折腾几步。
我的建议是:如果你做的项目对安全性有一定要求,优先考虑 HttpOnly Cookie 承载 JWT;如果团队更习惯 Authorization Header 的写法,且相信自己的 XSS 防护做得到位,再考虑 localStorage。同时把这两件事也纳入检查清单里:
- 日志系统里绝不能打完整 Token,打出来的话,排错日志本身就成了泄露源。
- 用户修改密码后,旧 Token 要按 4.3 节的方式全部作废。
- 登录接口要做限流,防止别人拿用户名密码撞库后换取大量合法的 JWT Token。
5.4 注册断言与回调接口的Token校验
如果你的系统接入了第三方回调,比如支付回调、外部登录回调,回调 URL 上带不放业务 Token,服务端接到回调时必须重新验签,绝不能因为"这个 URL 是配置好的"就默认安全。这个环节用的还是同一套验证逻辑,但要注意在回调场景中设置更严格的aud(受众)校验,防止一个回调 Token 被拿到别的接口里复用。
安全这块没有一劳永逸的解法,能做的就是把这些边界一个个收紧。像 jwt伪造这种问题,大多数时候不是算法被攻破,而是密钥管理、算法白名单、日志泄露这类工程细节没做到位。把密钥藏好、把算法钉死、把 Payload 当明文看待、把日志里的 Token 清干净,这几条守住,JWT 这套方案在绝大多数业务场景下都能站得稳。
本文还有配套的精品资源,点击获取