fix(api): 字典和 MinIO 接入认证守卫

This commit is contained in:
sunlei 2026-05-17 14:56:26 +08:00
parent 5c526106bd
commit a920a92ad0
9 changed files with 65 additions and 14 deletions

4
API.md
View File

@ -48,7 +48,7 @@
### 后台认证 ### 后台认证
Admin 与 Component 业务接口统一走 `JwtAuthGuard`。请求可以通过 `Authorization: Bearer <accessToken>` 传递 accessToken也可以携带登录接口写入的 httpOnly `admin_access_token` cookie。未认证时接口返回 HTTP `401` Admin、Component、Dict 与 MinIO 业务接口统一走 `JwtAuthGuard`。请求可以通过 `Authorization: Bearer <accessToken>` 传递 accessToken也可以携带登录接口写入的 httpOnly `admin_access_token` cookie。未认证时接口返回 HTTP `401`
`ADMIN_COOKIE_SECURE=false` 适用于当前内网 HTTP 访问;如果后续切到 HTTPS 域名,可以改为 `true`cookie 会使用 `Secure + SameSite=None` `ADMIN_COOKIE_SECURE=false` 适用于当前内网 HTTP 访问;如果后续切到 HTTPS 域名,可以改为 `true`cookie 会使用 `Secure + SameSite=None`
@ -56,7 +56,7 @@ Admin 与 Component 业务接口统一走 `JwtAuthGuard`。请求可以通过 `A
### 数据库字典翻译 ### 数据库字典翻译
组件数据维护在 `admin_component` 表中,字典数据维护在新的 `admin_dict` 表中。`Component.typeMsg`、`Component.componentTypeMsg` 会在 TypeORM `AfterLoad` 阶段根据字典缓存自动映射;旧 `/dict/*` 接口路径保持兼容。 组件数据维护在 `admin_component` 表中,字典数据维护在新的 `admin_dict` 表中。`Component.typeMsg`、`Component.componentTypeMsg` 会在 TypeORM `AfterLoad` 阶段根据字典缓存自动映射;旧 `/dict/*` 接口路径保持兼容,但仍需要登录态
`admin_dict` 表核心字段: `admin_dict` 表核心字段:

View File

@ -104,7 +104,7 @@ pnpm test:e2e # e2e 测试
- 如果基础后台菜单的 `meta` 被旧数据覆盖为空,执行 `sql/fix-admin-menu-meta.sql` 可以恢复初始化菜单的 `title/icon/order` 等元数据。 - 如果基础后台菜单的 `meta` 被旧数据覆盖为空,执行 `sql/fix-admin-menu-meta.sql` 可以恢复初始化菜单的 `title/icon/order` 等元数据。
- 旧 `component` 表迁移到 `admin_component` 时,执行 `sql/migrate-component-to-admin-component.sql`,脚本会把旧表重命名为备份表。 - 旧 `component` 表迁移到 `admin_component` 时,执行 `sql/migrate-component-to-admin-component.sql`,脚本会把旧表重命名为备份表。
- 如果旧版本曾写入 `admin_user.id=0`,先执行 `sql/fix-admin-user-zero-id.sql` 修复脏数据,再重启服务。 - 如果旧版本曾写入 `admin_user.id=0`,先执行 `sql/fix-admin-user-zero-id.sql` 修复脏数据,再重启服务。
- Admin 与 Component 业务接口统一走 `JwtAuthGuard`;登录、刷新 token、退出登录和部分示例状态测试接口通过 `@Public()` 放行。 - Admin、Component、Dict 与 MinIO 业务接口统一走 `JwtAuthGuard`;登录、刷新 token、退出登录和部分示例状态测试接口通过 `@Public()` 放行。
- `kt-template-admin` 登录会写入 access token 与刷新 token cookie`kt-template-online-web` 和 `kt-template-online-playground` 可在回跳后通过刷新 token 重新持久化登录态。 - `kt-template-admin` 登录会写入 access token 与刷新 token cookie`kt-template-online-web` 和 `kt-template-online-playground` 可在回跳后通过刷新 token 重新持久化登录态。
- `kt-template-admin` 开发环境通过 `/api` 代理到本服务 `48085`,已关闭 Vben Nitro Mock。 - `kt-template-admin` 开发环境通过 `/api` 代理到本服务 `48085`,已关闭 Vben Nitro Mock。
- `POST /component/save` 新增组件,`POST /component/update` 编辑组件。 - `POST /component/save` 新增组件,`POST /component/update` 编辑组件。
@ -114,8 +114,8 @@ pnpm test:e2e # e2e 测试
## 联调关系 ## 联调关系
- `kt-template-online-web` 读取 `/component/list`、`/component/detail`、`/dict/*` 展示组件列表,并生成 Playground 跳转链接;组件接口返回 `401` 时跳转到 `kt-template-admin` 登录。 - `kt-template-online-web` 读取 `/component/list`、`/component/detail`、`/dict/*` 展示组件列表,并生成 Playground 跳转链接;业务接口返回 `401` 时跳转到 `kt-template-admin` 登录。
- `kt-template-online-playground` 读取 `/dict/*` 初始化分类,保存时上传截图到 `/minio/upload`,再调用 `/component/save``/component/update`组件接口返回 `401` 时跳转到 `kt-template-admin` 登录并在回跳后刷新 token。 - `kt-template-online-playground` 读取 `/dict/*` 初始化分类,保存时上传截图到 `/minio/upload`,再调用 `/component/save``/component/update`业务接口返回 `401` 时跳转到 `kt-template-admin` 登录并在回跳后刷新 token。
- 前端项目通过 Vite 代理把 `/api` 转发到 `http://localhost:48085/` - 前端项目通过 Vite 代理把 `/api` 转发到 `http://localhost:48085/`
## 轻量验证 ## 轻量验证

View File

@ -1,9 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuthGuardModule } from './auth/admin-auth-guard.module';
import { AdminAuthController } from './auth/admin-auth.controller'; import { AdminAuthController } from './auth/admin-auth.controller';
import { AdminAuthService } from './auth/admin-auth.service';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { AdminTokenService } from './auth/admin-token.service';
import { ComponentController } from './component/component.controller'; import { ComponentController } from './component/component.controller';
import { Component } from './component/component.entity'; import { Component } from './component/component.entity';
import { ComponentService } from './component/component.service'; import { ComponentService } from './component/component.service';
@ -35,6 +33,7 @@ import { MinioClientModule } from '@/minio/minio.module';
AdminDept, AdminDept,
Component, Component,
]), ]),
AdminAuthGuardModule,
DictModule, DictModule,
MinioClientModule, MinioClientModule,
], ],
@ -49,15 +48,12 @@ import { MinioClientModule } from '@/minio/minio.module';
AdminExampleController, AdminExampleController,
], ],
providers: [ providers: [
AdminAuthService,
ComponentService, ComponentService,
AdminDeptService, AdminDeptService,
AdminMenuService, AdminMenuService,
AdminRoleService, AdminRoleService,
AdminTimezoneService, AdminTimezoneService,
AdminTokenService,
AdminUserService, AdminUserService,
JwtAuthGuard,
ToolsService, ToolsService,
], ],
}) })

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminUser } from '../user/admin-user.entity';
import { AdminAuthService } from './admin-auth.service';
import { AdminTokenService } from './admin-token.service';
import { JwtAuthGuard } from './jwt-auth.guard';
@Module({
imports: [ConfigModule, TypeOrmModule.forFeature([AdminUser])],
providers: [AdminAuthService, AdminTokenService, JwtAuthGuard],
exports: [AdminAuthService, AdminTokenService, JwtAuthGuard],
})
export class AdminAuthGuardModule {}

View File

@ -5,11 +5,13 @@ import {
ParseIntPipe, ParseIntPipe,
Query, Query,
Res, Res,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { DictService } from './dict.service'; import { DictService } from './dict.service';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { ApiArrayResponse, ToolsService } from '@/common'; import { ApiArrayResponse, ToolsService } from '@/common';
import { DictDto } from './dict.dto'; import { DictDto } from './dict.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
const componentTypeDictExample = [ const componentTypeDictExample = [
{ {
@ -35,6 +37,7 @@ const chartDictExample = [
@ApiTags('dict') @ApiTags('dict')
@Controller('dict') @Controller('dict')
@UseGuards(JwtAuthGuard)
export class DictController { export class DictController {
constructor( constructor(
private readonly toolsService: ToolsService, private readonly toolsService: ToolsService,

View File

@ -1,12 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuthGuardModule } from '../auth/admin-auth-guard.module';
import { DictController } from './dict.controller'; import { DictController } from './dict.controller';
import { DictService } from './dict.service'; import { DictService } from './dict.service';
import { ToolsService } from '@/common'; import { ToolsService } from '@/common';
import { AdminDict } from './admin-dict.entity'; import { AdminDict } from './admin-dict.entity';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([AdminDict])], imports: [AdminAuthGuardModule, TypeOrmModule.forFeature([AdminDict])],
controllers: [DictController], controllers: [DictController],
providers: [DictService, ToolsService], providers: [DictService, ToolsService],
exports: [DictService], exports: [DictService],

View File

@ -9,6 +9,7 @@ import {
Query, Query,
Res, Res,
UploadedFile, UploadedFile,
UseGuards,
UseInterceptors, UseInterceptors,
} from '@nestjs/common'; } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
@ -34,6 +35,7 @@ import {
MinioObjectDto, MinioObjectDto,
MinioUploadResultDto, MinioUploadResultDto,
} from './minio.dto'; } from './minio.dto';
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
const PROXY_RESOURCE_TIMEOUT = 1000 * 15; const PROXY_RESOURCE_TIMEOUT = 1000 * 15;
const PROXY_RESOURCE_CONTENT_TYPES = [ const PROXY_RESOURCE_CONTENT_TYPES = [
@ -49,6 +51,7 @@ const PROXY_RESOURCE_EXTENSION_RE =
@Controller('minio') @Controller('minio')
@ApiTags('minio') @ApiTags('minio')
@UseGuards(JwtAuthGuard)
export class MinioClientController { export class MinioClientController {
constructor( constructor(
private readonly toolsService: ToolsService, private readonly toolsService: ToolsService,

View File

@ -1,11 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
import { MinioClientController } from './minio.controller'; import { MinioClientController } from './minio.controller';
import { MinioClientService } from './minio.service'; import { MinioClientService } from './minio.service';
import { ToolsService } from '@/common'; import { ToolsService } from '@/common';
@Module({ @Module({
imports: [ConfigModule], imports: [AdminAuthGuardModule, ConfigModule],
controllers: [MinioClientController], controllers: [MinioClientController],
providers: [MinioClientService, ToolsService], providers: [MinioClientService, ToolsService],
exports: [MinioClientService], exports: [MinioClientService],

View File

@ -1,4 +1,4 @@
import { INestApplication } from '@nestjs/common'; import { HttpException, HttpStatus, INestApplication } from '@nestjs/common';
import { APP_INTERCEPTOR, Reflector } from '@nestjs/core'; import { APP_INTERCEPTOR, Reflector } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import request = require('supertest'); import request = require('supertest');
@ -76,6 +76,17 @@ const authServiceMock = {
currentUser: jest.fn(), currentUser: jest.fn(),
}; };
const unauthorizedException = () =>
new HttpException(
{
code: -1,
data: null,
error: 'Unauthorized Exception',
message: 'Unauthorized Exception',
},
HttpStatus.UNAUTHORIZED,
);
const dictServiceMock = { const dictServiceMock = {
getDictByKey: jest.fn(), getDictByKey: jest.fn(),
getComponentDictByType: jest.fn(), getComponentDictByType: jest.fn(),
@ -518,6 +529,10 @@ describe('KT Template Online API (e2e)', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
authServiceMock.currentUser.mockResolvedValue({
id: '2041739550026043001',
username: 'admin',
});
}); });
afterAll(async () => { afterAll(async () => {
@ -560,4 +575,22 @@ describe('KT Template Online API (e2e)', () => {
data: false, data: false,
}); });
}); });
it('protects dict and minio endpoints with jwt auth', async () => {
authServiceMock.currentUser.mockRejectedValue(unauthorizedException());
await request(app.getHttpServer())
.get('/dict/getDictByKey')
.query({ dictKey: 'COMPONENT_TYPE' })
.expect(401);
expect(dictServiceMock.getDictByKey).not.toHaveBeenCalled();
jest.clearAllMocks();
authServiceMock.currentUser.mockRejectedValue(unauthorizedException());
await request(app.getHttpServer()).get('/minio/check').expect(401);
expect(minioServiceMock.checkConnection).not.toHaveBeenCalled();
});
}); });