简介
认证JWT令牌登录@nestjs/passportpassport-jwt微信登录
为了让用户更便捷的登录,我们都会采取多种登录方式进行登录,在微信环境下直接使用微信的授权登录,H5则使用密码或验证码登录,内部应用使用JSBridge传递token。当然现在也有一些统一登录的三方可以接入,但是实现一个自己的登录认证也并不麻烦。
其实登录和认证就是两个部分:
- 登录 - 根据登录方式进行成功后则换取JWT令牌(access-token) 传递给前端
- 认证JWT - 前端Headers里携带access-token,nestjs后端认证JWT令牌是否正确
其实你可以这么理解认证:认证也是登录。使用JWT令牌方式进行登录。
准备
在前面一节中已经对NestJS的Guard守卫有了一定的了解,在Nest登录和认证同样是用守卫实现并且引入passport-jwt @nestjs/passport
在这之前肯定要对JWT及passport有一定的了解
关于passport
在使用 NestJS 和 Passport 做认证的时候,*.guard.ts 和 *.strategy.ts 是两个关键的文件,它们分别负责不同的任务:
值得注意的是:passport有默认的几个认证方式 如
jwt和local。当我们使用jwt和local(用户名和密码)的时候,你可以直接使用passport-jwt 和 passport-local,尤其是jwt你可以直接参考官方示例使用即可,它默认给你验证了jwt和返回payload。
*.guard.ts:
定义了一个守卫,用于在请求到达处理程序之前进行验证。
职责 :AuthGuard 是一个守卫(guard),它负责在请求到达处理程序(controller)之前拦截请求,并执行身份验证检查。
实现方式:继承自 AuthGuard('yourAuthName'),其中 'yourAuthName' 是在 *.strategy.ts 中定义的策略名称。
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('yourAuthName') {}
*.strategy.ts:
职责:
Strategy是一个策略(strategy),描述如何提取和验证以及如何处理验证后的用户信息。确保只有经过验证的请求才能访问受保护的资源。
在 passport 策略中,定义 validate 方法;返回值会直接影响到认证过程。如果 validate 方法返回一个用户对象,那么认证将被视为成功,用户信息将被附加到请求对象 req.user上。如果 validate 方法返回 null 或抛出异常,则认证将被视为失败。
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy,"yourAuthName") {
constructor(private readonly configService: ConfigService) {
super();
}
async validate(req: any) {
// TODO 这里要求返回一个user,表示认证成功并默认会附加到请求req.user上,如果返回 null或者抛出异常则认证失败
}
}
Code it
整体逻辑:
- 我们先实现JWT的认证再处理登录,一般大多数接口都是要求登录,那我们就把JWT的认证守卫挂载到全局
app.module.ts上。 - 接着我们要将登录接口屏蔽处理掉,不然登录接口也会要求携带JWT令牌访问。
- 实现具体的登录方法,在具体的controller接口上,增加具体的守卫。接下来以微信为例,其他用户名密码登录做对应调整。
流程图
准备工作
安装 passport
npm install --save @nestjs/passport passport passport-local passport-jwt passport-custom
npm install --save-dev @types/passport-local
JWT认证+允许屏蔽JWT令牌
我们把第一步和第二步一起做的原因是因为为了屏蔽掉JWT令牌的守卫,需要在JWT中增加逻辑。
定义屏蔽
使用SetMetadata设置isPublic = true; 如果Controller带着@Public(),则会将其isPublic=true
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
重头戏:JWT守卫
- 这个守卫实现了canActivate方法,判断isPublic直接返回,达到屏蔽的作用
- 否则检测JWT是否存在,如果存在则尝试使用@nest/jwt进行验证
为什么不直接用passport-jwt的默认strategy?因为除了要处理public屏蔽JWT验证外,我想在token中仅存储有用的信息,而经过auth验证后,会根据token中的payload查询对应用户的权限信息,不会将过多的权限信息存放在JWT-token中,也是为了数据安全。
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from './jwt.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
async canActivate(context: ExecutionContext): Promise<any> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
// 重点是这里,validate 其实是已经验证通过了,return的对象将绑定在req.user上
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
jwt守卫注册
- 在auth.module中引入
import { JwtModule, JwtService } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
global: true,
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '2days' },
}),
inject: [ConfigService],
}),
PassportModule,
],
providers:[JwtStrategy]})
import { JwtAuthGuard } from './auth/jwt.guard';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
]
})
全局绑定jwt.guard
import { JwtAuthGuard } from './auth/jwt.guard';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [AuthModule],
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
]
})
微信登录认证守卫(自定义)
- 定义微信守卫
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class WechatAuthGuard extends AuthGuard('wechat') {}
- 实现微信策略
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-custom';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
// CustomException 是自定义的一个错误,可以参考上一篇文章
import { CustomException } from 'src/common/exception/custom-exception';
@Injectable()
export class WechatStrategy extends PassportStrategy(Strategy, 'wechat') {
constructor(private authService: AuthService) {
super();
}
async validate(req: any): Promise<any> {
const { code } = req.query;
if (code) {
// 这里调用了微信认证方法,通过从url.query中找到code字段进行微信认证
// 具体实现方式,按照自己业务逻辑来,这里只是示例
const user = await this.authService.validateWechat(code);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
throw new CustomException({ message: '缺少code' });
}
}
- 在auth.module.ts中
注册策略
import { WechatStrategy } from './wechat.strategy';
import { PassportModule } from '@nestjs/passport';
@Module({
imports: [
PassportModule,
JwtModule.register({
global: true,
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1days' },
}),
],
providers: [
WechatStrategy,
]
- 在auth.controller.ts中使用守卫
请务必注意使用
守卫以及装饰器的顺序
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
// 注意执行顺序
@Public() // 屏蔽JWT认证
@UseGuards(WechatAuthGuard) // 使用微信认证
@Post('login-wechat')
async loginByWechat(@Request() req) {
// 生成token,这一步可以直接在具体的WechatAuthGuard中做,这里直接return req.token即可
return this.authService.generateToken(req.user);
}
}
需要注意:
各个模块的执行顺序(生命周期)必须要注意。
为什么无法需要在Controller中需要按照顺序定义,如装饰器,守卫,如果顺序错了,那可能导致代码结果和你预想的不一样,甚至路由都到达不了你所自定义的守卫或者拦截器。所以清楚生命周期顺序是很有必要的。
-
定义位置的顺序 全局(app.module.ts / main.ts) -> controller -> route方法
-
各个模块的顺序 (从其他作者偷的图,我就懒得画了) go偷图出处