☰
SpringBoot集成JWT认证:双Token机制与分布式部署实战
2026/10/13 14:53:57 网站建设 项目流程

1. 从Session到Token:为什么现代项目都在用JWT做认证

做过Web开发的人都知道,用户登录之后得有个东西记住"你是谁",不然每次请求都得重新输账号密码。早期最常用的方案是Session,服务端存一份用户状态,浏览器拿个SessionId,每次请求带上,服务端一查就知道是谁。这套逻辑简单直接,单体应用时代基本够用。

但事情在变化。当服务从单体拆成多个模块,或者前端从服务端渲染变成前后端分离,Session的问题就暴露出来了。最典型的是水平扩展:你部署了三台服务器,用户第一次请求落在A机器上,Session存在A的内存里,第二次请求被负载均衡甩到B机器上,B找不到这个Session,用户就被踢回登录页了。解决办法当然有,比如Session粘滞、Session复制、把Session集中存到Redis里,但这些都是在打补丁,架构上多了一层依赖,运维复杂度也上去了。

JWT(JSON Web Token)换了个思路:状态不存服务端,全部塞进一个Token里,由客户端自己保管。服务端只负责签发和校验,不需要存储。这个Token自带签名,篡改会被发现,所以服务端可以放心地信任它携带的信息。这就意味着,任何一台服务器拿到Token都能独立完成验证,天然适合分布式部署和水平扩展。

我最初接触JWT是在一个前后端分离的后台管理系统项目里。前端用Vue,后端SpringBoot提供RESTful接口,移动端App也要复用同一套接口。如果用Session,App端处理Cookie很别扭,跨域也麻烦。换成JWT之后,前端把Token存在localStorage里,每次请求往Header里一塞,后端一个拦截器统一校验,清爽很多。后来陆续在几个项目里都用了这套方案,踩过一些坑,也总结了一些经验,下面完整地拆一遍。

这篇文章适合已经有SpringBoot基础、想搞清楚JWT怎么落地的人。我会从整体设计思路讲起,然后拆解Token的生成、校验、刷新、注销等核心环节,附上可直接参考的代码和配置,最后把常见问题和排查方法整理出来。如果你正在做前后端分离项目,或者准备把单体应用改造成分布式架构,这套方案可以直接抄作业。

2. 整体设计思路与方案选型

2.1 为什么选JWT而不是继续用Session

先说清楚一个前提:JWT不是银弹,它解决的是特定场景下的问题。如果你的项目就是一个单体应用,用户量不大,部署在一台机器上,那Session完全够用,没必要为了技术时髦硬上JWT。JWT真正发挥优势的场景是这几个:

  • 前后端分离:前端可能是Web、App、小程序,后端只提供API,用Token比Cookie更自然。
  • 分布式部署:多台服务器无状态,不需要共享Session存储。
  • 跨服务调用:微服务之间可以用Token传递身份信息,不需要每个服务都去查用户中心。
  • 移动端友好:App端处理Cookie本来就麻烦,Token放在请求头里更符合移动端的习惯。

反过来,JWT也有它的短板,选型时必须心里有数:

  • Token无法主动失效:签发出去之后,在过期时间到达之前,服务端没法单方面作废它(除非引入黑名单机制,那就又变成有状态了)。
  • Token体积比SessionId大:每次请求都要携带,如果塞太多信息进去,请求体积会膨胀。
  • 续期麻烦:Token快过期时怎么无感续期,需要额外设计。

所以我的选型逻辑是:前后端分离 + 分布式部署的场景用JWT,单体且用户量可控的场景继续用Session。这不是技术优劣的问题,是场景匹配的问题。

2.2 Token方案的核心结构设计

一个完整的JWT认证方案,不只是"生成Token、校验Token"这么简单。我在实际项目中会把整个认证体系拆成这么几块:

