Nest.js从0到1搭建博客系统---基于JWT实现注册、登录、全局身份验证(7)

573 阅读7分钟

基于JWT实现注册、登录、全局身份验证

前言

  • JWT,即 JSON Web Token,定义了一种紧凑的、自包含的方式,用于在网络应用环境间以 JSON 对象安全地传输信息。
  • JWT 是一个开放的行业标准 RFC 7519。JWT 传输的信息可以被验证和信任,因为它经过了数字签名。
  • JWT 一般被用来在身份提供者和服务提供者间传递被认证用户的身份信息,以便于从资源服务器获取资源,也可以增加一些额外的业务逻辑所需的声明信息

JWT 的使用场景

  • 认证授权 (Authorization) :

    这是使用 JWT 的最常见场景。一旦用户登录,后续每个请求都将包含 JWT,允许用户访问该 Token 允许的路由、服务和资源。单点登录是现在广泛使用的 JWT 的一个特性,因为它的开销很小,而且可以轻松地跨域使用。

  • 信息交换 (Information Exchange) :

    对于安全的在各方之间传输信息而言,JSON Web Token 无疑是一种很好的方式。因为 JWT 可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

JWT 组成结构

  • 头部-header:声明类型和使用的哈希算法,通常直接使用HMAC SHA256,就是HS256。
{
    "typ": "JWT", // Token类型,一般JWT默认为JWT
    "alg": "HS256" // 签名算法
}

  • 载荷-payload:保存了用户的信息,设置了token的过期时间等

    JWT规定了7个官方字段

    • iss (issuer):签发人
    • exp (expiration time):过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号
    {
        "iss": "jwt.io", // 签发人
        "exp": 1496199995458, // 过期时间
        "name": "sinwaj", //定义私有字段
        "role": "admin"," //定义私有字段
    }
    
    • 签证-Singnature:对前两部分的签名,防止数据篡改;首先,需要指定一个密钥。这个密钥只 有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256);

    • 签名公式如下:

      HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
      

JWT 工作流程

3.png

  • 用户登录:提供用户名和密码;

  • JWT生成token和refresh_token,返回客户端;注意:refresh_token的过期时间长于token的过期时间

  • 客户端保存token和refresh_token,并携带token,请求服务端资源;

  • 服务端判断token是否过期,若没有过期,则解析token获取认证相关信息,认证通过后,将服务器资源返回给客户端;

  • 服务端判断token是否过期,若token已过期,返回token过期提示;

  • 客户端获取token过期提示后,用refresh_token接着继续上一次请求

  • 服务端判断refresh_token是否过期,若没有过期,则生成新的token和refresh_token,并返回给客户端,客户端丢弃旧的token,保存新的token;

  • 服务端判断refresh_token是否过期,若refresh_token已过期,则返回给客户端token过期,需要重新登录的提示。

安装依赖

npm i passport passport-jwt passport-local @nestjs/passport @nestjs/jwt -S
  • 创建 Auth 模块
nest g mo auth
nest g s auth --no-spec
nest g co auth --no-spec
  • 在 auth 文件夹下新增一个 auth.strategy.ts,用于编写 JWT 的验证策略:
/*
 * @Author: vhen
 * @Date: 2023-12-31 03:28:15
 * @LastEditTime: 2024-01-01 13:06:35
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \nest-vhen-blog\src\modules\auth\auth.strategy.ts
 * 
 */

