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('APPLE_BUNDLE_ID') || ''; this.accessTokenSecret = this.configService.get('JWT_ACCESS_SECRET') || 'your-access-token-secret-key'; this.refreshTokenSecret = this.configService.get('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 { 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 { 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 ')) { return '' } return authHeader.substring(7); } /** * 获取访问令牌的过期时间(秒) */ getAccessTokenExpiresIn(): number { return this.accessTokenExpiresIn; } /** * 获取刷新令牌的过期时间(秒) */ getRefreshTokenExpiresIn(): number { return this.refreshTokenExpiresIn; } }