模块职责关键设计点
Token生成登录成功后签发Token载荷放什么、签名算法选什么、过期时间设多长
Token校验每次请求验证Token合法性拦截器还是过滤器、校验哪些字段、异常怎么处理
Token刷新过期前换取新Token双Token机制还是单Token续期
Token注销用户主动退出黑名单还是短过期时间兜底
异常处理认证失败的统一响应401状态码、错误信息格式

这套结构里,双Token机制是我比较推荐的方案。所谓双Token,就是登录时同时签发两个Token:一个AccessToken(访问令牌),过期时间短,比如30分钟;一个RefreshToken(刷新令牌),过期时间长,比如7天。AccessToken用来访问业务接口,过期后用RefreshToken去换新的AccessToken。这样既保证了安全性(AccessToken泄露了也很快失效),又保证了用户体验(不用频繁重新登录)。

提示:RefreshToken的过期时间不宜过长,7天到30天是比较常见的范围。如果业务对安全要求高,可以缩短到1天甚至更短,配合强制重新登录。

2.3 签名算法的选择

JWT的签名算法分两大类:对称加密(HMAC)和非对称加密(RSA/ECDSA)。

  • HMAC(HS256/HS384/HS512):用一个密钥同时做签名和验证。优点是快、简单,缺点是所有验证方都持有同一个密钥,一旦泄露,任何人都能伪造Token。
  • RSA(RS256/RS384/RS512):私钥签名,公钥验证。优点是验证方只拿公钥,无法伪造Token,适合多服务场景。缺点是慢一些,密钥管理复杂一些。

我的选择逻辑是:单体或少量服务用HS256,微服务架构用RS256。大部分中小项目用HS256就够了,密钥放在配置文件里,注意不要提交到代码仓库。如果服务多了,验证方分散,那就上RS256,把公钥分发给各个服务,私钥只有认证中心持有。

这里有个容易踩的坑:密钥长度。HS256要求密钥至少256位(32字节),很多人随便写个"secret"当密钥,这在实际项目里是不安全的。我一般用随机生成的Base64字符串,长度控制在64字节左右。

3. 核心细节解析与实操要点

3.1 JWT的三段式结构拆解

JWT的字符串长这样:xxxxx.yyyyy.zzzzz,三个部分用点号分隔。

第一部分是Header(头部),Base64Url编码的JSON,描述Token的元信息:

{ "alg": "HS256", "typ": "JWT" }

alg表示签名算法,typ表示Token类型。这部分是明文可解的,不要放敏感信息。

第二部分是Payload(载荷),也是Base64Url编码的JSON,存放实际要传递的数据。标准字段有:

  • iss:签发者
  • sub:主题
  • exp:过期时间(Unix时间戳)
  • iat:签发时间
  • jti:Token唯一标识

除了标准字段,可以放自定义字段,比如用户ID、用户名、角色等。但要注意,Payload只是Base64编码,不是加密,任何人拿到Token都能解出里面的内容。所以绝对不要往Payload里放密码、手机号、身份证号这类敏感信息。

第三部分是Signature(签名),用Header里指定的算法,对Header.Payload进行签名。以HS256为例:

HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )

签名的目的是防篡改。服务端收到Token后,用同样的密钥和算法重新算一遍签名,和Token里的签名对比,不一致就说明被改过了。

注意:JWT的Payload是"可读但不可改"。可读意味着你不能放敏感数据,不可改意味着签名保证了数据完整性。这两点必须同时记住。

3.2 依赖选型:jjwt还是java-jwt

Java生态里操作JWT的库主要有两个:jjwt和java-jwt(Auth0出的)。我两个都用过,说说区别。

jjwt是社区维护的,API设计比较流畅,链式调用写起来舒服,文档也全。java-jwt是Auth0维护的,API更简洁,但功能相对少一些。我目前项目里用的是jjwt,版本选0.11.5或0.12.x。0.12.x的API有较大变化,如果是从0.11.x升级要注意。

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是编译时用的接口,jjwt-impl是运行时实现,jjwt-jackson是JSON序列化支持。这样设计是为了让使用者只依赖API,实现可以替换。实际用的时候三个都要加,impl和jackson的scope设为runtime。

