feat: 添加用户认证系统
引入基于 Basic Auth 的用户认证系统,包括用户管理、登录界面和 API 鉴权 - 新增用户实体和管理功能 - 实现前端登录界面和凭证管理 - 重构 API 鉴权为 Basic Auth 模式 - 添加用户管理脚本工具
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user