基于前端视角的web服务端搭建(一)

440 阅读17分钟

导语

本文是将如何搭建一个功能全面的web服务端作为目的,通过列出一个完整服务端所需功能,针对这些功能一步步思考并搭建的过程。其中很多想法也是来自工作中与后端打交道的经验。

本文不是手把手教写所有代码,很多知识点更推荐大家翻阅官网文档(例如sql语法,nest语法等等)。本文是重在以前端视角探索后端,给大家一个后端开发的启蒙(前端不是切图仔,后端也不是sql boy)

如果大家发现哪些功能有遗漏,或者有更好的方案,也可以提出来讨论~

本文会长期更新

本文代码仓库地址 github.com/wangziweng7…

功能列表

  • 合理的目录结构规范、命名规范
  • 接口请求处理
    • 处理resful请求(getpostputdelete等)
    • 请求参数处理(url queryurl paramsform-urlencodejson等)
    • 参数校验和转换
  • 接口响应处理
    • 统一的返回响应格式
    • 状态码规范
    • 流式数据
  • 结构化数据存储(sqlitemysqlprismaortypeorm
    • orm操作
    • 数据迁移
    • 实体关系图
  • 对象存储服务(阿里oss
    • 文件安全
    • 分片
    • cdn加速
    • 版本控制
    • 图片处理
  • 日志
    • 记录和监视所有进出服务器 的请求,包括请求的来源、时间、响应数据等。用于故障排除、性能调优和安全审计等目的(winston, es)
    • 过滤敏感数据
  • 性能
  • api文档生成(swagger、apifox)
  • 消息队列(rabbit、Kafka)
  • 定时任务
  • 负载均衡(kong, apisix, nginx)
  • 远程调用( httpgrpctcp
  • 配置中心
    • ETCD
  • 监控
  • 会话管理
    • 能够管理用户与服务器的会话状态(jwtoauth2
  • 权限管理
    • 根据用户权限来进行有限的操作

一.目录规范,命名规范

因为技术选型是基于nestjs,所以首先是在github上面找开源的多星的项目,以他们的架构为基础,搭建自己的项目。

参考项目 github.com/CatsMiaow/n…

1.目录结构

+-- bin // 自定义脚本
+-- dist // 打包生成目录
+-- public // 静态文件
+-- src
|   +-- config // 环境变量
|   +-- base // 公共业务
|   +-- common // 公共与业务无关
|   |   +-- constants // 常量 和 Enum
|   |   +-- controllers // Nest Controllers
|   |   +-- decorators // Nest Decorators
|   |   +-- dto // DTO (Data Transfer Object) Schema, Validation
|   |   +-- filters // Nest Filters
|   |   +-- guards // Nest Guards
|   |   +-- interceptors // Nest Interceptors
|   |   +-- interfaces // TypeScript Interfaces
|   |   +-- middleware // Nest Middleware
|   |   +-- pipes // Nest Pipes
|   |   +-- providers // Nest Providers
|   |   +-- * // models, repositories, services...
|   +-- shared // Shared Nest Modules
|   +-- * // 其它
+-- test // Jest testing
+-- typings // Modules and global type 定义
// Module structure
//业务代码如下
+-- src/greeter
|   +-- * // folders
|   +-- greeter.constant.ts
|   +-- greeter.controller.ts
|   +-- greeter.service.ts
|   +-- greeter.module.ts
|   +-- greeter.*.ts

2.文件命名&类命名

export class PascalCaseSuffix {} //= pascal-case.suffix.ts
// Except for suffix, PascalCase to hyphen-case
class FooBarNaming {} //= foo-bar.naming.ts
class FooController {} //= foo.controller.ts
class BarQueryDto {} //= bar-query.dto.ts

3.Interface 命名

interface User {}
interface CustomeUser extends User {}
interface ThirdCustomeUser extends CustomeUser {}

4.新建项目

确定结构之后,我们接下来开始新建一个nest项目

1.安装nestjs并创建我们的项目

npm i -g @nestjs/cli
nest new project-name
pnpm install

2.运行

npm run start:dev

二.接口请求处理

这一节我们将完成一个restful(getpostputdelete)类型接口,同时支持请求参数的获取(url queryurl paramsform-urlencodejson)与校验

主要内容对照官网文档即可控制器 | NestJS 中文网 (nodejs.cn)

用户注册,登录,登出,修改密码作为场景

1.使用命令创建模块相关

nest g resource base/auth

生成以下代码 image.png

这里我们主要用到三个文件

  • controller 分发路由,验证请求参数
  • service 具体的业务逻辑处理
  • dto 参数类型,用来实例化参数类和验证参数

其它几个文件,我们在数据库的时候再用

2.参数校验还需要下面的依赖

pnpm i class-transformer class-validator

3.我们先编写controller来处理请求

auth.controller.ts

import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { AuthService } from './auth.service';
import { CreateAuthDto, UpdateAuthDto } from './dto/auth.dto';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) { }

  // 注册
  // @Body同时能处理form-urlencode和json格式
  @Post('register')
  register(@Body() createAuthDto: CreateAuthDto) {
    return this.authService.register(createAuthDto);
  }
  
  // 登录 
  @Post('login')
  login(@Body() createAuthDto: CreateAuthDto) {
    return this.authService.login(createAuthDto);
  }

  //登出
  @Post('logout')
  logout(@Body() createAuthDto: CreateAuthDto) {
    return this.authService.logout(createAuthDto);
  }

  // 获取用户信息
  @Get('getUser')
  findOne(@Query('id') id: string) {
    return this.authService.findOne(+id);
  }

  // 修改用户信息
  @Patch(':id')
  update(@Param('id') id: string, @Body() updateAuthDto: UpdateAuthDto) {
    return this.authService.update(+id, updateAuthDto);
  }
}

4.dto来声明参数类型和校验

auth.dto.ts

import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';
export class CreateAuthDto {
    @IsNotEmpty()
    @MinLength(3)
    @MaxLength(20)
    username: string;

    @IsNotEmpty()
    @MinLength(6)
    @MaxLength(20)
    password: string;
}

export class UpdateAuthDto { 
    @IsNotEmpty()
    @MinLength(3)
    @MaxLength(20)
    password: string;
}

5.同时还需要在全局开启参数类转换为实例

main.ts

import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({
    transform: true, // 转换属性,
    whitelist: true, // 去除未声明的属性
  }
  ));
  await app.listen(3000);
}