3.3 密钥管理:别把密钥写死在代码里

这是我最想强调的一点。很多教程里直接写String secret = "mySecretKey",然后硬编码在代码里。这在demo里没问题,但实际项目里是大忌。密钥一旦泄露,任何人都能签发合法Token,整个认证体系就崩了。

我的做法是:

  1. 密钥放在配置文件里,通过@Value或@ConfigurationProperties注入。
  2. 配置文件不提交到代码仓库,用.gitignore排除,或者用环境变量覆盖。
  3. 不同环境用不同密钥,开发、测试、生产各一套。
  4. 密钥定期轮换,虽然JWT的密钥轮换会导致旧Token失效,但安全要求高的场景值得这么做。

配置示例:

jwt: secret: ${JWT_SECRET:默认开发密钥至少32字节长度哦} access-token-expire: 1800 refresh-token-expire: 604800

${JWT_SECRET:默认值}的写法表示优先从环境变量JWT_SECRET读取,读不到就用默认值。生产环境一定要设置环境变量,不要用默认值。

提示:密钥长度至少32字节(256位),我一般用openssl rand -base64 64生成一个64字节的随机字符串。

4. 实操过程与核心环节实现

4.1 项目结构与依赖配置

先看一下整体结构,我习惯按职责分包:

src/main/java/com/example/demo/ ├── config/ │ └── JwtProperties.java // 配置绑定 ├── util/ │ └── JwtUtil.java // Token生成与解析工具 ├── interceptor/ │ └── JwtInterceptor.java // 请求拦截器 ├── controller/ │ └── AuthController.java // 登录、刷新、注销接口 ├── entity/ │ └── User.java // 用户实体 └── common/ └── Result.java // 统一响应

除了jjwt的三个包,还需要SpringBoot Web的starter。如果要用拦截器,不需要额外依赖,SpringMVC自带。

4.2 配置绑定类:把JWT参数集中管理

@Data @Component @ConfigurationProperties(prefix = "jwt") public class JwtProperties { private String secret; private long accessTokenExpire; private long refreshTokenExpire; }

用@ConfigurationProperties而不是@Value,好处是参数集中、类型安全、支持嵌套。@Data是Lombok的注解,自动生成getter/setter。

4.3 Token生成:载荷放什么、过期时间怎么算

@Component public class JwtUtil { @Resource private JwtProperties jwtProperties; private Key getSigningKey() { byte[] keyBytes = jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8); return Keys.hmacShaKeyFor(keyBytes); } public String generateAccessToken(Long userId, String username, String role) { Date now = new Date(); Date expiry = new Date(now.getTime() + jwtProperties.getAccessTokenExpire() * 1000); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .claim("role", role) .setIssuedAt(now) .setExpiration(expiry) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } public String generateRefreshToken(Long userId) { Date now = new Date(); Date expiry = new Date(now.getTime() + jwtProperties.getRefreshTokenExpire() * 1000); return Jwts.builder() .setSubject(String.valueOf(userId)) .setIssuedAt(now) .setExpiration(expiry) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } }

几个关键点解释一下:

setSubject放用户ID,而不是用户名。因为用户ID是唯一的、不变的,用户名可能被修改。后续从Token里取用户身份,用ID去查库最可靠。

claim放自定义字段,这里放了用户名和角色。角色字段很有用,可以在拦截器里做权限判断,不用每次都查库。但要注意,角色变更后旧Token里的角色还是旧的,直到Token过期。如果角色变更需要立即生效,那就不能把角色放Token里,得每次查库。

过期时间用秒计算,配置里写的是秒数,乘以1000转成毫秒。accessTokenExpire设1800秒(30分钟),refreshTokenExpire设604800秒(7天)。

