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,
      },
    });
  }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容