在实际项目中,我们经常需要调用外部系统的接口,这些接口通常通过 Access Token 做鉴权。Token 可能会过期或者被重置,如果不做处理,接口调用就会失败。本文用"IM 系统"作为示例,介绍一个 通用客户端设计方案,支持 Token 缓存、自动刷新以及失败重试。
⚠️ 这里的 IM 系统仅作为示例,方案可应用于其他系统。本文用的IM是box IM,如果相同可以直接使用
实现逻辑
1. 配置类
客户端需要的配置信息如下:
properties
package cn.iocoder.yudao.framework.common.im;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @Author: zr
* @Date: 2025/12/02/10:41
* @Description:
*/
@Component
@ConfigurationProperties(prefix = "im.auth")
@Data
public class ImAuthProperties {
private String baseUrl;
private String tenantId;
private String username;
private String password;
private String clientId;
private String grantType;
}
YAML 配置示例:
yaml
im:
auth:
base-url: http://127.0.0.1:8889
tenant-id: 000000
username: admin
password: xxxxxxxxxxxxx
client-id: xxxxxxxxxxxx
grant-type: password
2. Token 管理器
IMTokenManager 负责管理 Token,包括:
- Redis 缓存 Token
- 分布式锁防止并发刷新
- Token 失效自动刷新
- 接口调用失败自动重试
java
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.im.vo.ImResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Author: zr
* @Date: 2025/12/02/10:42
* @Description:
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class IMTokenManager {
private final ImAuthProperties properties;
private final RedisTemplate<String, String> redisTemplate;
private final RestTemplate restTemplate = new RestTemplate();
private static final String TOKEN_KEY = "IM:access_token";
private static final String LOCK_KEY = "IM:token_lock";
private static final String LOGIN_PATH = "/auth/login";
/** 获取 Token(自动刷新) */
public String getIMToken() {
String token = redisTemplate.opsForValue().get(TOKEN_KEY);
if (StrUtil.isNotBlank(token)) {
return token;
}
return refreshTokenWithLock();
}
/** 强制刷新 Token(不管 Redis 中有没有) */
public String refreshTokenForce() {
return refreshTokenWithLock();
}
/** 刷新 token 并加锁防止并发刷新 */
private String refreshTokenWithLock() {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY, "1", 10, TimeUnit.SECONDS);
if (BooleanUtil.isFalse(locked)) {
// 等待其他线程刷新
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
return redisTemplate.opsForValue().get(TOKEN_KEY);
}
try {
return refreshToken();
} finally {
redisTemplate.delete(LOCK_KEY);
}
}
/** 真正刷新 token 方法 */
private String refreshToken() {
String loginUrl = properties.getBaseUrl() + LOGIN_PATH;
Map<String, Object> req = new HashMap<>();
req.put("tenantId", properties.getTenantId());
req.put("username", properties.getUsername());
req.put("password", properties.getPassword());
req.put("rememberMe", properties.getRememberMe());
req.put("clientId", properties.getClientId());
req.put("grantType", properties.getGrantType());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(req, headers);
ResponseEntity<ImResult<ImResult.AuthRes>> responseEntity = restTemplate.exchange(
loginUrl,
HttpMethod.POST,
entity,
new ParameterizedTypeReference<ImResult<ImResult.AuthRes>>() {}
);
ImResult<ImResult.AuthRes> resp = responseEntity.getBody();
if (resp == null || resp.getCode() != 200) {
throw new RuntimeException("刷新 IM Token 失败:" + (resp != null ? resp.getMsg() : "无响应"));
}
String token = resp.getData().getAccess_token();
long expireSec = resp.getData().getExpire_in();
// 提前 60 秒刷新
long cacheExpire = Math.max(expireSec - 60, 30);
redisTemplate.opsForValue().set(TOKEN_KEY, token, cacheExpire, TimeUnit.SECONDS);
log.info("IM Token 刷新成功,有效期 {} 秒", expireSec);
return token;
}
/** 用于接口调用失败自动刷新 token 并重试一次 */
public <T> T callWithRetry(IMApiCall<T> call) {
T result = call.execute(getIMToken());
// 判断返回对象是否包含 code=401
if (result instanceof Map) {
Map<?, ?> map = (Map<?, ?>) result;
Object code = map.get("code");
if ("401".equals(String.valueOf(code))) {
log.warn("IM Token 失效,刷新后重试接口调用");
refreshTokenWithLock();
result = call.execute(getIMToken());
}
}
return result;
}
/** 接口调用函数式接口 */
@FunctionalInterface
public interface IMApiCall<T> {
T execute(String token);
}
}
3. 客户端封装
IMClient 封装 GET/POST 请求,自动带 Token 和 clientid Header,并支持参数传递。
java
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Map;
/**
* @Author: zr
* @Date: 2025/12/02/10:51
* @Description:
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class IMClient {
private final ImAuthProperties properties;
private final IMTokenManager tokenManager;
private final RestTemplate restTemplate = new RestTemplate();
/**
* GET 请求(自动刷新 token 并重试一次)
*/
public <T> T get(String path, Map<String, Object> params, Class<T> responseType) {
return tokenManager.callWithRetry(token -> {
// 构建带参数的 URL
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(properties.getBaseUrl() + path);
if (params != null) {
params.forEach((k, v) -> builder.queryParam(k, v));
}
String url = builder.toUriString();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.add("clientid", properties.getClientId()); // 添加 clientid Header
HttpEntity<Void> entity = new HttpEntity<>(headers);
// 打印调试信息
log.info("IM GET 请求 URL: {}", url);
log.info("IM GET 请求 Headers: {}", headers);
T response = restTemplate.exchange(url, HttpMethod.GET, entity, responseType).getBody();
log.info("IM GET Response: {}", response);
return response;
});
}
/**
* POST 请求(自动刷新 token 并重试一次)
*/
public <T> T post(String path, Object body, Class<T> responseType) {
return tokenManager.callWithRetry(token -> {
String url = properties.getBaseUrl() + path;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
headers.add("clientid", properties.getClientId());
HttpEntity<Object> entity = new HttpEntity<>(body, headers);
// 打印请求信息
log.info("IM POST 请求 URL: {}", url);
log.info("IM POST 请求 Headers: {}", headers);
log.info("IM POST 请求 Body: {}", body);
T response = restTemplate.postForObject(url, entity, responseType);
// 打印响应
log.info("IM POST Response: {}", response);
return response;
});
}
/**
* 获取当前有效的 IM Access Token
*/
public String getToken() {
return tokenManager.getIMToken();
}
}
4. 返回结果封装
java
package cn.iocoder.yudao.framework.common.im.vo;
import lombok.Data;
/**
* @Author: zr
* @Date: 2025/12/02/10:49
* @Description:
*/
@Data
public class ImResult <T>{
private Integer code;
private String msg;
private T data;
/**
* 认证返回的 Data
*/
@Data
public static class AuthRes {
private String access_token;
private Integer expire_in;
private String client_id;
private String openid;
private String refresh_token;
private Integer refresh_expire_in;
private String scope;
}
}
5. 使用示例