signWith指定算法和密钥,HS256对应256位密钥。如果密钥长度不够,Keys.hmacShaKeyFor会抛异常,这是好事,能提前发现问题。

4.4 Token解析与校验:异常处理是关键

public Claims parseToken(String token) { try { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { throw new BusinessException("Token已过期"); } catch (SignatureException e) { throw new BusinessException("Token签名无效"); } catch (MalformedJwtException e) { throw new BusinessException("Token格式错误"); } catch (Exception e) { throw new BusinessException("Token校验失败"); } }

这里把不同类型的异常分开处理,是为了给前端返回更明确的错误信息。ExpiredJwtException表示过期,前端收到这个错误码后应该去刷新Token;SignatureException表示签名不对,可能是被篡改了,前端应该跳登录页;MalformedJwtException表示格式都不对,基本是乱传的。

注意:parseClaimsJws和parseClaimsJwt的区别。前者会校验签名,后者不会。一定要用parseClaimsJws,用错了等于没校验。

4.5 拦截器实现:统一拦截需要认证的接口

@Component public class JwtInterceptor implements HandlerInterceptor { @Resource private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { writeError(response, 401, "未提供Token"); return false; } token = token.substring(7); try { Claims claims = jwtUtil.parseToken(token); request.setAttribute("userId", Long.valueOf(claims.getSubject())); request.setAttribute("username", claims.get("username")); request.setAttribute("role", claims.get("role")); return true; } catch (BusinessException e) { writeError(response, 401, e.getMessage()); return false; } } private void writeError(HttpServletResponse response, int code, String msg) throws IOException { response.setStatus(code); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":" + code + ",\"msg\":\"" + msg + "\"}"); } }

拦截器的逻辑是:从Authorization头里取Token,去掉Bearer前缀,解析后把用户信息塞到request属性里,后续Controller可以直接取。解析失败就返回401。

为什么用Bearer前缀?这是HTTP认证的标准格式,Bearer表示持有者令牌。加上前缀是为了和Basic认证等其他方式区分。前端请求时这样设置:

axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;

拦截器注册:

@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private JwtInterceptor jwtInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/refresh"); } }

addPathPatterns指定拦截哪些路径,excludePathPatterns排除登录和刷新接口,这两个接口不需要Token。

4.6 登录接口:签发双Token

