
大家好,我是小悟。
客户端层 → 传输层 → 服务端层
↓ ↓ ↓
签名生成 HTTPS加密 签名验证
Token管理 证书校验 频率限制
请求加密 IP白名单┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 客户端 │ │ 网关/拦截器 │ │ 业务系统 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│1.获取Token │─────>│ │ │ │
│2.生成签名 │ │3.验证签名 │─────>│ │
│3.发起请求 │ │4.频率限制 │ │ │
│ │<─────│5.IP白名单 │<─────│ │
└─────────────┘ └─────────────┘ └─────────────┘<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
</dependencies>server:
port: 8080
spring:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 5000ms
api:
security:
# Token有效期(分钟)
token-expire: 30
# 签名有效期(秒)
sign-expire: 300
# 接口限流配置
rate-limit:
# 每秒允许请求数
permits-per-second: 10
# 限流时长(秒)
duration: 60
# IP白名单
ip-whitelist:
- 127.0.0.1
- 192.168.1.0/24package com.example.security.util;
import org.apache.commons.codec.digest.HmacUtils;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
public class SignUtil {
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final String SECRET_KEY = "your-secret-key-here-change-in-production";
/**
* 生成签名
* @param params 请求参数
* @param timestamp 时间戳
* @param nonce 随机数
* @return 签名字符串
*/
public String generateSign(Map<String, Object> params, String timestamp, String nonce) {
// 1. 参数排序
TreeMap<String, Object> sortedParams = new TreeMap<>(params);
// 2. 构建待签名字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Object> entry : sortedParams.entrySet()) {
if (entry.getValue() != null && !"sign".equals(entry.getKey())) {
sb.append(entry.getKey())
.append("=")
.append(entry.getValue())
.append("&");
}
}
sb.append("timestamp=").append(timestamp)
.append("&nonce=").append(nonce);
// 3. HMAC-SHA256加密
return HmacUtils.hmacSha256Hex(SECRET_KEY, sb.toString());
}
/**
* 验证签名
*/
public boolean verifySign(Map<String, Object> params, String sign,
String timestamp, String nonce) {
// 验证时间戳(防止重放攻击)
long currentTime = System.currentTimeMillis() / 1000;
long requestTime = Long.parseLong(timestamp);
if (currentTime - requestTime > 300) { // 5分钟有效期
return false;
}
// 验证nonce是否已使用(Redis中存储)
// 这里需要调用Redis服务,省略具体实现
String expectedSign = generateSign(params, timestamp, nonce);
return expectedSign.equals(sign);
}
}package com.example.security.service;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
public class TokenService {
@Value("${api.security.token-expire}")
private Integer tokenExpire;
private final StringRedisTemplate redisTemplate;
private final SecretKey secretKey;
public TokenService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
String secret = "your-256-bit-secret-key-for-jwt-generation-here";
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
/**
* 生成Token
*/
public String generateToken(String userId) {
String tokenId = UUID.randomUUID().toString();
Date now = new Date();
Date expireDate = new Date(now.getTime() + tokenExpire * 60 * 1000);
String jwt = Jwts.builder()
.setId(tokenId)
.setSubject(userId)
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
// 存储到Redis(支持服务端主动失效)
redisTemplate.opsForValue().set(
"token:" + tokenId,
userId,
tokenExpire,
TimeUnit.MINUTES
);
return jwt;
}
/**
* 验证Token
*/
public boolean validateToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
String tokenId = claims.getId();
// 检查Redis中是否存在
Boolean hasKey = redisTemplate.hasKey("token:" + tokenId);
return hasKey != null && hasKey;
} catch (ExpiredJwtException e) {
return false;
} catch (JwtException e) {
return false;
}
}
/**
* 注销Token
*/
public void logout(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
String tokenId = claims.getId();
redisTemplate.delete("token:" + tokenId);
} catch (Exception e) {
// 忽略异常
}
}
}package com.example.security.interceptor;
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Component
public class RateLimiterService {
@Autowired
private StringRedisTemplate redisTemplate;
// 本地缓存限流器(针对高频访问)
private final ConcurrentHashMap<String, RateLimiter> localLimiters = new ConcurrentHashMap<>();
private static final int DEFAULT_PERMITS = 10;
private static final int DEFAULT_DURATION = 60;
/**
* 检查是否允许访问(基于IP + 接口)
*/
public boolean allowRequest(String key, int permitsPerSecond, int duration) {
// 方式1:使用Guava RateLimiter(本地限流)
RateLimiter limiter = localLimiters.computeIfAbsent(
key,
k -> RateLimiter.create(permitsPerSecond)
);
if (!limiter.tryAcquire()) {
return false;
}
// 方式2:使用Redis实现分布式限流(滑动窗口)
String redisKey = "rate_limit:" + key;
Long currentTime = System.currentTimeMillis() / 1000;
// 使用Redis的ZSet实现滑动窗口
redisTemplate.opsForZSet().add(redisKey, String.valueOf(currentTime), currentTime);
redisTemplate.expire(redisKey, duration, TimeUnit.SECONDS);
// 统计当前时间窗口内的请求数
Long count = redisTemplate.opsForZSet().count(
redisKey,
currentTime - duration,
currentTime
);
return count != null && count <= permitsPerSecond * duration;
}
/**
* 更精确的令牌桶算法实现(Redis Lua脚本)
*/
public boolean allowRequestWithLua(String key, int permitsPerSecond) {
String luaScript =
"local key = KEYS[1] " +
"local permits = tonumber(ARGV[1]) " +
"local current = redis.call('get', key) " +
"if current and tonumber(current) > 0 then " +
" redis.call('decr', key) " +
" return 1 " +
"else " +
" return 0 " +
"end";
// 初始化令牌桶
redisTemplate.opsForValue().setIfAbsent(key, String.valueOf(permitsPerSecond));
redisTemplate.expire(key, 1, TimeUnit.SECONDS);
// 执行Lua脚本
Long result = redisTemplate.execute(
(connection) -> connection.eval(
luaScript.getBytes(),
ReturnType.INTEGER,
1,
key.getBytes(),
String.valueOf(1).getBytes()
),
(key)
);
return result != null && result == 1;
}
}package com.example.security.interceptor;
import com.example.security.util.SignUtil;
import com.example.security.service.TokenService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
@Component
public class ApiSecurityInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Autowired
private SignUtil signUtil;
@Autowired
private RateLimiterService rateLimiterService;
@Value("${api.security.rate-limit.permits-per-second}")
private int permitsPerSecond;
@Value("${api.security.rate-limit.duration}")
private int duration;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1. IP白名单检查
String clientIp = getClientIp(request);
if (!checkIpWhitelist(clientIp)) {
sendError(response, 403, "IP not allowed");
return false;
}
// 2. 获取请求头
String token = request.getHeader("X-Auth-Token");
String sign = request.getHeader("X-Sign");
String timestamp = request.getHeader("X-Timestamp");
String nonce = request.getHeader("X-Nonce");
// 3. Token验证
if (token == null || !tokenService.validateToken(token)) {
sendError(response, 401, "Invalid or expired token");
return false;
}
// 4. 验证必要参数
if (sign == null || timestamp == null || nonce == null) {
sendError(response, 400, "Missing security headers");
return false;
}
// 5. 获取请求参数
Map<String, Object> params = getRequestParams(request);
// 6. 签名验证
if (!signUtil.verifySign(params, sign, timestamp, nonce)) {
sendError(response, 401, "Invalid signature");
return false;
}
// 7. 频率限制(基于用户+接口)
String rateLimitKey = token + ":" + request.getRequestURI();
if (!rateLimiterService.allowRequest(rateLimitKey, permitsPerSecond, duration)) {
sendError(response, 429, "Too many requests");
return false;
}
return true;
}
/**
* 获取客户端真实IP
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多级代理取第一个IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
/**
* IP白名单检查(支持CIDR格式)
*/
private boolean checkIpWhitelist(String clientIp) {
// 从配置读取白名单列表
List<String> whitelist = Arrays.asList("127.0.0.1", "192.168.1.0/24");
for (String allowed : whitelist) {
if (allowed.contains("/")) {
// CIDR格式匹配
if (isInCidr(clientIp, allowed)) {
return true;
}
} else {
if (allowed.equals(clientIp)) {
return true;
}
}
}
return false;
}
/**
* CIDR匹配
*/
private boolean isInCidr(String ip, String cidr) {
// 简化实现,实际可使用Apache Commons Net或自行实现
// 这里省略具体实现
return true;
}
/**
* 获取请求参数(GET和POST)
*/
@SuppressWarnings("unchecked")
private Map<String, Object> getRequestParams(HttpServletRequest request) throws Exception {
Map<String, Object> params = new HashMap<>();
// GET参数
Map<String, String[]> paramMap = request.getParameterMap();
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
// POST JSON参数
if ("POST".equalsIgnoreCase(request.getMethod()) &&
request.getContentType() != null &&
request.getContentType().contains("application/json")) {
Map<String, Object> jsonParams = objectMapper.readValue(
request.getInputStream(),
Map.class
);
params.putAll(jsonParams);
}
return params;
}
private void sendError(HttpServletResponse response, int status, String message)
throws Exception {
response.setStatus(status);
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("code", status);
result.put("message", message);
result.put("timestamp", System.currentTimeMillis());
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}package com.example.security.config;
import com.example.security.interceptor.ApiSecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private ApiSecurityInterceptor apiSecurityInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiSecurityInterceptor)
.addPathPatterns("/api/**") // 拦截所有API接口
.excludePathPatterns(
"/api/auth/**", // 认证接口放行
"/api/public/**", // 公共接口放行
"/api/health" // 健康检查接口
);
}
}// 前端签名工具类
class ApiSecurity {
constructor(appKey, appSecret) {
this.appKey = appKey;
this.appSecret = appSecret;
}
// 生成随机数
generateNonce() {
return Math.random().toString(36).substring(2, 15);
}
// 生成时间戳
getTimestamp() {
return Math.floor(Date.now() / 1000);
}
// 参数排序并拼接
sortParams(params) {
const keys = Object.keys(params).sort();
let str = '';
for (let key of keys) {
if (key !== 'sign' && params[key] !== undefined && params[key] !== null) {
str += `${key}=${params[key]}&`;
}
}
return str;
}
// 生成签名
async generateSign(params) {
const timestamp = this.getTimestamp();
const nonce = this.generateNonce();
const sortedStr = this.sortParams(params);
const signStr = `${sortedStr}timestamp=${timestamp}&nonce=${nonce}`;
// 使用HMAC-SHA256加密
const encoder = new TextEncoder();
const keyData = encoder.encode(this.appSecret);
const messageData = encoder.encode(signStr);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign(
'HMAC',
cryptoKey,
messageData
);
// 转换为十六进制
const hashArray = Array.from(new Uint8Array(signature));
const signHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return {
sign: signHex,
timestamp,
nonce
};
}
// 发起请求
async request(url, method, data = null) {
const token = localStorage.getItem('auth_token');
const { sign, timestamp, nonce } = await this.generateSign(data || {});
const headers = {
'Content-Type': 'application/json',
'X-Auth-Token': token,
'X-Sign': sign,
'X-Timestamp': timestamp,
'X-Nonce': nonce
};
const options = {
method: method,
headers: headers
};
if (data && method !== 'GET') {
options.body = JSON.stringify(data);
}
const response = await fetch(url, options);
if (response.status === 401) {
// Token失效,跳转登录
window.location.href = '/login';
}
if (response.status === 429) {
console.error('请求过于频繁,请稍后再试');
}
return response.json();
}
}
// 使用示例
const api = new ApiSecurity('your_app_key', 'your_app_secret');
api.request('/api/user/info', 'GET').then(data => {
console.log(data);
});// 使用配置中心管理密钥,定期轮换
@ConfigurationProperties(prefix = "api.secret")
public class SecretConfig {
private String key;
private String version;
// 支持密钥版本管理
}@Component
public class SecurityMonitor {
// 记录异常请求
public void recordAbnormalRequest(String ip, String reason) {
// 使用ELK或Prometheus监控
// 短时间内异常次数过多自动加入黑名单
}
}攻击类型 | 防护措施 | 有效性 |
|---|---|---|
重放攻击 | 时间戳+Nonce | ★★★★★ |
参数篡改 | HMAC签名 | ★★★★★ |
CC攻击 | 多级限流 | ★★★★☆ |
Token窃取 | HTTPS+短时效 | ★★★★☆ |
越权访问 | Token绑定用户 | ★★★★★ |
中间人攻击 | HTTPS强制 | ★★★★★ |
yaml
api:
security:
# 根据业务调整
rate-limit:
# 登录接口:5次/分钟
login: 5/60
# 查询接口:100次/分钟
query: 100/60
# 写入接口:20次/分钟
write: 20/60Q: 签名验证失败率高? A: 检查客户端和服务端时间同步,使用NTP服务;确保参数排序算法一致
Q: 限流误伤正常用户? A: 针对不同接口设置差异化限流策略;使用滑动窗口算法更平滑
Q: Token如何刷新? A: 使用双Token机制:Access Token(短时效)+ Refresh Token(长时效)
Q: 分布式环境下的限流精度? A: 使用Redis + Lua脚本保证原子性;考虑使用Sentinel等专业组件
通过以上方案的实施,可以有效防止API接口被恶意调用,保障系统的安全性和稳定性。根据实际业务场景,在安全性和用户体验之间找到平衡点,持续优化和完善安全策略。

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 [email protected] 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 [email protected] 删除。