下面用postman发请求验证

参数错误时,返回了相关错误提示 image.png

参数正确时,返回响应

image.png

小结

  • 用nest自带的Get、Post、Patch等装饰器处理不同类型的请求
  • 用Query、Body、Param装饰器去接收了不同的请求参数,
  • 用ValidationPipe、class-validator、class-transformer做了参数的自动转换和校验

nest还有很多其它请求相关装饰器,需要的时候在去查文档就行

二.接口响应处理

从上面POSTMAN请求返回的响应结果可以看到,我们的数据结构并不统一,这样前端会很难判断什么时候成功什么时候失败什么时候需要重新登录请求超时从哪里拿返回数据

image.png

所以统一的返回响应格式,加状态码区分状态对我们来说非常重要

1.首先定义数据结构

请求正确时

import { HttpStatus, StreamableFile } from '@nestjs/common';

// 普通返回类型
type NormalDataType = {
    message: string
    statusCode: HttpStatus
    data?: string | number | Array<any> | Record<string, any>
}
// 流式返回类型
type StreamType = StreamableFile

// 同理我们还可以加上graphql等类型,然后针对不同类型定义响应结果
// 具体代码略,后面需要时在加上来

type ResponseType = NormalDataType | StreamableFile

请求错误时

type ErrorResponseType = {
    message: Array<string>
    error?: string
    statusCode: HttpStatus
}

2.接着新建拦截器处理正确请求

nest g interceptor common/interceptor/response-format
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, HttpStatus } from '@nestjs/common';
import { Observable, map } from 'rxjs';

// 普通返回类型
export type NormalDataType = {
    message: string
    statusCode: HttpStatus
    data?: string | number | Array<any> | Record<string, any>
}
// 流式返回类型
export type StreamType = StreamableFile

// 同理我们还可以加上graphql等类型,然后针对不同类型定义响应结果
// 具体代码略,后面需要时在加上来

export type ResponseType = NormalDataType | StreamableFile

@Injectable()
export class ResponseFormatInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<ResponseType> {
    return next.handle().pipe(map(data => {
      // 流式就直接返回
      if (data instanceof StreamableFile) return data
      return {
        statusCode: HttpStatus.OK,
        message: 'success',
        data
      }
    }))
  }
}

