nest.js 集成 auth 鉴权 JWT

身份验证是大多数应用程序的重要组成部分。有许多不同的方法和策略来处理身份验证。任何项目所采用的方法都取决于其特定的应用需求。本章介绍了几种可以适应各种不同要求的身份验证方法。

Passport是最流行的 node.js 身份验证库,在社区中广为人知,并成功用于许多生产应用程序。使用该模块将此库与Nest应用程序集成起来非常简单。@nestjs/passport在高层次上,Passport 执行一系列步骤来:

  • 通过验证用户的“凭据”(例如用户名/密码、JSON Web 令牌 ( JWT ) 或来自身份提供者的身份令牌)对用户进行身份验证
  • 管理经过身份验证的状态(通过发布便携式令牌,例如 JWT,或创建Express 会话
  • 将有关经过身份验证的用户的信息附加到Request对象,以便在路由处理程序中进一步使用

Passport 具有丰富的策略生态系统,可实现各种身份验证机制。虽然概念简单,但您可以选择的 Passport 策略集非常丰富且种类繁多。Passport 将这些不同的步骤抽象为一个标准模式,该@nestjs/passport模块将此模式包装并标准化为熟悉的 Nest 结构。

在本章中,我们将使用这些强大而灵活的模块为 RESTful API 服务器实现一个完整的端到端身份验证解决方案。您可以使用此处描述的概念来实施任何 Passport 策略来自定义您的身份验证方案。您可以按照本章中的步骤来构建这个完整的示例。您可以在此处找到包含完整示例应用程序的存储库。

使用

http-headers

{
  "Authorization": "Bearer xxxxxx"
}

目标

  • webapi登录接口获取jwt
  • 请求验证
  • 获取当前登录对象

安装依赖

$ yarn add @nestjs/passport passport passport-local passport-jwt @nestjs/jwt  crypto-js
$ yarn add @types/passport-local -D   
$ yarn add @types/crypto-js -D      
$ yarn add @types/passport-jwt -D

基础辅助类

  • /src/utils/aes-secret.ts
import CryptoJS from 'crypto-js';

const key = CryptoJS.enc.Utf8.parse('i8761286317826ABCDEF'); //十六位十六进制数作为密钥
const iv = CryptoJS.enc.Utf8.parse('fasdo978ouiojiocsdj'); //十六位十六进制数作为密钥偏移量

/**
 * 解密
 * @param word
 * @returns
 */
export const secretDecrypt = (word: string) => {
  const encryptedHexStr = CryptoJS.enc.Hex.parse(word);
  const srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);
  const decrypt = CryptoJS.AES.decrypt(srcs, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
  return decryptedStr.toString();
};

/**
 * 加密
 * @param word
 * @returns
 */
export const secretEncrypt = (word: string) => {
  const srcs = CryptoJS.enc.Utf8.parse(word);
  const encrypted = CryptoJS.AES.encrypt(srcs, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return encrypted.ciphertext.toString().toUpperCase();
};

创建auth module

$ nest g module auth
$ nest g service auth
$ nest g controller auth
  • /src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { SequelizeModule } from '@nestjs/sequelize';
import { UserModel } from 'src/model/customer/user.model';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { ConfigurationType } from 'config/configuration';

@Module({
  imports: [
    SequelizeModule.forFeature([UserModel]),
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService<ConfigurationType>) => {
        const setting = {
          secret: configService.get<string>('jwtsecret'),
          signOptions: { expiresIn: '7d' },
        };
        return setting;
      },
      inject: [ConfigService],
    }),
  ],
  controllers: [AuthController],
  providers: [
    AuthService,
    JwtService,
    LocalStrategy,
    JwtService,
    JwtStrategy,
    ConfigService,
  ],
})
export class AuthModule {}

  • /src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { InjectModel } from '@nestjs/sequelize';
import { USER, UserModel } from 'src/model/customer/user.model';
import { User } from 'src/user/entities/user.entity';
import { secretEncrypt } from 'src/utils/aes-secret';
import { CONST_CONFIG } from 'src/utils/const-config';

@Injectable()
export class AuthService {
  constructor(
    @InjectModel(UserModel)
    private userModel: typeof UserModel,
    private jwtService: JwtService,
    private configService: ConfigService,
  ) {}

