SpringBoot3基于 Sa-Token 实现 API 接口签名校验实战

 更新时间:2025年12月26日 08:25:13   作者:九月生  
本文介绍了基于Spring Boot 3和Sa-Token框架实现API接口签名校验的解决方案,通过sa-token-sign模块提供开箱即用的签名校验功能,下面就来详细的介绍一下,感兴趣的可以了解一下

在微服务架构中,系统之间的调用往往需要保证 安全性。如果缺乏有效的防护机制,接口极易遭受伪造请求攻击。
接口签名校验 就是一种常见的安全手段,可以有效避免参数篡改、重放攻击。
本文将基于 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 注解,零侵入接入

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

项目依赖

<!-- 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 + "&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 示例

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 分钟),防止重放攻击。

到此这篇关于SpringBoot3基于 Sa-Token 实现 API 接口签名校验实战的文章就介绍到这了,更多相关SpringBoot API接口签名校验内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • MyBatis中动态SQL语句@Provider的用法

    MyBatis中动态SQL语句@Provider的用法

    本文主要介绍了MyBatis中动态SQL语句@Provider的用法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • java判断字符串是正整数的实例

    java判断字符串是正整数的实例

    今天小编就为大家分享一篇java判断字符串是正整数的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-07-07
  • Java实现文件检索系统的示例代码

    Java实现文件检索系统的示例代码

    这篇文章主要为大家详细介绍了如何刘Java语言实现简易的文件检索系统,文中的示例代码讲解详细,对我们学习Java开发有一定的帮助,需要的可以参考一下
    2022-07-07
  • 使用Java8进行分组(多个字段的组合分组)

    使用Java8进行分组(多个字段的组合分组)

    本文主要介绍了使用Java8进行分组(多个字段的组合分组),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • java配置沙箱支付的实现示例

    java配置沙箱支付的实现示例

    箱支付是支付平台提供的模拟支付环境,包含测试用的 APPID、密钥、网关、测试账号等资源,下面就来介绍一下如何实现,感兴趣的可以了解一下
    2025-08-08
  • SpringBoot中的@ControllerAdvice使用方法详细解析

    SpringBoot中的@ControllerAdvice使用方法详细解析

    这篇文章主要介绍了SpringBoot中的@ControllerAdvice使用方法详细解析, 加了@ControllerAdvice的类为那些声明了@ExceptionHandler、@InitBinder或@ModelAttribute注解修饰的 方法的类而提供的专业化的@Component,以供多个 Controller类所共享,需要的朋友可以参考下
    2024-01-01
  • SpringBoot整合Scala构建Web服务的方法

    SpringBoot整合Scala构建Web服务的方法

    这篇文章主要介绍了SpringBoot整合Scala构建Web服务的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • java性能优化之代码缓存优化

    java性能优化之代码缓存优化

    这篇文章主要介绍了java性能优化之代码缓存优化,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-07-07
  • 微信小程序 navigator 跳转url传递参数

    微信小程序 navigator 跳转url传递参数

    这篇文章主要介绍了 微信小程序 navigator 跳转url传递参数的相关资料,需要的朋友可以参考下
    2017-03-03
  • spring-cloud-gateway动态路由的实现方法

    spring-cloud-gateway动态路由的实现方法

    这篇文章主要介绍了spring-cloud-gateway动态路由的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01

最新评论