主要流程
【添加接口管理和密钥管理】
↓
【服务端生成密钥(AccessKey + SecretKey)】
↓
【服务端加密存储SecretKey到数据库】
↓
【通过安全渠道发送AccessKey + SecretKey给调用方】
↓
【调用方每次请求时用SecretKey生成签名】
↓
【服务端用相同的SecretKey验证签名】
1、添加接口管理和密钥管理
主要是三张表:接口表、密钥表、密钥和接口关联表。
参考(只展示主要字段):
接口表,还有一张子表用来记录参数、响应、请求头等信息这里不做展示。
sql
CREATE TABLE `information_exchange_father` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
`name` varchar(100) DEFAULT '' COMMENT '接口名称',
`dizhi` varchar(200) DEFAULT '' COMMENT '接口地址',
`fanshi` varchar(100) DEFAULT '' COMMENT '请求方式(1:GET,2:POST,3:PUT,4:DELETE)',
`jiekou_lx` varchar(10) DEFAULT '' COMMENT '接口类型(1:引接接口,2:推送接口)',
`qiyong_zt` varchar(10) DEFAULT '' COMMENT '启用状态(1:启用,2:停用)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='接口表';
API密钥表
sql
CREATE TABLE `information_exchange_api_key` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`key_name` varchar(100) NOT NULL COMMENT '密钥名称',
`access_key` varchar(64) NOT NULL COMMENT '访问密钥(公钥)',
`secret_key` varchar(128) NOT NULL COMMENT '私密密钥(加密存储)',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0-禁用 1-启用)',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间(为空表示永久有效)',
`system_name` varchar(100) DEFAULT NULL COMMENT '调用方系统名称',
`rate_limit` int DEFAULT NULL COMMENT '调用频率限制(次/分钟,为空表示不限制)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注说明',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_access_key` (`access_key`) COMMENT '访问密钥唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='API密钥管理表';
密钥接口关联表
sql
CREATE TABLE `information_exchange_api_key_interface` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`api_key_id` bigint NOT NULL COMMENT '密钥ID',
`api_information_id` bigint NOT NULL COMMENT '接口ID',
PRIMARY KEY (`id`),
KEY `idx_api_key_id` (`api_key_id`) COMMENT '密钥ID索引',
KEY `idx_interface_id` (`api_information_id`) COMMENT '接口路径索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='API密钥接口关联表';
1、服务端生成密钥
注意:AccessKey + SecretKey一定是后端生成,不能让前端自己输入。
生成AccessKey + SecretKey的工具类
java
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
/**
* API密钥生成工具类
*/
public class ApiKeyGenerator {
/**
* AccessKey前缀
*/
private static final String ACCESS_KEY_PREFIX = "YD_";
/**
* SecretKey前缀
*/
private static final String SECRET_KEY_PREFIX = "sk_";
/**
* SecretKey长度(不包含前缀)
*/
private static final int SECRET_KEY_LENGTH = 32;
/**
* 生成AccessKey
* 格式: YD_ + UUID(去掉横杠)
* 示例: YD_a1b2c3d4e5f67890a1b2c3d4e5f67890
*
* @return AccessKey
*/
public static String generateAccessKey() {
// 使用hutool的IdUtil生成UUID,去掉横杠
String uuid = IdUtil.simpleUUID();
return ACCESS_KEY_PREFIX + uuid;
}
/**
* 生成SecretKey
* 格式: sk_ + 32位随机字符串(包含数字和字母)
* 示例: sk_9f8e7d6c5b4a3210fedcba0987654321
*
* @return SecretKey
*/
public static String generateSecretKey() {
// 使用hutool的RandomUtil生成随机字符串
String randomStr = RandomUtil.randomString(SECRET_KEY_LENGTH);
return SECRET_KEY_PREFIX + randomStr;
}
/**
* 生成AccessKey(带自定义前缀)
*
* @param prefix 自定义前缀
* @return AccessKey
*/
public static String generateAccessKey(String prefix) {
String uuid = IdUtil.simpleUUID();
return prefix + uuid;
}
/**
* 生成SecretKey(带自定义前缀和长度)
*
* @param prefix 自定义前缀
* @param length 随机字符串长度
* @return SecretKey
*/
public static String generateSecretKey(String prefix, int length) {
String randomStr = RandomUtil.randomString(length);
return prefix + randomStr;
}
}
2、服务端加密存储SecretKey到数据库
SecretKey存到数据库时加密解密的工具类
java
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
/**
* AES-256 加密工具类
*/
@Slf4j
@Component
public class AesEncryptUtil {
@Value("${system.aes.secret-key}")
private String secretKey; // 这个secretKey跟密钥里面的secretKey不是一个东西,这个里面的是专门用来加密解密的
private AES aes;
@PostConstruct
public void init() {
// 确保密钥长度为32字节(256位)
if (secretKey.length() != 32) {
throw new IllegalArgumentException("AES-256密钥长度必须为32字符");
}
aes = SecureUtil.aes(secretKey.getBytes(StandardCharsets.UTF_8));
}
/**
* 加密
*/
public String encrypt(String content) {
if (content == null || content.isEmpty()) {
return content;
}
String jmh = content;
try {
jmh = aes.encryptHex(content);
} catch (Exception e) {
log.error("AES加密失败", e);
}
return jmh;
}
/**
* 解密
*/
public String decrypt(String encryptedContent) {
if (encryptedContent == null || encryptedContent.isEmpty()) {
return encryptedContent;
}
String jmh = encryptedContent;
try {
jmh = aes.decryptStr(encryptedContent);
} catch (Exception e) {
log.error("AES解密失败", e);
}
return jmh;
}
}
yaml文件添加ase加密解密时用的system.aes.secret-key
yaml
system:
aes:
# AES-256密钥,必须是32位字符串,随意填写
secret-key: ck345678901234567890123456789012
3、通过安全渠道发送AccessKey + SecretKey给调用方
通过钉钉、企业微信、邮箱、线下交付等
4、调用方每次请求时用SecretKey生成签名
这是当时写的测试类,注意用到的SignatureUtil.generateSign在下面第五步里面有
java
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.util.exchangeapikey.SignatureUtil;
import java.util.HashMap;
import java.util.Map;
/**
* HTTP客户端模拟测试
* 模拟调用方如何调用API
*
* 使用前请确保:
* 1. 服务已启动
* 2. 数据库中已有测试密钥数据
* 3. 修改下面的测试数据(accessKey、secretKey、apiUrl)
*
*/
public class ApiClientSimulatorTest {
/**
* 服务器地址
*/
private static final String BASE_URL = "http://127.0.0.1:48081";
/**
* 测试用的AccessKey
*/
private static final String ACCESS_KEY = "YD_a36f9d287c5f4e38afc99c9dd68f10db";
/**
* 测试用的SecretKey
*/
private static final String SECRET_KEY = "sk_WklmDcZMlL2uNC1Mi0KYSff9YMLT2HHt";
public static void main(String[] args) {
System.out.println("========== API客户端模拟测试 ==========\n");
// 测试1: 正常请求(应该成功)
test1_正常请求_get();
test1_正常请求_post();
// 测试2: 缺少签名(应该失败)
test2_缺少签名();
// 测试3: 签名错误(应该失败)
test3_签名错误();
// 测试4: 时间戳过期(应该失败)
test4_时间戳过期();
// 测试5: 密钥不存在(应该失败)
test5_密钥不存在();
System.out.println("\n========== 所有测试完成 ==========");
}
/**
* 测试1.1: 正常请求
*/
private static void test1_正常请求_get() {
System.out.println("【测试1.1】GET正常请求(应该成功)");
try {
// 准备参数
Map<String, String> params = new HashMap<>();
params.put("id", "11");
// 生成时间戳
long timestamp = System.currentTimeMillis();
// 生成签名
String sign = SignatureUtil.generateSign(params, timestamp, SECRET_KEY);
// 发送请求(转换为 Object 类型)
Map<String, Object> formParams = new HashMap<>(params);
// 发送请求
String url = BASE_URL + "/admin-api/exchange/test001/get";
HttpResponse response = HttpRequest.get(url)
.header("X-Access-Key", ACCESS_KEY)
.header("X-Timestamp", String.valueOf(timestamp))
.header("X-Sign", sign)
.form(formParams)
.execute();
// 打印结果
System.out.println(" 请求URL: " + url);
System.out.println(" 请求参数: " + params);
System.out.println(" AccessKey: " + ACCESS_KEY);
System.out.println(" Timestamp: " + timestamp);
System.out.println(" Sign: " + sign);
System.out.println(" 响应状态码: " + response.getStatus());
System.out.println(" 响应内容: " + response.body());
if (response.getStatus() == 200) {
System.out.println(" ✅ 请求成功\n");
} else {
System.out.println(" ❌ 请求失败\n");
}
} catch (Exception e) {
System.out.println(" ❌ 请求异常: " + e.getMessage());
System.out.println(" 提示:请确保服务已启动,且URL配置正确\n");
}
}
/**
* 测试1.2: 正常请求
*/
private static void test1_正常请求_post() {
System.out.println("【测试1.2】POST正常请求(应该成功)");
try {
// 准备参数
Map<String, String> body = new HashMap<>();
body.put("id", "22");
body.put("name", "王室");
body.put("dizhi", "666asf");
body.put("aaa", "214325");
// 将参数转换为JSON字符串
String jsonBody = JSONUtil.toJsonStr(body);
// 生成时间戳
long timestamp = System.currentTimeMillis();
Map<String, String> params = new HashMap<>();
params.put("body", jsonBody);
// 生成签名
String sign = SignatureUtil.generateSign(params, timestamp, SECRET_KEY);
// 发送请求
String url = BASE_URL + "/admin-api/exchange/test001/post";
HttpResponse response = HttpRequest.post(url)
.header("X-Access-Key", ACCESS_KEY)
.header("X-Timestamp", String.valueOf(timestamp))
.header("X-Sign", sign)
.header("Content-Type", "application/json;charset=UTF-8")
.body(jsonBody)
.execute();
// 打印结果
System.out.println(" 请求URL: " + url);
System.out.println(" 请求参数: " + params);
System.out.println(" AccessKey: " + ACCESS_KEY);
System.out.println(" Timestamp: " + timestamp);
System.out.println(" Sign: " + sign);
System.out.println(" 响应状态码: " + response.getStatus());
System.out.println(" 响应内容: " + response.body());
if (response.getStatus() == 200) {
System.out.println(" ✅ 请求成功\n");
} else {
System.out.println(" ❌ 请求失败\n");
}
} catch (Exception e) {
System.out.println(" ❌ 请求异常: " + e.getMessage());
System.out.println(" 提示:请确保服务已启动,且URL配置正确\n");
}
}
/**
* 测试2: 缺少签名
*/
private static void test2_缺少签名() {
System.out.println("【测试2】缺少签名(应该失败)");
try {
Map<String, String> params = new HashMap<>();
params.put("userId", "123");
long timestamp = System.currentTimeMillis();
// 发送请求(转换为 Object 类型)
Map<String, Object> formParams = new HashMap<>(params);
// 故意不发送签名
String url = BASE_URL + "/admin-api/exchange/test001/get";
HttpResponse response = HttpRequest.get(url)
.header("X-Access-Key", ACCESS_KEY)
.header("X-Timestamp", String.valueOf(timestamp))
// .header("X-Sign", sign) // ← 故意不发送
.form(formParams)
.execute();
System.out.println(" 响应状态码: " + response.getStatus());
System.out.println(" 响应内容: " + response.body());
if (response.body().contains("请求头未包含X-Sign")) {
System.out.println(" ✅ 正确拒绝:缺少签名\n");
} else {
System.out.println(" ❌ 应该拒绝但没有拒绝\n");
}
} catch (Exception e) {
System.out.println(" ❌ 请求异常: " + e.getMessage() + "\n");
}
}
/**
* 测试3: 签名错误
*/
private static void test3_签名错误() {
System.out.println("【测试3】签名错误(应该失败)");
try {
Map<String, String> params = new HashMap<>();
params.put("userId", "123");
long timestamp = System.currentTimeMillis();
// 故意使用错误的签名
String wrongSign = "wrongsignature123456";
// 发送请求(转换为 Object 类型)
Map<String, Object> formParams = new HashMap<>(params);
String url = BASE_URL + "/admin-api/exchange/test001/get";
HttpResponse response = HttpRequest.get(url)
.header("X-Access-Key", ACCESS_KEY)
.header("X-Timestamp", String.valueOf(timestamp))
.header("X-Sign", wrongSign)
.form(formParams)
.execute();
System.out.println(" 响应状态码: " + response.getStatus());
System.out.println(" 响应内容: " + response.body());
if (response.body().contains("签名验证失败")) {
System.out.println(" ✅ 正确拒绝:签名错误\n");
} else {
System.out.println(" ❌ 应该拒绝但没有拒绝\n");
}
} catch (Exception e) {
System.out.println(" ❌ 请求异常: " + e.getMessage() + "\n");
}
}
/**
* 测试4: 时间戳过期
*/
private static void test4_时间戳过期() {
System.out.println("【测试4】时间戳过期(应该失败)");
try {
Map<String, String> params = new HashMap<>();
params.put("userId", "123");
// 使用10分钟前的时间戳(超过5分钟有效期)
long timestamp = System.currentTimeMillis() - (10 * 60 * 1000);
String sign = SignatureUtil.generateSign(params, timestamp, SECRET_KEY);
// 发送请求(转换为 Object 类型)
Map<String, Object> formParams = new HashMap<>(params);
String url = BASE_URL + "/admin-api/exchange/test001/get";
HttpResponse response = HttpRequest.get(url)
.header("X-Access-Key", ACCESS_KEY)
.header("X-Timestamp", String.valueOf(timestamp))
.header("X-Sign", sign)
.form(formParams)
.execute();
System.out.println(" 响应状态码: " + response.getStatus());
System.out.println(" 响应内容: " + response.body());
if (response.body().contains("请求已过期")) {
System.out.println(" ✅ 正确拒绝:时间戳过期\n");
} else {
System.out.println(" ❌ 应该拒绝但没有拒绝\n");
}
} catch (Exception e) {
System.out.println(" ❌ 请求异常: " + e.getMessage() + "\n");
}
}
/**
* 测试5: 密钥不存在
*/
private static void test5_密钥不存在() {
System.out.println("【测试5】密钥不存在(应该失败)");
try {
Map<String, String> params = new HashMap<>();
params.put("userId", "123");
long timestamp = System.currentTimeMillis();
String sign = SignatureUtil.generateSign(params, timestamp, SECRET_KEY);
// 使用不存在的AccessKey
String wrongAccessKey = "YD_notexist123456";
// 发送请求(转换为 Object 类型)
Map<String, Object> formParams = new HashMap<>(params);
String url = BASE_URL + "/admin-api/exchange/test001/get";
HttpResponse response = HttpRequest.get(url)
.header("X-Access-Key", wrongAccessKey)
.header("X-Timestamp", String.valueOf(timestamp))
.header("X-Sign", sign)
.form(formParams)
.execute();
System.out.println(" 响应状态码: " + response.getStatus());
System.out.println(" 响应内容: " + response.body());
if (response.body().contains("密钥不存在")) {
System.out.println(" ✅ 正确拒绝:密钥不存在\n");
} else {
System.out.println(" ❌ 应该拒绝但没有拒绝\n");
}
} catch (Exception e) {
System.out.println(" ❌ 请求异常: " + e.getMessage() + "\n");
}
}
}
5、服务端用相同的SecretKey验证签名
注册验证过滤器
java
import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter;
import cn.iocoder.yudao.framework.apilog.core.filter.ApiKeyAuthFilter;
import cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor;
import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiAccessLogCommonApi;
import cn.iocoder.yudao.framework.common.biz.system.exchangeapikey.ExchangeApiKeyApi;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration;
import javax.servlet.Filter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@AutoConfiguration(after = YudaoWebAutoConfiguration.class)
public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer {
/**
* 密钥验证过滤器
* @param webProperties
* @param exchangeApiKeyApi后续用来查询密钥信息的
* @return
*/
@Bean
public FilterRegistrationBean<ApiKeyAuthFilter> apiKeyAuthFilter(WebProperties webProperties,
ExchangeApiKeyApi exchangeApiKeyApi) {
ApiKeyAuthFilter filter = new ApiKeyAuthFilter(webProperties, exchangeApiKeyApi);
return createFilterBean(filter, WebFilterOrderEnum.EXCHANGE_API_FILTER);
}
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
bean.setOrder(order);
return bean;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ApiAccessLogInterceptor());
}
}
过滤器
java
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.biz.system.exchangeapikey.ExchangeApiKeyApi;
import cn.iocoder.yudao.framework.common.biz.system.exchangeapikey.dto.ExchangeApiKeyDTO;
import cn.iocoder.yudao.framework.common.biz.system.exchangeapikey.dto.ExchangeApiKeyInterfaceDTO;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.exchangeapikey.RateLimitUtil;
import cn.iocoder.yudao.framework.common.util.exchangeapikey.SignatureUtil;
import cn.iocoder.yudao.framework.common.util.exchangeapikey.UrlPathValidator;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* API密钥验证过滤器
*
* 目的:在请求到达Controller之前进行密钥验证和鉴权
*
* @author your name
*/
@Slf4j
public class ApiKeyAuthFilter extends ApiRequestFilter {
/**
* Header中AccessKey的键名
*/
private static final String HEADER_ACCESS_KEY = "X-Access-Key";
/**
* Header中时间戳的键名
*/
private static final String HEADER_TIMESTAMP = "X-Timestamp";
/**
* Header中签名的键名
*/
private static final String HEADER_SIGN = "X-Sign";
/**
* 时间戳有效期(毫秒),默认5分钟
*/
private static final long TIMESTAMP_VALIDITY = 5 * 60 * 1000;
private final ExchangeApiKeyApi exchangeApiKeyApi;
public ApiKeyAuthFilter(WebProperties webProperties, ExchangeApiKeyApi exchangeApiKeyApi) {
super(webProperties);
this.exchangeApiKeyApi = exchangeApiKeyApi;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
// 判断是否是信息交换接口
if(UrlPathValidator.validateExchangeSubPath(request.getRequestURI())){
// 执行密钥验证
boolean authSuccess = authenticate(request, response);
if (!authSuccess) {
return; // 验证失败,已经写入响应,直接返回
}
// 验证通过,继续执行后续过滤器
filterChain.doFilter(request, response);
}else {
// 不是对外开发接口,放行
filterChain.doFilter(request, response);
}
} finally {}
}
/**
* 执行认证逻辑
*
* @return true-验证通过,false-验证失败
*/
private boolean authenticate(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 1. 提取Header信息
String accessKey = request.getHeader(HEADER_ACCESS_KEY);
String timestamp = request.getHeader(HEADER_TIMESTAMP);
String clientSign = request.getHeader(HEADER_SIGN);
try {
// 2. 校验必要参数
if (StrUtil.isEmpty(accessKey)) {
writeErrorResponse(response, GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "请求头未包含" + HEADER_ACCESS_KEY);
return false;
}
if (StrUtil.isEmpty(timestamp)) {
writeErrorResponse(response, GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "请求头未包含" + HEADER_TIMESTAMP);
return false;
}
if (StrUtil.isEmpty(clientSign)) {
writeErrorResponse(response, GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "请求头未包含" + HEADER_SIGN);
return false;
}
// 3. 查询密钥信息
ExchangeApiKeyDTO exchangeDTO = exchangeApiKeyApi.getExchangeApiKey(accessKey);
if (exchangeDTO == null) {
writeErrorResponse(response, GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), "密钥不存在");
return false;
}
// 4. 验证密钥状态
if (exchangeDTO.getStatus() == null || exchangeDTO.getStatus() != 1) {
writeErrorResponse(response, GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), "密钥已禁用");
return false;
}
// 5. 验证密钥是否过期
if (exchangeDTO.getExpireTime() != null && exchangeDTO.getExpireTime().isBefore(LocalDateTime.now())) {
writeErrorResponse(response, GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), "密钥已过期");
return false;
}
// 6. 验证时间戳
long requestTime;
try {
requestTime = Long.parseLong(timestamp);
} catch (NumberFormatException e) {
writeErrorResponse(response, GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), HEADER_TIMESTAMP + "格式错误");
return false;
}
long currentTime = System.currentTimeMillis();
if (Math.abs(currentTime - requestTime) > TIMESTAMP_VALIDITY) {
writeErrorResponse(response, GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), "请求已过期,请检查系统时间");
return false;
}
// 7. 验证签名
Map<String, String> params = ServletUtils.getParamMap(request);
// 如果是JSON请求,从Body中获取参数
if (ServletUtils.isJsonRequest(request)) {
String body = ServletUtils.getBody(request);
if (StrUtil.isNotEmpty(body)) {
params.put("body", body);
}
}
// 使用工具类计算签名
String serverSign = SignatureUtil.generateSign(params, requestTime, exchangeDTO.getSecretKey());
if (!serverSign.equals(clientSign)) {
writeErrorResponse(response, GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), "签名验证失败");
return false;
}
// 8. 验证接口权限
String currentApiPath = request.getRequestURI();
boolean hasPermission = checkApiPermission(exchangeDTO, currentApiPath);
if (!hasPermission) {
writeErrorResponse(response, GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), "无权限访问此接口");
return false;
}
// 10. 验证频率限制(可选,如果配置了频率限制)
if (exchangeDTO.getRateLimit() != null && exchangeDTO.getRateLimit() > 0) {
boolean exceedLimit = RateLimitUtil.checkRateLimit(exchangeDTO.getId(), exchangeDTO.getRateLimit());
if (exceedLimit) {
writeErrorResponse(response, GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), "调用频率超限");
return false;
}
}
log.info("[ApiKeyAuth][验证通过][AccessKey:{}][接口:{}]", accessKey, currentApiPath);
return true;
} catch (Exception e) {
log.error("[ApiKeyAuth][验证异常][AccessKey:{}]", accessKey, e);
writeErrorResponse(response, GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), "密钥验证异常");
return false;
}
}
/**
* 检查接口权限
*/
private boolean checkApiPermission(ExchangeApiKeyDTO apiKey, String currentApiPath) {
List<ExchangeApiKeyInterfaceDTO> interfaces = apiKey.getExchangeApiKeyInterfaces();
if (CollUtil.isEmpty(interfaces)) {
return false;
}
// 遍历密钥关联的接口列表,判断当前请求路径是否匹配
for (ExchangeApiKeyInterfaceDTO apiInterface : interfaces) {
// 判断接口是否启用
if (!"1".equals(apiInterface.getQiyongZt())) {
continue;
}
// 判断接口是否推送接口
if (!"2".equals(apiInterface.getJiekouLx())) {
continue;
}
String interfacePath = apiInterface.getDizhi();
// 支持通配符匹配
if (pathMatches(currentApiPath, interfacePath)) {
return true;
}
}
return false;
}
/**
* 路径匹配(支持通配符)
*/
private boolean pathMatches(String currentPath, String pattern) {
// 精确匹配
if (currentPath.equals(pattern)) {
return true;
}
// 通配符匹配:/api/user/* 可以匹配 /api/user/info
if (pattern.endsWith("/*")) {
String prefix = pattern.substring(0, pattern.length() - 2);
return currentPath.startsWith(prefix);
}
// 通配符匹配:/api/**/info 可以匹配 /api/user/info、/api/admin/user/info
if (pattern.contains("**")) {
String regex = pattern.replace("**", ".*");
return currentPath.matches(regex);
}
return false;
}
/**
* 写入错误响应
*/
private void writeErrorResponse(HttpServletResponse response, Integer code, String message) throws IOException {
response.setStatus(HttpServletResponse.SC_OK); // 统一返回200,通过code区分错误
response.setContentType("application/json;charset=UTF-8");
CommonResult<?> result = CommonResult.error(code, message);
String json = JsonUtils.toJsonString(result);
response.getWriter().write(json);
}
}
过滤器里面用到的工具类和其他类
java
public class UrlPathValidator {
/**
* 验证URL路径是否严格在 /admin-api/exchange 的子路径下(不包含 /admin-api/exchange 本身)
*
* @param path 要验证的URL字符串
* @return true-验证通过,false-验证失败
*/
public static boolean validateExchangeSubPath(String path) {
return StrUtil.isNotBlank(path) && path.startsWith("/admin-api/exchange/");
}
}
java
@Data
public class ExchangeApiKeyDTO {
/**
* 主键ID
*/
private Long id;
/**
* 密钥名称
*/
private String keyName;
/**
* 访问密钥(公钥)
*/
private String accessKey;
/**
* 私密密钥(加密存储)
*/
private String secretKey;
/**
* 状态(0-禁用 1-启用)
*/
private Integer status;
/**
* 过期时间(为空表示永久有效)
*/
private LocalDateTime expireTime;
/**
* 调用方系统名称
*/
private String systemName;
/**
* 调用频率限制(次/分钟,为空表示不限制)
*/
private Integer rateLimit;
/**
* 备注说明
*/
private String remark;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* API密钥接口关联列表
*/
private List<ExchangeApiKeyInterfaceDTO> exchangeApiKeyInterfaces;
}
java
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Map;
/**
* 客户端工具类
*
* @author 芋道源码
*/
public class ServletUtils {
public static boolean isJsonRequest(ServletRequest request) {
return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
}
public static String getBody(HttpServletRequest request) {
// 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
if (isJsonRequest(request)) {
return ServletUtil.getBody(request);
}
return null;
}
public static byte[] getBodyBytes(HttpServletRequest request) {
// 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
if (isJsonRequest(request)) {
return ServletUtil.getBodyBytes(request);
}
return null;
}
public static String getClientIP(HttpServletRequest request) {
return ServletUtil.getClientIP(request);
}
public static Map<String, String> getParamMap(HttpServletRequest request) {
return ServletUtil.getParamMap(request);
}
public static Map<String, String> getHeaderMap(HttpServletRequest request) {
return ServletUtil.getHeaderMap(request);
}
}
java
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;
/**
* 签名工具类
*
* @author your name
*/
public class SignatureUtil {
/**
* 生成签名
*
* @param params 请求参数
* @param timestamp 时间戳
* @param secretKey 密钥(需要先解密)
* @return 签名字符串
*/
public static String generateSign(Map<String, String> params, long timestamp, String secretKey) {
// 1. 参数排序(使用TreeMap自动按key排序)
TreeMap<String, String> sortedParams = new TreeMap<>(params);
// 2. 拼接字符串
StringBuilder signString = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
signString.append(entry.getKey())
.append("=")
.append(entry.getValue())
.append("&");
}
signString.append("timestamp=").append(timestamp);
// 3. HMAC-SHA256签名
return hmacSha256(signString.toString(), secretKey);
}
/**
* HMAC-SHA256算法
*
* @param data 待签名数据
* @param secretKey 密钥
* @return 签名结果(十六进制字符串)
*/
private static String hmacSha256(String data, String secretKey) {
HMac hMac = new HMac(HmacAlgorithm.HmacSHA256, secretKey.getBytes(StandardCharsets.UTF_8));
return hMac.digestHex(data);
}
}
java
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 频率限制工具类(基于内存实现)
* 使用滑动窗口算法
*
* @author your name
*/
@Slf4j
public class RateLimitUtil {
/**
* 存储每个密钥的请求时间戳列表
* Key: apiKeyId
* Value: 请求时间戳列表
*/
private static final Map<Long, List<Long>> REQUEST_RECORDS = new ConcurrentHashMap<>();
/**
* 时间窗口大小(毫秒),默认1分钟
*/
private static final long WINDOW_SIZE = 60 * 1000;
/**
* 检查是否超过频率限制
*
* @param apiKeyId 密钥ID
* @param rateLimit 频率限制(次/分钟)
* @return true-超限,false-未超限
*/
public static boolean checkRateLimit(Long apiKeyId, Integer rateLimit) {
long currentTime = System.currentTimeMillis();
// 获取或创建该密钥的请求记录列表
List<Long> timestamps = REQUEST_RECORDS.computeIfAbsent(apiKeyId, k -> new CopyOnWriteArrayList<>());
// 移除时间窗口之外的旧记录
timestamps.removeIf(timestamp -> currentTime - timestamp > WINDOW_SIZE);
// 判断当前窗口内的请求次数是否超过限制
if (timestamps.size() >= rateLimit) {
log.warn("[RateLimitUtil][密钥:{}超过频率限制,限制:{}/分钟,当前:{}/分钟]",
apiKeyId, rateLimit, timestamps.size());
return true;
}
// 记录本次请求时间
timestamps.add(currentTime);
return false;
}
/**
* 清理指定密钥的请求记录
*
* @param apiKeyId 密钥ID
*/
public static void clear(Long apiKeyId) {
REQUEST_RECORDS.remove(apiKeyId);
}
/**
* 清理所有请求记录
*/
public static void clearAll() {
REQUEST_RECORDS.clear();
}
/**
* 获取指定密钥的当前请求次数
*
* @param apiKeyId 密钥ID
* @return 当前时间窗口内的请求次数
*/
public static int getCurrentRequestCount(Long apiKeyId) {
List<Long> timestamps = REQUEST_RECORDS.get(apiKeyId);
if (timestamps == null) {
return 0;
}
long currentTime = System.currentTimeMillis();
// 移除过期记录
timestamps.removeIf(timestamp -> currentTime - timestamp > WINDOW_SIZE);
return timestamps.size();
}
}