Nestjs鉴权使用Jwt最佳实践

431 阅读3分钟

为什么要使用Jwt?

总结:获取用户信息不需要每次查询数据!后端可以直接拿密钥解密加密的用户信息。当然这是我的理解,至于官方的语言,请移步GPT

使用

这里我不再赘述项目的创建,环境的配置,假设环境已经配置好,有一个Auth模块

目录结构如下

.
└──src
    ├── auth
    │   ├── auth.controller.ts
    │   ├── auth.guard.ts
    │   ├── auth.module.ts
    │   ├── auth.service.ts
    │   ├── decorators
    │   │   └── public.decorator.ts
    │   ├── dto
    │   │   └── login-auth.dto.ts
    │   ├── entities
    │   │   └── auth.entity.ts
    │   └── strategies
    │       └── jwt.strategy.ts

按照写代码的逻辑,我将从controller层开始,我演示两个接口,一个是login这样的公共接口

公共接口编写(Public装饰器)

// auth.controller.ts

import {Get, Post, Body} from '@nestjs/common';
import {Public} from './decorators/public.decorator';
import {ApiBody, ApiOperation, ApiQuery} from '@nestjs/swagger';
import {LoginAuthDto} from './dto/login-auth.dto';
import {JwtService} from '@nestjs/jwt';

...
@Post('login') // 请求的接口路径
@ApiOperation({summary: "app登录", description: "user login"}) // swagger文档配置
@ApiBody({type: LoginAuthDto}) // 请求携带数据类型
@Public() // 这里是自定义的装饰器,我们需要自己配置。用这个装饰器表示不需要鉴权就可以访问
async login(@Body() req: LoginAuthDto): Promise<any> {
    // 调用service层的逻辑,里面应该是逻辑处理,这里的authServic.login方法是返回数据库的数据
    const {id, userName, openid, phone} = await this.authService.login(req);

    // 这里也可以简写,我这里为了方便阅读,展开来了
    const payload = {
        username: userName,
        id: id,
        openid: openid,
        phone: phone,
    }
    // 返回客户端的数据,access_token
    return {
        access_token: this.jwtService.sign(payload, {secret: process.env.JWT_ACCESS_SECRET})
    }
}

...

...
// 使用鉴权模块
@UseGuards(JwtAuthGuard)
@Get('protocol')
@ApiOperation({summary: "协议", description: "protocol"})
findProtocol(@Req() req) {
    // 使用req.user获取用户信息!
    console.log('protocol controller')
    console.log(req.user)
    console.log('-----------------')
    return this.initService.findProtocol();
}
...

自定义Public装饰器

// public.decorator.ts

// 设置自定义装饰器
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

有了Public装饰器,我们编写拦截验证的代码,我这里没有提到需要安装的库,如果报红直接npm i,这就是我的最佳实践

// auth.guard.ts
import {
    ExecutionContext,
    Injectable,
    UnauthorizedException,
} from '@nestjs/common';
import {Reflector} from '@nestjs/core';

import {IS_PUBLIC_KEY} from './decorators/public.decorator';
import {AuthGuard} from "@nestjs/passport";

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
    constructor(private reflector: Reflector) {
        super();
    }

    canActivate(context: ExecutionContext) {
        // 在这里检查路由是否有 `@Public()` 装饰器
        const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
            context.getHandler(),
            context.getClass(),
        ]);

        // 如果路由是公共的,则跳过 JWT 验证
        if (isPublic) {
            return true;
        }

        // 否则,继续执行 JWT 验证
        return super.canActivate(context);
    }

    handleRequest(err, user, info, context: ExecutionContext) {
        console.log('handleRequest')
        console.log(user)
        // 如果用户不存在且路由不是公共的,抛出异常
        if (err || (!user && !this.isPublic(context))) {
            throw err || new UnauthorizedException();
        }
        return user;
    }

    private isPublic(context: ExecutionContext) {
        const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
            context.getHandler(),
            context.getClass(),
        ]);
        return isPublic;
    }
}
// strategies --> jwt.strategy.ts
import { ConfigService } from '@nestjs/config';
import { UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { StrategyOptions, Strategy, ExtractJwt } from 'passport-jwt';
import {InjectRepository} from "@nestjs/typeorm";
import {User} from "../../user/entities/user.entity";
import {Repository} from "typeorm";
import {UserService} from "../../user/user.service";

export class JwtStorage extends PassportStrategy(Strategy, 'jwt') {
    // 实例化对象
    constructor(
        // 查询数据库的基本操作
        @InjectRepository(User)
        private readonly userRepository: Repository<User>,
        // 访问配置文件,如生成jwt的密钥
        private readonly configService: ConfigService,
        // Service层实例化
        private readonly userService: UserService,
    ) {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            secretOrKey: configService.get('JWT_ACCESS_SECRET'),
        } as StrategyOptions);
    }


    async validate(user: User) {
        // 查询数据库操作,如需要可以自行编写
        const existUser = await this.userService.findBy({phone: user.phone});
        if (!existUser) {
            throw new UnauthorizedException('token不正确');
        }
        return existUser;
    }
}

最后,在模块代码里面注册代码

// auth.module.ts
import {Module} from '@nestjs/common';
import {AuthService} from './auth.service';
import {AuthController} from './auth.controller';
import {UserModule} from 'src/user/user.module';
import {ConfigModule, ConfigService} from '@nestjs/config';
import {JwtModule} from '@nestjs/jwt';
import {RedisModule} from 'src/redis/redis.module';
import {PassportModule} from '@nestjs/passport';
import {JwtAuthGuard} from './auth.guard';
import {JwtStorage} from "./strategies/jwt.strategy";


@Module({
    imports: [
        // 不需要可以直接省略
        UserModule,
        RedisModule,
        PassportModule,
        // 重点是这一块,需要获取环境配置的密钥
        JwtModule.registerAsync({
            imports: [ConfigModule],
            useFactory: async (configService: ConfigService) => ({
                secret: configService.get<string>('JWT_ACCESS_SECRET'),
                signOptions: {
                    expiresIn: configService.get<string>('JWT_EXPIRATION_TIME'),
                }
            }),
            inject: [ConfigService],
        }),
    ],

    controllers: [AuthController],
    providers: [
        // 重点
        JwtAuthGuard,
        AuthService,
        JwtStorage
    ],
    exports: [
        // 重点
        PassportModule,
        JwtModule,
        JwtAuthGuard
    ]
})
export class AuthModule {
}

最后app.module.ts把auth模块注册就好了!

请求成功

image.png

不携带Authorization请求

image.png