NestJS 认证与授权:JWT、OAuth 和 RBAC 实现

在上一篇文章中,我们介绍了 NestJS 的数据库操作和 TypeORM 集成。本文将深入探讨如何在 NestJS 中实现完整的认证和授权系统。

JWT 认证实现

1. 安装依赖

npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install -D @types/passport-jwt @types/bcrypt

2. JWT 策略配置

// src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    return { 
      userId: payload.sub, 
      username: payload.username,
      roles: payload.roles 
    };
  }
}

// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtStrategy } from './strategies/jwt.strategy';
import { AuthService } from './auth.service';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get('JWT_SECRET'),
        signOptions: { 
          expiresIn: '1d',
          issuer: 'nestjs-app'
        },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

3. 认证服务实现

// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.usersService.findByUsername(username);
    if (user && await bcrypt.compare(password, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { 
      username: user.username, 
      sub: user.id,
      roles: user.roles 
    };

    return {
      access_token: this.jwtService.sign(payload),
      user: {
        id: user.id,
        username: user.username,
        email: user.email,
        roles: user.roles
      }
    };
  }

  async register(createUserDto: CreateUserDto) {
    // 检查用户是否已存在
    const existingUser = await this.usersService.findByUsername(
      createUserDto.username
    );
    if (existingUser) {
      throw new ConflictException('Username already exists');
    }

    // 加密密码
    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);

    // 创建新用户
    const newUser = await this.usersService.create({
      ...createUserDto,
      password: hashedPassword,
    });

    // 返回用户信息和令牌
    const { password, ...result } = newUser;
    return this.login(result);
  }
}

4. 认证控制器

// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Get } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { GetUser } from './decorators/get-user.decorator';

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

  @Post('login')
  async login(@Body() loginDto: LoginDto) {
    const user = await this.authService.validateUser(
      loginDto.username,
      loginDto.password
    );
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }
    return this.authService.login(user);
  }

  @Post('register')
  async register(@Body() createUserDto: CreateUserDto) {
    return this.authService.register(createUserDto);
  }

  @UseGuards(JwtAuthGuard)
  @Get('profile')
  getProfile(@GetUser() user: any) {
    return user;
  }
}

OAuth2.0 集成

1. Google OAuth2 实现