3.在app.module.ts通过APP_INTERCEPTOR全局使用该拦截器

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ResponseFormatInterceptor } from './common/interceptor/response-format/response-format.interceptor';
// 略
@Module({
  // 略
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: ResponseFormatInterceptor,
    }
    // 略
  ],
})
export class AppModule {}

最终验证

1.正常请求格式验证通过

image.png

2.请求错误验证

让业务代码手动抛错

import { HttpException, HttpStatus } from '@nestjs/common';
// 略
  @Post('register')
  register(@Body() createAuthDto: CreateAuthDto) {
    throw new HttpException('没有权限', HttpStatus.FORBIDDEN);
  }

错误请求 验证成功(请求错误nestjs自动处理了格式,不需要我们处理)

image.png

3.构造验证流式数据

import { createReadStream } from 'fs';
import { join } from 'path';
import { StreamableFile } from '@nestjs/common';

  @Get('file')  
  getFile(): StreamableFile {
    const file = createReadStream(join(process.cwd(), 'src/main.ts'));
    return new StreamableFile(file);
  }

流式数据验证通过

image.png

小结

  • 通过APP_INTERCEPTOR注册全局拦截器实现了响应对象的格式化
  • 通过内置httpstatus枚举与HttpException来返回不同类型的错误或者格式
  • 通过StreamableFile来实现了流式响应
  • 对于graphql等其它响应我们后面在详细说明

三.结构化数据存储

在上面的例子中,我们注册的用户并没有存储,会反复注册。所以我们需要一个数据库来保存用户。

数据库选型

数据库的选型很多,下面以sqlite为例方便演示。

sqlite和其它数据库它们的语法在很大程度上是相同的,所以用它来练习非常方便,它不用去搭数据库服务,因为sqlite是以文件为库,存储在本地,非常轻量级。

(如果你想接入其它数据库,看文档就行数据库 | NestJS 中文网 (nodejs.cn)

为什么用orm框架

大部分后端服务都不会直接写sql,因为很容易发生sql注入等安全问题,基本都是用orm框架,以对象形式来操作sql。

数据库ORM也有很多,这里笔者分别使用了typeormprisma。 下面也将体验两者的优缺点。

为什么要用ORM的数据迁移来生成sql

  1. 手动写容易写错
  2. 非敏捷迭代下,一个项目周期可能是几个月,这几个月我们陆陆续续执行了很多表结构的修改,如果没有一个统一的地方保存这些结构sql,最后很容易发生测试和生产环境数据表结构不一致问题(某某的sql又忘记执行了)

用orm框架来生成迁移sql并保存,最后统一放到sql审计平台执行能减少流程上的错误

为什么数据库表ER图很重要

清晰方便的查看表关系

image.png

所以综上来说,ORM框架对我们来说非常重要,在项目启动阶段的选型会影响我们后续的开发体验

typeorm

1.首先安装所需依赖

pnpm install  @nestjs/typeorm typeorm sqlite3

2.将 TypeOrmModule 导入到根 AppModule 中

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'db/database.sqlite', // 指定数据库地址
      entities: [],
      busyErrorRetry: 5,
      enableWAL: true,
      autoLoadEntities: true,
      synchronize: true, // 自动根据entities建表. true in development, false in production,
      logging: true, // 打印sql语句. true in development, false in production,
    }),
  ],
})
export class AppModule {}

3.新建数据库实体

这时我们用上之前没用到的auth.entity.ts文件,在里面新建一个类。当我们在第二步的配置里开启了synchronize后,nest启动时便会根据实体类为我们自动建表。(生产环境一般是在sql审计平台执行sql,这里只是为了开发方便才开启)

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('varchar', { nullable: false, length: 255 })
  username: string;

  @Column('varchar', { nullable: false, length: 255 })
  password: string;

  @Column('datetime', { nullable: false, transformer: { to: () => new Date().getTime(), from: (val) => val } })
  updated_at: string;

  @Column('datetime', { nullable: false, transformer: { to: (val) => val ?? new Date().getTime(), from: (val) => val } })
  created_at: string;
}

4.在auth.modules.ts 中注册该实体

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/auth.entity';
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

5.最后我们修改我们的auth.service.ts

通过userRepository来操作数据库user表

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateAuthDto } from './dto/auth.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/auth.entity';
@Injectable()
export class AuthService {
  @InjectRepository(User)
  private readonly userRepository: Repository<User>

