117 lines
2.6 KiB
TypeScript
117 lines
2.6 KiB
TypeScript
/**
|
||
* 下载 Token 生成和验证
|
||
* 用于外部接口的文件下载鉴权
|
||
*/
|
||
|
||
import jwt from 'jsonwebtoken';
|
||
|
||
// Token 有效期:永不过期
|
||
// 注意:任务平台会缓存下载链接,所以设置为永不过期
|
||
// 安全性依赖于 DOWNLOAD_TOKEN_SECRET 的保密性
|
||
const TOKEN_EXPIRY = '100y'; // 100年 ≈ 永不过期
|
||
|
||
// 下载类型
|
||
export type DownloadType =
|
||
| 'all'
|
||
| 'main'
|
||
| 'sku'
|
||
| 'detail'
|
||
| 'psd'
|
||
| 'single'
|
||
| 'image-source'
|
||
| 'white-background-single'
|
||
| 'white-background-zip';
|
||
|
||
export interface DownloadTokenPayload {
|
||
materialId: string;
|
||
downloadType: DownloadType;
|
||
assetId?: string; // 单个资产下载时使用
|
||
skuId?: string; // SKU 级资源下载时使用
|
||
}
|
||
|
||
interface DecodedToken extends DownloadTokenPayload {
|
||
iat: number;
|
||
exp: number;
|
||
}
|
||
|
||
/**
|
||
* 获取 Token 密钥
|
||
*/
|
||
function getTokenSecret(): string {
|
||
const secret = process.env.DOWNLOAD_TOKEN_SECRET;
|
||
if (!secret) {
|
||
throw new Error('DOWNLOAD_TOKEN_SECRET 环境变量未配置');
|
||
}
|
||
return secret;
|
||
}
|
||
|
||
/**
|
||
* 生成下载 Token
|
||
*/
|
||
export function generateDownloadToken(payload: DownloadTokenPayload): string {
|
||
const secret = getTokenSecret();
|
||
return jwt.sign(payload, secret, { expiresIn: TOKEN_EXPIRY });
|
||
}
|
||
|
||
/**
|
||
* 验证下载 Token
|
||
* @returns 解码后的 payload,验证失败返回 null
|
||
*/
|
||
export function verifyDownloadToken(token: string): DownloadTokenPayload | null {
|
||
try {
|
||
const secret = getTokenSecret();
|
||
const decoded = jwt.verify(token, secret) as DecodedToken;
|
||
return {
|
||
materialId: decoded.materialId,
|
||
downloadType: decoded.downloadType,
|
||
assetId: decoded.assetId,
|
||
skuId: decoded.skuId,
|
||
};
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查 Token 是否过期
|
||
*/
|
||
export function isTokenExpired(token: string): boolean {
|
||
try {
|
||
const secret = getTokenSecret();
|
||
jwt.verify(token, secret);
|
||
return false;
|
||
} catch (error: any) {
|
||
if (error.name === 'TokenExpiredError') {
|
||
return true;
|
||
}
|
||
// 其他错误(如无效 token)也返回 true
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取 Token 过期时间
|
||
*/
|
||
export function getTokenExpiry(token: string): Date | null {
|
||
try {
|
||
const decoded = jwt.decode(token) as DecodedToken | null;
|
||
if (decoded?.exp) {
|
||
return new Date(decoded.exp * 1000);
|
||
}
|
||
return null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成带 Token 的下载 URL
|
||
*/
|
||
export function generateDownloadUrl(
|
||
baseUrl: string,
|
||
payload: DownloadTokenPayload
|
||
): string {
|
||
const token = generateDownloadToken(payload);
|
||
return `${baseUrl}/api/external/download?token=${encodeURIComponent(token)}`;
|
||
}
|