java
@Resource
private IMClient imClient;
@Test
public void testGet() {
// 调用任意 GET 接口
String path = "/im/group/list"; // 替换为实际接口
Map response = imClient.get(path,null, Map.class);
}
6. 接口调用失败自动刷新 Token 并重试演示
Redis 缓存的 token 还在有效期内,但它本身已经失效 (比如被后台强制作废、用户密码/权限变更、IM 服务端回收等)。单纯依赖过期时间无法保证 token 一定有效,所以需要在
IMClient或IMTokenManager中增加 "失败重试 + 刷新 token"机制。
- 修改redis中token的最后一位从E改为Q, 人为的制造token过期的异常情况


- 可以看到请求结果一次是token认证异常,会自动刷新 Token 并重试

7. 核心特点
- Redis 缓存 Token,减少重复登录。
- 分布式锁 防止并发刷新。
- 接口调用失败自动刷新 Token 并重试一次。
- GET/POST 请求封装 ,自动带 Token 和
clientid。 - 支持返回对象泛型,可处理 JSON、Map 或自定义对象。
这套方案通用性很强,适合调用任何需要 Token 鉴权的系统接口,不局限于 IM 系统。
2. 把 IMClient 抽取为 Spring Boot Starter (可选)
0. 源码地址
1. 新建一个maven的starter项目

项目结构如下:把第一部分的代码按照结构整理即可