  async register(createAuthDto: CreateAuthDto) {
    const hasUser = await this.userRepository.existsBy({
      username: createAuthDto.username,
    })
    if (hasUser) {
      throw new HttpException('用户名已存在', HttpStatus.BAD_REQUEST)
    }
    await this.userRepository.save(createAuthDto)
    return '创建成功'
  }
}

6.我们使用postman来发请求和NavicatforSqlite来可视化查看数据库

www.navicat.com.cn/download/di… 下载地址

image.png

同时查看数据库成功带保存了我们的用户信息

image.png

我们在重新请求一下,发现重复注册失败,因为已有该用户名

image.png

迁移功能

过程略,直接说结论了

Typeorm使用对比本地实体与数据库的差异生成迁移sql,这点对我来说觉得很不友好

  • 操作麻烦
  • 一般生产环境不会给你账号密码,所以不会对比生产。
  • 用测试环境去生成时,可能多人并行开发的情况下,帮别人的也生成了,别人又帮你的部分生成了,有心智负担。也可能测试环境某人手动操作了,导致有污染。

小结

  • 通过TypeOrmModule.forRoot建立数据库链接
  • 通过entity装饰器修饰的实体类建表,并TypeOrmModule.forFeature实例化该表对象方便操作
  • 通过InjectRepository装饰器在我们服务中拿到了该类实例操作数据

TypeOrmModule.forRoot可以注册多次,建立多个数据库的链接,但是需要为每个数据源建立name字段。通过TypeOrmModule.forFeature([实体类], 'name')实例化, InjectRepository(实体类, 'name')来使用。和单体数据源使用几乎没啥区别。下面就不演示了

优点:

  • 用类的写法定义表,可以很方便的处理数据的转换和生成默认数据,例如我们自动生成创建时间和更新时间,而不用业务代码去处理
  • 代码提示友好

缺点:

  • 写法麻烦,使用一个表要要在两个文件导四次包。
  • 数据库迁移麻烦

prisma

下面我们演示prisma

1.安装&初始化

pnpm install prisma nestjs-prisma @prisma/client

接着vscode商店搜索prisma语法提示及代码高亮插件

然后初始化文件

npx prisma init

此命令创建一个新的 prisma 目录,其中包含以下内容:

  • schema.prisma:指定你的数据库连接并包含数据库架构
  • .envdotenv 文件,通常用于将你的数据库凭据存储在一组环境变量中

2.设置数据库连接 schema.prisma

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
  relationMode = "prisma"
}

generator client {
  provider = "prisma-client-js"
}

其中relationMode = "prisma" 代表生成sql时并不会生成真正的外键关系,这个在生成表关系图时很有用

.env

DATABASE_URL="file:./db/database.sqlite"

使用.env文件我们需要依赖用ConfigModule

pnpm i @nestjs/config

在app.module.ts中引入

import { ConfigModule } from '@nestjs/config';
@Module({
  imports: [ConfigModule.forRoot()],
})

3.在schema.prisma新建数据表

model User {
  id        String   @id @default(uuid())
  username  String   @unique
  password  String   
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  posts     Post[]
}

model Post {
  id        Int      @default(autoincrement()) @id
  title     String
  content   String?
  published Boolean? @default(false)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  String?
  @@index([authorId])
}

业务使用

1.在跟模块app.modules.ts引入

import { PrismaModule, loggingMiddleware } from 'nestjs-prisma';
import { Module, Logger } from '@nestjs/common';

imports: [
    ConfigModule.forRoot(),
    PrismaModule.forRoot({
      isGlobal: true,
      prismaServiceOptions: {
        middlewares: [
          // configure your prisma middleware
          loggingMiddleware({
            logger: new Logger('PrismaMiddleware'),
            logLevel: 'log',
          }),
        ],
      },
    })
  ],

2.在业务代码中使用

auth.service.ts

import { PrismaService } from 'nestjs-prisma';
@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService) {}
  async register(createAuthDto: CreateAuthDto) {
    const hasUser = await this.prisma.user.findUnique({
      where: { username: createAuthDto.username } 
    })
    if (hasUser) {
      throw new HttpException('用户名已存在', HttpStatus.BAD_REQUEST)
    }
    await this.prisma.user.create({ data: createAuthDto })
    return '创建成功'
  }
}

3.postman调用,然后查看数据库

image.png

image.png

迁移功能

1.执行命令

npx prisma migrate dev --name init

