当然!我们来攻克任何严肃应用都必须跨过的第一道大门:认证 (Authentication)。这就像是为你的应用配备一位严谨可靠的“门禁保安”,他的唯一职责就是回答一个问题:“你是谁?请证明你的身份。”
为你的应用配备“门禁保安”:NestJS 认证 (Authentication) 完全指南
想象你的应用是一个高度机密的科研基地。
不是任何人都能随意进出。在基地大门口,站着一位尽职尽责的保安。每当有人试图进入时,保安都会执行一套严格的程序:
- 出示凭证:访客需要出示他的“身份证”或“通行卡”(在 Web 世界里,这通常是用户名/密码,或者一个令牌 Token)。
- 验证凭证:保安会拿出验证设备,检查这张卡的真伪(比如,验证密码是否正确,或令牌是否由基地内部签发且未过期)。
- 放行或拒绝:
- 如果验证通过,保安会点点头,并在访客身上贴一个“已认证”的标签(将用户信息附加到请求对象上),然后放行。
- 如果验证失败,保安会立刻将其拦下(返回一个
401 Unauthorized
错误)。
这个验证身份的过程,就是认证 (Authentication)。
它与授权 (Authorization) 是两个不同的概念:
- 认证 (Authentication):回答“你是谁?”。比如,确认你是“张三研究员”。
- 授权 (Authorization):回答“你能做什么?”。比如,确认“张三研究员”有权限进入 A 级实验室,但没有权限进入 S 级核心区。授权通常在认证之后进行。
NestJS 作为一个模块化的框架,本身不强制任何特定的认证策略。但它与著名的 Node.js 认证库 Passport 进行了深度、优雅的集成。@nestjs/passport
模块让我们可以用一种非常“NestJS-Style”的方式来实现各种复杂的认证流程。
1. 认证的核心思想:Passport 的“策略”模式
Passport 的设计非常巧妙,它把每一种认证方式(如用户名密码、JWT、OAuth 等)都看作是一种独立的策略 (Strategy)。
- 你想用用户名/密码登录?那就使用
passport-local
策略。 - 你想用 JWT (JSON Web Token) 进行无状态认证?那就使用
passport-jwt
策略。 - 你想支持微信或 GitHub 登录?那就使用
passport-weixin
或passport-github
策略。
你需要做的,就是选择并配置你需要的策略,然后告诉 Passport 在何时使用它们。
2. 实战:搭建经典的 JWT 认证流程
JWT (JSON Web Token) 是现代 Web API 中最流行的无状态认证方案。
流程简介:
- 用户使用用户名和密码进行登录。
- 服务器验证通过后,不使用 Session,而是生成一个加密签名的 JWT(一个长字符串),并将其返回给客户端。
- 客户端(如前端应用)将这个 JWT 存储起来(通常在
localStorage
或Authorization
Header 中)。 - 之后,客户端在每一次请求需要认证的接口时,都必须在
Authorization
请求头中带上这个 JWT,格式为Bearer <token>
。 - 服务器端的“门禁保安”(JWT 策略守卫)会拦截请求,提取并验证这个 JWT 的签名和有效期。验证通过则放行。
第一步:安装“保安系统”和“令牌工具”
# NestJS 的 Passport 和 JWT 模块
npm install @nestjs/passport @nestjs/jwt passport passport-jwt
# 类型定义文件
npm install -D @types/passport-jwt
第二步:配置“令牌签发中心” (AuthModule
和 JwtModule
)
src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module'; // 假设我们有一个 UsersModule
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy'; // 稍后创建
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
UsersModule,
PassportModule,
// 注册 JwtModule,并进行配置
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'), // 签名的密钥,必须保密!
signOptions: { expiresIn: '60m' }, // 令牌有效期 60 分钟
}),
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy], // 注册 AuthService 和我们的 JWT 策略
})
export class AuthModule {}
JwtModule.registerAsync
: 我们从ConfigService
动态获取JWT_SECRET
,这是安全最佳实践。这个secret
是用来给 JWT 签名的,如果泄露,任何人都可以伪造你的令牌。
第三步:创建“登录服务”和“令牌签发逻辑” (AuthService
)
AuthService
负责核心的业务逻辑:验证用户身份,并签发 JWT。
src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService // 注入 JwtService
) {}
// 1. 验证用户密码 (实际项目中密码应被哈希存储和比较)
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
// 简化比较,实际应使用 bcrypt.compare
const { password, ...result } = user;
return result;
}
return null;
}
// 2. 登录成功后,生成 JWT
async login(user: any) {
// payload 是你想放在 JWT 中的信息
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
第四步:实现 JWT 策略 (JwtStrategy
) - “保安的验证手册”
JwtStrategy
告诉 Passport 如何从请求中提取 JWT,以及如何验证它。
src/auth/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../users/users.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly configService: ConfigService,
private readonly usersService: UsersService,
) {
super({
// 1. 指定从何处提取 JWT
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// 2. 忽略过期检查 (false 表示不忽略,由 Passport 自动处理)
ignoreExpiration: false,
// 3. 提供用于验证签名的密钥
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
// 4. Passport 在验证 JWT 签名和有效期后,会调用这个 validate 方法
// payload 是从 JWT 中解码出的 JSON 对象
async validate(payload: { sub: number; username: string }) {
// 在这里,你可以根据 payload 中的信息(如 userId)去数据库查找完整的用户信息
const user = await this.usersService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException();
}
// 这个方法返回的对象,将被附加到 request.user 上
return user;
}
}
第五步:部署“门禁保安” (AuthGuard
)
现在,我们有了所有的部件,只需要在需要保护的路由上,部署一个“门禁保安”——AuthGuard
。
src/users/users.controller.ts
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('users')
export class UsersController {
// 使用 @UseGuards() 并传入 AuthGuard('jwt')
// 这会激活我们编写的 JwtStrategy
@UseGuards(AuthGuard('jwt'))
@Get('profile')
getProfile(@Request() req) {
// 因为 JwtStrategy 的 validate 方法返回了 user 对象
// 所以现在我们可以通过 req.user 来访问它
return req.user;
}
}
现在,任何未提供有效 JWT 的请求,在访问 /users/profile
时,都会被 AuthGuard
自动拦截,并返回 401 Unauthorized
错误。
4. 中国企业级方案的思考
虽然标准的 JWT 流程非常通用,但在中国的复杂业务场景下,企业往往会构建更精细、更安全的认证体系。
痛点与演进:
- JWT 的“无状态”困境: JWT 一旦签发,在它过期之前,服务器是无法使其失效的。如果用户修改了密码,或者管理员想强制某个用户下线,标准的 JWT 方案做不到。
- 多设备登录管理: 如何管理一个用户在 App, Web, 小程序等多个设备上的登录状态?如何允许用户“踢掉”其他设备?
- SSO (单点登录): 在企业内部,通常需要一个统一的认证中心(如基于 OAuth2/OIDC 的身份提供商),用户只需登录一次,就可以访问所有相互信任的应用。
企业级方案 Demo:JWT + Refresh Token + Redis
这是一个非常流行的、兼顾了无状态和可控性的增强方案。
- 登录时: 服务器签发两个 Token:
- Access Token: 和我们上面实现的一样,生命周期很短(如 15 分钟)。它用于访问受保护的资源。
- Refresh Token: 一个随机生成的、无意义的长字符串,生命周期很长(如 7 天或 30 天)。它只用于获取新的 Access Token。
- 存储:
- 客户端存储 Access Token 和 Refresh Token。
- 服务器在 Redis 中,以
userId
为键,存储一份 Refresh Token 的白名单(或黑名单)。
- 请求时:
- 客户端使用 Access Token 访问 API。
- Access Token 过期后:
- 客户端发现 Access Token 过期(收到 401 错误),就会调用一个专门的
/auth/refresh
接口,并带上那个长周期的 Refresh Token。 - 服务器收到 Refresh Token 后,去 Redis 中校验它是否存在且有效。
- 校验通过后,服务器签发一个新的 Access Token 和新的 Refresh Token,返回给客户端。客户端用新的替换掉旧的。
- 客户端发现 Access Token 过期(收到 401 错误),就会调用一个专门的
- 强制下线/修改密码:
- 只需要从 Redis 中将该用户的 Refresh Token 删除即可。这样,即使用户的旧 Refresh Token 还在有效期内,他也无法再用它来换取新的 Access Token,从而被强制下线。
伪代码:
AuthService.refreshToken()
:
async refreshToken(userId: number, oldRefreshToken: string) {
// 1. 从 Redis 中获取该用户的有效 refresh token
const validToken = await this.redisClient.get(`refresh_token:${userId}`);
// 2. 校验传入的 token 是否匹配
if (!validToken || validToken !== oldRefreshToken) {
throw new UnauthorizedException('Invalid refresh token');
}
// 3. 签发新的 Access Token 和 Refresh Token
const newAccessToken = this.jwtService.sign({ sub: userId });
const newRefreshToken = generateRandomString(); // 生成新的 Refresh Token
// 4. 将新的 Refresh Token 更新到 Redis 中
await this.redisClient.set(`refresh_token:${userId}`, newRefreshToken, { EX: 7 * 24 * 3600 }); // 7天有效期
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}
这种模式在保持了 JWT 高性能、跨域友好的优点的同时,通过引入 Redis 作为状态存储,巧妙地解决了 JWT 无法主动失效的问题,是目前构建可控、安全、用户体验良好的认证系统的最佳实践之一。
总结
认证是应用的“门禁系统”,是安全的第一道关卡。
- 核心工具: NestJS 通过
@nestjs/passport
与 Passport.js 强强联合,使用策略 (Strategy) 模式来应对各种认证需求。 - 主流方案: JWT (JSON Web Token) 是现代无状态 API 的首选认证方案。
- 实现步骤: 配置
JwtModule
-> 实现JwtStrategy
-> 在需要保护的路由上使用AuthGuard('jwt')
。 - 企业级演进: 面对 JWT 无法失效的问题,采用 JWT + Refresh Token + Redis 的组合方案,是兼顾性能、安全和用户体验的行业标准。
掌握了认证,你就掌握了控制谁可以进入你的应用,以及他们进来后是谁的关键权力。