引言
最近完成一个turborepo模式下的Next+Nest的博客网站全栈项目,整体写下来感觉还有很多可以修改优化,接下来可能写一些博客理清思路,欢迎大家去star
技术栈介绍
- 前端:
React@19+Next15
+TypeScript
+shadcnUI
- 后端:
NestJS
+TypeScript
+GraphQL
+Prisma
- 身份验证:
JWT
主题
今天主要想讲解一些Nest里JWT的实现过程, 数据库是GraphQL
+ Prisma
,业务逻辑是实现注册和登录,然后去需要token的接口进行jwt验证,这里graphql和prisma的配置就不说了,官网文档其实都有的
支持用户名和密码注册和登录,做一些校验,登录后获取用户信息和 token
存储,登录成功后,跳转到欢迎页面
注册
首先我们先nest g res user
模块,然后创建一个Mutation,用Args去接受前端传来的参数,配置dto
@Resolver(() => User)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Mutation(() => User)
async createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
return await this.userService.create(createUserInput);
}
}
可以用class-validator去做参数验证,记得去main.ts
app.useGlobalPipes(new ValidationPipe());
import { InputType, Int, Field } from '@nestjs/graphql';
import { IsEmail } from 'class-validator';
@InputType()
export class CreateUserInput {
@Field(() => String)
name: string;
@Field()
password: string;
@Field()
@IsEmail()
email: string;
@Field({ nullable: true })
bio?: string;
@Field({ nullable: true })
avatar?: string;
}
然后到service文件里写create,用argon2加密密码,后面会用argon2的vertify验证,然后注册功能就完成了,到浏览器试一下
import { Injectable } from '@nestjs/common';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { PrismaService } from 'src/prisma/prisma.service';
import { hash } from 'argon2';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async create(createUserInput: CreateUserInput) {
const { password, ...user } = createUserInput;
const hashedPassword = await hash(password);
const res = await this.prisma.user.create({
data: {
password: hashedPassword,
...user,
},
});
console.log('🚀 ~ UserService ~ create ~ res:', res);
return res;
}
}
效果
对应的打印也有结果
登录
首先我们先nest g res auth
模块,一样写一个Mutation,需要先通过邮箱去数据库查是否有这个用户,查到在进行登录的token生成
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { AuthService } from './auth.service';
import { SignInInput } from './dto/signin-input';
import { AuthPayload } from './entities/auth-payload.entity';
@Resolver()
export class AuthResolver {
constructor(private readonly authService: AuthService) {}
@Mutation(() => AuthPayload)
async signIn(@Args('signInInput') signInInput: SignInInput) {
const user = await this.authService.validateLocalUser(signInInput);
return await this.authService.login(user);
}
}
Entity,这是返回值
import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class AuthPayload {
@Field(() => Int)
id: number;
@Field()
name: string;
@Field({ nullable: true })
avatar?: string;
@Field()
accessToken: string;
}
Dto,邮箱作为账号,加上密码
import { Field, InputType } from '@nestjs/graphql';
import { IsString, MinLength } from 'class-validator';
@InputType()
export class SignInInput {
@Field()
email: string;
@Field()
@IsString()
@MinLength(1)
password: string;
}
service文件,validateLocalUser去验证是否有这个用户,并且验证verify密码是否正确,如果有返回user的信息去登录,没有就返回UnauthorizedException
import { verify } from 'argon2';
async validateLocalUser({ email, password }: SignInInput) {
const user = await this.prisma.user.findUnique({
where: {
email,
},
});
if (!user) throw new UnauthorizedException('User not found');
const matchedUser = await verify(user.password!, password);
if (!matchedUser) throw new UnauthorizedException('Invalid password');
return user;
}
通过之后在去调用login,因为我们返回是需要生成token的,所以需要安装@nestjs/jwt来生成,npm install @nestjs/jwt
,需要去auth.module去import注册这个jwtService,主要配置secret密钥和expiresIn过期时间,这两个内容没有什么要求,随机生成一个就可以,密钥生成
JWT_SECRET=e55a10d70db7d94593ce3275cc712fe79cbd2c5be13a19ce26e826f081a78
JWT_EXPRISEIN=24h
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPRISEIN'),
},
}),
}),
],
providers: [
AuthResolver,
AuthService,
PrismaService,
],
controllers: [AuthController],
})
export class AuthModule {}
到login,根据userId生成token或者其他验证用户的参数,返回token给前端,我们到浏览器试一下
async generateToken(userId: number) {
const payload: AuthJwtPayload = { sub: userId };
const accessToken = this.jwtService.sign(payload);
return { accessToken };
}
async login(user: User) {
const { accessToken } = await this.generateToken(user.id);
return {
id: user.id,
name: user.name,
avatar: user.avatar,
accessToken,
};
}
效果和打印
这样登录和注册功能就完成了,现在我们去用户文章列表,需要登录并且token有效才可以访问
passport
一样我们nest g res post模块,写getUserPost,获取自己的文章列表,这里需要实现就是我们要查到这个用户的信息,那么我们需要知道这个用户的userId,那么我们从请求头的 Authorization 字段提取Token,解析获取id,基本的流程是这样的,nestjs去实现我们需要通过passport
这个实现过程需要我们来倒推,可以看到代码中使用了守卫Guard,guard一般我们是用于请求认证或者鉴权的,所以回去把jwt的验证放在这样,一般我们会从guard中去获取context里的req
npm install @nestjs/passport passport-jwt
@Query(() => [Post])
@UseGuards(JwtAuthGuard)
getUserPost(
@Context() context,
@Args('skip', { nullable: true, type: () => Int }) skip?: number,
@Args('take', { nullable: true, type: () => Int }) take?: number,
) {
const userId = context.req.user.id;
return this.postService.findPostByUser({
userId,
skip: skip ?? 1,
take: take ?? DEFAULT_PAGE_SIZE,
});
}
我们nest g guard jwt-auth的guard文件,guards/jwt.strategy.ts,然后我们去修改文件的默认代码,我们需要将JwtAuthGuard 继承自 AuthGuard('jwt'),触发 Passport 的 JWT 策略
我用的是GraphQL,所以需要从GqlExecutionContext创建context
如果我们用的是mysql或者pg的数据库,是可以直接从canActivate(context: ExecutionContext)直接获取的
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
// JwtAuthGuard 继承自 AuthGuard('jwt'),触发 Passport 的 JWT 策略:
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req; // 从 GraphQL 上下文提取请求对象
}
}
strategy
我们去写strategy策略,可以建一个strategies/jwt.strategy.ts,这里面有一些PassportStrategy的配置,一样我们要将JwtStrategy去继承PassportStrategy
- fromAuthHeaderAsBearerToken从请求头的 Authorization 字段提取 Bearer Token,自动验证
- ignoreExpiration: false, 会拒绝过期令牌
- 自动触发validate,我们拿到jwt解密后的数据,之前是用userId加密,现在在拿userId去验证User
我们需要@Injectable()
声明,让其变成可以被注入的类(依赖)
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthJwtPayload } from '../types/auth-jwtPayload';
import { AuthService } from '../auth.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private configService: ConfigService,
private authService: AuthService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>('JWT_SECRET'),
ignoreExpiration: false, //会拒绝过期令牌
});
}
// 解密后的 JWT 载荷(Payload)传入 validate 方法
// 返回的用户对象会被附加到 req.user,供后续逻辑使用。
// validate 返回 null 或抛出异常 → 认证失败。
validate(payload: AuthJwtPayload) {
const userId = payload.sub;
return this.authService.jwtValidateUser(userId);
}
}
auth.service里写一下jwtValidateUser,通过在返回id,返回的用户对象会被附加到 req.user,供后续逻辑使用
async jwtValidateUser(userId: number) {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) throw new UnauthorizedException('User not found');
const currentUser = { id: user.id };
return currentUser;
}
然后我们JwtStrategy使用了authService,记得去auth.module去把JwtStrategy添加到providers,我们去graphql试一下,去postman也一样的
providers: [
AuthResolver,
AuthService,
PrismaService,
JwtStrategy,
GoogleStrategy,
],
效果
至此,JWT守卫的功能就全部实现,这是一种登录方式,平时登录方式其实还会去接三方,像谷歌、GitHub等
基本原理是一致的,代码在这里,欢迎大家去star
谷歌登录部分代码
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { AuthService } from '../auth.service';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
constructor(
private configServie: ConfigService,
private readonly authService: AuthService,
) {
super({
clientID: configServie.get<string>('GOOGLE_CLIENT_ID'),
clientSecret: configServie.get<string>('GOOGLE_CLIENT_SECRET'),
callbackURL: configServie.get<string>('GOOGLE_CALLBACK_URL'),
proxy: true,
scope: ['profile', 'email'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
) {
const user = await this.authService.validateGoogleUser({
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photosp[0].value,
password: '',
});
done(null, user);
}
}