feat: 初始化项目

This commit is contained in:
richarjiang
2025-08-13 15:17:33 +08:00
commit 4f9d648a50
72 changed files with 29051 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
import { Injectable, Logger, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as jwt from 'jsonwebtoken';
import * as jwksClient from 'jwks-rsa';
import axios from 'axios';
export interface AppleTokenPayload {
iss: string; // 发行者,应该是 https://appleid.apple.com
aud: string; // 受众,应该是你的 bundle ID
exp: number; // 过期时间
iat: number; // 签发时间
sub: string; // 用户的唯一标识符
email?: string; // 用户邮箱(可选)
email_verified?: boolean; // 邮箱是否验证(可选)
is_private_email?: boolean; // 是否是私有邮箱(可选)
real_user_status?: number; // 真实用户状态
transfer_sub?: string; // 转移的用户标识符(可选)
}
export interface AccessTokenPayload {
sub: string; // 用户ID
email?: string; // 用户邮箱
iat: number; // 签发时间
exp: number; // 过期时间
type: 'access'; // token类型
}
export interface RefreshTokenPayload {
sub: string; // 用户ID
iat: number; // 签发时间
exp: number; // 过期时间
type: 'refresh'; // token类型
}
@Injectable()
export class AppleAuthService {
private readonly logger = new Logger(AppleAuthService.name);
private readonly bundleId: string;
private readonly jwksClient: jwksClient.JwksClient;
private readonly accessTokenSecret: string;
private readonly refreshTokenSecret: string;
private readonly accessTokenExpiresIn = 30 * 24 * 60 * 60; // 30天
private readonly refreshTokenExpiresIn = 90 * 24 * 60 * 60; // 90天
constructor(
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
) {
this.bundleId = this.configService.get<string>('APPLE_BUNDLE_ID') || '';
this.accessTokenSecret = this.configService.get<string>('JWT_ACCESS_SECRET') || 'your-access-token-secret-key';
this.refreshTokenSecret = this.configService.get<string>('JWT_REFRESH_SECRET') || 'your-refresh-token-secret-key';
// 初始化 JWKS 客户端,用于获取 Apple 的公钥
this.jwksClient = jwksClient({
jwksUri: 'https://appleid.apple.com/auth/keys',
cache: true,
cacheMaxAge: 24 * 60 * 60 * 1000, // 24小时缓存
rateLimit: true,
jwksRequestsPerMinute: 10,
});
if (!this.bundleId) {
this.logger.warn('APPLE_BUNDLE_ID 环境变量未设置Apple登录验证可能失败');
}
}
/**
* 获取 Apple 的公钥
*/
private async getApplePublicKey(kid: string): Promise<string> {
try {
const key = await this.jwksClient.getSigningKey(kid);
return key.getPublicKey();
} catch (error) {
this.logger.error(`获取Apple公钥失败: ${error.message}`);
throw new UnauthorizedException('无法验证Apple身份令牌');
}
}
/**
* 验证 Apple Identity Token
*/
async verifyAppleToken(identityToken: string): Promise<AppleTokenPayload> {
try {
// 解码 token header 获取 kid
const decodedHeader = jwt.decode(identityToken, { complete: true });
if (!decodedHeader || !decodedHeader.header.kid) {
throw new BadRequestException('无效的身份令牌格式');
}
const kid = decodedHeader.header.kid;
// 获取 Apple 公钥
const publicKey = await this.getApplePublicKey(kid);
this.logger.log(`Apple 公钥: ${publicKey}`);
this.logger.log(`this.bundleId: ${this.bundleId}`);
// 验证 token
const payload = jwt.verify(identityToken, publicKey, {
algorithms: ['RS256'],
audience: this.bundleId,
issuer: 'https://appleid.apple.com',
}) as AppleTokenPayload;
// 验证必要字段
if (!payload.sub) {
throw new BadRequestException('身份令牌缺少用户标识符');
}
// 验证 token 是否过期
const now = Math.floor(Date.now() / 1000);
if (payload.exp < now) {
throw new UnauthorizedException('身份令牌已过期');
}
this.logger.log(`Apple身份令牌验证成功用户ID: ${payload.sub}`);
return payload;
} catch (error) {
if (error instanceof UnauthorizedException || error instanceof BadRequestException) {
throw error;
}
this.logger.error(`验证Apple身份令牌失败: ${error.message}`);
throw new UnauthorizedException('身份令牌验证失败');
}
}
/**
* 生成访问令牌
*/
generateAccessToken(userId: string, email?: string): string {
const payload: AccessTokenPayload = {
sub: userId,
email,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + this.accessTokenExpiresIn,
type: 'access',
};
return jwt.sign(payload, this.accessTokenSecret, {
algorithm: 'HS256',
});
}
/**
* 生成刷新令牌
*/
generateRefreshToken(userId: string): string {
const payload: RefreshTokenPayload = {
sub: userId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + this.refreshTokenExpiresIn,
type: 'refresh',
};
return jwt.sign(payload, this.refreshTokenSecret, {
algorithm: 'HS256',
});
}
/**
* 验证访问令牌
*/
verifyAccessToken(token: string): AccessTokenPayload {
try {
const payload = jwt.verify(token, this.accessTokenSecret, {
algorithms: ['HS256'],
}) as AccessTokenPayload;
if (payload.type !== 'access') {
throw new UnauthorizedException('无效的令牌类型');
}
return payload;
} catch (error) {
this.logger.error(`验证访问令牌失败: ${error.message}`);
throw new UnauthorizedException('访问令牌无效或已过期');
}
}
/**
* 验证刷新令牌
*/
verifyRefreshToken(token: string): RefreshTokenPayload {
try {
const payload = jwt.verify(token, this.refreshTokenSecret, {
algorithms: ['HS256'],
}) as RefreshTokenPayload;
if (payload.type !== 'refresh') {
throw new UnauthorizedException('无效的令牌类型');
}
return payload;
} catch (error) {
this.logger.error(`验证刷新令牌失败: ${error.message}`);
throw new UnauthorizedException('刷新令牌无效或已过期');
}
}
/**
* 刷新访问令牌
*/
async refreshAccessToken(refreshToken: string, userEmail?: string): Promise<{ accessToken: string; expiresIn: number }> {
const payload = this.verifyRefreshToken(refreshToken);
const newAccessToken = this.generateAccessToken(payload.sub, userEmail);
return {
accessToken: newAccessToken,
expiresIn: this.accessTokenExpiresIn,
};
}
/**
* 从 Bearer token 中提取 token
*/
extractTokenFromHeader(authHeader: string): string {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('无效的授权头格式');
}
return authHeader.substring(7);
}
/**
* 获取访问令牌的过期时间(秒)
*/
getAccessTokenExpiresIn(): number {
return this.accessTokenExpiresIn;
}
/**
* 获取刷新令牌的过期时间(秒)
*/
getRefreshTokenExpiresIn(): number {
return this.refreshTokenExpiresIn;
}
}