feat: 添加用户认证系统

引入基于 Basic Auth 的用户认证系统,包括用户管理、登录界面和 API 鉴权
- 新增用户实体和管理功能
- 实现前端登录界面和凭证管理
- 重构 API 鉴权为 Basic Auth 模式
- 添加用户管理脚本工具
This commit is contained in:
dmy
2026-01-18 12:47:16 +08:00
parent a55dfd78d2
commit b6a6398864
30 changed files with 2042 additions and 82 deletions

View File

@@ -6,78 +6,55 @@ import {
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
import { UsersService } from '../../users/users.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private configService: ConfigService) {}
constructor(
private configService: ConfigService,
private usersService: UsersService,
) {}
canActivate(context: ExecutionContext): boolean {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const clientIp = this.getClientIp(request);
// 检查是否为本地 IP
if (this.isLocalIp(clientIp)) {
return true; // 本地访问直接放行
}
// 检查是否启用 Basic Auth
const enableBasicAuth =
this.configService.get<string>('ENABLE_BASIC_AUTH') === 'true';
// 公网访问需要验证 API Key
const apiKey = request.headers['x-api-key'] as string;
const expectedApiKey = this.configService.get<string>('API_KEY');
if (!expectedApiKey) {
// 如果未配置 API_KEY允许所有访问开发环境
if (!enableBasicAuth) {
// 如果未启用 Basic Auth允许所有访问
return true;
}
if (!apiKey || apiKey !== expectedApiKey) {
throw new UnauthorizedException('Invalid or missing API Key');
// 解析 Authorization header
const authHeader = request.headers['authorization'] as string;
if (!authHeader || !authHeader.startsWith('Basic ')) {
throw new UnauthorizedException('Missing or invalid Authorization header');
}
// 解码 Basic Auth
const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString(
'utf-8',
);
const [username, password] = credentials.split(':');
if (!username || !password) {
throw new UnauthorizedException('Invalid credentials format');
}
// 验证用户
const user = await this.usersService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException('Invalid username or password');
}
// 将用户信息附加到请求对象
(request as any).user = user;
return true;
}
/**
* 获取客户端真实 IP
* 支持反向代理场景X-Forwarded-For
*/
private getClientIp(request: Request): string {
const forwardedFor = request.headers['x-forwarded-for'];
if (forwardedFor) {
// X-Forwarded-For 可能包含多个 IP取第一个
const ips = (forwardedFor as string).split(',');
return ips[0].trim();
}
return request.ip || request.socket.remoteAddress || 'unknown';
}
/**
* 判断是否为本地 IP
*/
private isLocalIp(ip: string): boolean {
if (!ip || ip === 'unknown') {
return false;
}
// IPv4 本地地址
if (ip === '127.0.0.1' || ip === 'localhost' || ip.startsWith('127.')) {
return true;
}
// IPv6 本地地址
if (ip === '::1' || ip === '::ffff:127.0.0.1') {
return true;
}
// 内网地址(可选,根据需求决定是否允许)
// 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
const privateIpRegex =
/^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/;
if (privateIpRegex.test(ip)) {
// 如果需要内网也免鉴权,可以返回 true
// 这里默认返回 false内网也需要鉴权
return false;
}
return false;
}
}