SpringBoot3基于 Sa-Token 實(shí)現(xiàn) API 接口簽名校驗(yàn)實(shí)戰(zhàn)
在微服務(wù)架構(gòu)中,系統(tǒng)之間的調(diào)用往往需要保證 安全性。如果缺乏有效的防護(hù)機(jī)制,接口極易遭受偽造請(qǐng)求攻擊。
接口簽名校驗(yàn) 就是一種常見(jiàn)的安全手段,可以有效避免參數(shù)篡改、重放攻擊。
本文將基于 Spring Boot 3 + Sa-Token 的 sa-token-sign 模塊,手把手帶你實(shí)現(xiàn)接口簽名校驗(yàn),并擴(kuò)展到數(shù)據(jù)庫(kù)存儲(chǔ),支持動(dòng)態(tài)接入。
簽名校驗(yàn)流程圖
Client Server
| |
| appid, params, sign, timestamp, nonce |
| -----------------------------------> |
| | 1. 從數(shù)據(jù)庫(kù)加載密鑰配置
| | 2. 驗(yàn)證 timestamp 是否在有效期內(nèi)
| | 3. 驗(yàn)證 nonce 是否重復(fù)
| | 4. 計(jì)算簽名并比對(duì)
| | 5. Redis 緩存配置(12小時(shí))
| <----------------------------------|
| Response |
Sa-Token 簽名模塊簡(jiǎn)介
sa-token-sign 模塊開箱即用,提供了:
? 支持 MD5 / SHA256 / SHA512
? 內(nèi)置 timestamp / nonce 校驗(yàn)
? 支持 多應(yīng)用配置
? 提供 @SaCheckSign 注解,零侵入接入
? 支持 自定義配置源(數(shù)據(jù)庫(kù))
項(xiàng)目依賴
<!-- Sa-Token Starter -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.44.0</version>
</dependency>
<!-- API 參數(shù)簽名 -->
<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>
數(shù)據(jù)庫(kù)設(shè)計(jì)
1 表結(jié)構(gòu)
create table t_app_sign_config
(
id bigint auto_increment comment '主鍵ID' primary key,
app_id varchar(64) not null comment '應(yīng)用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 '時(shí)間戳允許誤差(毫秒) 默認(rèn)15分鐘',
create_by varchar(50) null comment '創(chuàng)建人',
create_time datetime default CURRENT_TIMESTAMP null comment '創(chuàng)建時(shí)間',
update_by varchar(50) null comment '更新人',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新時(shí)間',
constraint uk_app_id unique (app_id)
) comment '應(yīng)用簽名配置表';
2 初始化數(shù)據(jù)
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');
后端實(shí)現(xiàn)
1 實(shí)體類
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_app_sign_config")
public class AppSignConfig extends BaseEntity {
/**
* 應(yīng)用ID
*/
private String appId;
/**
* 密鑰
*/
private String secretKey;
/**
* md5 / sha256 / sha512
*/
private String digestAlgo;
/**
* 時(shí)間戳誤差(秒)
*/
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小時(shí))
redisTemplate.opsForValue()
.set(cacheKey, JsonUtils.toJson(appSignConfig), Duration.ofHours(12));
return appSignConfig;
}
}
3 自定義簽名配置加載器
為什么能夠動(dòng)態(tài)加載簽名信息,這一步很重要
@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("查找應(yīng)用簽名配置,appid: {}", appid);
AppSignConfig appSignConfig = appSignConfigService.getByAppId(appid);
if (appSignConfig == null) {
logger.warn("未找到應(yīng)用簽名配置,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("成功加載應(yīng)用簽名配置,appid: {}, algorithm: {}",
appid, appSignConfig.getDigestAlgo());
return config;
} catch (Exception e) {
logger.error("加載應(yīng)用簽名配置失敗,appid: {}, error: {}", appid, e.getMessage(), e);
throw new BusinessException("加載應(yīng)用簽名配置失敗: " + 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;
// 從請(qǐng)求參數(shù)中動(dòng)態(tài)獲取 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
客戶端調(diào)用示例
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));
// 發(fā)送請(qǐng)求
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=***"
架構(gòu)設(shè)計(jì)亮點(diǎn)
| 設(shè)計(jì)點(diǎn) | 說(shuō)明 |
|---|---|
| 數(shù)據(jù)庫(kù)存儲(chǔ) | 密鑰配置持久化,支持動(dòng)態(tài)擴(kuò)展新應(yīng)用 |
| Redis 緩存 | 緩存簽名配置 12 小時(shí),減少數(shù)據(jù)庫(kù)查詢 |
| 自定義加載器 | 通過(guò) @PostConstruct 覆蓋默認(rèn)配置加載邏輯 |
| 動(dòng)態(tài) appid | 使用 SpEL 表達(dá)式 #{appid} 從請(qǐng)求參數(shù)獲取 |
| 異常處理 | 統(tǒng)一的日志記錄和異常封裝 |
| 唯一約束 | 數(shù)據(jù)庫(kù) app_id 唯一索引防止重復(fù) |
常見(jiàn)問(wèn)題
Q1: 如何添加新的應(yīng)用?
直接在數(shù)據(jù)庫(kù) t_app_sign_config 表插入新記錄即可,無(wú)需重啟服務(wù)。
Q2: 緩存失效后如何刷新?
緩存 TTL 為 12 小時(shí),過(guò)期后自動(dòng)從數(shù)據(jù)庫(kù)重新加載。
Q3: timestamp_disparity 的含義?
表示時(shí)間戳允許的誤差范圍(毫秒),默認(rèn) 900000ms(15 分鐘),防止重放攻擊。
到此這篇關(guān)于SpringBoot3基于 Sa-Token 實(shí)現(xiàn) API 接口簽名校驗(yàn)實(shí)戰(zhàn)的文章就介紹到這了,更多相關(guān)SpringBoot API接口簽名校驗(yàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis中動(dòng)態(tài)SQL語(yǔ)句@Provider的用法
本文主要介紹了MyBatis中動(dòng)態(tài)SQL語(yǔ)句@Provider的用法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
Java實(shí)現(xiàn)文件檢索系統(tǒng)的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何劉Java語(yǔ)言實(shí)現(xiàn)簡(jiǎn)易的文件檢索系統(tǒng),文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Java開發(fā)有一定的幫助,需要的可以參考一下2022-07-07
使用Java8進(jìn)行分組(多個(gè)字段的組合分組)
本文主要介紹了使用Java8進(jìn)行分組(多個(gè)字段的組合分組),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
SpringBoot中的@ControllerAdvice使用方法詳細(xì)解析
這篇文章主要介紹了SpringBoot中的@ControllerAdvice使用方法詳細(xì)解析, 加了@ControllerAdvice的類為那些聲明了@ExceptionHandler、@InitBinder或@ModelAttribute注解修飾的 方法的類而提供的專業(yè)化的@Component,以供多個(gè) Controller類所共享,需要的朋友可以參考下2024-01-01
SpringBoot整合Scala構(gòu)建Web服務(wù)的方法
這篇文章主要介紹了SpringBoot整合Scala構(gòu)建Web服務(wù)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
微信小程序 navigator 跳轉(zhuǎn)url傳遞參數(shù)
這篇文章主要介紹了 微信小程序 navigator 跳轉(zhuǎn)url傳遞參數(shù)的相關(guān)資料,需要的朋友可以參考下2017-03-03
spring-cloud-gateway動(dòng)態(tài)路由的實(shí)現(xiàn)方法
這篇文章主要介紹了spring-cloud-gateway動(dòng)態(tài)路由的實(shí)現(xiàn)方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01

