大家好,我是小悟。
一、需求分析
1.1 核心需求
- 身份认证:确保请求来自合法用户/应用
- 请求防篡改:保证请求内容在传输过程中未被修改
- 防重放攻击:防止合法请求被恶意重复发送
- 频率控制:限制单位时间内的请求次数
- 来源验证:确保请求来自合法的前端应用
1.2 安全层次
客户端层 → 传输层 → 服务端层
↓ ↓ ↓
签名生成 HTTPS加密 签名验证
Token管理 证书校验 频率限制
请求加密 IP白名单
二、详细实现步骤
2.1 技术选型
- 后端框架:Spring Boot 2.7+
- 缓存:Redis(存储Token、计数)
- 加密算法:HMAC-SHA256、AES
- Token方案:JWT + Redis双重验证
2.2 整体架构图
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 客户端 │ │ 网关/拦截器 │ │ 业务系统 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│1.获取Token │─────>│ │ │ │
│2.生成签名 │ │3.验证签名 │─────>│ │
│3.发起请求 │ │4.频率限制 │ │ │
│ │<─────│5.IP白名单 │<─────│ │
└─────────────┘ └─────────────┘ └─────────────┘
三、核心代码实现
3.1 项目依赖(pom.xml)
<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>
3.2 配置文件(application.yml)
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/24
3.3 签名工具类
package 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);
}
}
3.4 JWT Token管理
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) {
// 忽略异常
}
}
}
3.5 接口限流器(基于Redis + 令牌桶)
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;
}
}
3.6 核心拦截器
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));
}
}
3.7 配置拦截器
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" // 健康检查接口
);
}
}
3.8 客户端实现示例(JavaScript)
// 前端签名工具类
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);
});
四、详细总结
4.1 实现的核心安全机制
- 多层防护体系
- 传输层:强制HTTPS加密,防止中间人攻击
- 认证层:JWT + Redis双重验证,支持服务端主动失效
- 签名层:HMAC-SHA256签名,防止参数篡改
- 限流层:令牌桶算法 + 滑动窗口,防止CC攻击
- 访问层:IP白名单,限制访问来源
- 防重放攻击策略
- 时间戳验证:5分钟有效期
- Nonce随机数:Redis缓存已使用的nonce
- Token轮换:定期刷新token
- 性能优化
- Guava本地限流器 + Redis分布式限流结合
- 签名计算缓存
- 异步日志记录
4.2 最佳实践
-
密钥管理
// 使用配置中心管理密钥,定期轮换 @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监控 // 短时间内异常次数过多自动加入黑名单 } }
4.3 安全性评估
| 攻击类型 | 防护措施 | 有效性 |
|---|---|---|
| 重放攻击 | 时间戳+Nonce | ★★★★★ |
| 参数篡改 | HMAC签名 | ★★★★★ |
| CC攻击 | 多级限流 | ★★★★☆ |
| Token窃取 | HTTPS+短时效 | ★★★★☆ |
| 越权访问 | Token绑定用户 | ★★★★★ |
| 中间人攻击 | HTTPS强制 | ★★★★★ |
4.4 运维注意事项
-
Redis高可用
- 使用Redis Cluster或Sentinel
- 设置合理的过期策略
- 监控内存使用情况
-
性能指标
- 签名验证耗时:< 5ms
- 限流检查耗时:< 2ms
- Token验证耗时:< 3ms
-
配置调优
yaml
api: security: # 根据业务调整 rate-limit: # 登录接口:5次/分钟 login: 5/60 # 查询接口:100次/分钟 query: 100/60 # 写入接口:20次/分钟 write: 20/60
4.5 扩展建议
- 加入验证码机制
- 登录失败3次后要求验证码
- 使用Google reCAPTCHA或自研验证码
- 设备指纹
- 记录设备指纹信息
- 异常设备切换触发二次验证
- 行为分析
- 机器学习识别异常行为模式
- 自动化IP封禁系统
- API网关
- 考虑使用Spring Cloud Gateway
- 统一管理所有安全策略
- 支持动态路由和限流
4.6 常见问题及解决方案
Q: 签名验证失败率高? A: 检查客户端和服务端时间同步,使用NTP服务;确保参数排序算法一致
Q: 限流误伤正常用户? A: 针对不同接口设置差异化限流策略;使用滑动窗口算法更平滑
Q: Token如何刷新? A: 使用双Token机制:Access Token(短时效)+ Refresh Token(长时效)
Q: 分布式环境下的限流精度? A: 使用Redis + Lua脚本保证原子性;考虑使用Sentinel等专业组件
通过以上方案的实施,可以有效防止API接口被恶意调用,保障系统的安全性和稳定性。根据实际业务场景,在安全性和用户体验之间找到平衡点,持续优化和完善安全策略。
谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海