本次完整实践nestjs的注册登陆,到jwt授权认证,到guard守卫拦截验证,到strategy的JWT策略。

注册登录controller

// auth.controller.ts
import { Body, Controller, Get, Post, UseGuards } from "@nestjs/common";
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
import { AuthService } from "@libs/common/auth/auth.service";
import { LoginDto } from "@libs/common/auth/auth.dto";


@Controller("auth")
@ApiTags("认证中心")
export class AuthController {
  constructor(private authService: AuthService) {
  }

  @Post("login")
  @ApiOperation({ summary: "登录" })
  async login(@Body() body: LoginDto) {
    return this.authService.validateUser(body.username, body.password);
  }

  @Post("register")
  @ApiOperation({ summary: "注册" })
  async register(@Body() body: LoginDto) {
    await this.authService.register(body);
    return this.login(body);
  }
}

注册登陆及JWT的service实现

// auth.service.ts

import { HttpException, HttpStatus, Inject, Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { User } from "@libs/db/schemas/user.schema";
import { ReturnModelType } from "@typegoose/typegoose";
import { compareSync } from "bcryptjs";
import { ConfigService } from "@nestjs/config";
import { LoginDto } from "@libs/common/auth/auth.dto";

@Injectable()
export class AuthService {
  constructor(
    @Inject(User.name) private readonly userModel: ReturnModelType<typeof User>,
    private jwtService: JwtService,
    private config: ConfigService
  ) {
  }

  async validateUser(username: string, password: string) {
    const user = await this.userModel.findOne({ username }).select("+password");
    if (!user) {
      throw new HttpException("用户不存在", HttpStatus.BAD_REQUEST);
    }
    if (!compareSync(password, user.password)) {
      throw new HttpException("密码不正确", HttpStatus.BAD_REQUEST);
    }
    const { accessToken, refreshToken } = this.genToken({ data: user._id });
    return {
      accessToken,
      refreshToken,
      id: user.id,
      username: user.username,
      avatar: user.avatar
    };
  }

  async register(body: LoginDto) {
    const { username } = body;
    const user = await this.userModel.findOne({ username });
    if (user) {
      throw new HttpException("用户已存在", HttpStatus.BAD_REQUEST);
    }
    return this.userModel.create(body);
  }

  async findUserById(id: string) {
    const user = await this.userModel.findById(id);
    if (!user) {
      throw new HttpException("用户不存在", HttpStatus.BAD_REQUEST);
    } else {
      return user;
    }
  }

  genToken(payload: any) {
    const accessToken = `Bearer ${this.jwtService.sign(payload, {
      secret: this.config.get("JWT_SECRET"),
      expiresIn: "30m"
    })}`;
    const refreshToken = this.jwtService.sign(payload, {
      secret: this.config.get("JWT_SECRET"),
      expiresIn: "2h"
    });
    return { accessToken, refreshToken };
  }

  verifyToken(token: string) {
    try {
      if (!token) return null;
      const { data } = this.jwtService.verify(token, { secret: this.config.get("JWT_SECRET") });
      return data;
    } catch (error) {
      return null;
    }
  }
}

守卫实现,拦截accessToken和refreshToken,过期进行续签。

// jwt-auth.guard.ts
import { AuthGuard } from "@nestjs/passport";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { AuthService } from "@libs/common/auth/auth.service";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async canActivate(context) {
    const req = context.switchToHttp().getRequest();
    const res = context.switchToHttp().getResponse();

    const reqAccessToken = req.get("Authorization").split(" ")[1];
    if (!reqAccessToken) throw new UnauthorizedException("请先登录");
    const atUserId = this.authService.verifyToken(reqAccessToken);
    if (atUserId) return this.activate(context);

    const reqRefreshToken = req.get("RefreshToken");
    const rtUserId = this.authService.verifyToken(reqRefreshToken);
    if (!rtUserId) throw new UnauthorizedException("当前登录已过期,请重新登录");

    if (await this.authService.findUserById(rtUserId)) {
      const { accessToken, refreshToken } = this.authService.genToken({ data: rtUserId });

      req.headers["authorization"] = accessToken;
      req.headers["refreshtoken"] = refreshToken;
      // 在响应头中加入新的token,客户端判断响应头有无 Authorization 字段,有则重置
      res.header("Authorization", accessToken);
      res.header("RefreshToken", refreshToken);
      // 将当前请求交给下一级
      return this.activate(context);
    }
  }

  async activate(context) {
    return super.canActivate(context) as Promise<boolean>;
  }
}

JWT解析后进行校验

// jwt.strategy.ts
import { ExtractJwt, Strategy, StrategyOptions } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Inject } from "@nestjs/common";
import { ReturnModelType } from "@typegoose/typegoose";
import { User } from "@libs/db/schemas/user.schema";
import { ConfigService } from "@nestjs/config";

export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @Inject(User.name) private readonly userModel: ReturnModelType<typeof User>,
    configService: ConfigService
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get("JWT_SECRET")
    } as StrategyOptions);
  }

  async validate(payload) {
    return this.userModel.findById(payload.data);
  }
}