基于 Sa-Token 实现 API 接口签名校验(Spring Boot 3 实战)
本文介绍了基于Spring Boot 3和Sa-Token框架实现API接口签名校验的解决方案。通过sa-token-sign模块提供开箱即用的签名校验功能,支持多种哈希算法和防重放攻击机制。文章详细展示了数据库表设计、实体类定义、Service层实现(含Redis缓存)以及自定义签名配置加载器的关键代码。该方案能够有效防止参数篡改和重放攻击,同时支持动态加载签名配置,适合微服务架构下的接口安全防
·
在微服务架构中,系统之间的调用往往需要保证 安全性。如果缺乏有效的防护机制,接口极易遭受伪造请求攻击。
接口签名校验 就是一种常见的安全手段,可以有效避免参数篡改、重放攻击。
本文将基于 Spring Boot 3 + Sa-Token 的 sa-token-sign 模块,手把手带你实现接口签名校验,并扩展到数据库存储,支持动态接入。
签名校验流程图
Client Server
| |
| appid, params, sign, timestamp, nonce |
| -----------------------------------> |
| | 1. 从数据库加载密钥配置
| | 2. 验证 timestamp 是否在有效期内
| | 3. 验证 nonce 是否重复
| | 4. 计算签名并比对
| | 5. Redis 缓存配置(12小时)
| <----------------------------------|
| Response |
Sa-Token 签名模块简介
sa-token-sign 模块开箱即用,提供了:
✅ 支持 MD5 / SHA256 / SHA512
✅ 内置 timestamp / nonce 校验
✅ 支持 多应用配置
✅ 提供 @SaCheckSign 注解,零侵入接入
✅ 支持 自定义配置源(数据库)
项目依赖
<!-- Sa-Token Starter -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.44.0</version>
</dependency>
<!-- API 参数签名 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sign</artifactId>
<version>1.44.0</version>
</dependency>
<!-- Sa-Token 与 Redis 集成(用于缓存签名配置) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>1.44.0</version>
</dependency>
数据库设计
1 表结构
create table t_app_sign_config
(
id bigint auto_increment comment '主键ID' primary key,
app_id varchar(64) not null comment '应用ID',
secret_key varchar(128) not null comment '密钥',
digest_algo varchar(32) default 'md5' not null comment '签名算法: md5 / sha256 / sha512',
timestamp_disparity bigint default 900000 null comment '时间戳允许误差(毫秒) 默认15分钟',
create_by varchar(50) null comment '创建人',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_by varchar(50) null comment '更新人',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
constraint uk_app_id unique (app_id)
) comment '应用签名配置表';
2 初始化数据
INSERT INTO t_app_sign_config (app_id, secret_key, digest_algo, timestamp_disparity, create_by)
VALUES ('AppId1', '601b8ddd3037c782476e4be8102f6a07', 'md5', 900000, 'admin');
INSERT INTO t_app_sign_config (app_id, secret_key, digest_algo, timestamp_disparity, create_by)
VALUES ('AppId2', '954911e93f7e14fe1e09a713bf96b0da', 'md5', 900000, 'admin');
后端实现
1 实体类
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_app_sign_config")
public class AppSignConfig extends BaseEntity {
/**
* 应用ID
*/
private String appId;
/**
* 密钥
*/
private String secretKey;
/**
* md5 / sha256 / sha512
*/
private String digestAlgo;
/**
* 时间戳误差(秒)
*/
private Long timestampDisparity;
}
2 Service 层(含 Redis 缓存)
public interface IAppSignConfigService extends IService<AppSignConfig> {
AppSignConfig getByAppId(String appId);
}
@Service
public class AppSignConfigServiceImpl extends ServiceImpl<AppSignConfigRepository, AppSignConfig>
implements IAppSignConfigService {
private static final String CACHE_PREFIX = "rbom-sync:api:signconfig:";
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public AppSignConfig getByAppId(String appId) {
String cacheKey = CACHE_PREFIX + appId;
String cache = redisTemplate.opsForValue().get(cacheKey);
if (cache != null) {
return JsonUtils.fromJson(cache, AppSignConfig.class);
}
AppSignConfig appSignConfig = lambdaQuery()
.eq(AppSignConfig::getAppId, appId)
.one();
// 缓存配置(12小时)
redisTemplate.opsForValue()
.set(cacheKey, JsonUtils.toJson(appSignConfig), Duration.ofHours(12));
return appSignConfig;
}
}
3 自定义签名配置加载器
为什么能够动态加载签名信息,这一步很重要
@Component
public class MySignConfigLoader {
private static final Logger logger = LoggerFactory.getLogger(MySignConfigLoader.class);
@Autowired
private IAppSignConfigService appSignConfigService;
@PostConstruct
public void init() {
logger.info("初始化自定义签名配置加载器...");
// 覆盖 SaSignMany 的查找逻辑
SaSignMany.findSaSignConfigMethod = (appid) -> {
try {
logger.debug("查找应用签名配置,appid: {}", appid);
AppSignConfig appSignConfig = appSignConfigService.getByAppId(appid);
if (appSignConfig == null) {
logger.warn("未找到应用签名配置,appid: {}", appid);
throw new RuntimeException("appid 不存在: " + appid);
}
SaSignConfig config = new SaSignConfig();
config.setSecretKey(appSignConfig.getSecretKey());
config.setDigestAlgo(appSignConfig.getDigestAlgo());
config.setTimestampDisparity(appSignConfig.getTimestampDisparity());
logger.debug("成功加载应用签名配置,appid: {}, algorithm: {}",
appid, appSignConfig.getDigestAlgo());
return config;
} catch (Exception e) {
logger.error("加载应用签名配置失败,appid: {}, error: {}", appid, e.getMessage(), e);
throw new BusinessException("加载应用签名配置失败: " + e.getMessage(), e);
}
};
logger.info("自定义签名配置加载器初始化完成");
}
}
4 拦截器配置
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 开启 Sa-Token 注解拦截
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
5 Controller 使用
@RestController
@RequestMapping("/sync")
public class SyncController {
@Autowired
private IModelService modelService;
// 从请求参数中动态获取 appid
@SaCheckSign(appid = "#{appid}")
@GetMapping("/model")
public ResponseEntity<Res> model() {
List<Model> models = modelService.list();
return ResponseEntity.ok(Res.success(models));
}
}
6 application.properties 配置
# Sa-Token 配置
sa-token.token-name=***-sync:satoken
客户端调用示例
1 Java 客户端(OkHttp)
OkHttpClient client = new OkHttpClient().newBuilder().build();
String appid = "AppId1";
String nonce = UUID.randomUUID().toString();
long timestamp = System.currentTimeMillis();
// 拼接签名字符串
String raw = "appid=" + appid + "&nonce=" + nonce + "×tamp=" + timestamp
+ "&key=601b8ddd3037c782476e4be8102f6a07";
// 生成 MD5 签名
String sign = DigestUtils.md5DigestAsHex(raw.getBytes(StandardCharsets.UTF_8));
// 发送请求
Request request = new Request.Builder()
.url("http://***:8080/sync/model"
+ "?appid=" + appid
+ "&sign=" + sign
+ "&nonce=" + nonce
+ "×tamp=" + timestamp)
.get()
.build();
Response response = client.newCall(request).execute();
System.out.println(response.body().string());
6.2 curl 示例
curl "http://***:8080/sync/model?appid=MDM&sign=***&nonce=***×tamp=***"
架构设计亮点
| 设计点 | 说明 |
|---|---|
| 数据库存储 | 密钥配置持久化,支持动态扩展新应用 |
| Redis 缓存 | 缓存签名配置 12 小时,减少数据库查询 |
| 自定义加载器 | 通过 @PostConstruct 覆盖默认配置加载逻辑 |
| 动态 appid | 使用 SpEL 表达式 #{appid} 从请求参数获取 |
| 异常处理 | 统一的日志记录和异常封装 |
| 唯一约束 | 数据库 app_id 唯一索引防止重复 |
常见问题
Q1: 如何添加新的应用?
直接在数据库 t_app_sign_config 表插入新记录即可,无需重启服务。
Q2: 缓存失效后如何刷新?
缓存 TTL 为 12 小时,过期后自动从数据库重新加载。
Q3: timestamp_disparity 的含义?
表示时间戳允许的误差范围(毫秒),默认 900000ms(15 分钟),防止重放攻击。
更多推荐


所有评论(0)