案例:某电商因未部署防盗链,竞品网站直接引用商品图,导致服务器成本激增300%。
// ImageSecurityInterceptor.java:基础防盗链拦截器
@Component
public class ImageSecurityInterceptor implements HandlerInterceptor {
@Value("${image.security.allowed-domains}") // 配置文件读取
private String[] allowedDomains;
@Value("${image.security.allow-browser-access}")
private boolean allowBrowserAccess;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1. 判断是否为图片请求
String requestURI = request.getRequestURI();
if (!isImageRequest(requestURI)) {
return true;
}
// 2. 提取Referer来源
String referer = request.getHeader("Referer");
if (StringUtils.isEmpty(referer)) {
// 无来源请求处理
return handleNoReferer(request, response);
}
// 3. 域名白名单校验
String host = new URL(referer).getHost();
boolean isAllowed = Arrays.stream(allowedDomains)
.anyMatch(domain -> domain.equalsIgnoreCase(host));
if (isAllowed) {
return true;
}
// ❌拒绝策略:返回403或替换图片
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
return false;
}
// 辅助方法:图片请求检测
private boolean isImageRequest(String uri) {
String[] imageExtensions = {".jpg", ".jpeg", ".png", ".webp"};
return Arrays.stream(imageExtensions)
.anyMatch(uri::endsWith);
}
// 无Referer请求处理
private boolean handleNoReferer(HttpServletRequest request,
HttpServletResponse response) {
if (allowBrowserAccess) {
// 允许浏览器直接访问(移动端常见)
return true;
}
// 禁止直接访问
response.sendRedirect("/static/403-image.jpg");
return false;
}
}
// DynamicTokenService.java:基于HMAC-SHA256的签名生成
@Service
public class DynamicTokenService {
@Value("${image.security.secret-key}") // 加密密钥
private String secretKey;
@Value("${image.security.token-expire-seconds}")
private int tokenExpireSeconds;
// 生成带时效的签名URL
public String generateSecureUrl(String imageId) {
long timestamp = System.currentTimeMillis() / 1000;
String signature = generateSignature(imageId, timestamp);
return String.format(
"/api/image/%s?timestamp=%d&signature=%s",
imageId, timestamp, signature
);
}
// 签名生成算法
private String generateSignature(String imageId, long timestamp) {
String dataToSign = String.format("%s|%d", imageId, timestamp);
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec key = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
mac.init(key);
byte[] hmacData = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hmacData);
} catch (Exception e) {
throw new RuntimeException("签名生成失败", e);
}
}
// 验证签名合法性
public boolean validateSignature(String imageId,
long timestamp,
String providedSignature) {
if (System.currentTimeMillis() / 1000 - timestamp > tokenExpireSeconds) {
return false; // 超时失效
}
String generatedSignature = generateSignature(imageId, timestamp);
return generatedSignature.equals(providedSignature);
}
}
// FastDFSImageService.java:分布式存储防盗链
@Service
public class FastDFSImageService {
@Value("${fdfs.http.anti-steal.secret-key}") // FastDFS密钥
private String secretKey;
@Value("${fdfs.http-server-base-url}")
private String baseUrl;
// 生成带防盗链的URL
public String getSecureImageUrl(String group, String remoteFilename) {
try {
String token = generateToken(remoteFilename);
long timestamp = System.currentTimeMillis() / 1000;
return String.format(
"%s/group%s/%s?token=%s&ts=%d",
baseUrl, group, remoteFilename, token, timestamp
);
} catch (Exception e) {
throw new RuntimeException("防盗链URL生成失败", e);
}
}
// FastDFS专用token生成算法
private String generateToken(String remoteFilename) throws Exception {
long timestamp = System.currentTimeMillis() / 1000;
String data = remoteFilename + timestamp;
MessageDigest md = MessageDigest.getInstance("MD5");
md.update((data + secretKey).getBytes());
return new BigInteger(1, md.digest()).toString(16);
}
}
// SecurityConfig.java:Spring Security深度防护
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private ImageSecurityInterceptor imageInterceptor;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.requestMatchers("/api/image/**").permitAll() // 允许图片接口访问
.anyRequest().authenticated()
)
.addFilterBefore(new CsrfTokenResponseHeaderBindingFilter(),
CsrfFilter.class)
.sessionManagement(session -> session
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
)
.headers(headers -> headers
.frameOptions().sameOrigin()
.contentSecurityPolicy("default-src 'self'")
);
return http.build();
}
// 密码加密配置
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
// 自定义异常处理
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return (request, response, authException) ->
response.sendRedirect("/unauthorized");
}
}
// ImageCacheService.java:Redis缓存加速
@Service
public class ImageCacheService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Value("${image.cache.expire-seconds}")
private int cacheExpire;
// 缓存合法图片请求
public boolean isRequestAllowed(HttpServletRequest request) {
String cacheKey = generateCacheKey(request);
return redisTemplate.hasKey(cacheKey);
}
// 记录合法请求
public void recordValidRequest(HttpServletRequest request) {
String cacheKey = generateCacheKey(request);
redisTemplate.opsForValue()
.set(cacheKey, "ALLOWED", cacheExpire, TimeUnit.SECONDS);
}
// 生成缓存键
private String generateCacheKey(HttpServletRequest request) {
return String.format(
"image:cache:%s|%s",
request.getRequestURI(),
request.getHeader("Referer")
);
}
}
// ImageController.java:防盗链接口
@RestController
public class ImageController {
@Autowired
private DynamicTokenService tokenService;
@Autowired
private ImageRepository imageRepo;
@GetMapping("/api/image/{id}")
public ResponseEntity<byte[]> getImage(@PathVariable String id,
@RequestParam long timestamp,
@RequestParam String signature) {
// 1. 动态令牌验证
ImageEntity image = imageRepo.findById(id)
.orElseThrow(() -> new RuntimeException("图片不存在"));
if (!tokenService.validateSignature(id, timestamp, signature)) {
return ResponseEntity.status(403).body("Forbidden".getBytes());
}
// 2. 返回图片
return ResponseEntity
.ok()
.contentType(MediaType.IMAGE_JPEG)
.body(image.getData());
}
// 生成带令牌的URL
@GetMapping("/api/generate-url")
public String generateUrl(@RequestParam String imageId) {
return tokenService.generateSecureUrl(imageId);
}
}
// FastDFSConfig.java:分布式存储配置
@Configuration
public class FastDFSConfig {
@Value("${fdfs.http.anti-steal.secret-key}")
private String secretKey;
@Bean
public TrackerClient trackerClient() {
TrackerClient client = new TrackerClient();
try {
TrackerServer trackerServer = client.getConnection();
StorageServer storageServer = client.getStoreStorage(trackerServer);
return client;
} catch (Exception e) {
throw new RuntimeException("FastDFS连接失败", e);
}
}
// 防盗链参数注入
@Bean
public FastDFSClient fastDFSClient() {
return new FastDFSClient(
secretKey,
"http://fastdfs-server:8080",
300 // token有效期5分钟
);
}
}
// AntiCrawlerFilter.java:行为分析过滤器
@Component
public class AntiCrawlerFilter implements Filter {
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String clientIP = httpRequest.getRemoteAddr();
// IP请求频率检测
String ipKey = "crawler:ip:" + clientIP;
Integer count = redisTemplate.opsForValue().increment(ipKey, 1);
if (count > 100) { // 每分钟超过100次请求
((HttpServletResponse) response).sendError(429, "Too Many Requests");
return;
}
redisTemplate.expire(ipKey, 60, TimeUnit.SECONDS);
// ️请求头完整性检测
if (httpRequest.getHeader("User-Agent") == null
|| httpRequest.getHeader("Accept") == null) {
((HttpServletResponse) response).sendError(403);
return;
}
chain.doFilter(request, response);
}
}
// PostQuantumKeyService.java:抗量子密钥管理
@Service
public class PostQuantumKeyService {
@Value("${image.security.post-quantum-key}")
private String postQuantumKey;
// 量子安全签名生成
public String generatePostQuantumSignature(String data) {
try {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("NTRU");
kpg.initialize(2048);
KeyPair keyPair = kpg.generateKeyPair();
Cipher cipher = Cipher.getInstance("NTRU");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
return Base64.getEncoder().encodeToString(
cipher.doFinal((data + postQuantumKey).getBytes())
);
} catch (Exception e) {
throw new RuntimeException("量子签名失败", e);
}
}
}
// KeyRotationService.java:密钥动态更新
@Service
public class KeyRotationService {
@Value("${image.security.secret-key}")
private String currentKey;
@Value("${image.security.rotation-interval}")
private int rotationInterval;
@Scheduled(fixedRate = 3600000) // 每小时轮换
public void rotateKey() {
String newKey = generateRandomKey();
// 更新配置中心
ConfigClient.updateConfig("image.security.secret-key", newKey);
// 记录旧密钥(兼容旧签名)
LegacyKeyStore.addLegacyKey(currentKey);
currentKey = newKey;
}
// 生成安全密钥
private String generateRandomKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[32];
random.nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}
}
// AIFraudDetector.java:机器学习模型集成
@Service
public class AIFraudDetector {
@Autowired
private ImageRepository imageRepo;
// 实时行为分析
public boolean detectFraud(HttpServletRequest request) {
ImageRequestLog log = new ImageRequestLog();
log.setIpAddress(request.getRemoteAddr());
log.setReferer(request.getHeader("Referer"));
log.setUserAgent(request.getHeader("User-Agent"));
// 特征提取
double[] features = extractFeatures(log);
// 模型预测
boolean isFraud = model.predict(features) > 0.9;
if (isFraud) {
FraudDB.save(log);
}
return isFraud;
}
// 特征工程
private double[] extractFeatures(ImageRequestLog log) {
return new double[]{
log.getIpAddress().hashCode(),
log.getReferer().length(),
log.getUserAgent().contains("bot") ? 1 : 0
};
}
}
// AdaptivePolicyEngine.java:动态策略调整
@Service
public class AdaptivePolicyEngine {
@Autowired
private AIFraudDetector aiDetector;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 实时策略调整
public void adjustPolicies() {
// 检测攻击波峰
List<ImageRequestLog> recentLogs =
FraudDB.getRecentLogs(60); // 最近1分钟请求
if (recentLogs.size() > 500) {
// 紧急模式:关闭浏览器直接访问
redisTemplate.opsForValue().set("allow_browser_access", false);
}
// 自动更新白名单
List<String> newDomains = aiDetector.getRecommendedDomains();
redisTemplate.opsForList().rightPushAll("allowed_domains", newDomains);
}
}