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接口签名校验内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java基于redis实现分布式锁代码实例

    Java基于redis实现分布式锁代码实例

    这篇文章主要介绍了Java基于redis实现分布式锁代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • 程序员最喜欢的ThreadLocal使用姿势

    程序员最喜欢的ThreadLocal使用姿势

    ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些,下面这篇文章主要给大家介绍了程序员最喜欢的ThreadLocal使用姿势,需要的朋友可以参考下
    2022-02-02
  • 微服务和分布式的区别详解

    微服务和分布式的区别详解

    在本篇文章里小编给各位整理了关于微服务和分布式的区别以及相关知识点总结,有兴趣的朋友们学习下。
    2019-07-07
  • Java中八种基本数据类型的默认值

    Java中八种基本数据类型的默认值

    这篇文章主要介绍了Java中八种基本数据类型的默认值 的相关资料,需要的朋友可以参考下
    2016-07-07
  • RocketMq同组消费者如何自动设置InstanceName

    RocketMq同组消费者如何自动设置InstanceName

    这篇文章主要介绍了RocketMq同组消费者如何自动设置InstanceName问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • 浅析Java内存模型与垃圾回收

    浅析Java内存模型与垃圾回收

    下面小编就为大家带来一篇浅析Java内存模型与垃圾回收。小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧,祝大家游戏愉快哦
    2016-05-05
  • springboot单独使用feign简化接口调用方式

    springboot单独使用feign简化接口调用方式

    这篇文章主要介绍了springboot单独使用feign简化接口调用方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • vue3实现一个todo-list

    vue3实现一个todo-list

    这篇文章主要为大家详细介绍了基于vuejs实现一个todolist项目,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能给你带来帮助
    2021-08-08
  • Java注解如何基于Redission实现分布式锁

    Java注解如何基于Redission实现分布式锁

    这篇文章主要介绍了Java注解如何基于Redission实现分布式锁,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • SpringBoot 项目中的图片处理策略之本地存储与路径映射

    SpringBoot 项目中的图片处理策略之本地存储与路径映射

    在SpringBoot项目中,静态资源存放在static目录下,使得前端可以通过URL来访问这些资源,我们就需要将文件系统的文件路径与URL建立一个映射关系,把文件系统中的文件当成我们的静态资源即可,本文给大家介绍SpringBoot本地存储与路径映射的相关知识,感兴趣的朋友一起看看吧
    2023-12-12

最新评论