// src/auth/strategies/google.strategy.ts
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(private configService: ConfigService) {
    super({
      clientID: configService.get('GOOGLE_CLIENT_ID'),
      clientSecret: configService.get('GOOGLE_CLIENT_SECRET'),
      callbackURL: 'http://localhost:3000/auth/google/callback',
      scope: ['email', 'profile'],
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: any,
    done: VerifyCallback,
  ): Promise<any> {
    const { name, emails, photos } = profile;
    const user = {
      email: emails[0].value,
      firstName: name.givenName,
      lastName: name.familyName,
      picture: photos[0].value,
      accessToken,
    };
    done(null, user);
  }
}

// src/auth/controllers/oauth.controller.ts
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('auth/google')
export class OAuthController {
  @Get()
  @UseGuards(AuthGuard('google'))
  async googleAuth(@Req() req) {}

  @Get('callback')
  @UseGuards(AuthGuard('google'))
  async googleAuthRedirect(@Req() req) {
    // 处理 Google 认证回调
    return this.authService.googleLogin(req.user);
  }
}

2. GitHub OAuth2 实现

// src/auth/strategies/github.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-github2';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, 'github') {
  constructor(private configService: ConfigService) {
    super({
      clientID: configService.get('GITHUB_CLIENT_ID'),
      clientSecret: configService.get('GITHUB_CLIENT_SECRET'),
      callbackURL: 'http://localhost:3000/auth/github/callback',
      scope: ['user:email'],
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: any,
    done: Function,
  ) {
    const user = {
      githubId: profile.id,
      username: profile.username,
      email: profile.emails[0].value,
      accessToken,
    };
    done(null, user);
  }
}

RBAC 权限控制

1. 角色定义

// src/auth/enums/role.enum.ts
export enum Role {
  USER = 'user',
  ADMIN = 'admin',
  MODERATOR = 'moderator',
}

// src/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

2. 角色守卫

// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../enums/role.enum';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

3. 权限实体设计

// src/auth/entities/permission.entity.ts
import { Entity, Column, ManyToMany } from 'typeorm';
import { BaseEntity } from '../../common/entities/base.entity';
import { Role } from '../enums/role.enum';

@Entity('permissions')
export class Permission extends BaseEntity {
  @Column()
  name: string;

  @Column()
  description: string;

  @Column('simple-array')
  allowedRoles: Role[];
}

// src/users/entities/user.entity.ts
import { Entity, Column, ManyToMany, JoinTable } from 'typeorm';
import { Role } from '../../auth/enums/role.enum';
import { Permission } from '../../auth/entities/permission.entity';

@Entity('users')
export class User extends BaseEntity {
  // ... 其他字段

  @Column('simple-array')
  roles: Role[];

  @ManyToMany(() => Permission)
  @JoinTable({
    name: 'user_permissions',
    joinColumn: { name: 'user_id' },
    inverseJoinColumn: { name: 'permission_id' },
  })
  permissions: Permission[];
}

4. 权限检查服务

// src/auth/services/permission.service.ts
import { Injectable, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Permission } from '../entities/permission.entity';
import { User } from '../../users/entities/user.entity';

@Injectable()
export class PermissionService {
  constructor(
    @InjectRepository(Permission)
    private permissionRepository: Repository<Permission>,
  ) {}

  async checkPermission(user: User, permissionName: string): Promise<boolean> {
    const permission = await this.permissionRepository.findOne({
      where: { name: permissionName },
    });

    if (!permission) {
      throw new ForbiddenException('Permission not found');
    }

    // 检查用户角色是否有权限
    return permission.allowedRoles.some(role => user.roles.includes(role));
  }

  async grantPermission(user: User, permissionName: string): Promise<void> {
    const permission = await this.permissionRepository.findOne({
      where: { name: permissionName },
    });

    if (!permission) {
      throw new ForbiddenException('Permission not found');
    }

    user.permissions = [...user.permissions, permission];
    await this.userRepository.save(user);
  }
}

5. 使用示例

// src/posts/posts.controller.ts
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '../auth/enums/role.enum';
import { GetUser } from '../auth/decorators/get-user.decorator';
import { User } from '../users/entities/user.entity';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';

@Controller('posts')
@UseGuards(JwtAuthGuard, RolesGuard)
export class PostsController {
  constructor(
    private postsService: PostsService,
    private permissionService: PermissionService,
  ) {}

  @Post()
  @Roles(Role.USER)
  async createPost(
    @GetUser() user: User,
    @Body() createPostDto: CreatePostDto,
  ) {
    // 检查用户是否有创建文章的权限
    const hasPermission = await this.permissionService.checkPermission(
      user,
      'create_post'
    );

    if (!hasPermission) {
      throw new ForbiddenException('You do not have permission to create posts');
    }

    return this.postsService.create(user, createPostDto);
  }

  @Post('publish')
  @Roles(Role.MODERATOR, Role.ADMIN)
  async publishPost(
    @GetUser() user: User,
    @Body('postId') postId: string,
  ) {
    return this.postsService.publish(user, postId);
  }
}

安全最佳实践

1. 密码加密

// src/common/utils/crypto.util.ts
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';

export class CryptoUtil {
  static async hashPassword(password: string): Promise<string> {
    const salt = await bcrypt.genSalt(10);
    return bcrypt.hash(password, salt);
  }

  static async comparePasswords(
    password: string,
    hashedPassword: string,
  ): Promise<boolean> {
    return bcrypt.compare(password, hashedPassword);
  }

  static generateRandomToken(length: number = 32): string {
    return crypto.randomBytes(length).toString('hex');
  }
}

2. 请求限流

// src/common/guards/throttle.guard.ts
import { Injectable } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';

@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
  protected getTracker(req: Record<string, any>): string {
    return req.ips.length ? req.ips[0] : req.ip;
  }
}

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60,
      limit: 10,
    }),
  ],
})
export class AppModule {}

3. 安全头部配置

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as helmet from 'helmet';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 安全头部
  app.use(helmet());

  // CORS 配置
  app.enableCors({
    origin: process.env.ALLOWED_ORIGINS.split(','),
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    credentials: true,
  });

  await app.listen(3000);
}
bootstrap();

写在最后