生成如下代码

image.png

2.重复执行命令

不会生成新的迁移sql,prisma会提示没有修改

image.png

3.新增字段在执行

当我们给Post表新增一个star字段在执行

image.png

npx prisma migrate dev --name add

生成了新的迁移sql

image.png

prisma的迁移功能是根据已有migrationmodel之间的差异来生成,比typeorm的对比方式更合理

实体关系图

1.首先安装所需依赖

pnpm i -D prisma-erd-generator @mermaid-js/mermaid-cli

2.然后执行生成命令

npx prisma generate

3.最后查看效果

image.png

小结

  • 优点:使用非常方便,代码提示也很友好,文档全面,功能也非常多
  • 缺点:没有发现有typeorm那样的transformer来处理表字段的转换,只能使用内置关键字

所以综上,笔者更推荐prisma来代替typeorm

四.文件存储服务

文件存储与文件下载是web服务端常见的需求,一般我们不会用本地服务器来存储文件,而是会把文件上传到专门的OSS (Object Storage Service)对象存储服务,比起存到自己服务器,它主要有以下几个特点。

  • 方便以后的资源扩展
  • CDN加速访问
  • 定时备份
  • 文件的版本控制
  • 安全性
  • ...

其中安全性是需要我们特别关注的。安全性需要我们和云服务共同维护。

那么我们自己该怎么做?

其实阿里给出了解决方案

image.png

image.png

概况总结如下

  • 通过创建子用户,对子用户授权
    • 如果子用户泄露,可以立马禁用子用户减少损失。
  • 子用户在服务端对客户端授权
    • 对于前端层面,使用临时凭证来上传,防止前端泄漏,造成数据安全。

下面我们以阿里OSS为例进行操作,进行实战

  • 开通阿里OSS服务
  • 创建2个Bucket
    • 一个用于用户数据(安全要求高,每次读需要鉴权)
    • 一个用于网站静态数据(无安全要求,读不需要鉴权)
  • 代码相关

开通服务

首先我们进入阿里云官网进行OSS服务购买 阿里云-搜索推荐页 (aliyun.com)

1.我们先购买半年OSS存储服务(5元半年40GB)

image.png

  1. 接着点击管理控制台 image.png

3.接着点击立即开通

image.png

4.接着点击立即购买

image.png

5.完成购买后,我们点击对象存储OSS管理控制台

image.png

创建bucket,以及安全配置

6.进入Bucket列表新建一个bucket

image.png

  1. 选择快捷创建(默认所有文件操作需要进行鉴权),我们只需要填写bucket名称选择地域即可(随便填无所谓)

image.png

8.创建角色&授权

image.png

总共三个权限如下

image.png

image.png

9.创建用户&授权

image.png

image.png

image.png

10.全部配置好后,我们在确定文件上传方式,选择客户端直传服务端生产STS临时访问凭证

image.png

image.png

image.png

  1. 拿到我们所需要的所有参数
interface OssType {
  accessKeyId: string
  accessKeySecret: string
  roleArn: string
  bucket: string
  region: string
}

其中bucketregionbucket域名里面拿,以.分割,第一个是bucket"my-bucket-wzw",第二个是region: "oss-cn-shenzhen"

image.png

roleArn 从角色里面拿

image.png

accessKeyIdaccessKeySecret从用户里拿

image.png

代码

配置都准备好后,可以开始进行代码编辑

1.安装相关依赖

pnpm i ali-oss @types/ali-oss @nestjs/schedule

2.准备配置文件

src\config\configuration.ts

export default () => ({
    oss: {
        accessKeyId: 'xxxx', // 参数需要替换为你自己的oss配置
        accessKeySecret: 'xxxx',
        roleArn: 'xxxxx',
        bucket: 'xxxx',
        region: 'xxxxx'
    }
  });

3. app.modules.ts进行全局注册配置

同时引入定时模块,方便获取临时凭证

import { ConfigModule } from '@nestjs/config';
import configuration from './config/configuration';
import { ScheduleModule } from '@nestjs/schedule';

imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [configuration], // 会和.env进行合并
    }),
    ScheduleModule.forRoot(), // 定时任务
]

4.创建我们的oss模块

nest g resource base/oss

这里我们只需要重点关注service和controller

oss.controller.ts

import { Controller, Get, Query } from '@nestjs/common';
import { OssService } from './oss.service';