  /**
   * 用户名密码校验
   * @param username
   * @param password
   * @returns
   */
  async validateUser(username: string, password: string): Promise<User> {
    const secretPwd = secretEncrypt(password);
    const user = await this.userModel.findOne({
      where: {
        [USER.USERNAME]: username.trim(),
        [USER.PASSWORD]: secretPwd,
      },
    });
    if (!user) {
      return user;
    }
    return null;
  }

  /**
   * 登录
   * @param user
   * @returns
   */
  async login(user: User) {
    const payload = {
      username: user.username,
      userId: user.id,
    };
    return {
      accessToken: this.jwtService.sign(payload, {
        secret: this.configService.get<string>(CONST_CONFIG.JWTSECRET),
        expiresIn: '7d',
      }),
      user: {
        id: user.id,
        phoneNumber: user.phoneNumber,
        userName: user.username,
      },
    };
  }
}

本地策略

策略使用方法 @UseGuards(LocalGuard) 根据接口场景采用不同策略

本地策略指本地登录策略(用户用户名密码请求认证,返回jwt 后续认证走jwt策略)

  • /src/auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { AuthService } from './auth.service';
import { User } from 'src/user/entities/user.entity';

/**
 * 本地登录策略
 */
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<User> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new HttpException('用户名或者密码错误!', HttpStatus.UNAUTHORIZED);
    }
    return user;
  }
}

我们可以在调用中传递一个选项对象来自super()定义护照策略的行为。在此示例中,默认情况下,护照本地策略需要在请求正文中调用username和的属性。password传递一个选项对象来指定不同的属性名称,例如:super({ usernameField: 'email' }). 有关详细信息,请参阅Passport 文档

  • /src/auth/jwt.strategy.ts
    jwt策略是指请求的校验方式采用jwt校验(登录后校验)
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ConfigurationType } from 'config/configuration';

/**
 * jwt 校验策略
 */
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService<ConfigurationType>) {
    const secret = configService.get<string>('jwtsecret');
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: secret,
    });
  }

  async validate(payload: any) {
    console.log(payload);
    return payload;
  }
}


async nacos secretKey 方案

Configure Strategy

configure-strategy

The JWT authentication strategy is constructed as follows:

new JwtStrategy(options, verify)

options is an object literal containing options to control how the token is extracted from the request or verified.

  • secretOrKey is a string or buffer containing the secret (symmetric) or PEM-encoded public key (asymmetric) for verifying the token's signature. REQUIRED unless secretOrKeyProvider is provided.
  • secretOrKeyProvider is a callback in the format function secretOrKeyProvider(request, rawJwtToken, done), which should call done with a secret or PEM-encoded public key (asymmetric) for the given key and request combination. done accepts arguments in the format function done(err, secret). Note it is up to the implementer to decode rawJwtToken. REQUIRED unless secretOrKey is provided.
  • jwtFromRequest (REQUIRED) Function that accepts a request as the only parameter and returns either the JWT as a string or null. See Extracting the JWT from the request for more details.
  • issuer: If defined the token issuer (iss) will be verified against this value.
  • audience: If defined, the token audience (aud) will be verified against this value.
  • algorithms: List of strings with the names of the allowed algorithms. For instance, ["HS256", "HS384"].
  • ignoreExpiration: if true do not validate the expiration of the token.
  • passReqToCallback: If true the request will be passed to the verify callback. i.e. verify(request, jwt_payload, done_callback).
  • jsonWebTokenOptions: passport-jwt is verifying the token using jsonwebtoken. Pass here an options object for any other option you can pass the jsonwebtoken verifier. (i.e maxAge)

verify is a function with the parameters verify(jwt_payload, done)

  • jwt_payload is an object literal containing the decoded JWT payload.
  • done is a passport error first callback accepting arguments done(error, user, info)
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

import { ConfigurationType } from '../config/configuration';
import { NacosService } from '../config/nacos.service';

/**
 * jwt 校验策略
 */
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    private configService: ConfigService<ConfigurationType>,
    private readonly nacosService: NacosService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      // secretOrKey: secret,
      secretOrKeyProvider: async (request, rawJwtToken, done) => {
        const dataId = this.configService.get<string>('nacos.databaseConfigId');
        const group = this.configService.get<string>('nacos.databaseGroup');
        const initialConfig = await this.nacosService.getConfig(dataId, group);
        done(undefined, initialConfig.jwtSecret);
      },
    });
  }

  async validate(payload: any) {
    console.log(payload);
    return payload;
  }
}