本文详细介绍了 NestJS 中的认证与授权实现:

  1. JWT 认证的完整实现
  2. OAuth2.0 社交登录集成
  3. RBAC 权限控制系统
  4. 安全最佳实践

在下一篇文章中,我们将探讨 NestJS 的中间件与拦截器实现。

如果觉得这篇文章对你有帮助,别忘了点个赞 👍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/943689.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

指针与数组:深入C语言的内存操作艺术

数组名的理解 在上⼀个章节我们在使⽤指针访问数组的内容时&#xff0c;有这样的代码&#xff1a; int arr[10] {1,2,3,4,5,6,7,8,9,10}; int *p &arr[0]; 这⾥我们使⽤ &arr[0] 的⽅式拿到了数组…

Python的数字类型

python的数字类型包括&#xff1a;整数&#xff0c;浮点数&#xff0c;复数。 整数 python的整数没有长度限制&#xff0c;无限大&#xff0c;有无限的精度 python的整数除法&#xff0c;即便能整除&#xff0c;结果也是小数&#xff0c;小数 在python中用float类型表示&…

【连续学习之SS-IL算法】2021年CPVR会议论文Ss-il:Separated softmax for incremental learning

1 介绍 年份&#xff1a;2021 期刊&#xff1a; 2021CPVR Ahn H, Kwak J, Lim S, et al. Ss-il: Separated softmax for incremental learning[C]//Proceedings of the IEEE/CVF International conference on computer vision. 2021: 844-853. 本文提出的SS-IL&#xff08…

3.BMS系统原理图解读

一、BMS电池板 (1)电池的连接关系&#xff1a;串联 (2)采样控制点&#xff1a;CELL0 - CELL5 (3)端子P1和P3&#xff1a;BAT和BAT- (4)开关S1&#xff1a;控制充放电回路的机械开关 二、BMS控制板 (1)主控MCU 电源 复位 晶振 (2)LED指示灯&#xff1a;4电量指示 1调试指…

洛谷P5250 【深基17.例5】木材仓库(c嘎嘎)

题目链接&#xff1a;P5250 【深基17.例5】木材仓库 - 洛谷 | 计算机科学教育新生态 题目难度&#xff1a;普及/提高 解题心得:本题借鉴了大佬的做法&#xff08;因为没想多好的处理方法~~&#xff09;&#xff0c;本题可以用map&#xff0c;对于操作1&#xff0c;存的话直接另…

pyqt和pycharm环境搭建

安装 python安装&#xff1a; https://www.python.org/downloads/release/python-3913/ python3.9.13 64位(记得勾选Path环境变量) pycharm安装&#xff1a; https://www.jetbrains.com/pycharm/download/?sectionwindows community免费版 换源&#xff1a; pip config se…

ArcGIS Pro地形图四至角图经纬度标注与格网标注

今天来看看ArcGIS Pro 如何在地形图上设置四至角点的经纬度。方里网标注。如下图的地形图左下角经纬度标注。 如下图方里网的标注 如下为本期要介绍的例图&#xff0c;如下&#xff1a; 图片可点击放大 接下来我们来介绍一下 推荐学习&#xff1a;GIS入门模型构建器Arcpy批量…

深度学习与图像处理(国产深度学习框架——飞桨官方指定教材)

计算机视觉从小白到大师之路 《深度学习与图像处理&#xff08;PaddlePaddle版&#xff09;》这一本就够了 1.引言 随着人工智能技术的飞速发展&#xff0c;各行各业对深度学习、图像处理相关领域的人才需求日益迫切。本书旨在通过系统的理论讲解与丰富的实战案例&#xff0…

Bluetooth Spec【0】蓝牙核心架构

蓝牙核心系统由一个主机、一个主控制器和零个或多个辅助控制器组成蓝牙BR/ EDR核心系统的最小实现包括了由蓝牙规范定义的四个最低层和相关协议&#xff0c;以及一个公共服务层协议&#xff1b;服务发现协议&#xff08;SDP&#xff09;和总体配置文件要求在通用访问配置文件&a…

代码随想录Day51 99. 岛屿数量,99. 岛屿数量,100. 岛屿的最大面积。

1.岛屿数量深搜 卡码网题目链接&#xff08;ACM模式&#xff09;(opens new window) 题目描述&#xff1a; 给定一个由 1&#xff08;陆地&#xff09;和 0&#xff08;水&#xff09;组成的矩阵&#xff0c;你需要计算岛屿的数量。岛屿由水平方向或垂直方向上相邻的陆地连接…

