JWT+Passport.js身份验证实现完整步骤
作者:qq_36437172
前言
在互联网应用开发中,「身份验证」「权限控制」「跨系统登录」是保障安全与用户体验的核心需求。在现代 Web 应用中,登录注册是用户使用系统的入口,而 JWT+Passport.js 实现身份验证、RBAC 落地权限控制、Redis 支撑单点登录(SSO),共同构成完整的用户身份管理体系。
一、基础概念
1. 身份验证 vs 授权
概念 | 核心目标 | 常见场景/方式 |
---|---|---|
身份验证 | 确认当前用户信息(第一道防线) | 账号密码、手机验证码、人脸/指纹登录 |
授权 | 用户允许第三方(App/小程序)访问特定资源 | 1. 登录微信时授权昵称/手机号; 2. 安装 App 授权相机/位置权限 |
2. 凭证
- 作用:连接「身份验证」与「授权」的桥梁,用于标识用户身份。
- 互联网场景:用户登录后,服务器会返回
SessionID
(会话ID)或Token
(令牌);后续请求携带这个凭证,服务器才能识别身份并判断是否有权操作。
3. Cookie vs Session:传统会话维持方案
传统 Web 应用常用这对组合维持用户登录状态,但存在明显差异:
对比维度 | Cookie(客户端存储) | Session(服务端存储) |
---|---|---|
存储位置 | 浏览器(文本文件,≤4KB) | 服务器(数据库/内存,无大小限制) |
安全性 | 可被用户修改,风险高 | 用户无法直接访问,更安全(需防CSRF) |
生命周期 | 可设长有效期(如“记住登录”) | 短,关闭浏览器/超时后失效 |
核心问题:Session依赖服务端存储,多服务器部署时需同步会话(如用Redis);Cookie受同源策略限制,跨域名无法共享——这也是后续引入Token的原因。
4. Token vs JWT:无状态身份凭证
为解决Session的“服务端存储依赖”,无状态的Token应运而生,而JWT是Token的一种标准化实现。
- Token:加密字符串,会话信息编码在 Token 中,服务器无需存Session,仅解析 Token 即可验证身份(如访问令牌、刷新令牌)。
类型 | 作用 | 核心流程 |
---|---|---|
访问令牌 | 授权访问特定资源/操作(有效期短) | 登录请求→服务器验证→发令牌→客户端存令牌→请求带令牌→服务器验证 |
刷新令牌 | 刷新过期访问令牌(有效期长,免重登) | 访问令牌过期→带刷新令牌求新令牌→服务器验证后发新令牌 |
- JWT:遵循RFC 7519标准,基于JSON结构,自身加密存储用户信息(如用户ID、角色),服务器解密后直接验证,无需查数据库,效率更高。
JWT核心优势:服务端无状态、支持跨域、验证效率高,是当前分布式应用的首选身份凭证。
二、登录注册与Passport+JWT的关系
1. 核心职责划分
- 注册:用户提交账号密码,系统验证合法性后创建用户记录(存数据库)
- 登录:用户提交账号密码,系统验证通过后生成 JWT
令牌(身份凭证) - Passport+JWT:拦截请求,验证 JWT 令牌有效性,确认用户身份并赋予访问权限
2. 完整流程可视化
flowchart LR A[用户] -->|1. 注册| B[提交账号密码] B --> C{验证合法性 (用户名唯一等)} C -->|合法| D[创建用户记录 (密码加密存储)] A -->|2. 登录| E[提交账号密码] E --> F{验证账号密码 (与数据库比对)} F -->|通过| G[生成JWT令牌 (包含用户ID等信息)] A -->|3. 访问受保护接口| H[请求头携带JWT] H --> I[Passport拦截请求] I --> J[JWT策略验证令牌] J -->|有效| K[解析用户信息 (附在req.user)] K --> L[接口处理逻辑] J -->|无效| M[返回401未授权]
三、代码实现
Passport.js是Node.js生态最流行的身份验证中间件,支持“本地账号密码”“OAuth”“JWT”等多种策略。我们以NestJS(Node.js框架)为例,一步步实现JWT认证。
1. 核心原理
Passport.js基于“策略模式”扩展验证方式:
- 用
passport-local
策略处理“账号密码登录”,验证通过后生成JWT; - 用
passport-jwt
策略处理“接口访问验证”,解析请求头中的JWT,确认用户身份。
2. 安装依赖
# 核心依赖:Passport本地策略、JWT策略、Nest集成包、加密 pnpm add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt bcryptjs # 类型定义(开发依赖) pnpm add -D @types/passport-local @types/passport-jwt @types/bcryptjs
3. 实现注册功能
注册核心是合法验证与密码加密,避免明文存储密码导致安全风险。注册生成的加密用户记录是登录验证的唯一依据——没有注册的用户无法通过登录获取JWT令牌。
// src/auth/auth.controller.ts import { Controller, Post, Body, HttpException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { genSalt, hash } from 'bcryptjs'; import { User } from '../users/entities/user.entity'; // 用户实体(含id/username/password/role) @Controller('auth') export class AuthController { constructor( @InjectRepository(User) private userRepo: Repository<User>, ) {} // 注册接口 @Post('register') async register( @Body() dto: { username: string; password: string }, ) { // 1. 验证用户名是否已存在(防重复注册) const existUser = await this.userRepo.findOne({ where: { username: dto.username }, }); if (existUser) { throw new HttpException('用户名已存在', 400); } // 2. 密码加密(核心!bcrypt自动加盐,无需手动处理) const salt = await genSalt(10); // 生成盐值(复杂度10) const hashedPassword = await hash(dto.password, salt); // 3. 创建用户记录(存入数据库,角色默认为普通用户) await this.userRepo.save({ username: dto.username, password: hashedPassword, // 存储加密后的密码 role: 'user', // 初始角色(后续可通过管理员修改) }); return { message: '注册成功,请登录' }; } }
4.实现登录功能(本地策略)
登录核心是验证用户合法性并生成JWT令牌,令牌将作为后续访问受保护接口的“通行证”。
4.1 配置Local策略(账号密码验证)
先定义 LocalStrategy
,校验用户输入的账号密码是否正确(密码需用bcrypt加密存储,避免明文)。
// src/auth/local.strategy.ts import { BadRequestException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { compareSync } from 'bcryptjs'; // 密码比对(bcrypt加密) import { Strategy } from 'passport-local'; import { Repository } from 'typeorm'; import { User } from '../users/entities/user.entity'; // 用户实体 // 继承PassportStrategy,指定策略类型为local export class LocalStrategy extends PassportStrategy(Strategy) { constructor( @InjectRepository(User) private userRepo: Repository<User>, ) { super({ usernameField: 'username', // 对应请求体中的“用户名”字段 passwordField: 'password', // 对应请求体中的“密码”字段 }); } // 验证逻辑:Passport会自动调用该方法 async validate(username: string, password: string) { // 1. 根据用户名查用户(需额外查询密码,默认不返回) const user = await this.userRepo .createQueryBuilder('user') .addSelect('user.password') // 显式查询密码 .where('user.username = :username', { username }) .getOne(); // 2. 校验用户是否存在、密码是否匹配 if (!user || !compareSync(password, user.password)) { throw new BadRequestException('用户名或密码错误'); } // 3. 返回用户信息(后续会被注入到req.user中) return user; } }
4.2 配置JWT模块
登录验证通过后,需要生成JWT给客户端,后续接口访问需携带该令牌。在 AuthModule
中注册JWT模块,指定密钥(从配置文件读取,避免硬编码)和过期时间:
// src/auth/auth.module.ts import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/entities/user.entity'; import { AuthService } from './auth.service'; import { LocalStrategy } from './local.strategy'; import { JwtStrategy } from './jwt.strategy'; // 异步配置JWT(从环境变量读取密钥) const jwtModule = JwtModule.registerAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ secret: config.get('JWT_SECRET', 'dev-secret-123'), // 密钥(生产环境需复杂) signOptions: { expiresIn: '4h' }, // JWT过期时间(4小时) }), }); @Module({ imports: [ TypeOrmModule.forFeature([User]), // 导入用户Repository PassportModule, jwtModule, // 注册JWT模块 ], providers: [AuthService, LocalStrategy, JwtStrategy], // 注入服务和策略 exports: [AuthService, JwtModule], // 导出JWT模块,供其他模块使用 }) export class AuthModule {}
4.3 实现JWT生成逻辑
在 AuthService
中,用 JwtService
生成JWT(Payload包含用户ID、用户名、角色,供后续权限判断):
// src/auth/auth.service.ts import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { User } from '../users/entities/user.entity'; @Injectable() export class AuthService { constructor(private jwtService: JwtService) {} // 生成JWT(登录成功后调用) login(user: Partial<User>) { // Payload:存储用户核心信息(避免敏感数据,如密码) const payload = { id: user.id, username: user.username, role: user.role // 角色信息,后续RBAC权限控制用 }; // 签名生成JWT令牌 const token = this.jwtService.sign(payload); return { token }; // 返回给客户端 } }
4.4 实现登录接口
用 AuthGuard('local')
触发Local策略验证,验证通过后调用 login
生成JWT:
// src/auth/auth.controller.ts import { Controller, Post, Req, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} // 登录接口:使用local策略验证 @UseGuards(AuthGuard('local')) @Post('login') login(@Req() req) { // req.user由LocalStrategy的validate方法返回 return this.authService.login(req.user); } }
5. Passport+JWT实现接口验证(身份确认)
客户端登录后,需在请求头携带 Authorization: Bearer <JWT>
,我们用Passport的 passport-jwt
策略负责拦截请求、验证令牌有效性,确认用户身份。
5.1 定义JWT策略
// src/auth/jwt.strategy.ts import { UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { User } from '../users/entities/user.entity'; import { AuthService } from './auth.service'; export class JwtStrategy extends PassportStrategy(Strategy) { constructor( private config: ConfigService, private authService: AuthService, ) { super({ // 从请求头的Authorization: Bearer <JWT>中提取令牌 jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: config.get('JWT_SECRET'), // 用相同密钥解密 ignoreExpiration: false, // 不忽略过期(过期则返回401) }); } // 验证JWT有效性(解密后调用) async validate(payload: Partial<User>) { // 可额外校验用户状态(如是否被禁用) const user = await this.authService.findUserById(payload.id); if (!user) { throw new UnauthorizedException('令牌无效或用户不存在'); } return user; // 返回用户信息,注入到req.user } }
5.2 封装JWT守卫(统一错误处理)
自定义 JwtAuthGuard
,统一处理“未登录”错误提示,避免重复代码:
// src/auth/jwt-auth.guard.ts import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { // 处理验证结果 handleRequest(err: any, user: any) { if (err || !user) { throw new UnauthorizedException('请先登录'); } return user; } }
5.3 保护接口
在需要登录的接口上使用 JwtAuthGuard
,未携带有效JWT会被拦截:
// src/users/users.controller.ts import { Controller, Get, Req, UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; @Controller('users') export class UsersController { // 需登录才能访问的接口:获取当前用户信息 @UseGuards(JwtAuthGuard) @Get('info') getUserInfo(@Req() req) { // req.user由JwtStrategy的validate方法返回 return req.user; } }
6. RBAC 权限控制
6.1 RolesGuard 实现
自定义RolesGuard
,校验用户角色是否符合接口要求,实现权限细化:
// src/auth/roles.guard.ts import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; @Injectable() export class RolesGuard implements CanActivate { constructor(private requiredRoles: string[]) {} // 接收需要的角色列表 canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const user = request.user; // 从req.user中获取用户角色(JWT验证后注入) // 校验用户角色是否在允许的列表中 const hasPermission = this.requiredRoles.some(role => user.role === role); if (!hasPermission) { throw new ForbiddenException('无权限访问,需管理员权限'); } return true; } } // 自定义装饰器:简化角色配置(如@Roles('admin')) import { SetMetadata } from '@nestjs/common'; export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
6.2 接口示例
// src/admin/admin.controller.ts import { Controller, Get, UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { RolesGuard, Roles } from '../auth/roles.guard'; @Controller('admin') @UseGuards(JwtAuthGuard) // 先验证登录状态 export class AdminController { // 仅角色为admin的用户可访问:查询所有用户(管理员权限) @Get('all-users') @Roles('admin') // 指定需要的角色 @UseGuards(new RolesGuard(['admin'])) // 应用角色守卫 getAllUsers() { // 业务逻辑:查询数据库所有用户信息 return { data: [], message: '所有用户列表' }; } }
7.JWT过期处理:避免频繁重新登录
JWT一旦生成无法主动作废,若有效期过短会影响用户体验,过长则增加安全风险。解决方案是**“访问令牌+刷新令牌”双令牌机制**:
令牌类型 | 有效期 | 作用 | 安全策略 |
---|---|---|---|
访问令牌 | 短(如30分钟) | 访问受保护接口(核心凭证) | 过期后用刷新令牌获取新令牌 |
刷新令牌 | 长(如7天) | 仅用于刷新访问令牌(无接口访问权限) | 存储在Redis中,支持主动作废(如登出) |
实现逻辑:
- 登录时同时生成访问令牌(accessToken)和刷新令牌(refreshToken);
- 刷新令牌存储在Redis中(键:refreshToken,值:用户ID+过期时间);
- 访问令牌过期时,客户端携带刷新令牌请求
/auth/refresh-token
接口; - 服务器验证刷新令牌(Redis中是否存在、是否过期),验证通过则生成新的访问令牌。
// src/auth/auth.service.ts(新增刷新令牌逻辑) import { InjectRedis } from '@nestjs-modules/ioredis'; import { Redis } from 'ioredis'; import { v4 as uuidv4 } from 'uuid'; // 生成唯一刷新令牌 @Injectable() export class AuthService { // ... 其他代码 ... // 登录时生成双令牌 async loginWithDoubleToken(user: Partial<User>) { // 1. 生成访问令牌(短有效期) const accessToken = this.jwtService.sign( { sub: user.id, username: user.username, role: user.role }, { expiresIn: '30m' }, ); // 2. 生成刷新令牌(唯一ID,长有效期) const refreshToken = uuidv4(); const refreshTokenExpire = 7 * 24 * 60 * 60; // 7天(秒) // 3. 刷新令牌存储到Redis(支持主动作废) await this.redis.set( `refresh_token:${refreshToken}`, user.id, 'EX', refreshTokenExpire, ); return { accessToken, refreshToken, expiresIn: 30 * 60, // 访问令牌有效期(秒) }; } // 刷新访问令牌 async refreshToken(refreshToken: string) { // 1. 验证刷新令牌是否存在于Redis const userId = await this.redis.get(`refresh_token:${refreshToken}`); if (!userId) { throw new UnauthorizedException('刷新令牌无效或已过期'); } // 2. 查询用户信息 const user = await this.findUserById(Number(userId)); if (!user) { throw new UnauthorizedException('用户不存在'); } // 3. 生成新的访问令牌 const newAccessToken = this.jwtService.sign( { sub: user.id, username: user.username, role: user.role }, { expiresIn: '30m' }, ); return { accessToken: newAccessToken, expiresIn: 30 * 60 }; } // 登出:删除Redis中的刷新令牌(主动作废) async logout(refreshToken: string) { await this.redis.del(`refresh_token:${refreshToken}`); return { message: '登出成功' }; } }
四、关键安全与最佳实践
身份管理系统的安全性直接决定应用安全,需严格遵循以下最佳实践:
1. 密码安全:从存储到传输全链路防护
- 存储加密:必须使用bcrypt、Argon2等自适应哈希算法加密密码,禁止明文或MD5等弱哈希存储;
- 传输加密:所有登录注册接口必须使用HTTPS,防止密码在传输过程中被截取;
- 强度校验:注册时强制密码复杂度(如8位以上+字母+数字+特殊符号),避免弱密码。
2. JWT安全:避免令牌泄露与篡改
- 密钥管理:JWT密钥需复杂(如32位以上随机字符串),生产环境中存储在环境变量或密钥管理服务(如AWS KMS),禁止硬编码;
- 令牌存储:前端优先将JWT存储在
httpOnly Cookie
(防XSS攻击),若用localStorage
需额外做XSS防护(如输入过滤、CSP策略); - Payload控制:JWT的Payload仅存储非敏感信息(如用户ID、角色),禁止包含密码、手机号等敏感数据。
3. 接口安全:防御常见攻击
- 防CSRF攻击:使用
httpOnly Cookie
存储令牌时,需配合CSRF Token验证; - 限流防护:登录、注册接口添加限流(如1分钟内最多5次请求),防止暴力给破解;
- 异常处理:统一错误提示(如“账号或密码错误”而非“用户名不存在”),避免泄露用户存在性信息。
4. 权限最小化:避免过度授权
- 基于RBAC模型严格划分角色(如user、admin、super_admin),每个角色仅赋予必要权限;
- 敏感操作(如修改密码、删除数据)需二次验证(如短信验证码、当前密码确认)。
总结
到此这篇关于JWT+Passport.js身份验证实现的文章就介绍到这了,更多相关JWT+Passport.js身份验证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!