Vue3 + Nest 实现权限管理系统 后端篇(三):基于RBAC 权限控制实现

简介: RBAC(Role Based Access Control)是基于角色的权限控制,简单来说就是给用户赋予一些角色,那么该用户就会拥有这些角色的所有权限。接下来我们就用 NestJS 来实现基于 RBAC 的权限控制

RBAC简介

RBAC(Role Based Access Control)是基于角色的权限控制,简单来说就是给用户赋予一些角色,那么该用户就会拥有这些角色的所有权限。接下来我们就用 NestJS 来实现基于 RBAC 的权限控制

创建表

除了我们已有的 user 表,我们还需创建角色表(role),权限字段表(permission)。可以直接用命令新增两个模块

nest g res role
nest g res permission

进入权限 permission 模块的 entity 中编写 permission 表的字段

//permission.entity.ts
import {
   
   
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity()
export class Permission {
   
   
  @PrimaryGeneratedColumn()
  id: string;

  @Column({
   
   
    length: 50,
  })
  name: string;

  @Column({
   
   
    length: 100,
    nullable: true,
  })
  desc: string;

  @CreateDateColumn()
  createTime: Date;

  @UpdateDateColumn()
  updateTime: Date;
}

一个权限可以赋给多个角色,同时一个角色也可以有多个权限,因此它们是多对多(ManyToMany)的关系,所以我们可以用role_permission_relation关系表将它们关联起来,角色(role)模块下的 entity 如下

//role.entity.ts
import {
   
   
  Column,
  CreateDateColumn,
  Entity,
  JoinTable,
  ManyToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';
import {
   
    Permission } from '../../permission/entities/permission.entity';
@Entity()
export class Role {
   
   
  @PrimaryGeneratedColumn()
  id: string;

  @Column({
   
   
    length: 20,
  })
  name: string;

  @CreateDateColumn()
  createTime: Date;

  @UpdateDateColumn()
  updateTime: Date;

  @ManyToMany(() => Permission)
  @JoinTable({
   
   
    name: 'role_permission_relation',
  })
  permissions: Permission[];
}

同样的用户(user)与角色(role)直接也是多对多的关系,因此 user 的 entity 修改如下

import {
   
   
  Column,
  Entity,
  PrimaryGeneratedColumn,
  BeforeInsert,
  ManyToMany,
  JoinTable,
} from 'typeorm';
import encry from '../../utils/crypto';
import * as crypto from 'crypto';
import {
   
    Role } from '../../role/entities/role.entity';
@Entity('user')
export class User {
   
   
  @PrimaryGeneratedColumn()
  id: string; // 标记为主键,值自动生成

  @Column({
   
    length: 30 })
  username: string; //用户名
  @Column({
   
    nullable: true })
  nickname: string; //昵称
  @Column()
  password: string; //密码
  @Column({
   
    nullable: true })
  avatar: string; //头像
  @Column({
   
    nullable: true })
  email: string; //邮箱

  @ManyToMany(() => Role)
  @JoinTable({
   
   
    name: 'user_role_relation',
  })
  roles: Role[]; //角色

  @Column({
   
    nullable: true })
  salt: string;
  @Column({
   
    type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  create_time: Date;

  @Column({
   
    type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  update_time: Date;
  @BeforeInsert()
  beforeInsert() {
   
   
    this.salt = crypto.randomBytes(4).toString('base64');
    this.password = encry(this.password, this.salt);
  }
}

最后在各自的 module 导入实体,以 permission.module.ts 为例

import {
   
    Module } from '@nestjs/common';
import {
   
    PermissionService } from './permission.service';
import {
   
    PermissionController } from './permission.controller';
import {
   
    Permission } from './entities/permission.entity';
import {
   
    TypeOrmModule } from '@nestjs/typeorm';
@Module({
   
   
  controllers: [PermissionController],
  providers: [PermissionService],
  imports: [TypeOrmModule.forFeature([Permission])],
})
export class PermissionModule {
   
   }

启动项目,我们就会看到数据库中多了几张表

image.png

添加模拟数据

接下来给每个模块添加一个新增数据的接口用于添加数据来观察它们之间的关系

permission 模块

新增 permission 数据很简单,直接保存前端传来的的namedesc字段即可

//permission.service.ts
import {
   
    Injectable } from '@nestjs/common';
import {
   
    InjectRepository } from '@nestjs/typeorm';
import {
   
    ApiErrorCode } from 'src/common/enums/api-error-code.enum';
import {
   
    ApiException } from 'src/common/filter/http-exception/api.exception';
import {
   
    Repository } from 'typeorm';
import {
   
    CreatePermissionDto } from './dto/create-permission.dto';
import {
   
    Permission } from './entities/permission.entity';
@Injectable()
export class PermissionService {
   
   
  constructor(
    @InjectRepository(Permission)
    private permissionRepository: Repository<Permission>,
  ) {
   
   }
  async create(createPermissionDto: CreatePermissionDto) {
   
   
    const name = createPermissionDto.name;
    const existPermission = await this.permissionRepository.findOne({
   
   
      where: {
   
    name },
    });

    if (existPermission)
      throw new ApiException('权限字段已存在', ApiErrorCode.PERMISSSION_EXIST);
    return await this.permissionRepository.save(createPermissionDto);
  }
}
//create-user.dto.ts
export class CreatePermissionDto {
   
   
  name: string;
  desc: string;
}

然后我们可以调用http://localhost:3000/permission接口新增一些权限字段create,read,update,delete

image.png

新增完毕之后,permission 表如下

image.png

role 模块

新增 role 表中的数据和上面有所不同,因为它有一个关系字段permissions,因此需要导入 permission 实体,然后根据前端传来的权限字段 id(permissionIds)查询对应 permission 实体插入 role 表中

//role.module.ts
import {
   
    Module } from '@nestjs/common';
import {
   
    RoleService } from './role.service';
import {
   
    RoleController } from './role.controller';
import {
   
    Role } from './entities/role.entity';
import {
   
    Permission } from '../permission/entities/permission.entity';
import {
   
    TypeOrmModule } from '@nestjs/typeorm';
@Module({
   
   
  controllers: [RoleController],
  providers: [RoleService],
  imports: [TypeOrmModule.forFeature([Role, Permission])],
})
export class RoleModule {
   
   }
//create-role.dto.ts
export class CreateRoleDto {
   
   
  name: string;
  permissionIds: number[];
}
//role.service.ts
import {
   
    Injectable } from '@nestjs/common';
import {
   
    InjectRepository } from '@nestjs/typeorm';
import {
   
    In, Repository } from 'typeorm';
import {
   
    CreateRoleDto } from './dto/create-role.dto';
import {
   
    Role } from './entities/role.entity';
import {
   
    Permission } from '../permission/entities/permission.entity';
import {
   
    ApiException } from 'src/common/filter/http-exception/api.exception';
import {
   
    ApiErrorCode } from 'src/common/enums/api-error-code.enum';
@Injectable()
export class RoleService {
   
   
  constructor(
    @InjectRepository(Role)
    private roleRepository: Repository<Role>,
    @InjectRepository(Permission)
    private permissionRepository: Repository<Permission>,
  ) {
   
   }
  async create(createRoleDto: CreateRoleDto) {
   
   
    //查询传入数组permissionIds的全部permission实体
    const permissions = await this.permissionRepository.find({
   
   
      where: {
   
   
        id: In(createRoleDto.permissionIds),
      },
    });
    const name = createRoleDto.name;
    const existRole = await this.roleRepository.findOne({
   
   
      where: {
   
    name },
    });

    if (existRole)
      throw new ApiException('角色已存在', ApiErrorCode.ROLE_EXIST);
    return this.roleRepository.save({
   
    permissions, name });
  }
}

最后调用http://localhost:3000/role就能添加一些角色,分别添加超级管理员(create,read,update,delete),管理员(create,read,update),用户(read)

image.png

添加完成之后 role 表如下

image.png

同时我们可以查看role_permission_relation关系表来看一下它们的多对多关系

image.png

user 模块

user 模块和 role 模块逻辑差不多,需要查询 role 表,因此要引入 role 实体

//user.module.ts
import {
   
    Module } from '@nestjs/common';
import {
   
    UserService } from './user.service';
import {
   
    UserController } from './user.controller';
import {
   
    User } from './entities/user.entity';
import {
   
    Role } from '../role/entities/role.entity';
import {
   
    TypeOrmModule } from '@nestjs/typeorm';
import {
   
    JwtModule } from '@nestjs/jwt';
@Module({
   
   
  controllers: [UserController],
  providers: [UserService],
  imports: [
    TypeOrmModule.forFeature([User, Role]),
    JwtModule.register({
   
    secret: process.env.JWT_SECRET }),
  ],
  exports: [UserService],
})
export class UserModule {
   
   }

在 user.service.ts 写对应逻辑

import {
   
    HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
   
    CreateUserDto } from './dto/create-user.dto';
import {
   
    User } from './entities/user.entity';
import {
   
    Role } from '../role/entities/role.entity';
import {
   
    InjectRepository } from '@nestjs/typeorm';
import {
   
    In, Repository } from 'typeorm';
import {
   
    ApiException } from 'src/common/filter/http-exception/api.exception';
import {
   
    ApiErrorCode } from 'src/common/enums/api-error-code.enum';

@Injectable()
export class UserService {
   
   
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
    @InjectRepository(Role)
    private roleRepository: Repository<Role>,
  ) {
   
   }
  async create(createUserDto: CreateUserDto) {
   
   
    const {
   
    username, password, roleIds } = createUserDto;
    const existUser = await this.userRepository.findOne({
   
   
      where: {
   
    username },
    });

    if (existUser)
      throw new ApiException('用户已存在', ApiErrorCode.USER_EXIST);
    try {
   
   
      //查询数组roleIds对应所有role的实例
      const roles = await this.roleRepository.find({
   
   
        where: {
   
   
          id: In(roleIds),
        },
      });
      const newUser = await this.userRepository.create({
   
   
        username,
        password,
        roles,
      });
      await this.userRepository.save(newUser);
      return '注册成功';
    } catch (error) {
   
   
      throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }
  async findOne(username: string) {
   
   
    const user = await this.userRepository.findOne({
   
   
      where: {
   
    username },
    });

    if (!user)
      throw new ApiException('用户名不存在', ApiErrorCode.USER_NOTEXIST);
    return user;
  }
}

在 create-user.dto.ts 规定前端传来参数格式

//create-user.dto.ts
export class CreateUserDto {
   
   
  username: string;
  password: string;
  roleIds: number[];
}

创建两个用户分别为 admin 和 user1 分别赋予全部角色和用户角色

image.png

权限控制

我们可以通过自定义装饰器在调用接口之前传入一个权限数组,即必须拥有数组内的权限才能够调用此接口。所以可与创建一个全局的导航守卫,里面查询到该用户的所有权限然后与自定义装饰器传入的权限进行对比即可

现在 public 目录下新增一个@Permissions装饰器

//public.decorator.ts
import {
   
    SetMetadata } from '@nestjs/common';

export const Public = () => SetMetadata('isPublic', true);

export const Permissions = (...permissions: string[]) =>
  SetMetadata('permissions', permissions);

然后在 permission 模块下创建一个导航守卫

nest g guard permission --no-spec --flat

同样的,这个守卫需要设置为全局守卫

//permission.module.ts
import {
   
    Module } from '@nestjs/common';
import {
   
    PermissionService } from './permission.service';
import {
   
    PermissionController } from './permission.controller';
import {
   
    Permission } from './entities/permission.entity';
import {
   
    TypeOrmModule } from '@nestjs/typeorm';
import {
   
    APP_GUARD } from '@nestjs/core';
import {
   
    PermissionGuard } from '../permission/permission.guard';
@Module({
   
   
  controllers: [PermissionController],
  providers: [
    PermissionService,
    {
   
   
      provide: APP_GUARD,
      useClass: PermissionGuard,
    },
  ],
  imports: [TypeOrmModule.forFeature([Permission])],
})
export class PermissionModule {
   
   }

接下来我们开始写相关逻辑,在 user 中简单写一个测试接口,同时用我们刚写的自定义装饰器传入两个权限

//user.controller.ts
  @Post('test')
  @Permissions('create', 'read')
  test(@Body() testParams) {
   
   
    return this.userService.test(testParams);
  }

修改一下权限守卫,获取传入的元数据


//permission.guard.ts
import {
   
    CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import {
   
    Reflector } from '@nestjs/core';
import {
   
    Observable } from 'rxjs';

@Injectable()
export class PermissionGuard implements CanActivate {
   
   
  constructor(private reflector: Reflector) {
   
   }
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
   
   
    const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
      'permissions',
      [context.getClass(), context.getHandler()],
    );

    console.log(requiredPermissions);
    return true;
  }
}

在调用接口之前我们先把登录守卫修改一下,让其都放行,不然接口是调不通的,只需要修改auth.guard.ts让其都返回 true 即可

import {
   
   
  CanActivate,
  ExecutionContext,
  HttpException,
  HttpStatus,
  Injectable,
} from '@nestjs/common';
import {
   
    Request } from 'express';
import {
   
    JwtService } from '@nestjs/jwt';
import {
   
    ConfigService } from '@nestjs/config';
import {
   
    Reflector } from '@nestjs/core';
import {
   
    ApiErrorCode } from 'src/common/enums/api-error-code.enum';
import {
   
    ApiException } from 'src/common/filter/http-exception/api.exception';
@Injectable()
export class AuthGuard implements CanActivate {
   
   
  constructor(
    private jwtService: JwtService,
    private configService: ConfigService,
    private reflector: Reflector,
  ) {
   
   }
  async canActivate(context: ExecutionContext): Promise<boolean> {
   
   
    const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
      //即将调用的方法
      context.getHandler(),
      //controller类型
      context.getClass(),
    ]);



    //这里暂时都放行
    return true;



    if (isPublic) {
   
   
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) throw new ApiException('验证不通过', ApiErrorCode.FORBIDDEN);
    try {
   
   
      const payload = await this.jwtService.verifyAsync(token, {
   
   
        secret: this.configService.get('JWT_SECRET'),
      });
      request['user'] = payload;
    } catch {
   
   
      throw new ApiException(
        '登录状态已过期,请重新登录',
        ApiErrorCode.LOGIN_EXPIRE,
      );
    }

    return true;
  }
  private extractTokenFromHeader(request: Request): string | undefined {
   
   
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

调用 user/test 接口查看我们的打印结果

image.png

可以发现已经获取到装饰器传来的权限数组了,接下来我们要做的是查询用户的所有权限与之对比,看是否包含装饰器传来的数组,包含的话则放行否则抛出没有权限的错误给前端。

因为需要查询该用户的权限字段,所以在user.service.ts写一个查询权限字段名的方法,这里模拟查询admin用户的权限

//user.service.ts
  async findPermissionNames() {
   
   
    const user = await this.userRepository.findOne({
   
   
      where: {
   
    username: 'admin' },
      relations: ['roles', 'roles.permissions'],
    });
    if (user) {
   
   
      const permissions = user.roles.flatMap((role) => role.permissions);
      const permissionNames = permissions.map((item) => item.name);

      return [...new Set(permissionNames)];
    } else {
   
   
      return [];
    }
  }

因为这个方法要提供给权限守卫使用,所以需要暴露出去,并在permission.module.ts中导入

//...
import {
   
    UserModule } from 'src/user/user.module';
@Module({
   
   
  //...
  imports: [UserModule, TypeOrmModule.forFeature([Permission])],
})
export class PermissionModule {
   
   }

然后在导航守卫中引入,因为获取的是异步的,因此守卫做了一点修改

//permission.guard.ts
import {
   
    CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import {
   
    Reflector } from '@nestjs/core';
import {
   
    UserService } from 'src/user/user.service';
@Injectable()
export class PermissionGuard implements CanActivate {
   
   
  constructor(
    private reflector: Reflector,
    private userServicese: UserService,
  ) {
   
   }
  async canActivate(context: ExecutionContext): Promise<boolean> {
   
   
    const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
      'permissions',
      [context.getClass(), context.getHandler()],
    ) || [];
    const permissionNames = await this.userServicese.findPermissionNames();
    console.log(requiredPermissions);
    console.log(permissionNames);

    return true;
  }
}

调用user/test接口即可获得接口权限字段与该用户拥有的权限字段

image.png

最后看该角色权限是否包含接口权限即可

//permission.guard.ts
//...
const isContainedPermission = requiredPermissions.every((item) =>
  permissionNames.includes(item),
);
if (!isContainedPermission) {
   
   
  throw new ApiException('权限不足', ApiErrorCode.Forbidden);
}
return true;

完善一下

auth.guard.ts我们已经将 jwt 验证的信息放入了 request['user']中了

image.png

所以可以在权限守卫中获取到登录用户的信息,然后调用 user 模块中的findPermissionNames即可获取当前登录人的权限集合,权限守卫修改如下

//permission.guard.ts
import {
   
    CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import {
   
    Reflector } from '@nestjs/core';
import {
   
    UserService } from 'src/user/user.service';
import {
   
    Request } from 'express';
import {
   
    ApiException } from 'src/common/filter/http-exception/api.exception';
import {
   
    ApiErrorCode } from 'src/common/enums/api-error-code.enum';
@Injectable()
export class PermissionGuard implements CanActivate {
   
   
  constructor(
    private reflector: Reflector,
    private userServicese: UserService,
  ) {
   
   }
  async canActivate(context: ExecutionContext): Promise<boolean> {
   
   
    interface CusRequest extends Request {
   
   
      user?: any;
    }
    const request: CusRequest = context.switchToHttp().getRequest();
    const requiredPermissions =
      this.reflector.getAllAndOverride<string[]>('permissions', [
        context.getClass(),
        context.getHandler(),
      ]) || [];

    if (requiredPermissions.length === 0) return true;
    const [, token] = request.headers.authorization?.split(' ') ?? [];

    const permissionNames = await this.userServicese.findPermissionNames(
      token,
      request.user,
    );

    const isContainedPermission = requiredPermissions.every((item) =>
      permissionNames.includes(item),
    );
    if (!isContainedPermission) {
   
   
      throw new ApiException('权限不足', ApiErrorCode.Forbidden);
    }
    return true;
  }
}

findPermissionNames获取到传来的用户信息,将查询的 username 替换即可

//user.service.ts
  async findPermissionNames(token: string, userInfo) {
   
   
    const user = await this.userRepository.findOne({
   
   
      where: {
   
    username: userInfo.username },
      relations: ['roles', 'roles.permissions'],
    });
    if (user) {
   
   
      const permissions = user.roles.flatMap((role) => role.permissions);
      const permissionNames = permissions.map((item) => item.name);
      return [...new Set(permissionNames)];
    } else {
   
   
      return [];
    }
  }

最后调用登录接口登录只有一个用户角色的 user1 用户,拿到它的 token 调用user/test接口

image.png

因为角色用户只有read权限,而 test 接口需要create, read权限,所以被拦截了,我们将接口改成只需要read权限再试一下

//user.controller.ts
  @Post('test')
  @Permissions('read')
  test(@Body() testParams) {
   
   
    return this.userService.test(testParams);
  }

image.png

此时我们发现请求成功了!

到这里基于RBAC权限控制基本完成了,但是还有很多不足之处,比如不需要每次调用接口都查询数据库中用户的权限,我们只需要在登录时查询一次缓存在Redis中即可,这部分后续文章会进行介绍,欢迎点赞收藏加关注~

源码地址

Vue3 + Nest 实现权限管理系统 后端

相关文章
|
自然语言处理 JavaScript 前端开发
一夜获千星!已获 1.7k+,Art Design Pro:Vue3 + Vite + TypeScript 打造的高颜值管理系统模板,这个让后端小哥直呼救命的后台系统
Art Design Pro 是一款基于 Vue 3、Vite 和 TypeScript 的高颜值后台管理系统模板,已获 1.7k+ 星标。项目专注于用户体验与视觉设计,支持主题切换、多语言、权限管理及图表展示等功能,内置常用业务组件,便于快速搭建现代化管理界面。其技术栈先进,开发体验流畅,适配多设备,满足企业级应用需求。项目地址:[GitHub](https://github.com/Daymychen/art-design-pro)。
2431 11
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
1309 1
|
前端开发 JavaScript Java
导出excel的两个方式:前端vue+XLSX 导出excel,vue+后端POI 导出excel,并进行分析、比较
这篇文章介绍了使用前端Vue框架结合XLSX库和后端结合Apache POI库导出Excel文件的两种方法,并对比分析了它们的优缺点。
3148 0
|
JavaScript 前端开发 API
vue获取图片的blob传给django后端
vue获取图片的blob传给django后端
409 4
|
JavaScript 前端开发
vue3教程,如何手动获取后端数据(入门到精通3,新人必学篇)
本文提供了一个Vue 3教程,讲解了如何使用axios库手动从后端获取数据,包括安装axios、配置后端访问地址、编写路由地址、发起HTTP请求以及在组件中读取和打印响应数据的步骤。
3064 0
vue3教程,如何手动获取后端数据(入门到精通3,新人必学篇)
|
前端开发 Java Spring
Spring与Angular/React/Vue:当后端大佬遇上前端三杰,会擦出怎样的火花?一场技术的盛宴,你准备好了吗?
【8月更文挑战第31天】Spring框架与Angular、React、Vue等前端框架的集成是现代Web应用开发的核心。通过RESTful API、WebSocket及GraphQL等方式,Spring能与前端框架高效互动,提供快速且功能丰富的应用。RESTful API简单有效,适用于基本数据交互;WebSocket支持实时通信,适合聊天应用和数据监控;GraphQL则提供更精确的数据查询能力。开发者可根据需求选择合适的集成方式,提升用户体验和应用功能。
598 1
|
JavaScript NoSQL Redis
Vue中实现修改邮箱、手机号等流程的大致过程、验证码由后端的redis生成验证(版本1.0)
这篇文章记录了在Vue中实现修改手机号和邮箱的大致流程,包括使用过滤器部分隐藏展示的手机号和邮箱,以及通过点击触发路由跳转的便捷方式。文章还描述了旧号码和新号码验证的界面实现,其中验证码由后端生成并通过弹窗展示给用户,未来可以接入真正的手机验证码接口。此外,还提供了修改邮箱的页面效果截图,并强调了学习是一个永无止境的过程。
Vue中实现修改邮箱、手机号等流程的大致过程、验证码由后端的redis生成验证(版本1.0)
|
JavaScript 前端开发
Vue中传递自定义参数到后端、后端获取数据(使用Map接收参数)
这篇文章讲述了如何在Vue中通过Axios二次封装传递自定义参数到后端,并展示了后端如何使用Map接收这些参数,以及如何避免参数转换错误和统一接口设计的方法。
|
JavaScript 前端开发 安全
VUE——APP,后端,前端三端AES加密解密
VUE——APP,后端,前端三端AES加密解密
1179 0
|
开发框架 前端开发 JavaScript
ABP框架测试信息---Winform端、动态网站、Vue&Element管理后端等
ABP框架测试信息---Winform端、动态网站、Vue&Element管理后端等

热门文章

最新文章