feat: admin auth 支持 httpOnly cookie
- main.ts: 添加 cookie-parser 中间件 - admin-auth.controller.ts: 登录/刷新设置 httpOnly cookie - admin-auth.controller.ts: 登出清除 cookie - admin-auth.controller.ts: 刷新支持 cookie 读取 token(兼容旧 body 方式) - admin-auth.guard.ts: 优先从 cookie 读取 token,兼容 Authorization header 向后兼容: Authorization header 仍可用(iOS/鸿蒙客户端) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a8ac081490
commit
7da4145d0a
@ -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];
|
||||
|
||||
@ -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/(.*)'] });
|
||||
|
||||
|
||||
@ -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: '已退出登录' };
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user