api

post -> /auth/login ->加载本地策略(local-auth.guard.ts)-> 牌照校验(local.strategy.ts)validate -> auth.controller.login(req 被牌照校验后返回值替换)-> return result

  • /src/user/auth.controller.ts
import {
  Controller,
  HttpException,
  HttpStatus,
  Post,
  Req,
  UseGuards,
} from '@nestjs/common';
import { User } from 'src/user/entities/user.entity';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './local-auth.guard';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}
  
  @UseGuards(LocalAuthGuard)
  @Post('/login')
  async login(@Req() req: { user: User }) {
    const result = this.authService.login(req.user);
    if (result) {
      return result;
    }
    throw new HttpException('用户名或者密码错误!', HttpStatus.FORBIDDEN);
  }
}

  • /src/auth/local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

  • /src/auth/jwt-auth-entity.ts
/**
 * CurrentUser
 */
export class JwtAuthEntity {
  username: string;
  /**
   * user.id
   */
  userId: string;
  iat: number;
  exp: number;
}

graphql 使用

  • /src/auth/current-user.ts
    graphql resolver 请求参数获取
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

/**
 * 自定义参数装饰器
 */
export const CurrentUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req.user;
  },
);

  • /src/auth/gql-auth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}

  • /src/user/user.resolver.ts
import {
  Resolver,
  Query,
  Mutation,
  Args,
  Info,
  Parent,
  ResolveField,
} from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { FindAllInput } from 'src/utils/common.input';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from 'src/auth/gql-auth.guard';
import { CurrentUser } from 'src/auth/current-user';
import { JwtAuthEntity } from 'src/auth/jwt-auth-entity';
import { OrgroleUser } from 'src/orgrole-user/entities/orgrole-user.entity';
import { OrgroleUserService } from 'src/orgrole-user/orgrole-user.service';

@Resolver(() => User)
export class UserResolver {
  constructor(
    private readonly userService: UserService,
    private readonly orgroleUserService: OrgroleUserService,
  ) {}

  @UseGuards(GqlAuthGuard)
  @Mutation(() => User)
  createUser(
    @Args('createUserInput') createUserInput: CreateUserInput,
    @CurrentUser() user: JwtAuthEntity,
  ) {
    return this.userService.create(createUserInput, user);
  }

  @UseGuards(GqlAuthGuard)
  @Query(() => [User], { name: 'UserAll' })
  findAll(
    @Args('param') param: FindAllInput,
    @CurrentUser() user: JwtAuthEntity,
  ) {
    return this.userService.findAll(param, user);
  }

  @UseGuards(GqlAuthGuard)
  @Query(() => User, { name: 'User' })
  findOne(
    @Args('id', { type: () => String }) id: string,
    @CurrentUser() user: JwtAuthEntity,
  ) {
    return this.userService.findByPk(id, user);
  }

  @UseGuards(GqlAuthGuard)
  @Mutation(() => User)
  updateUser(
    @Args('updateUserInput') updateUserInput: UpdateUserInput,
    @CurrentUser() user: JwtAuthEntity,
  ) {
    return this.userService.update(updateUserInput.id, updateUserInput, user);
  }

  @UseGuards(GqlAuthGuard)
  @Mutation(() => User)
  removeUser(
    @Args('id', { type: () => String }) id: string,
    @CurrentUser() user: JwtAuthEntity,
  ) {
    return this.userService.remove(id, user);
  }

  @ResolveField(() => [OrgroleUser], { nullable: true })
  async orgroleUserUserId(
    @Parent() parent: User, // Resolved object that implements Character
    @Info() { info }, // Type of the object that implements Character
    @Args('param', { type: () => FindAllInput, nullable: true })
    param: FindAllInput,
  ) {
    if (parent.id) {
      return undefined;
    }
    // Get character's friends
    return this.orgroleUserService.findAll({
      ...param,
      where: {
        userId: parent.id,
        ...param?.where,
      },
    });
  }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容