nest.js 集成 auth 鑒權(quán) JWT

身份驗證是大多數(shù)應(yīng)用程序的重要組成部分嵌言。有許多不同的方法和策略來處理身份驗證。任何項目所采用的方法都取決于其特定的應(yīng)用需求。本章介紹了幾種可以適應(yīng)各種不同要求的身份驗證方法积仗。

Passport是最流行的 node.js 身份驗證庫品姓,在社區(qū)中廣為人知寝并,并成功用于許多生產(chǎn)應(yīng)用程序。使用該模塊將此庫與Nest應(yīng)用程序集成起來非常簡單腹备。@nestjs/passport在高層次上衬潦,Passport 執(zhí)行一系列步驟來:

  • 通過驗證用戶的“憑據(jù)”(例如用戶名/密碼、JSON Web 令牌 ( JWT ) 或來自身份提供者的身份令牌)對用戶進(jìn)行身份驗證
  • 管理經(jīng)過身份驗證的狀態(tài)(通過發(fā)布便攜式令牌植酥,例如 JWT镀岛,或創(chuàng)建Express 會話
  • 將有關(guān)經(jīng)過身份驗證的用戶的信息附加到Request對象,以便在路由處理程序中進(jìn)一步使用

Passport 具有豐富的策略生態(tài)系統(tǒng)友驮,可實現(xiàn)各種身份驗證機(jī)制漂羊。雖然概念簡單,但您可以選擇的 Passport 策略集非常豐富且種類繁多卸留。Passport 將這些不同的步驟抽象為一個標(biāo)準(zhǔn)模式走越,該@nestjs/passport模塊將此模式包裝并標(biāo)準(zhǔn)化為熟悉的 Nest 結(jié)構(gòu)。

在本章中耻瑟,我們將使用這些強(qiáng)大而靈活的模塊為 RESTful API 服務(wù)器實現(xiàn)一個完整的端到端身份驗證解決方案旨指。您可以使用此處描述的概念來實施任何 Passport 策略來自定義您的身份驗證方案。您可以按照本章中的步驟來構(gòu)建這個完整的示例喳整。您可以在此處找到包含完整示例應(yīng)用程序的存儲庫。

使用

http-headers

{
  "Authorization": "Bearer xxxxxx"
}

目標(biāo)

  • webapi登錄接口獲取jwt
  • 請求驗證
  • 獲取當(dāng)前登錄對象

安裝依賴

$ 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

基礎(chǔ)輔助類

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

const key = CryptoJS.enc.Utf8.parse('i8761286317826ABCDEF'); //十六位十六進(jìn)制數(shù)作為密鑰
const iv = CryptoJS.enc.Utf8.parse('fasdo978ouiojiocsdj'); //十六位十六進(jìn)制數(shù)作為密鑰偏移量

/**
 * 解密
 * @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();
};

創(chuàng)建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) 根據(jù)接口場景采用不同策略

本地策略指本地登錄策略(用戶用戶名密碼請求認(rèn)證,返回jwt 后續(xù)認(rèn)證走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;
  }
}

我們可以在調(diào)用中傳遞一個選項對象來自super()定義護(hù)照策略的行為不翩。在此示例中津坑,默認(rèn)情況下眉反,護(hù)照本地策略需要在請求正文中調(diào)用username和的屬性耿币。password傳遞一個選項對象來指定不同的屬性名稱,例如:super({ usernameField: 'email' }). 有關(guān)詳細(xì)信息雇初,請參閱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 請求參數(shù)獲取
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

/**
 * 自定義參數(shù)裝飾器
 */
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,
      },
    });
  }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市凸郑,隨后出現(xiàn)的幾起案子裳食,更是在濱河造成了極大的恐慌,老刑警劉巖芙沥,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诲祸,死亡現(xiàn)場離奇詭異浊吏,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)救氯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門找田,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人着憨,你說我怎么就攤上這事墩衙。” “怎么了甲抖?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵漆改,是天一觀的道長。 經(jīng)常有香客問我惧眠,道長籽懦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任氛魁,我火速辦了婚禮暮顺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘秀存。我一直安慰自己捶码,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布或链。 她就那樣靜靜地躺著惫恼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪澳盐。 梳的紋絲不亂的頭發(fā)上祈纯,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機(jī)與錄音叼耙,去河邊找鬼腕窥。 笑死,一個胖子當(dāng)著我的面吹牛筛婉,可吹牛的內(nèi)容都是我干的簇爆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼爽撒,長吁一口氣:“原來是場噩夢啊……” “哼入蛆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起硕勿,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤哨毁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后首尼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挑庶,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡言秸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年软能,在試婚紗的時候發(fā)現(xiàn)自己被綠了迎捺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡查排,死狀恐怖凳枝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情跋核,我是刑警寧澤岖瑰,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站砂代,受9級特大地震影響蹋订,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜刻伊,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一露戒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捶箱,春花似錦智什、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至晨川,卻和暖如春证九,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背共虑。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工愧怜, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人看蚜。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓叫搁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親供炎。 傳聞我的和親對象是個殘疾皇子渴逻,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容