@RestController @RequestMapping("/api/auth") public class AuthController { @Resource private JwtUtil jwtUtil; @PostMapping("/login") public Result<Map<String, String>> login(@RequestBody LoginDTO dto) { // 省略:校验用户名密码,查库 User user = userService.login(dto.getUsername(), dto.getPassword()); if (user == null) { return Result.fail("用户名或密码错误"); } String accessToken = jwtUtil.generateAccessToken(user.getId(), user.getUsername(), user.getRole()); String refreshToken = jwtUtil.generateRefreshToken(user.getId()); Map<String, String> data = new HashMap<>(); data.put("accessToken", accessToken); data.put("refreshToken", refreshToken); return Result.success(data); } }

登录成功后返回两个Token,前端把AccessToken放在内存或localStorage里,RefreshToken建议放在更安全的地方(比如HttpOnly Cookie,但前后端分离场景下放localStorage也常见,安全性靠HTTPS和短过期时间兜底)。

4.7 刷新接口:用RefreshToken换新AccessToken

@PostMapping("/refresh") public Result<Map<String, String>> refresh(@RequestBody Map<String, String> body) { String refreshToken = body.get("refreshToken"); if (refreshToken == null) { return Result.fail("缺少RefreshToken"); } try { Claims claims = jwtUtil.parseToken(refreshToken); Long userId = Long.valueOf(claims.getSubject()); // 查库获取最新用户信息 User user = userService.getById(userId); if (user == null) { return Result.fail("用户不存在"); } String newAccessToken = jwtUtil.generateAccessToken(user.getId(), user.getUsername(), user.getRole()); Map<String, String> data = new HashMap<>(); data.put("accessToken", newAccessToken); return Result.success(data); } catch (BusinessException e) { return Result.fail("RefreshToken无效,请重新登录"); } }

刷新接口的逻辑是:验证RefreshToken,取出用户ID,查库拿最新信息,签发新的AccessToken。注意这里重新查库了,因为RefreshToken的有效期长,期间用户信息可能变了,不能直接用旧Token里的信息。

4.8 注销接口:黑名单机制的取舍

JWT本身无法主动失效,注销是个难题。我的做法是引入一个短期的黑名单,用Redis存被注销的Token,过期时间和Token本身的过期时间一致。拦截器校验时先查黑名单,命中就拒绝。

@PostMapping("/logout") public Result<Void> logout(HttpServletRequest request) { String token = request.getHeader("Authorization").substring(7); Claims claims = jwtUtil.parseToken(token); long ttl = claims.getExpiration().getTime() - System.currentTimeMillis(); if (ttl > 0) { redisTemplate.opsForValue().set("jwt:blacklist:" + token, "1", ttl, TimeUnit.MILLISECONDS); } return Result.success(); }

拦截器里加一段:

if (Boolean.TRUE.equals(redisTemplate.hasKey("jwt:blacklist:" + token))) { writeError(response, 401, "Token已注销"); return false; }

这个方案的本质是"用有状态换安全",黑名单的存储量取决于注销频率和Token剩余有效期。如果注销很频繁,Redis压力会大。所以我的建议是:AccessToken过期时间设短一点(15-30分钟),注销时只把AccessToken加黑名单,RefreshToken通过其他机制失效(比如用户表里存一个tokenVersion,刷新时校验)。

5. 常见问题与排查技巧实录

5.1 Token过期与时钟偏移问题

最常见的问题是"Token明明没过期,却报过期"。这通常是服务器时钟不同步导致的。JWT的exp是绝对时间戳,如果签发服务器和验证服务器的时钟差了几分钟,就会出现提前过期或延后过期。

解决办法有两个:一是所有服务器开启NTP时间同步;二是在解析时允许一定的时钟偏移:

Jwts.parserBuilder() .setSigningKey(getSigningKey()) .setAllowedClockSkewSeconds(60) .build()

setAllowedClockSkewSeconds(60)表示允许60秒的偏差。这个值不要设太大,否则等于变相延长了Token有效期。

5.2 跨域请求Token丢失

前后端分离项目里,前端请求后端接口时如果跨域,Authorization头可能被浏览器拦截。需要在后端配置CORS:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

注意allowedOriginPatterns和allowedOrigins的区别。当allowCredentials(true)时,不能用allowedOrigins("*"),要用allowedOriginPatterns。另外,OPTIONS预检请求要放行,拦截器里对OPTIONS请求直接返回true。

5.3 常见问题速查表

问题现象可能原因排查方向
401未提供Token前端没带Authorization头检查axios拦截器是否设置
401 Token格式错误缺少Bearer前缀或Token被截断检查Header拼接逻辑
401 Token签名无效密钥不一致或Token被篡改检查多环境密钥配置
401 Token已过期过期时间到了或时钟偏移检查exp配置和服务器时间
刷新后仍401新Token没更新到前端检查刷新接口的响应处理
注销后仍能访问黑名单没生效检查Redis连接和key前缀

5.4 几个我踩过的坑

坑一:Payload塞太多东西。有次我把用户的完整信息(包括权限列表)都塞进Token,结果Token长度超过2KB,每次请求Header都很大,Nginx还报了Header过大的错误。后来精简到只放userId、username、role三个字段,Token长度控制在500字节以内。

坑二:RefreshToken没有单独校验。一开始我用同一个方法解析AccessToken和RefreshToken,结果发现RefreshToken也能访问业务接口。后来在Payload里加了个type字段区分,拦截器里校验type必须是access。

坑三:密钥用了默认值上线。测试环境用的默认密钥,上线时忘了改环境变量,结果生产环境和测试环境密钥一样。虽然没出安全事故,但这是个隐患。后来我在启动时加了个校验,如果密钥是默认值且是生产环境,直接启动失败。

坑四:并发刷新导致Token覆盖。前端多个请求同时发现Token过期,同时去刷新,后刷新的覆盖了先刷新的,导致部分请求用了旧Token。解决办法是前端加个刷新锁,或者后端刷新接口做幂等处理。

提示:Token刷新最好在前端统一拦截器里做,检测到401且是过期错误时,暂停其他请求,刷新完成后再重放。这个逻辑稍微复杂,但能避免很多并发问题。

6. 安全加固与性能优化建议

6.1 安全加固的几个关键点

JWT方案的安全性,很大程度上取决于细节处理。我总结了几个必须做的加固点:

强制HTTPS。Token在网络上传输,如果走HTTP,中间人可以直接截获。生产环境必须上HTTPS,这是底线。

Token存储位置。localStorage方便但有XSS风险,HttpOnly Cookie安全但有CSRF风险。我的选择是AccessToken放内存(页面刷新就没了,靠RefreshToken恢复),RefreshToken放HttpOnly Cookie。这样XSS拿不到RefreshToken,CSRF因为AccessToken在内存里也难利用。

敏感操作二次验证。改密码、转账这类操作,不能只靠Token,要重新验证密码或短信验证码。

限制Token使用范围。可以在Payload里加aud(受众)字段,标明这个Token是给哪个服务用的,防止Token被跨服务滥用。

6.2 性能优化的实际经验

JWT的校验是纯计算,不查库,性能本来就很好。但有几个地方可以优化:

密钥解析缓存。Keys.hmacShaKeyFor每次调用都会重新生成Key对象,可以缓存起来:

private volatile Key cachedKey; private Key getSigningKey() { if (cachedKey == null) { synchronized (this) { if (cachedKey == null) { cachedKey = Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)); } } } return cachedKey; }

黑名单查询优化。如果黑名单用Redis,每次请求都查一次Redis,QPS高的时候Redis压力不小。可以用本地缓存(Caffeine)做一层,缓存几秒钟,减少Redis查询。

Token长度控制。前面说过,Payload越小越好。如果角色权限很多,不要全塞Token里,放个角色ID,权限从缓存查。

6.3 微服务场景下的扩展思路

如果项目演进到微服务,JWT方案需要调整。核心变化是:认证中心统一签发Token,各服务用公钥验证。这时候签名算法要从HS256换成RS256,认证中心持有私钥,各服务持有公钥。

网关层做统一校验,校验通过后把用户信息放到请求头里传给下游服务。下游服务信任网关传来的信息,不再重复校验Token。这样既保证了安全,又避免了每个服务都解析Token的开销。

如果服务间调用也需要认证,可以用Token透传或者服务间单独签发Token。这块展开又是一大篇,这里先点到为止。

7. 我个人的一些实操体会

这套JWT方案我在三个项目里落地过,从最初的手忙脚乱到现在的驾轻就熟,中间踩的坑基本都写在上面的内容里了。如果让我给正在准备上JWT的人一句建议,那就是:先把双Token机制和黑名单方案想清楚再动手,不要一开始就图简单只搞一个Token,后面续期和注销的需求一定会来,到时候改起来很痛苦。

另外,JWT的配置参数没有标准答案,AccessToken设15分钟还是30分钟,RefreshToken设7天还是30天,取决于你的业务安全等级和用户体验要求。金融类应用可以设短一点,内部管理系统可以设长一点。我的经验值是AccessToken 30分钟、RefreshToken 7天,这个组合在大多数场景下比较平衡。

最后分享一个小技巧:在开发阶段,可以把AccessToken的过期时间设成几秒钟,方便测试刷新逻辑。等刷新流程跑通了,再改回正常值。这个办法帮我省了很多手动改Token的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询