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

在微服务架构中,系统之间的调用往往需要保证 安全性 。如果缺乏有效的防护机制,接口极易遭受伪造请求攻击。
接口签名校验 就是一种常见的安全手段,可以有效避免参数篡改、重放攻击。

本文将基于 Spring Boot 3 + Sa-Tokensa-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 注解,零侵入接入

✅ 支持 自定义配置源(数据库)

项目依赖

xml 复制代码
<!-- 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 表结构

sql 复制代码
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 初始化数据

sql 复制代码
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 实体类

java 复制代码
@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 缓存)

java 复制代码
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 自定义签名配置加载器

为什么能够动态加载签名信息,这一步很重要

java 复制代码
@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 拦截器配置

java 复制代码
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {

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

5 Controller 使用

java 复制代码
@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 配置

properties 复制代码
# Sa-Token 配置
sa-token.token-name=***-sync:satoken

客户端调用示例

1 Java 客户端(OkHttp)

java 复制代码
OkHttpClient client = new OkHttpClient().newBuilder().build();

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

// 拼接签名字符串
String raw = "appid=" + appid + "&nonce=" + nonce + "&timestamp=" + 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
             + "&timestamp=" + timestamp)
        .get()
        .build();

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

6.2 curl 示例

bash 复制代码
curl "http://***:8080/sync/model?appid=MDM&sign=***&nonce=***&timestamp=***"

架构设计亮点

设计点 说明
数据库存储 密钥配置持久化,支持动态扩展新应用
Redis 缓存 缓存签名配置 12 小时,减少数据库查询
自定义加载器 通过 @PostConstruct 覆盖默认配置加载逻辑
动态 appid 使用 SpEL 表达式 #{appid} 从请求参数获取
异常处理 统一的日志记录和异常封装
唯一约束 数据库 app_id 唯一索引防止重复

常见问题

Q1: 如何添加新的应用?

直接在数据库 t_app_sign_config 表插入新记录即可,无需重启服务。

Q2: 缓存失效后如何刷新?

缓存 TTL 为 12 小时,过期后自动从数据库重新加载。

Q3: timestamp_disparity 的含义?

表示时间戳允许的误差范围(毫秒),默认 900000ms(15 分钟),防止重放攻击。

相关推荐
白帽子凯哥哥3 小时前
Misc题目中图片隐写和流量分析的详细工具使用技巧
linux·运维·web安全·网络安全·docker·渗透测试
Neolnfra4 小时前
当“同时发生”成为攻击武器
web安全·网络安全·并发·高并发产生的漏洞
乾元5 小时前
AI 驱动的网络攻防演练与安全态势推演——从“规则检测”到“行为级对抗”的工程体系
网络·人工智能·安全·web安全·架构·自动化·运维开发
熙丫 133814823866 小时前
CISAW-SS安全软件认证|2026年培训日程公布,赋能安全开发,从代码源头筑牢防线
网络·安全·web安全
网安_秋刀鱼6 小时前
【java安全】URL链拆解
java·开发语言·安全·web安全·网络安全
白帽黑客-晨哥6 小时前
Web安全中SQL注入绕过WAF的具体手法和实战案例
sql·安全·web安全·职场和发展·渗透测试
tmj0115 小时前
Sqlmap命令详解
web安全·sqlmap
科技云报道20 小时前
科技云报到:2026网络安全六大新趋势:AI重构攻防,信任成为新防线
人工智能·科技·web安全
tianyuanwo21 小时前
深入理解iptables:规则管理与匹配机制深度解析
网络·安全·web安全