feat(auth): implement API key authentication and enhance request handling
- Add AuthGuard to validate API key for public access. - Create AuthModule to provide the AuthGuard globally. - Update API request interceptor to automatically include API key for non-localhost requests. - Modify .env and .env.example to include API_KEY configuration. - Enhance API request handling with improved error logging and client IP detection.
This commit is contained in:
3
.env
3
.env
@@ -43,3 +43,6 @@ LOG_LEVEL=debug
|
|||||||
ARK_API_KEY=a63d58b6-cf56-434b-8a42-5c781ba0822a
|
ARK_API_KEY=a63d58b6-cf56-434b-8a42-5c781ba0822a
|
||||||
|
|
||||||
SSH_PASSPHRASE=x
|
SSH_PASSPHRASE=x
|
||||||
|
|
||||||
|
API_KEY=22c64b60-6e60-433c-991d-f6d658024b9e
|
||||||
|
|
||||||
|
|||||||
@@ -25,3 +25,5 @@ LOG_LEVEL=info
|
|||||||
|
|
||||||
# OpenAI API Key (用于 AI 推荐)
|
# OpenAI API Key (用于 AI 推荐)
|
||||||
ARK_API_KEY=your_openai_api_key_here
|
ARK_API_KEY=your_openai_api_key_here
|
||||||
|
|
||||||
|
API_KEY=your_secure_api_key_here
|
||||||
@@ -1,34 +1,52 @@
|
|||||||
import axios from 'axios'
|
import axios, { type AxiosRequestConfig, type AxiosError } from 'axios';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API配置
|
* API配置
|
||||||
* 配置axios实例,设置baseURL和请求拦截器
|
* 配置axios实例,设置baseURL和请求拦截器
|
||||||
*/
|
*/
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: 'http://localhost:3000', // 设置后端服务地址
|
baseURL:
|
||||||
|
(import.meta.env.VITE_API_BASE_URL as string) || 'http://localhost:3000', // 设置后端服务地址
|
||||||
timeout: 120000, // 请求超时时间(120秒)
|
timeout: 120000, // 请求超时时间(120秒)
|
||||||
})
|
});
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
(config: AxiosRequestConfig) => {
|
||||||
// 可以在这里添加认证信息等
|
// 如果 baseURL 不是 localhost,自动添加 API Key
|
||||||
return config
|
const baseURL =
|
||||||
},
|
(config.baseURL as string) ||
|
||||||
(error) => {
|
(api.defaults.baseURL as string) ||
|
||||||
return Promise.reject(error)
|
'';
|
||||||
|
const isLocalhost =
|
||||||
|
baseURL.includes('localhost') || baseURL.includes('127.0.0.1');
|
||||||
|
|
||||||
|
if (!isLocalhost) {
|
||||||
|
// 从环境变量或 localStorage 获取 API Key
|
||||||
|
const apiKey =
|
||||||
|
(import.meta.env.VITE_API_KEY as string) ||
|
||||||
|
localStorage.getItem('apiKey');
|
||||||
|
if (apiKey && config.headers) {
|
||||||
|
config.headers['X-API-Key'] = apiKey;
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error: AxiosError) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 响应拦截器
|
// 响应拦截器
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
return response
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error: AxiosError) => {
|
||||||
console.error('API请求错误:', error)
|
console.error('API请求错误:', error);
|
||||||
return Promise.reject(error)
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
export default api
|
export default api;
|
||||||
@@ -11,6 +11,7 @@ import { TasksModule } from './schedule/schedule.module';
|
|||||||
import { LoggerModule } from './common/logger/logger.module';
|
import { LoggerModule } from './common/logger/logger.module';
|
||||||
import { LoggingMiddleware } from './common/logger/logging.middleware';
|
import { LoggingMiddleware } from './common/logger/logging.middleware';
|
||||||
import { AiModule } from './ai/ai.module';
|
import { AiModule } from './ai/ai.module';
|
||||||
|
import { AuthModule } from './common/auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -21,6 +22,7 @@ import { AiModule } from './ai/ai.module';
|
|||||||
exclude: ['/api'],
|
exclude: ['/api'],
|
||||||
}),
|
}),
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
|
AuthModule,
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
BidsModule,
|
BidsModule,
|
||||||
KeywordsModule,
|
KeywordsModule,
|
||||||
|
|||||||
83
src/common/auth/auth.guard.ts
Normal file
83
src/common/auth/auth.guard.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
constructor(private configService: ConfigService) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
|
const clientIp = this.getClientIp(request);
|
||||||
|
|
||||||
|
// 检查是否为本地 IP
|
||||||
|
if (this.isLocalIp(clientIp)) {
|
||||||
|
return true; // 本地访问直接放行
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公网访问需要验证 API Key
|
||||||
|
const apiKey = request.headers['x-api-key'] as string;
|
||||||
|
const expectedApiKey = this.configService.get<string>('API_KEY');
|
||||||
|
|
||||||
|
if (!expectedApiKey) {
|
||||||
|
// 如果未配置 API_KEY,允许所有访问(开发环境)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey || apiKey !== expectedApiKey) {
|
||||||
|
throw new UnauthorizedException('Invalid or missing API Key');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/common/auth/auth.module.ts
Normal file
13
src/common/auth/auth.module.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from './auth.guard';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: AuthGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
@@ -21,6 +21,10 @@ async function bootstrap() {
|
|||||||
// 启用 CORS
|
// 启用 CORS
|
||||||
app.enableCors();
|
app.enableCors();
|
||||||
|
|
||||||
|
// 信任代理(用于获取真实客户端 IP)
|
||||||
|
const httpAdapter = app.getHttpAdapter();
|
||||||
|
httpAdapter.getInstance().set('trust proxy', true);
|
||||||
|
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
}
|
}
|
||||||
void bootstrap();
|
void bootstrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user