@Controller('oss')
export class OssController {
  constructor(private readonly ossService: OssService) {}

  @Get('sts_token')
  getStsToken() {
    return this.ossService.getStsToken();
  }
  @Get('public_url')
  getPublicUrl(@Query('url') url: string) {
    return this.ossService.getPublicUrl(url);
  }
  @Get('process_url')
  getProcessUrl(@Query('url') url: string, @Query('process') process: string) {
    return this.ossService.getProcessUrl(url, process);
  }
  @Get('upload')
  upload() {
    return this.ossService.multipartUpload();
  }
}

  

oss.service.ts

import { Injectable, Inject, OnModuleInit, HttpException, HttpStatus } from '@nestjs/common';
import * as OSS from 'ali-oss';
import { STS } from 'ali-oss';
import { Interval } from '@nestjs/schedule';
import { ConfigService } from '@nestjs/config';
import * as path from "path";
type ReturnCredentials = {
  accessKeyId: string
  accessKeySecret: string,
  stsToken: string,
  region: string,
  bucket: string
}

type OssModuleOptions = {
  accessKeyId: string
  accessKeySecret: string
  roleArn: string
  bucket: string
  region: string
}

@Injectable()
export class OssService implements OnModuleInit {
  @Inject(ConfigService)
  private readonly ConfigService: ConfigService
  private Client: OSS
  private StsClient: STS
  private ossCredentials: ReturnCredentials

  async onModuleInit() {
    const options = this.ConfigService.get<OssModuleOptions>('oss')
    this.StsClient = new STS({
      accessKeyId: options.accessKeyId,
      accessKeySecret: options.accessKeySecret
    })
    this.Client = new OSS({
      region: options.region,
      accessKeyId: options.accessKeyId,
      accessKeySecret: options.accessKeySecret,
      bucket: options.bucket,
    })
    await this.fetchCredentials()
  }

  // assumeRole方法阿里有并发限制,因此用定时任务来定时获取
  @Interval(1000 * 1400)
  private async fetchCredentials() {
    const options = this.ConfigService.get<OssModuleOptions>('oss')
    // roleArn填写步骤2获取的角色ARN,例如acs:ram::175708322470****:role/ramtest。
    // policy填写自定义权限策略,用于进一步限制STS临时访问凭证的权限。如果不指定Policy,则返回的STS临时访问凭证默认拥有指定角色的所有权限。
    // 3000为过期时间,单位为秒。
    // sessionName用于自定义角色会话名称,用来区分不同的令牌,例如填写为sessiontest。
    const result = await this.StsClient.assumeRole(options.roleArn, ``, 3000, 'sessiontest')
    this.ossCredentials = {
      accessKeyId: result.credentials.AccessKeyId,
      accessKeySecret: result.credentials.AccessKeySecret,
      stsToken: result.credentials.SecurityToken,
      region: options.region,
      bucket: options.bucket,
    }
  }

  /**
   * 获取临时凭证
   */
  async getStsToken() {
    if (this.ossCredentials) return this.ossCredentials
    await this.fetchCredentials()
    return this.ossCredentials
  }

  /**
   * 获取可访问链接
  */
  async getPublicUrl(ossPath: string) {
    const signUrl = this.Client.signatureUrl(ossPath, {
      expires: 24 * 60 * 60,
    });
    return signUrl
  }

  /**
   * 图片处理
   */
  async getProcessUrl(ossPath: string, process: string) {
    const signUrl = this.Client.signatureUrl(ossPath, {
      process,
      expires: 24 * 60 * 60,
    });
    return signUrl
  }

  // 图片上传示例
  async multipartUpload() {
    try {
      const result = await this.Client.multipartUpload('exampledir/package.json',
        path.normalize(path.join(process.cwd(), 'package.json')),
        {});
      console.log(result);
    } catch (e) {
      // 捕获超时异常。
      if (e.code === 'ConnectionTimeoutError') {
        console.log('TimeoutError');
        // do ConnectionTimeoutError operation
      }
      console.log(e);
      
      throw new HttpException(e.message, HttpStatus.REQUEST_TIMEOUT)

    }
  }
}

验证

最后我们就演示一个接口获取凭证

在postman调用一下,成功返回客户端所需参数

image.png

小结

oss功能非常强大,具体还是需要大家看文档了解。这个也是一般公司用哪个,我们就学哪个

日志篇已更新 juejin.cn/spost/74070…