import { ConfigService } from '@nestjs/config'
import { UnauthorizedException, Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { Strategy, ExtractJwt } from 'passport-jwt'
import { AuthService } from './auth.service'

@Injectable()
export class AuthStrategy extends PassportStrategy(Strategy) {
    /**
   * 这里的构造函数向父类传递了授权时必要的参数,在实例化时,父类会得知授权时,客户端的请求必须使用 Authorization 作为请求头,
   * 而这个请求头的内容前缀也必须为 Bearer,在解码授权令牌时,使用秘钥 secretOrKey: 'secretKey' 来将授权令牌解码为创建令牌时的 payload。
   */
    constructor(
        private readonly configService: ConfigService,
        private readonly authService: AuthService,
    ) {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration: false,
            secretOrKey: configService.get('jwt').secret,
        })
    }
    /**
       * validate 方法实现了父类的抽象方法,在解密授权令牌成功后,即本次请求的授权令牌是没有过期的,
       * 此时会将解密后的 payload 作为参数传递给 validate 方法,这个方法需要做具体的授权逻辑,比如这里我使用了通过用户名查找用户是否存在。
       * 当用户不存在时,说明令牌有误,可能是被伪造了,此时需抛出 UnauthorizedException 未授权异常。
       * 当用户存在时,会将 user 对象添加到 req 中,在之后的 req 对象中,可以使用 req.user 获取当前登录用户。
       */
    async validate(payload: { id: number }) {
        const user = await this.authService.validateUser(payload)
        if (!user) throw new UnauthorizedException()
        // return { userId: payload.sub, username: payload.username };
        return user
    }
}
  • auth.controller.ts
/*
 * @Author: vhen
 * @Date: 2023-12-31 13:50:24
 * @LastEditTime: 2024-01-01 12:48:57
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \nest-vhen-blog\src\modules\auth\auth.controller.ts
 * 
 */
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'
import { SigninUserDto } from './dto/signin-user.dto';
import { CreateUserDto } from '../user/dto/create-user.dto';
import { CreateTokenDto } from './dto/create-token.dto';
import { AuthService } from './auth.service';
import { AllowAnon } from '@/common/decorator/allow-anon.decorator'
import { SwaggerApi } from '@/common/decorator/swagger.decorator';
import { ApiResult } from '@/utils/apiResult';
@Controller('auth')
@AllowAnon()
@ApiTags('登录注册')
export class AuthController {
    constructor(private readonly authService: AuthService) { }
    @Post('/signin')
    @ApiOperation({ summary: '登录' })
    @SwaggerApi(CreateTokenDto)
    async signin(@Body() signinUser: SigninUserDto): Promise<ApiResult> {
        return await this.authService.signin(signinUser);
    }
    @Post('/signup')
    @ApiOperation({ summary: '用户注册' })
    @SwaggerApi()
    async signup(@Body() dto: CreateUserDto): Promise<ApiResult> {
        return await this.authService.signup(dto);
    }
}

  • auth.module.ts
/*
 * @Author: vhen
 * @Date: 2023-12-30 17:14:21
 * @LastEditTime: 2023-12-31 15:18:41
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \nest-vhen-blog\src\modules\auth\auth.module.ts
 * 
 */
import { Module, forwardRef } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport'
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { AuthStrategy } from './auth.strategy';
import { UserModule } from '../user/user.module';
import { AuthController } from './auth.controller';


@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    forwardRef(() => UserModule),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => {
        return {
          secret: configService.get('jwt').secret,
        };
      },
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, AuthStrategy],
  exports: [
    AuthService,
    PassportModule],
  controllers: [AuthController]
})
export class AuthModule { }

  • auth.service.ts
/*
 * @Author: vhen
 * @Date: 2023-12-30 17:14:40
 * @LastEditTime: 2024-01-01 12:56:09
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \nest-vhen-blog\src\modules\auth\auth.service.ts
 * 
 */
