尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

NestJS 入门(3):Guard 如何挡住未登录请求?

NestJS 入门(3):Guard 如何挡住未登录请求? 上一篇NestJS 入门2依赖注入到底解决了什么问题 讲完了「对象怎么接到一起」。接下来更贴近业务的问题是有些接口谁都能调有些接口必须先登录。Nest 把这道门放在哪里答案是Guard守卫。实战里最常见的门是JWT Passport这一套。本文会先讲清 Guard 在链路中的位置再介绍相关插件各自干什么最后用一个最小可跑例子把「登录发 token → 带 token 访问受保护接口」串起来。1. Guard 在请求链路里的位置一个请求进 Nest 后大致顺序是请求进来 → 中间件Middleware → Guard能不能进 → 管道Pipe参数校验/转换 → Controller 方法 → Service 业务逻辑Guard 回答的是一个很单纯的问题这个请求允不允许继续往下走允许 → 进入 Controller不允许 → 直接返回 401 / 403业务代码根本不会执行这和 Service 里的业务判断不一样位置典型问题Guard你是谁有没有登录有没有权限Service密码对不对这个项目属不属于你数据怎么改鉴权门槛尽量放在 Guard业务规则放在 Service。2. JWT 相关插件各自干什么Nest 官方没有「只装一个包就搞定登录」。常见组合是下面几个职责要分清npmi nestjs/jwt nestjs/passport passport passport-jwtnpmi-Dtypes/passport-jwt包角色你平时会碰到它的地方nestjs/jwtNest 封装的 JWT 工具JwtModule.register(...)、注入JwtService去sign/verifynestjs/passport把 Passport 接到 NestPassportModule、AuthGuard(jwt)、PassportStrategypassportNode 生态里的认证框架一般不直接写很多代码但是底层依赖passport-jwtPassport 的 JWT 策略实现Strategy、ExtractJwt.fromAuthHeaderAsBearerToken()types/passport-jwtTypeScript 类型开发期类型提示可以记成一句话nestjs/jwt负责签发/校验 tokenpassportpassport-jwt负责「从请求里取出 token 并按策略认证」nestjs/passport把这套策略接到 Nest 的 Guard 上。和登录常一起出现、但不属于 JWT 本体的包作用bcrypt密码哈希与比对存库用哈希登录时compare它们在 Module 里通常这样装配import{Module}fromnestjs/common;import{JwtModule}fromnestjs/jwt;import{PassportModule}fromnestjs/passport;Module({imports:[PassportModule,JwtModule.register({secret:process.env.JWT_SECRET||your-secret-key,signOptions:{expiresIn:30m},}),],// controllers / providers ...})exportclassAuthModule{}PassportModule启用 Passport 集成JwtModule.register({ secret, signOptions })配置签名密钥和默认过期时间并提供可注入的JwtService3. 最直观的用法UseGuards登录接口本身不需要登录「我是谁」接口必须登录Controller(api/auth)exportclassAuthController{constructor(privatereadonlyauthService:AuthService){}Post(login)login(Body()body:{email:string;password:string}){returnthis.authService.login(body.email,body.password);}UseGuards(JwtAuthGuard)Get(me)me(Headers(authorization)auth:string){consttokenauth?.replace(Bearer ,);returnthis.authService.validateToken(token);}}对比一下POST /api/auth/login没有 Guard任何人都能尝试登录GET /api/auth/me挂了JwtAuthGuard没带有效 token 进不来项目列表这类业务接口也一样UseGuards(JwtAuthGuard)Get()findAll(Request()req:AuthenticatedRequest){constuserIdreq.user?.userId;returnthis.projectsService.findAll(userId);}先过 Guard再拿当前用户去查「属于我的项目」。4. 方法级 vs 控制器级Guard 可以挂在单个方法上也可以挂在整个 Controller上。方法级上面那些例子只保护某一个接口。控制器级这个 Controller 里所有接口默认都要登录Controller()UseGuards(JwtAuthGuard)exportclassTaskPromptsController{constructor(privatereadonlyservice:TaskPromptsService){}Get(api/projects/:projectId/task-prompts)list(Param(projectId)projectId:string){returnthis.service.listByProject(projectId);}Put(api/projects/:projectId/task-prompts/:templateKey)saveDraft(/* ... */){// ...}}怎么选只有少数接口要登录 → 方法级更清晰整个模块几乎都要登录 → 控制器级少写很多重复装饰器5.JwtAuthGuard为什么看起来这么短真实代码里Guard 本身可能只有一行import{Injectable}fromnestjs/common;import{AuthGuard}fromnestjs/passport;Injectable()exportclassJwtAuthGuardextendsAuthGuard(jwt){}这里用的是nestjs/passport提供的AuthGuard。jwt这个名字对应的是passport-jwt策略。真正的校验逻辑在Strategy里import{Injectable}fromnestjs/common;import{PassportStrategy}fromnestjs/passport;import{ExtractJwt,Strategy}frompassport-jwt;Injectable()exportclassJwtStrategyextendsPassportStrategy(Strategy){constructor(){super({jwtFromRequest:ExtractJwt.fromAuthHeaderAsBearerToken(),ignoreExpiration:false,secretOrKey:process.env.JWT_SECRET||your-secret-key,});}asyncvalidate(payload:any){return{userId:payload.sub,email:payload.email,name:payload.name};}}对照插件代码来自哪个包AuthGuard(jwt)nestjs/passportPassportStrategy(...)nestjs/passportStrategy/ExtractJwtpassport-jwt职责拆开角色干什么JwtAuthGuard声明这里用名为jwt的认证策略守门JwtStrategy从 Header 取 Bearer token、验签、解析 payloadvalidate()验签通过后把用户信息整理出来挂到请求上AuthGuard(jwt)里的jwt必须和策略名对得上。Nest passport-jwt的默认约定里策略名就是jwt。另外secretOrKey要和JwtModule.register({ secret })、登录时JwtService.sign用的密钥一致否则「能签发、验不过」。6. 通过 Guard 之后用户信息在哪里Strategy 的validate()返回值会被放到request.user上。所以受保护接口可以这样取当前用户interfaceAuthenticatedRequestextendsExpressRequest{user?:{userId:string;email:string;name:string};}UseGuards(JwtAuthGuard)Get(generation-preferences)getGenerationPreferences(Request()req:AuthenticatedRequest){constuserIdreq.user?.userId;if(!userId){thrownewUnauthorizedException(Unauthorized);}returnthis.authService.getGenerationPreferences(userId);}链路可以记成Authorization: Bearer token → JwtStrategy 取出并校验 tokenpassport-jwt → validate(payload) 返回 { userId, email, name } → 写入 req.user → Controller 用 Request() 读取Guard 负责「放行」Controller / Service 负责「用这个身份继续办事」。7. 签发 tokenJwtService用在 Service 里Guard / Strategy 管「验票」登录成功后「出票」通常在 Service注入nestjs/jwt的JwtServiceInjectable()exportclassAuthService{constructor(privatereadonlyjwtService:JwtService){}privateasyncgenerateTokens(user:{id:string;email:string;name:string}){constpayload{sub:user.id,email:user.email,name:user.name};constaccessTokenthis.jwtService.sign(payload,{expiresIn:30m,});constrefreshTokenthis.jwtService.sign(payload,{expiresIn:7d,});return{accessToken,refreshToken};}}常见分工动作常用 API出现位置登录成功发 tokenjwtService.sign(payload)AuthService手动校验 tokenjwtService.verify(token)少数自定义逻辑请求自动验票Strategy Guard受保护接口入口生产里 access token 短、refresh token 长是很常见的组合细节可以后续再展开入门先抓住「谁签发、谁校验」。8. 还要在 Module 里注册和上一篇 DI 一样Guard、Strategy 也是 Provider要登记。Module({imports:[PassportModule,JwtModule.register({secret:process.env.JWT_SECRET||your-secret-key,signOptions:{expiresIn:30m},}),],controllers:[AuthController],providers:[AuthService,JwtStrategy,JwtAuthGuard],exports:[AuthService,JwtAuthGuard],})exportclassAuthModule{}实用提醒JwtStrategy必须进providers否则 Guard 找不到策略其他模块要用UseGuards(JwtAuthGuard)通常需要 Auth 模块exports这个 GuardJwtModule的secret与JwtStrategy的secretOrKey必须一致nestjs/jwt、nestjs/passport、passport-jwt缺一个链路就接不上9. 可选登录OptionalJwtAuthGuard有些接口「登录了更好不登录也能看」。这时可以做可选 GuardInjectable()exportclassOptionalJwtAuthGuardextendsAuthGuard(jwt){handleRequest(err:any,user:any){returnuser;}}和强制 Guard 的差别在于校验失败时不直接把请求打死而是可能让user为空继续往下走。业务代码再判断有req.user就按登录态处理没有就按游客处理。入门阶段先掌握强制登录的JwtAuthGuard即可。10. 对照一张完整链路图1) POST /api/auth/login → AuthService 校验账号密码可用 bcrypt → JwtService.sign(...) 签发 accessToken / refreshToken 2) GET /api/projects Header: Authorization: Bearer accessToken → JwtAuthGuardnestjs/passport → JwtStrategypassport-jwt验签 → validate() 写入 req.user → Controller / Service 使用 userId如果第 2 步没有 token、token 过期、或签名不对Guard / Strategy 拦下请求Controller 根本不会执行11. 实战最小可跑的 JWT 登录 受保护接口下面用最少文件搭一条能跑通的链路。目标只有两个接口POST /auth/login账号密码正确 → 返回 tokenGET /auth/profile必须带Authorization: Bearer token11.1 安装依赖npmi nestjs/jwt nestjs/passport passport passport-jwtnpmi-Dtypes/passport-jwt11.2auth.module.ts装配插件import{Module}fromnestjs/common;import{JwtModule}fromnestjs/jwt;import{PassportModule}fromnestjs/passport;import{AuthController}from./auth.controller;import{AuthService}from./auth.service;import{JwtStrategy}from./jwt.strategy;import{JwtAuthGuard}from./jwt-auth.guard;Module({imports:[PassportModule,JwtModule.register({secret:dev-secret,// 实战请改成环境变量signOptions:{expiresIn:30m},}),],controllers:[AuthController],providers:[AuthService,JwtStrategy,JwtAuthGuard],})exportclassAuthModule{}记得在AppModule里imports: [AuthModule]。11.3auth.service.ts校验账号并签发 tokenimport{Injectable,UnauthorizedException}fromnestjs/common;import{JwtService}fromnestjs/jwt;Injectable()exportclassAuthService{// 演示用内存用户真实项目换成数据库privatereadonlyusers[{id:1,email:demoexample.com,password:password123,name:Demo},];constructor(privatereadonlyjwtService:JwtService){}login(email:string,password:string){constuserthis.users.find((u)u.emailemail);if(!user||user.password!password){thrownewUnauthorizedException(Invalid credentials);}constpayload{sub:user.id,email:user.email,name:user.name};constaccessTokenthis.jwtService.sign(payload);return{accessToken,user:{id:user.id,email:user.email,name:user.name},};}}说明这里为了最短路径用了明文密码。真实项目请用bcrypt.hash/bcrypt.compare。11.4jwt.strategy.ts从 Header 取票并验票import{Injectable}fromnestjs/common;import{PassportStrategy}fromnestjs/passport;import{ExtractJwt,Strategy}frompassport-jwt;Injectable()exportclassJwtStrategyextendsPassportStrategy(Strategy){constructor(){super({jwtFromRequest:ExtractJwt.fromAuthHeaderAsBearerToken(),ignoreExpiration:false,secretOrKey:dev-secret,// 必须和 JwtModule.register 的 secret 一致});}validate(payload:{sub:string;email:string;name:string}){return{userId:payload.sub,email:payload.email,name:payload.name};}}11.5jwt-auth.guard.ts一道门import{Injectable}fromnestjs/common;import{AuthGuard}fromnestjs/passport;Injectable()exportclassJwtAuthGuardextendsAuthGuard(jwt){}11.6auth.controller.ts公开登录 受保护资料import{Body,Controller,Get,Post,Request,UseGuards}fromnestjs/common;import{AuthService}from./auth.service;import{JwtAuthGuard}from./jwt-auth.guard;Controller(auth)exportclassAuthController{constructor(privatereadonlyauthService:AuthService){}Post(login)login(Body()body:{email:string;password:string}){returnthis.authService.login(body.email,body.password);}UseGuards(JwtAuthGuard)Get(profile)profile(Request()req:{user:{userId:string;email:string;name:string}}){returnreq.user;}}11.7 用 curl 验证登录curl-XPOST http://localhost:3000/auth/login\-HContent-Type: application/json\-d{\email\:\demoexample.com\,\password\:\password123\}把返回里的accessToken拷出来再访问受保护接口curlhttp://localhost:3000/auth/profile\-HAuthorization: Bearer 粘贴你的accessToken预期带正确 token → 返回{ userId, email, name }不带 token / token 错误 → 401进不了profile方法如果这一步跑通了你就已经同时练到了nestjs/jwt签发passport-jwt Strategy 校验nestjs/passport的 Guard 守门Controller 从req.user取当前用户12. 小结Guard 决定请求能不能进入ControllerJWT 常见插件分工nestjs/jwt出票/验票工具passport-jwt策略实现nestjs/passport接到 Nest GuardUseGuards(JwtAuthGuard)可挂在方法或整个 ControllerJwtModule.secret与JwtStrategy.secretOrKey必须一致登录用JwtService.sign访问受保护接口靠 Strategy Guard 自动验票并写入req.user对照前两篇请求相关的问题可以再多一句哪个 Controller 接请求哪个 Service 做业务哪个 Module 组装它们依赖从哪里注入这个接口有没有 GuardJWT 相关插件在这条链路上各负责哪一段下一篇可以讲统一响应与异常处理——为什么业务代码抛UnauthorizedException前端却总能收到结构一致的错误包。系列导航上一篇NestJS 入门2依赖注入到底解决了什么问题第一篇NestJS 入门1先搞懂 Module、Controller、Service
返回列表