software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,333 @@
/*
* 自然写教学资源管理与内容分发系统软件 V1.0
* service/CdnService.java - CDN分发与缓存管理服务
*/
package com.writech.resource.service;
import java.util.*;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* CDN分发与缓存管理服务
*
* 负责教学资源的CDN加速分发,包括:
* - 签名URL生成(防盗链)
* - CDN缓存预热与刷新
* - 资源分发策略管理
* - 下载流量统计
*/
public class CdnService {
private static final Logger logger =
Logger.getLogger(CdnService.class.getName());
// ============================================================
// CDN配置
// ============================================================
/** CDN域名 */
private static final String CDN_DOMAIN = "https://cdn.writech.com";
/** CDN签名密钥 */
private String cdnSignKey;
/** 签名URL默认有效期(秒) */
private static final int DEFAULT_EXPIRE_SECONDS = 1800;
/** Referer白名单 */
private static final Set<String> REFERER_WHITELIST = new HashSet<>(Arrays.asList(
"*.writech.com",
"localhost",
"127.0.0.1"
));
/** CDN缓存策略(按资源类型配置TTL) */
private static final Map<String, Integer> CACHE_TTL_MAP = new HashMap<>();
static {
CACHE_TTL_MAP.put("pdf", 86400 * 30); // PDF资源缓存30天
CACHE_TTL_MAP.put("image", 86400 * 90); // 图片缓存90天
CACHE_TTL_MAP.put("video", 86400 * 7); // 视频缓存7天
CACHE_TTL_MAP.put("template", 86400 * 30); // 模板缓存30天
CACHE_TTL_MAP.put("dotcode", 86400 * 365); // 点阵码缓存1年(不变内容)
}
public CdnService(String signKey) {
this.cdnSignKey = signKey;
logger.info("CDN服务初始化: domain=" + CDN_DOMAIN);
}
// ============================================================
// 签名URL生成(防盗链核心)
// ============================================================
/**
* 生成CDN签名下载URL
*
* 签名算法(TypeA鉴权):
* 1. 计算签名原文:path-timestamp-rand-uid
* 2. HMAC-SHA256(原文, 密钥)
* 3. 拼接签名URLdomain/path?auth_key=timestamp-rand-uid-signature
*
* @param objectKey OSS对象Key
* @param expireSeconds 有效期(秒)
* @return 签名后的CDN下载URL
*/
public String generateSignedUrl(String objectKey, int expireSeconds) {
if (expireSeconds <= 0) {
expireSeconds = DEFAULT_EXPIRE_SECONDS;
}
long timestamp = System.currentTimeMillis() / 1000 + expireSeconds;
String rand = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
String uid = "0"; // 用户标识(可选)
String path = "/" + objectKey;
// 签名原文
String signContent = String.format("%s-%d-%s-%s", path, timestamp, rand, uid);
// HMAC-SHA256计算签名
String signature = hmacSha256(signContent, cdnSignKey);
// 拼接签名URL
String authKey = String.format("%d-%s-%s-%s", timestamp, rand, uid, signature);
String signedUrl = String.format("%s%s?auth_key=%s", CDN_DOMAIN, path, authKey);
logger.info(String.format(
"生成签名URL: key=%s, expire=%ds", objectKey, expireSeconds
));
return signedUrl;
}
/**
* 验证签名URL是否有效
*
* @param url 待验证的URL
* @return 验证结果
*/
public boolean verifySignedUrl(String url) {
try {
// 解析auth_key参数
String authKey = extractParam(url, "auth_key");
if (authKey == null) return false;
String[] parts = authKey.split("-", 4);
if (parts.length != 4) return false;
long timestamp = Long.parseLong(parts[0]);
String rand = parts[1];
String uid = parts[2];
String receivedSignature = parts[3];
// 检查是否过期
if (System.currentTimeMillis() / 1000 > timestamp) {
return false;
}
// 重新计算签名对比
String path = extractPath(url);
String signContent = String.format("%s-%d-%s-%s", path, timestamp, rand, uid);
String expectedSignature = hmacSha256(signContent, cdnSignKey);
return expectedSignature.equals(receivedSignature);
} catch (Exception e) {
logger.warning("签名验证异常: " + e.getMessage());
return false;
}
}
/**
* HMAC-SHA256签名计算
*/
private String hmacSha256(String data, String key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"
);
mac.init(secretKey);
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
// 转换为十六进制字符串
StringBuilder sb = new StringBuilder();
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("HMAC-SHA256计算失败", e);
}
}
// ============================================================
// CDN缓存管理
// ============================================================
/**
* 预热CDN缓存
*
* 将指定资源推送到CDN所有边缘节点,确保用户首次访问也能快速响应。
* 通常在资源审核通过后触发预热。
*
* @param objectKeys 要预热的资源Key列表
*/
public void preheatResources(List<String> objectKeys) {
logger.info(String.format("CDN缓存预热: %d个资源", objectKeys.size()));
List<String> urls = new ArrayList<>();
for (String key : objectKeys) {
urls.add(CDN_DOMAIN + "/" + key);
}
// 调用CDN API预热
// PushObjectCacheRequest request = new PushObjectCacheRequest();
// request.setObjectPath(String.join("\n", urls));
// cdnClient.pushObjectCache(request);
logger.info("CDN预热任务已提交");
}
/**
* 刷新CDN缓存
*
* 资源更新或删除后,需要刷新CDN缓存使旧版本失效。
*
* @param objectKeys 要刷新的资源Key列表
*/
public void refreshCache(List<String> objectKeys) {
logger.info(String.format("CDN缓存刷新: %d个资源", objectKeys.size()));
List<String> urls = new ArrayList<>();
for (String key : objectKeys) {
urls.add(CDN_DOMAIN + "/" + key);
}
// 调用CDN API刷新
// RefreshObjectCachesRequest request = new RefreshObjectCachesRequest();
// request.setObjectPath(String.join("\n", urls));
// cdnClient.refreshObjectCaches(request);
logger.info("CDN刷新任务已提交");
}
/**
* 刷新目录缓存(用于整个类别的批量更新)
*/
public void refreshDirectoryCache(String directoryPath) {
logger.info("CDN目录缓存刷新: " + directoryPath);
// RefreshObjectCachesRequest request = new RefreshObjectCachesRequest();
// request.setObjectPath(CDN_DOMAIN + "/" + directoryPath);
// request.setObjectType("Directory");
// cdnClient.refreshObjectCaches(request);
}
// ============================================================
// Referer防盗链校验
// ============================================================
/**
* 校验请求Referer是否在白名单中
*
* @param referer 请求头中的Referer
* @return 是否允许访问
*/
public boolean validateReferer(String referer) {
if (referer == null || referer.isEmpty()) {
return false; // 空Referer拒绝
}
for (String pattern : REFERER_WHITELIST) {
if (pattern.startsWith("*.")) {
// 通配符匹配
String domain = pattern.substring(2);
if (referer.contains(domain)) {
return true;
}
} else {
if (referer.contains(pattern)) {
return true;
}
}
}
logger.warning("Referer校验失败: " + referer);
return false;
}
// ============================================================
// 流量统计
// ============================================================
/**
* 记录资源下载事件(异步写入ClickHouse)
*
* @param resourceId 资源ID
* @param userId 下载用户ID
* @param terminal 终端类型(pad/pc/mobile/board
* @param fileSize 文件大小(字节)
*/
public void recordDownloadEvent(
String resourceId,
String userId,
String terminal,
long fileSize
) {
// 异步写入ClickHouse使用统计表
// Map<String, Object> event = new HashMap<>();
// event.put("resource_id", resourceId);
// event.put("user_id", userId);
// event.put("terminal", terminal);
// event.put("file_size", fileSize);
// event.put("download_at", new Date());
// event.put("cdn_node", getCdnNodeId());
// clickhouseClient.insert("usage_stat", event);
}
/**
* 查询资源下载统计
*/
public Map<String, Object> getDownloadStats(
String resourceId, String startDate, String endDate
) {
// 从ClickHouse查询聚合统计
// SELECT count() as downloads, sum(file_size) as total_bytes,
// uniq(user_id) as unique_users
// FROM usage_stat
// WHERE resource_id = ? AND download_at BETWEEN ? AND ?
Map<String, Object> stats = new HashMap<>();
stats.put("resource_id", resourceId);
stats.put("total_downloads", 0);
stats.put("total_bytes", 0L);
stats.put("unique_users", 0);
stats.put("by_terminal", new HashMap<>());
stats.put("daily_trend", new ArrayList<>());
return stats;
}
// ============================================================
// 辅助方法
// ============================================================
/** 从URL中提取指定参数 */
private String extractParam(String url, String paramName) {
int start = url.indexOf(paramName + "=");
if (start < 0) return null;
start += paramName.length() + 1;
int end = url.indexOf("&", start);
return end > 0 ? url.substring(start, end) : url.substring(start);
}
/** 从URL中提取路径部分 */
private String extractPath(String url) {
int start = url.indexOf("/", url.indexOf("//") + 2);
int end = url.indexOf("?");
return end > 0 ? url.substring(start, end) : url.substring(start);
}
}