import { Injectable, Inject, ForbiddenException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { compareSync } from "bcryptjs"
import { User as UserEntity } from '../user/entities/user.entity';
import { UserService } from '../user/user.service';
import { SigninUserDto } from './dto/signin-user.dto';
import { ApiResult } from '@/utils/apiResult';
@Injectable()
export class AuthService {
    constructor(@Inject(UserService) private readonly userService: UserService,
        private readonly jwtService: JwtService,
        private readonly configService: ConfigService) { }
    /**
     * 登录
     * @param signinUser 
     * @returns 
     */
    async signin(signinUser: SigninUserDto): Promise<ApiResult> {
        const user = await this.userService.findOneByUserName(signinUser.username);
        if (!user) {
            return ApiResult.fail(20001, '用户不存在,请注册')
        }
        // 用户密码进行比对
        const checkPassword = await compareSync(signinUser.password, user.password)
        if (!checkPassword) {
            return ApiResult.fail(20002, '用户名或者密码错误')
        }
        if (!user.status) {
            // throw new ForbiddenException('用户未激活,请先激活');
            return ApiResult.fail(20003, '用户未激活,请先激活')
        }
        let data = await this.getToken({ username: user.password, userId: user.userId })
        return ApiResult.success(data);
    }
    /**
     * 注册
     * @param user 
     * @returns 
     */
    async signup(user): Promise<ApiResult> {
        const newUser = await this.userService.create(user);
        return newUser;
    }
    /**
     * 获取token
     * @param payload 
     * @returns 
     */
    async getToken(payload: {
        userId: string,
        username: string
    }) {
        const access_token = `Bearer ${await this.jwtService.signAsync({
            username: payload.username,
            sub: payload.userId,
        }, {
            expiresIn: this.configService.get('jwt.expires')
        })}`
        const refresh_token = await this.jwtService.signAsync({
            username: payload.username,
            sub: payload.userId,
        }, {
            expiresIn: this.configService.get('jwt.refresh')
        })
        return { access_token, refresh_token }
    }
    /**
     * 验证用户
     * @param payload 
     * @returns 
     */
    async validateUser(payload: { id: number }): Promise<UserEntity> {
        return await this.userService.findOneById(payload.id);
    }
}
  • auth.guard.ts守卫
/*
 * @Author: vhen
 * @Date: 2023-12-24 18:46:30
 * @LastEditTime: 2023-12-31 19:23:19
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \nest-vhen-blog\src\common\guard\auth.guard.ts
 * 
 */

import { Reflector } from '@nestjs/core'
import { AuthGuard } from '@nestjs/passport'
import { ExecutionContext, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'
import { UserService } from '@/modules/user/user.service';
import { ALLOW_ANON } from '@/common/decorator/allow-anon.decorator'
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
    constructor(private readonly reflector: Reflector, @Inject(UserService)
    private readonly userService: UserService) {
        super()
    }
    async canActivate(ctx: ExecutionContext): Promise<boolean> {
        const allowAnon = this.reflector.getAllAndOverride<boolean>(ALLOW_ANON, [ctx.getHandler(), ctx.getClass()])
        if (allowAnon) return true
        const req = ctx.switchToHttp().getRequest()
        const accessToken = req.get('Authorization')
        if (!accessToken) throw new ForbiddenException('您还没登录,请先登录')
        const UserId = this.userService.verifyToken(accessToken)
        if (!UserId) throw new UnauthorizedException('当前登录已过期,请重新登录!')
        return this.activate(ctx)

    }
    async activate(ctx: ExecutionContext): Promise<boolean> {
        return super.canActivate(ctx) as Promise<boolean>
    }
}
  • 调用登录接口/api/auth/signin生成access_tokenrefresh_token image.png

  • 使用生成access_token验证/api/v1/user/getUserList接口

  • 这里用 UseGuards 装饰器来使用 jwt 的验证,AuthGuard 的参数是 jwt,表示使用 jwt 的验证。

/*
 * @Author: vhen
 * @Date: 2023-12-21 17:39:47
 * @LastEditTime: 2024-01-01 13:20:31
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \nest-vhen-blog\src\modules\user\user.controller.ts
 */
import { Controller, Inject, Get, Post,UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller({
  path: "user",
  version: '1'
})
@ApiTags("用户管理")
@ApiBearerAuth()
@ApiHeader({
  name: 'Authorization',
  required: true,
  description: '本次请求请带上token',
})
@AllowAnon()
export class UserController {
  constructor(private readonly userService: UserService, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService) { }

  @Get("getUserList")
  @UseGuards(AuthGuard('jwt')) // 使用 'JWT' 进行验证
  findAll(@Query() query: { keyWord: string, page: number, pageSize: number }): Promise<ApiResult> {
    return this.userService.findAll(query);
  }

}

  • 未携带 Authorization

image.png

  • 携带 Authorization

image.png

github

项目地址: nest_vhen_blog