2. pom.xml 配置
Starter 需要暴露给 Spring Boot 自动装配:
xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
</parent>
<groupId>site.zr.boot</groupId>
<artifactId>springboot-box-im-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot-box-im-starter</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<spring.boot.version>3.2.2</spring.boot.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot 自动装配支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- Spring Boot 配置处理器(可生成元信息用于 IDE 自动补全 @ConfigurationProperties) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring.boot.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Hutool 核心工具包(集合、字符串、日期等工具类) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.38</version>
</dependency>
<!-- Hutool HTTP 模块,简化 GET/POST 请求 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>5.8.38</version>
</dependency>
</dependencies>
</project>
3. 自动装配类
java
package site.zr.boot.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import site.zr.boot.IMClient;
import site.zr.boot.IMTokenManager;
import site.zr.boot.ImAuthProperties;
/**
* @Author: zr
* @Date: 2025/12/02/15:04
* @Description:
*/
@Configuration
@EnableConfigurationProperties(ImAuthProperties.class)
public class IMAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public IMTokenManager imTokenManager(ImAuthProperties properties, RedisTemplate<String, String> redisTemplate) {
return new IMTokenManager(properties, redisTemplate);
}
@Bean
@ConditionalOnMissingBean
public IMClient imClient(ImAuthProperties properties, IMTokenManager tokenManager) {
return new IMClient(properties, tokenManager);
}
}
4. 声明自动配置类(我用的Spring Boot 3/4 的方式)
⚠️ 注意事项:Spring Boot 3/4 与旧版自动配置方式的区别
在 Spring Boot 2.x 及之前版本,第三方 Starter 通常通过在 META-INF/spring.factories 文件中声明自动配置类,实现 @EnableAutoConfiguration 自动装配。例如:
properties
# Spring Boot 2.x 的方式
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
site.zr.boot.config.IMAutoConfiguration
然而,从 Spring Boot 3.x(以及未来 4.x) 开始,这种方式已经被 **META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports** 文件替代。新的机制更安全、灵活,也更适合模块化设计:
properties
# Spring Boot 3/4 的方式
site.zr.boot.config.IMAutoConfiguration
同时要注意:
- **自动配置类必须加上
@Configuration和 **@EnableConfigurationProperties:
plain
@Configuration
@EnableConfigurationProperties(ImAuthProperties.class)
public class IMAutoConfiguration { ... }
- 不要再使用
spring.factories文件,否则在新版本的 Spring Boot 中可能会失效或者出现 Bean 创建异常。 - Starter 项目依赖必须和 Spring Boot 主项目版本一致 ,否则可能导致
NoSuchMethodError(比如UriComponentsBuilder.fromHttpUrl方法在旧版与新版不兼容)。 - 尽量使用 Spring Boot 推荐的
spring-boot-starter-***依赖管理,避免手动引入低版本依赖冲突。
💡 小结:
如果你的 Starter 是为 Spring Boot 3.x 或 4.x 设计的,一定要采用 AutoConfiguration.imports + @EnableConfigurationProperties 的新方式,而不是沿用 2.x 的 spring.factories。
5. 打包并安装至本地maven仓库
install的时候同时也会package 所以会生成jar
install 会将生成的jar 包放进本地maven仓库如下图所示


6. 使用示例
- 新建一个springboot项目引入我们的starter,注意,必须要install才能通过如下maven的方式导入包
- 在使用方项目中:
xml
<dependency>
<groupId>site.zr.boot</groupId>
<artifactId>springboot-box-im-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- YAML 配置:
yaml
spring:
data:
redis:
host: 192.168.1.130 # 地址
port: 3306 # 端口
database: 14 # 数据库索引
password: xxxxxxx # 密码,建议生产环境开启
im:
auth:
base-url: http://127.0.0.1:8889
tenant-id: 000000
username: admin
password: xxxxxxxx
client-id: xxxxxxxxxxxxxxxxxx
grant-type: password
- 创建测试类,然后直接注入:
java
package site.zr.boot.test;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import site.zr.boot.IMClient;
import java.util.Map;
@SpringBootTest(classes = TestApplication.class)
class TestApplicationTests {
@Resource
private IMClient imClient;
@Test
void contextLoads() {
// 调用任意 GET 接口
String path = "/im/group/list"; // 替换为实际接口
Map response = imClient.get(path,null, Map.class);
}
}
- 无需再手动创建
IMClient或IMTokenManager,完全自动装配。