【机器学习与数据挖掘实战】案例06:基于Apriori算法的餐饮企业菜品关联分析

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈机器学习与数据挖掘实战 ⌋ ⌋ ⌋ 机器学习是人工智能的一个分支,专注于让计算机系统通过数据学习和改进。它利用统计和计算方法,使模型能够从数据中自动提取特征并做出预测或决策。数据挖掘则是从大型数据集中发现模式、关联…

突破传统,探索单页网站的强大潜力!

单页网站简单、直接&#xff0c;而且设计通常令人惊叹&#xff0c;非常适合展示关键信息而不会让访问者不知所措。 然而&#xff0c;构建单页网站有其自身的挑战&#xff0c;尤其是在 SEO 方面。由于内容数量有限且针对特定关键字的页面较少&#xff0c;可能很难在 SERP 中进行…

攻防世界web新手第四题easyphp

<?php highlight_file(__FILE__); $key1 0; $key2 0;$a $_GET[a]; $b $_GET[b];if(isset($a) && intval($a) > 6000000 && strlen($a) < 3){if(isset($b) && 8b184b substr(md5($b),-6,6)){$key1 1;}else{die("Emmm...再想想&quo…

Python大数据可视化:基于Python的王者荣耀战队的数据分析系统设计与实现_flask+hadoop+spider

开发语言&#xff1a;Python框架&#xff1a;flaskPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录 管理员功能界面 比赛信息管理 看板展示 系统管理 摘要 本文使用Python与…

【已解决】pyinstaller打包ico图片报错:OSError: [WinError 225] 无法成功完成操作,因为文件包含病毒或潜在的垃圾软件。

起因&#xff1a; pyinstaller加上 --icon 参数打包时报错。 命令如下&#xff1a; 解决&#xff1a; 关闭 Windows 的病毒防护即可&#xff0c;步骤如下。 点屏幕右下角通知栏&#xff0c;进入“病毒和威胁防护”&#xff1a; 打开&#xff1a; 关闭实时保护&#xff08…

Cloudflare 边缘网络架构:无处不在的 BPF-2019

大家觉得有意义和帮助记得及时关注和点赞!!! 译者序边缘网络DDos Mitigation负载均衡TCP/UDP Socket DispatchSOCKMAPPrometheus - ebpf_exporter无处不在的 eBPF 边缘网络 Cloudflare 的服务器运行 Linux 系统。 我们的数据中心分为两类&#xff1a; 大的“核心”数据中心&a…

智慧园区小程序开发制作功能介绍

智慧园区小程序开发制作功能介绍 智慧园区小程序系统作为一款面向园区企业的一站式线上服务平台&#xff0c;可为企业提供数智化的园区办公服务。智慧园区小程序功能介绍 1、园区公告、政策信息查看足不出户掌握最新动态&#xff0c;“园区公告、政策信息”等信息。首页点击对应…

基于 Python Django 的农产品销售系统的研究与实现

大家好&#xff0c;我是stormjun&#xff0c;今天为大家带来的是基于 Python Django 的农产品销售系统的研究与实现。该系统采用 Python 语言 开发&#xff0c;MySql 作为数据库&#xff0c;系统功能完善 &#xff0c;实用性强 &#xff0c;可供大学生实战项目参考使用。 博主介…

WEB开发 - Flask 入门:Jinja2 模板语法进阶 Python

在上一阶段&#xff0c;我们一起学习了基于Python地 web框架Flask&#xff0c;并且初步了解了这个框架有一种渲染方式叫做 模板语法&#xff0c;今天&#xff0c;我们一起再来深入地了解和学习这个叫做Jinja2地模板语法。 WEB开发 - Flask 入门&#xff1a;由浅入深地带你学习…

基于SpringBoot在线音乐系统平台功能实现十八

一、前言介绍&#xff1a; 1.1 项目摘要 随着互联网技术的迅猛发展和普及&#xff0c;人们对音乐的获取和欣赏方式发生了巨大改变。传统的音乐播放方式&#xff0c;如CD、磁带或本地下载的音乐文件&#xff0c;已经不能满足用户日益增长的需求。用户更希望通过网络直接获取各…