diff --git a/src/common/guards/admin-auth.guard.ts b/src/common/guards/admin-auth.guard.ts index 7add6e3..df1308b 100644 --- a/src/common/guards/admin-auth.guard.ts +++ b/src/common/guards/admin-auth.guard.ts @@ -136,6 +136,11 @@ export class AdminAuthGuard implements CanActivate { } private extractToken(request: Request): string | undefined { + // 优先从 httpOnly cookie 读取(新前端) + const cookieToken = (request as any).cookies?.admin_access_token; + if (cookieToken) return cookieToken; + + // 兼容 Authorization: Bearer header(旧客户端、移动端) const authHeader = request.headers.authorization; if (!authHeader?.startsWith('Bearer ')) return undefined; return authHeader.split(' ')[1]; diff --git a/src/main.ts b/src/main.ts index 1b11c4b..1b62632 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import { NestFactory } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; import helmet from 'helmet'; +import * as cookieParser from 'cookie-parser'; import { ConfigService } from '@nestjs/config'; import { NestExpressApplication } from '@nestjs/platform-express'; @@ -15,6 +16,7 @@ async function bootstrap() { app.set("trust proxy", 1); app.use(helmet()); + app.use(cookieParser()); app.setGlobalPrefix('api', { exclude: ['health', 'admin-api/(.*)', 'internal/(.*)'] }); diff --git a/src/modules/admin-auth/admin-auth.controller.ts b/src/modules/admin-auth/admin-auth.controller.ts index 122322d..0d38b0e 100644 --- a/src/modules/admin-auth/admin-auth.controller.ts +++ b/src/modules/admin-auth/admin-auth.controller.ts @@ -1,12 +1,28 @@ import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; -import { Controller, Post, Body, Get, HttpCode, HttpStatus, Req, UseGuards } from '@nestjs/common'; +import { Controller, Post, Body, Get, HttpCode, HttpStatus, Req, Res, UseGuards } from '@nestjs/common'; import { AdminAuthService } from './admin-auth.service'; import { AdminLoginDto, AdminRefreshDto } from './dto'; import { AdminPublic } from '../../common/decorators/admin-public.decorator'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; import { AdminLoginRateLimit } from '../../common/decorators/rate-limit.decorator'; -import type { Request } from 'express'; +import type { Request, Response } from 'express'; + +const ACCESS_COOKIE = { + httpOnly: true, + secure: true, + sameSite: 'strict' as const, + path: '/admin-api', + maxAge: 60 * 60 * 1000, // 1 小时 +} as const; + +const REFRESH_COOKIE = { + httpOnly: true, + secure: true, + sameSite: 'strict' as const, + path: '/admin-api/auth', + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天 +} as const; @ApiTags('admin-auth') @Controller('admin-api/auth') @@ -22,9 +38,14 @@ export class AdminAuthController { @ApiResponse({ status: 200, description: '登录成功' }) @ApiResponse({ status: 401, description: '邮箱或密码错误' }) @ApiResponse({ status: 403, description: '账号已禁用或锁定' }) - async login(@Body() dto: AdminLoginDto, @Req() req: Request) { + async login(@Body() dto: AdminLoginDto, @Req() req: Request, @Res({ passthrough: true }) res: Response) { const ip = (req.ip || '').replace('::ffff:', ''); - return this.adminAuthService.login(dto.email, dto.password, ip, req.headers['user-agent']); + const result = await this.adminAuthService.login(dto.email, dto.password, ip, req.headers['user-agent']); + + res.cookie('admin_access_token', result.accessToken, ACCESS_COOKIE); + res.cookie('admin_refresh_token', result.refreshToken, REFRESH_COOKIE); + + return result; } @AdminPublic() @@ -33,9 +54,16 @@ export class AdminAuthController { @ApiOperation({ summary: '刷新管理员令牌' }) @ApiResponse({ status: 200, description: '刷新成功' }) @ApiResponse({ status: 401, description: '刷新令牌无效' }) - async refresh(@Body() dto: AdminRefreshDto, @Req() req: Request) { + async refresh(@Body() dto: AdminRefreshDto, @Req() req: Request, @Res({ passthrough: true }) res: Response) { const refreshIp = (req.ip || '').replace('::ffff:', ''); - return this.adminAuthService.refresh(dto.refreshToken, refreshIp, req.headers['user-agent']); + // 支持 body(旧客户端)或 cookie(新前端) + const token = dto.refreshToken || req.cookies?.admin_refresh_token; + const result = await this.adminAuthService.refresh(token, refreshIp, req.headers['user-agent']); + + res.cookie('admin_access_token', result.accessToken, ACCESS_COOKIE); + res.cookie('admin_refresh_token', result.refreshToken, REFRESH_COOKIE); + + return result; } @Post('logout') @@ -43,9 +71,17 @@ export class AdminAuthController { @ApiBearerAuth() @ApiOperation({ summary: '管理员退出登录' }) @ApiResponse({ status: 200, description: '退出成功' }) - async logout(@Req() req: Request, @Body() dto: AdminRefreshDto) { + async logout(@Req() req: Request, @Body() dto: AdminRefreshDto, @Res({ passthrough: true }) res: Response) { const adminUser = (req as any).adminUser; - await this.adminAuthService.logout(adminUser.id, dto.refreshToken); + // 支持 body(旧客户端)或 cookie(新前端) + const token = dto.refreshToken || req.cookies?.admin_refresh_token; + if (token) { + await this.adminAuthService.logout(adminUser.id, token); + } + + res.clearCookie('admin_access_token', { path: '/admin-api' }); + res.clearCookie('admin_refresh_token', { path: '/admin-api/auth' }); + return { success: true, message: '已退出登录' }; }