🚀 基于 Sa-Token 实现 API 接口签名校验(Spring Boot 3 实战)

在微服务架构中,系统之间的调用往往需要保证 安全性。如果缺乏有效的防护机制,接口极易遭受伪造请求攻击。

👉 接口签名校验 就是一种常见的安全手段,可以有效避免参数篡改、重放攻击。

本文将基于 Spring Boot 3 + Sa-Tokensa-token-sign 模块,手把手带你实现接口签名校验,并扩展到数据库存储,支持动态接入。

1️⃣ 签名校验原理

签名流程大致如下:

  1. 客户端:将 appid + nonce + timestamp + secret 拼接后生成 sign
  2. 服务端:根据 appId 找到对应的 secret,再生成一次签名并比对
  3. 同时校验:
    • timestamp 是否过期(防止超时请求)
    • nonce 是否已使用(防止重放攻击)

📊 流程图:

Client                Server
   |                     |
   | appId, params, sign | --> 验签(Secret、timestamp、nonce)
   |                     |
   | <----- Response ----|

2️⃣ Sa-Token 签名模块简介

sa-token-sign 模块开箱即用,提供了:

✅ 支持 MD5 / SHA256 / SHA512
✅ 内置 timestamp / nonce 校验
✅ 支持 多应用配置(配置文件 or 数据库)
✅ 提供 @SaCheckSign 注解,零侵入接入

3️⃣ 配置文件模式(入门最快)

3.1 引入依赖

<!-- 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>

3.2 application.properties 配置

# 应用1
sa-token.sign-many.MDM.secret-key=1044ebd2843c1a9c4b9900135e706ba8
sa-token.sign-many.MDM.digest-algo=md5

# 应用2
sa-token.sign-many.forum.secret-key=abcdefg123456
sa-token.sign-many.forum.digest-algo=sha256

3.3 配置拦截器

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 开启 Sa-Token 注解拦截
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }
}

3.4 控制器示例

@RestController
@RequestMapping("/api/shop")
public class ShopController {

    // 只允许 appid=MDM 的请求访问
    @SaCheckSign(appid = "MDM")
    @GetMapping("/data")
    public Object getShopData() {
        return Map.of("status", "ok", "msg", "shop data success");
    }
}

3.5 请求示例

Java 客户端

OkHttpClient client = new OkHttpClient().newBuilder().build();

String appid = "MDM";
String nonce = UUID.randomUUID().toString();
long timestamp = System.currentTimeMillis();

// 拼接签名字符串
String raw = "appid=" + appid + "&nonce=" + nonce + "&timestamp=" + timestamp + "&key=1044ebd2843c1a9c4b9900135e706ba8";
// 生成 MD5 签名
String sign = DigestUtils.md5DigestAsHex(raw.getBytes(StandardCharsets.UTF_8));

// 发送请求
Request request = new Request.Builder()
        .url("http://localhost:8080/api/shop/data"
             + "?appid=" + appid
             + "&sign=" + sign
             + "&nonce=" + nonce
             + "&timestamp=" + timestamp)
        .get()
        .build();

Response response = client.newCall(request).execute();
System.out.println(response.body().string());

curl 示例

curl "http://localhost:8080/api/shop/data?appid=MDM&sign=97c520a7...&nonce=c6ee2098-...&timestamp=1758695980450"

💡 优点:简单易用
⚠️ 缺点:修改配置必须重启服务

4️⃣ 数据库存储(推荐方案)

当调用方数量较多时,推荐把签名信息放入数据库,支持动态扩展。

4.1 表结构

create table t_app_sign_config (
    id bigint auto_increment primary key,
    app_id varchar(64) not null unique comment '应用ID',
    secret_key varchar(128) not null comment '密钥',
    digest_algo varchar(32) default 'md5' not null comment '签名算法',
    timestamp_disparity bigint default 900000 comment '时间戳误差(毫秒)',
    create_time datetime default CURRENT_TIMESTAMP,
    update_time datetime default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP
);

4.2 实体类

@Data
@TableName("t_app_sign_config")
public class AppSignConfig {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String appId;
    private String secretKey;
    private String digestAlgo;
    private Long timestampDisparity;
}

4.3 Service + Redis 缓存

@Service
public class AppSignConfigServiceImpl extends ServiceImpl<AppSignConfigRepository, AppSignConfig> {
    private static final String CACHE_PREFIX = "demo:api:signconfig:";

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    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 config = lambdaQuery().eq(AppSignConfig::getAppId, appId).one();
        redisTemplate.opsForValue().set(cacheKey, JsonUtils.toJson(config), Duration.ofHours(12));
        return config;
    }
}

4.4 自定义配置加载器

@Component
public class MySignConfigLoader {

    @Autowired
    private AppSignConfigServiceImpl appSignConfigService;

    @PostConstruct
    public void init() {
        // 覆盖 Sa-Token 默认加载逻辑
        SaSignMany.findSaSignConfigMethod = (appid) -> {
            AppSignConfig cfg = appSignConfigService.getByAppId(appid);
            if (cfg == null) throw new RuntimeException("appid 不存在: " + appid);

            SaSignConfig config = new SaSignConfig();
            config.setSecretKey(cfg.getSecretKey());
            config.setDigestAlgo(cfg.getDigestAlgo());
            config.setTimestampDisparity(cfg.getTimestampDisparity());
            return config;
        };
    }
}

5️⃣ Redis 记录 nonce 防重放

只需引入一个依赖即可:

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-template</artifactId>
    <version>1.44.0</version>
</dependency>

6️⃣ 两种方案对比

方案优点缺点适用场景
配置文件简单,快速接入修改需重启服务小规模,调用方固定
数据库动态扩展,易管理增加 DB/缓存依赖多应用,大规模接入

7️⃣ 最佳实践建议

  1. Redis 缓存:减少 DB 压力
  2. 开启 nonce 校验:避免重放攻击
  3. 日志监控:记录失败原因(签名错误、超时、nonce 重复)
  4. 灰度迁移:配置文件模式 → 数据库存储

🎯 总结

  • sa-token-sign 模块让接口签名校验变得轻量易用
  • 配置文件适合小规模场景,数据库模式适合大规模、多调用方接入
  • 搭配 Redis + 日志监控,可以快速构建企业级 API 防护体系
文章作者: LibSept24_
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LibSept24_
SaToken
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