背景
在内部项目中,微信授权登录功能的实现存在一些问题。首先,授权流程通常在页面加载后才触发,导致用户在授权成功后返回授权前的页面时,页面无法及时获取授权信息,从而引发一系列 Bug。其次,由于缺乏一套有效的会话管理机制(如 Session 或 Token),每次访问都需要重新授权,这不仅降低了用户体验,还增加了开发和维护的复杂性。此外,项目中缺乏统一的架构设计,每次新增功能都需要重新开发授权逻辑,导致开发效率低下且代码冗余。
为了解决这些问题,决定重新设计和实现微信授权登录流程。通过结合 Nest.js 和 Emp.js,我们构建了一个高效、可扩展的授权登录系统。Nest.js 作为后端服务,负责处理复杂的授权逻辑、会话管理以及接口聚合;而 Emp.js 作为微前端框架,简化了前端开发流程,提升了交互效率和代码可维护性。两者的结合不仅优化了授权流程,还通过模块化设计为后续功能扩展提供了统一的技术架构,显著提升了系统的稳定性和开发效率。
在这一方案中,BFF(Backend for Frontend)层 起到了关键作用。BFF 层作为前后端的桥梁,负责将复杂的后端逻辑抽象为前端友好的接口,同时聚合多个后端服务的数据,减少前端的请求复杂度。通过 BFF 层,我们实现了微信授权的统一管理,确保了前后端的高效协作和业务逻辑的清晰分离。这不仅提升了开发效率,还增强了系统的可维护性和扩展性。
本文将详细介绍这一实现过程,帮助开发者快速掌握微信授权登录技术,并为类似项目提供参考。通过本文,读者将了解如何设计一个无缝衔接的授权流程,解决页面加载与授权信息的同步问题,并通过会话管理机制避免重复授权,从而提升用户体验和开发效率。同时,本文还将分享如何通过模块化设计构建可复用的技术架构,为未来功能扩展奠定基础。
前言
在现代 Web 应用开发中,前后端分离架构已成为主流,但随着业务复杂度的增加,前端需要对接多个后端服务,导致开发效率降低、接口调用冗余、以及数据聚合困难等问题。为了解决这些问题,BFF(Backend for Frontend) 层应运而生。BFF 层作为前后端的桥梁,能够为前端提供定制化的接口,聚合多个后端服务的数据,并处理页面渲染、权限校验等逻辑,从而显著提升开发效率和用户体验。
本文将详细介绍如何基于 Nest.js 作为 BFF 层实现微信授权登录功能。Nest.js 是一个高效、模块化的 Node.js 框架,其强大的依赖注入机制和模块化设计使其成为构建 BFF 层的理想选择。通过 Nest.js,我们可以轻松实现微信授权登录的核心逻辑,包括用户授权、Token 获取、用户信息拉取等,同时支持页面渲染和接口聚合,为前端提供一体化的解决方案。
本文将涵盖以下内容:
BFF 层的核心作用与优势:介绍 BFF 层在微信授权登录场景中的价值。
微信授权登录的流程与原理:解析微信 OAuth2.0 授权登录的实现机制。
Nest.js 作为 BFF 层的实现方案:详细讲解如何使用 Nest.js 实现微信授权登录、接口聚合和页面渲染。
前后端协作的最佳实践:探讨如何通过 BFF 层优化前后端协作,提升开发效率。
完整代码示例:提供可运行的代码示例。
通过阅读本文,读者将掌握如何基于 Nest.js 构建一个高效、可扩展的 BFF 层,并实现微信授权登录功能。无论是从技术实现还是架构设计角度,本文都将为开发者提供有价值的参考。
1. BFF 层的核心作用与优势
1.1 BFF 层的定义与价值
BFF(Backend for Frontend) 层是为前端量身定制的后端服务,其主要作用是为前端提供定制化的接口和数据聚合能力,同时支持页面渲染、权限校验等逻辑。在微信授权登录场景中,BFF 层不仅负责处理授权流程,还能够在渲染页面前完成微信授权,确保页面加载时已具备用户授权信息,从而提升用户体验。
1.2 BFF 层的核心作用
接口聚合:BFF 层可以聚合多个后端服务的接口,减少前端请求次数,简化前端逻辑。
页面渲染:BFF 层支持在服务端渲染页面(SSR),能够在页面加载前完成微信授权,确保页面渲染时已包含用户授权信息。
授权管理:BFF 层统一管理微信授权流程,包括授权跳转、Token 获取、用户信息拉取等,避免前端直接处理复杂的授权逻辑。
会话管理:BFF 层维护用户的会话状态(如 Session 或 Token),避免每次访问都需要重新授权。
1.3 BFF 层的优势
提升用户体验:在页面渲染前完成授权,确保用户访问页面时已具备授权信息,避免页面加载后重新跳转授权。
简化前端逻辑:将复杂的授权流程和接口聚合逻辑封装在 BFF 层,前端只需关注页面交互和展示。
增强安全性:将敏感操作(如 Token 获取)放在后端,避免前端暴露关键信息。
提高开发效率:通过模块化设计,BFF 层可以为不同前端(如 Web、小程序、App)提供定制化的接口和页面渲染逻辑,减少重复开发。
统一技术架构:BFF 层为后续功能扩展提供了统一的技术架构,便于维护和扩展。
2. 微信授权登录
微信授权登录基于 OAuth2.0 协议,是一种用户身份验证机制,允许第三方应用通过微信用户的授权获取其基本信息(如昵称、头像等)。该功能广泛应用于 Web 应用、移动应用和小程序中,为用户提供便捷的登录方式,同时为开发者提供安全、可靠的身份验证方案。
2.1 测试公众号
在开发和测试微信授权登录功能时,使用 微信测试公众号 是一个理想的选择。测试公众号提供与正式公众号相同的接口能力,且无需经过微信官方审核,非常适合开发和调试。以下是申请和使用测试公众号的详细步骤:
1. 申请测试公众号
访问微信公众平台测试账号申请页面:
打开浏览器,访问 微信公众平台测试账号申请页面。
登录微信账号:
使用个人微信账号扫码登录。若无微信账号,需先注册。
获取测试公众号信息:
AppID:测试公众号的唯一标识。
AppSecret:用于接口调用的密钥。
测试号二维码:用于关注测试公众号。
登录成功后,系统会自动生成一个测试公众号,并显示以下关键信息:
关注测试公众号:
使用微信扫描测试号二维码,关注测试公众号。只有关注了测试公众号的用户才能进行授权登录测试。
2. 配置测试公众号
配置授权回调域名:
域名无需带
http://
或https://
。域名必须是有效的、已备案的域名。
本地开发可使用内网穿透工具(如 ngrok)生成临时域名。
在测试公众号页面中,找到 接口配置信息 部分。
在 授权回调页面域名 中填写你的服务器域名(例如:
test.com
)。注意:
配置 JS 接口安全域名(可选):
如需使用微信的 JS-SDK 功能(如分享、拍照等),可在 JS 接口安全域名 中配置域名。
配置网页授权获取用户基本信息:
在测试公众号页面中,找到 网页账号 部分。
在 网页授权获取用户基本信息 中填写你的回调页面路径(例如:
/auth/callback
)。
在完成测试公众号的申请和配置后,接下来我们将进入微信授权登录功能的开发阶段。
3、Nest.js 作为 BFF 层的实现方案
3.1 前端跳转至微信授权页面
用户通过浏览器访问 Node 服务提供的页面时,Node 服务会检测用户是否已授权或登录。若用户未授权或未登录,Node 服务将触发微信授权流程。
为此,我们创建了一个中间件来处理页面路由访问时的微信授权逻辑。该中间件的主要职责包括:
检查用户是否已授权。
在非微信环境中直接放行请求。
处理微信授权流程,包括重定向至微信授权页面和处理授权回调。
微信授权分为两种方式:
标准授权(
snsapi_userinfo
) :用户需手动确认授权,通常会弹出授权提示框。
适用于需要获取用户详细信息的场景,如用户昵称、头像等。
静默授权(
snsapi_base
) :无需用户手动确认,自动完成授权。
适用于仅需获取用户 OpenID 的场景,如用户身份识别。
这两种授权方式可根据业务需求灵活选择,以提升用户体验。
3.2 getMatchedDomain
方法的作用
当用户已在 A 公众号登录并访问某个页面时,若需授权 B 公众号,getMatchedDomain
方法通过路由与域名的映射关系,支持以下功能:
跨公众号用户身份识别:识别用户在 B 公众号的身份,实现跨平台用户匹配。
数据互通与业务整合:打通 A、B 公众号的数据,支持业务协同与资源共享。
统一用户体系:建立跨公众号的统一用户体系,提升运营效率与用户体验。
这种设计适用于多公众号协同运营的场景,但需严格遵守微信平台规则,确保用户隐私保护与数据安全。
private getMatchedDomain(originalUrl: string): string { logger.log('获取匹配的授权域名'); for (const [route, domain] of ROUTE_DOMAIN_MAP.entries()) { if (originalUrl.startsWith(route)) { logger.log(`匹配到的域名: ${domain}`); return domain; } } logger.log('未匹配到任何域名'); return ''; }
3.3 中间件的核心功能
微信授权流程管理:
检查用户是否已授权(通过
code
参数和authorized
状态)。若用户未授权,则根据当前路由和配置,构建微信授权 URL 并重定向用户至微信授权页面。
用户授权后,微信会回调至指定 URL,并附带
code
参数,中间件通过code
获取用户信息和access_token
。跨公众号授权支持:
通过
getMatchedDomain
方法,支持不同路由映射到不同的授权域名(如 A 公众号和 B 公众号)。适用于多公众号协同运营的场景,实现跨公众号的用户身份识别和数据互通。
静默授权与标准授权:
根据路由配置(
WECHAT_SILENT_AUTH_ROUTES
),判断是否需要静默授权(snsapi_base
)或标准授权(snsapi_userinfo
)。静默授权适用于无需用户手动确认的场景,标准授权适用于需要获取用户详细信息的场景。
用户信息与 Token 管理:
授权成功后,通过
wechatAuthService.getAccessToken
获取用户信息和access_token
。将用户信息和
access_token
存储到session
和cookie
中,方便后续请求使用。重定向与回调处理:
授权成功后,根据
redirectUrl
参数或当前 URL,将用户重定向至目标页面。支持在重定向 URL 中附加授权状态(
authorized=true
),避免重复授权。
3.4 中间件的主要流程
用户访问路由:
检查用户是否已登录(通过
req.session.user
)。若用户已登录,则直接放行。
非微信环境处理:
若当前环境不是微信(通过
isWechat
方法判断),则直接放行。授权状态检查:
检查请求中是否包含
code
参数(微信授权回调时附带)。若已授权(
authorized=true
),则直接放行。未授权处理:
若未授权,则根据当前路由获取匹配的授权域名(
getMatchedDomain
)。构建授权 URL(
buildAuthUrl
),并根据路由配置选择授权方式(静默授权或标准授权)。重定向用户至微信授权页面。
授权回调处理:
微信授权成功后,回调至指定 URL,并附带
code
参数。通过
code
获取用户信息和access_token
,并存储到session
和cookie
中。根据
redirectUrl
或当前 URL,重定向用户至目标页面。错误处理:
若授权流程中出现错误,记录错误日志并调用
next(error)
,交由后续中间件或错误处理器处理。
3.5 中间件的作用
支持多公众号相互授权:
通过路由与域名的映射关系,支持不同公众号的授权逻辑,适用于多公众号协同运营的场景。
用户身份识别与数据互通:
通过微信授权获取用户信息,实现跨公众号的用户身份识别和数据共享。
静默授权与标准授权:
根据业务需求,灵活选择静默授权或标准授权,提升用户体验。
授权流程自动化:
自动处理微信授权流程,减少用户操作步骤,提升系统效率。
完整代码如下:
import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { fillParams, goWechatUrl, isWechat } from '@app/utils/util'; import { ROUTE_DOMAIN_MAP } from '@app/constants/route-domain.constant'; import { wechatConfig } from '@app/config'; import { CacheService } from '@app/processors/redis/cache.service'; import { createLogger } from '@app/utils/logger'; import { WechatAuthService } from '@app/modules/wechatAuth/wechat-auth.service'; import { WECHAT_SILENT_AUTH_ROUTES } from '@app/constants/route-domain.constant'; // 创建一个日志记录器实例,用于记录日志信息 const logger = createLogger({ scope: 'WechatAuthMiddleware', time: true, }); /** * 微信授权中间件 * 用于处理微信网页授权流程 */ @Injectable() export class WechatAuthMiddleware implements NestMiddleware { constructor( private readonly cacheService: CacheService, private readonly wechatAuthService: WechatAuthService, ) {} /** * 获取当前路由对应的授权域名 * @param originalUrl - 请求的原始URL * @returns 匹配的授权域名 */ private getMatchedDomain(originalUrl: string): string { logger.log('获取匹配的授权域名'); for (const [route, domain] of ROUTE_DOMAIN_MAP.entries()) { if (originalUrl.startsWith(route)) { logger.log(`匹配到的域名: ${domain}`); return domain; } } logger.log('未匹配到任何域名'); return ''; } /** * 构建授权URL * @param req - 请求对象 * @param matchedDomain - 匹配的授权域名 * @returns 构建的授权URL */ private buildAuthUrl(req: Request, matchedDomain: string): string { logger.log('开始构建授权URL'); const currentUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`; logger.log(`当前URL: ${currentUrl}`); if (!matchedDomain) { logger.log('没有匹配的域名,返回当前URL'); return currentUrl; } // 构建授权URL const authUrl = `${req.protocol}://${matchedDomain}${req.originalUrl}`; logger.log(`构建的授权URL: ${authUrl}`); // 填充参数到授权URL中 const redirectUrl = fillParams( { redirectUrl: encodeURIComponent(currentUrl), }, authUrl, ); // 记录授权URL logger.log('最终授权URL:', redirectUrl); return redirectUrl; } /** * 处理微信授权重定向 * @param redirectUrl - 重定向的URL * @returns 处理后的重定向URL */ private handleRedirect(redirectUrl: string): string { logger.log('处理微信授权重定向'); const url = fillParams( { authorized: 'true', }, redirectUrl, ['code', 'state'], ); logger.log(`处理后的重定向URL: ${url}`); return url; } async use(req: Request, res: Response, next: NextFunction) { const user = req.session.user; logger.info('user 用户信息', user); if (user?.userId) { logger.log('用户已存在,直接通过授权'); return next(); } try { const { appId } = wechatConfig; logger.log('处理请求URL:', req.originalUrl); // 1. 非微信环境直接通过 if (!isWechat(req)) { logger.log('非微信环境,直接通过'); return next(); } // 2. 检查授权状态 logger.log('检查授权状态'); const code = req.query.code as string; const authorized = req.query.authorized as string; // 3. 已授权直接通过 if (authorized === 'true') { logger.log('已授权,直接通过'); return next(); } // 4. 无code则重定向授权 if (!code) { logger.log('无授权码,开始授权流程'); // 获取匹配的授权域名,这里可能同时静默授权两次不同的域名 const matchedDomain = this.getMatchedDomain(req.originalUrl); // 构建授权URL const authUrl = this.buildAuthUrl(req, matchedDomain); // 判断是否为静默授权路由 const scope = WECHAT_SILENT_AUTH_ROUTES.includes(req.path) ? 'snsapi_base' : 'snsapi_userinfo'; const redirectUrl = goWechatUrl(authUrl, appId, scope, 'STATE'); logger.log(`重定向到微信授权URL: ${redirectUrl}`); return res.redirect(redirectUrl); } // 5. 处理授权回调 logger.log('处理授权回调'); // 获取重定向URL const redirectUrl = req.query.redirectUrl as string; const { user: userInfo, token } = await this.wechatAuthService.getAccessToken(code); res.cookie('jwt', token.accessToken, { sameSite: true, httpOnly: true, }); res.cookie('userId', userInfo.userId); req.session.user = userInfo; logger.info(user, '获取用户信息成功==='); logger.info(token, '获取token成功==='); // 如果重定向URL存在,则处理授权回调 if (redirectUrl) { const decodedRedirectUrl = decodeURIComponent(redirectUrl); logger.log(`重定向URL存在,处理授权回调: ${decodedRedirectUrl}`); return res.redirect(this.handleRedirect(decodedRedirectUrl)); } else { // 如果重定向URL不存在,则重定向到当前URL logger.log('重定向URL不存在,重定向到当前URL'); return res.redirect(this.handleRedirect(req.originalUrl)); } } catch (error) { logger.error('授权处理出错:', error); // 添加错误日志记录 next(error); } } }
在授权逻辑中,已登录用户不会进行其他公众号的静默授权。需要根据具体业务逻辑自行处理此情况。
该中间件并非适用于所有场景,而是针对特定页面路由生效。通过路由与域名的映射关系,它能够灵活处理不同页面的授权需求,尤其适用于多公众号协同运营的场景。开发者需根据业务需求配置路由规则,确保授权逻辑的精准匹配。
在 Nest.js 中,可以通过 apply
方法将微信认证中间件(WechatAuthMiddleware
)单独应用到指定的路由控制器(如 RouterController
)。这种方式确保中间件仅对特定路由生效,避免不必要的授权流程,从而提升系统效率和用户体验。
// 单独应用微信认证中间件到RouterModule consumer.apply(WechatAuthMiddleware).forRoutes(RouterController); // RouterModule中定义的路由
4、Redis 作为 Session 与缓存
4.1 Redis和 ioredis 区别
首先我们简单了解下ioredis
和 redis
是 Node.js 中两个常用的 Redis 客户端库,它们都用于与 Redis 服务器进行交互,但在功能、性能和用法上有一些区别。以下是它们的详细对比
特性 | redis 库 | ioredis 库 |
---|---|---|
API 风格 | 回调函数为主,需手动 Promise 化 | 原生支持 Promise,同时支持回调 |
集群支持 | 需要额外配置 | 原生支持 Redis 集群和哨兵模式 |
性能 | 较好 | 更优,适合高并发场景 |
功能丰富度 | 基础功能 | 支持更多高级功能(如管道、Lua 脚本) |
自动重连 | 不支持 | 支持 |
社区活跃度 | 较老,社区支持稳定 | 较新,社区活跃 |
学习曲线 | 较简单 | 稍复杂,但功能更强大 |
在本项目中使用的是ioredis
, 因为ioredis
是一个更现代的 Redis 客户端库,功能更强大,性能更优,支持更多高级特性。
4.2 ioredis 模式
ioredis
是一个功能强大的 Redis 客户端库,支持多种 Redis 部署模式。以下是 ioredis
提供的主要模式及其特点:
1. 单节点模式(Standalone)
这是最简单的 Redis 部署模式,适用于单机或单实例 Redis 服务器。 特点:
直接连接到一个 Redis 实例。
适合小型应用或开发环境。
const Redis = require('ioredis'); const redis = new Redis({ host: '127.0.0.1', // Redis 服务器地址 port: 6379, // Redis 服务器端口 password: 'your_password', // 认证密码(可选) }); redis.set('key', 'value').then(() => { return redis.get('key'); }).then((result) => { console.log('Get key:', result); });
2. 主从复制模式(Master-Slave Replication)
主从复制模式通过将数据从主节点(Master)复制到从节点(Slave),实现读写分离和数据备份。
特点:
主节点负责写操作,从节点负责读操作。
从节点可以扩展读性能,并提供数据冗余。
const Redis = require('ioredis'); const redis = new Redis({ host: '127.0.0.1', // 主节点地址 port: 6379, password: 'your_password', }); const slaveRedis = new Redis({ host: '127.0.0.1', // 从节点地址 port: 6380, password: 'your_password', role: 'slave', // 指定为从节点 });
3. 哨兵模式(Sentinel)
哨兵模式用于实现 Redis 的高可用性(High Availability),通过哨兵节点监控主从节点的健康状态,并在主节点故障时自动进行故障转移。
特点:
自动故障检测和主从切换。
适合对高可用性要求较高的场景。
const Redis = require('ioredis'); const redis = new Redis({ sentinels: [ { host: 'sentinel1.example.com', port: 26379 }, // 哨兵节点 1 { host: 'sentinel2.example.com', port: 26379 }, // 哨兵节点 2 ], name: 'mymaster', // 主节点名称 password: 'your_password', // 主节点密码 sentinelPassword: 'sentinel_password', // 哨兵节点密码(可选) });
4. 集群模式(Cluster)
Redis 集群模式通过分片(Sharding)将数据分布到多个节点上,支持水平扩展和高性能。
特点:
数据分片存储,支持大规模数据和高并发。
自动数据分片和节点管理。
适合需要高扩展性和高性能的场景。
const Redis = require('ioredis'); const redis = new Redis.Cluster([ { host: '127.0.0.1', port: 7000 }, // 集群节点 1 { host: '127.0.0.1', port: 7001 }, // 集群节点 2 { host: '127.0.0.1', port: 7002 }, // 集群节点 3 ], { redisOptions: { password: 'your_password', // 集群节点密码 }, });
5. 管道模式(Pipeline)
管道模式允许将多个 Redis 命令一次性发送到服务器,减少网络往返时间,提升性能。
特点:
批量执行命令,减少网络延迟。
适合需要批量操作的场景。
const Redis = require('ioredis'); const redis = new Redis(); const pipeline = redis.pipeline(); pipeline.set('key1', 'value1'); pipeline.set('key2', 'value2'); pipeline.exec().then((results) => { console.log('Pipeline results:', results); });
6. 事务模式(Transaction)
事务模式通过 MULTI
和 EXEC
命令实现原子性操作,确保多个命令按顺序执行。
特点:
保证命令的原子性。
适合需要事务支持的场景。
const Redis = require('ioredis'); const redis = new Redis(); redis.multi() .set('key1', 'value1') .set('key2', 'value2') .exec().then((results) => { console.log('Transaction results:', results); });
7. 发布订阅模式(Pub/Sub)
发布订阅模式允许客户端订阅频道并接收消息,适用于消息通知和实时通信场景。
特点:
支持消息的发布和订阅。
适合实时通信和事件驱动的场景。
const Redis = require('ioredis'); const redis = new Redis(); // 订阅频道 redis.subscribe('news', (err) => { if (err) console.error('Subscribe error:', err); }); // 接收消息 redis.on('message', (channel, message) => { console.log(`Received message from ${channel}: ${message}`); }); // 发布消息 redis.publish('news', 'Hello, world!');
8. Lua 脚本支持
ioredis
支持通过 eval
命令执行 Lua 脚本,适用于需要复杂逻辑或原子性操作的场景。
特点:
在 Redis 服务器端执行 Lua 脚本。
适合需要复杂逻辑或原子性操作的场景。
const Redis = require('ioredis'); const redis = new Redis(); const script = ` return redis.call('set', KEYS[1], ARGV[1]) `; redis.eval(script, 1, 'key', 'value').then((result) => { console.log('Lua script result:', result); });
根据业务需求选择合适的模式,可以充分发挥 Redis 的性能和功能优势。不过建议像BFF层业务使用哨兵模式比较好。
4.3 Redis 模块
缓存和 Session 是现代 Web 应用开发中不可或缺的技术。通过合理的设计和实现,它们能够显著提升系统性能、优化用户体验,并增强系统的可扩展性和稳定性。在后续的开发中,我们将探讨ioredis
实现高效的缓存和 Session 管理。
1. 模块定义与全局声明
@Global() @Module({ providers: [RedisService, CacheService], exports: [RedisService, CacheService], }) export class RedisCoreModule { ... }
@Global()
:声明为全局模块,其他模块无需显式导入即可使用其服务。RedisService
:封装 Redis 连接与基础操作(如get
/set
)。CacheService
:提供业务层缓存逻辑(如防雪崩、击穿、穿透)。
2. 同步配置(forRoot
)
static forRoot(options: RedisModuleOptions): DynamicModule { const redisOptionsProvider = { provide: getRedisOptionsToken(), useValue: options }; const redisConnectionProvider = { provide: getRedisConnectionToken(), useValue: createRedisConnection(options) }; return { module: RedisCoreModule, providers: [redisOptionsProvider, redisConnectionProvider], exports: [...] }; }
功能:通过同步方式配置 Redis 连接(适用于简单场景)。
核心方法:
createRedisConnection
:根据配置创建 Redis 客户端实例(支持单机、哨兵、集群)。getRedisOptionsToken
/getRedisConnectionToken
:生成唯一 Token,避免依赖冲突。
3. 异步配置(forRootAsync
)
static forRootAsync(options: RedisModuleAsyncOptions): DynamicModule { const redisConnectionProvider = { provide: getRedisConnectionToken(), useFactory: (options: RedisModuleOptions) => createRedisConnection(options), inject: [getRedisOptionsToken()], }; return { module: RedisCoreModule, providers: [...this.createAsyncProviders(options), redisConnectionProvider] }; }
功能:支持从异步来源(如配置文件、远程服务)加载配置。
配置方式:
useClass
:通过类工厂生成配置(如从ConfigService
读取)。useFactory
:自定义工厂函数动态生成配置。useExisting
:复用已有的配置提供者。
4. 异步提供者工厂
public static createAsyncProviders(options: RedisModuleAsyncOptions): Provider[] { if (options.useFactory || options.useExisting) { return [this.createAsyncOptionsProvider(options)]; } return [this.createAsyncOptionsProvider(options), { provide: options.useClass, useClass: options.useClass }]; }
作用:根据不同的异步配置方式(类、工厂、实例),生成对应的依赖注入提供者。
错误处理:验证配置有效性,防止无效的配置方式。
完整代码如下:
import { RedisModuleAsyncOptions, RedisModuleOptions, RedisModuleOptionsFactory, RedisSingleOptions, } from '@app/interfaces/redis.interface'; import { DynamicModule, Global, Module, Provider } from '@nestjs/common'; import { createRedisConnection, getRedisConnectionToken, getRedisOptionsToken, } from './redis.util'; import { RedisService } from './redis.service'; import { createLogger } from '@app/utils/logger'; import { CacheService } from './cache.service'; // 创建日志记录器 const logger = createLogger({ scope: 'RedisCoreModule', time: true }); /** * Redis核心模块 * 提供Redis连接和缓存服务 */ @Global() // 声明为全局模块 @Module({ imports: [], providers: [RedisService, CacheService], // 提供Redis服务和缓存服务 exports: [RedisService, CacheService], // 导出服务供其他模块使用 }) export class RedisCoreModule { /** * 同步方式初始化Redis模块 * @param options Redis配置选项 * @returns 动态模块配置 */ static forRoot(options: RedisModuleOptions): DynamicModule { // 打印配置日志 logger.info('初始化Redis模块配置', { type: options.type, ...(options.type === 'single' && { url: options.url }), // 仅当单机模式时打印url options: { ...options.options, }, }); // 创建Redis配置提供器 const redisOptionsProvider: Provider = { provide: getRedisOptionsToken(), useValue: options, }; // 创建Redis连接提供器 const redisConnectionProvider: Provider = { provide: getRedisConnectionToken(), useValue: createRedisConnection(options), }; return { module: RedisCoreModule, providers: [redisOptionsProvider, redisConnectionProvider], exports: [redisOptionsProvider, redisConnectionProvider], }; } /** * 异步方式初始化Redis模块 * 支持依赖注入方式配置 * @param options 异步配置选项 * @returns 动态模块配置 */ static forRootAsync(options: RedisModuleAsyncOptions): DynamicModule { // 打印异步配置日志 logger.info('初始化异步Redis模块配置', { useClass: options.useClass?.name, useExisting: options.useExisting?.name, useFactory: !!options.useFactory, }); // 创建Redis连接提供器 const redisConnectionProvider: Provider = { provide: getRedisConnectionToken(), useFactory(options: RedisModuleOptions) { // 打印最终生成的配置 logger.info('生成Redis连接配置', { type: options.type, ...(options.type === 'single' && { url: options.url }), // 仅当单机模式时打印url options: { ...options.options, }, }); return createRedisConnection(options); }, inject: [getRedisOptionsToken()], // 注入Redis配置 }; return { module: RedisCoreModule, imports: options.imports, providers: [ ...this.createAsyncProviders(options), redisConnectionProvider, ], exports: [redisConnectionProvider], }; } /** * 创建异步配置提供器 * 支持useClass、useFactory、useExisting三种方式 * @param options 异步配置选项 * @returns 提供器数组 */ public static createAsyncProviders( options: RedisModuleAsyncOptions, ): Provider[] { // 验证配置方式是否有效 if (!(options.useExisting || options.useFactory || options.useClass)) { throw new Error( '无效配置,提供器只提供useClass、useFactory、useExisting这三种自定义提供器', ); } // 使用已存在的提供器或工厂方法 if (options.useExisting || options.useFactory) { return [this.createAsyncOptionsProvider(options)]; } // 使用类提供器 if (!options.useClass) { throw new Error( '无效配置,提供器只提供useClass、useFactory、useExisting这三种自定义提供器', ); } return [ this.createAsyncOptionsProvider(options), { provide: options.useClass, useClass: options.useClass }, ]; } /** * 创建异步配置选项提供器 * @param options 异步配置选项 * @returns 配置提供器 */ public static createAsyncOptionsProvider( options: RedisModuleAsyncOptions, ): Provider { // 验证配置方式是否有效 if (!(options.useExisting || options.useFactory || options.useClass)) { throw new Error( '无效配置,提供器只提供useClass、useFactory、useExisting这三种自定义提供器', ); } // 使用工厂方法方式 if (options.useFactory) { return { provide: getRedisOptionsToken(), useFactory: options.useFactory, inject: options.inject || [], }; } // 使用类或已存在实例方式 return { provide: getRedisOptionsToken(), async useFactory( optionsFactory: RedisModuleOptionsFactory, ): Promise<RedisModuleOptions> { const config = await optionsFactory.createRedisModuleOptions(); // 打印生成的配置 logger.info('通过工厂方法生成Redis配置', { type: config.type, ...(config.type === 'single' && { url: (config as RedisSingleOptions).url, }), options: config.options, }); return config; }, inject: [options.useClass || options.useExisting] as never, }; } }
4.4 Redis 缓存服务
RedisService
类提供了与 Redis 交互的全面接口,包括设置、获取和操作缓存数据的方法,该 Redis 服务模块提供了以下核心功能:
基础缓存操作:
支持单键值对的设置(
set
)和获取(get
)。支持批量设置(
mset
)和批量获取(mget
)。支持键的删除(
del
)和存在性检查(has
)。分布式锁:
通过
getWithLock
方法实现分布式锁,防止缓存击穿。支持自定义锁超时时间、重试延迟和最大重试次数。
高性能批量操作:
使用 Redis 管道技术(
pipelineExecute
)实现批量操作,提升性能。数据序列化与反序列化:
自动将 JavaScript 对象序列化为 JSON 字符串存储。
从 Redis 中获取数据时自动反序列化为指定类型。
import { Injectable } from '@nestjs/common'; import { Redis } from 'ioredis'; import { createLogger } from '@app/utils/logger'; import { isNil, UNDEFINED } from '@app/constants/value.constant'; import { InjectRedis } from '@app/decorators/redis.decorator'; import { isDevEnv } from '@app/configs'; // 创建日志记录器,用于记录Redis服务相关日志 const logger = createLogger({ scope: 'RedisService', time: isDevEnv }); /** * Redis服务类 * 提供Redis连接和缓存服务 */ @Injectable() export class RedisService { public client: Redis; // 公开的Redis客户端实例 private readonly LOCK_TIMEOUT = 10; // 分布式锁默认超时时间(秒) private readonly LOCK_RETRY_DELAY = 100; // 获取锁失败重试延迟(毫秒) private readonly MAX_LOCK_RETRIES = 5; // 最大重试次数 constructor(@InjectRedis() private readonly redis: Redis) { this.client = this.redis; // 将注入的Redis实例赋值给公共client this.registerEventListeners(); // 注册Redis事件监听器 } /** * 注册Redis事件监听器,用于监控Redis连接状态 */ private registerEventListeners() { this.redis.on('connect', () => logger.info('[Redis] connecting...')); // 连接中 this.redis.on('reconnecting', () => logger.warn('[Redis] reconnecting...')); // 重连中 this.redis.on('ready', () => logger.info('[Redis] readied!')); // 连接就绪 this.redis.on('end', () => logger.error('[Redis] Client End!')); // 连接结束 this.redis.on( 'error', (error) => logger.error('[Redis] Client Error!', error.message), // 错误处理 ); } /** * 序列化方法,将任意值转换为JSON字符串 * @param value 要序列化的值 * @returns 序列化后的JSON字符串 */ private serialize(value: unknown): string { return isNil(value) ? '' : JSON.stringify(value); } /** * 反序列化方法,将JSON字符串转换为指定类型 * @param value 要反序列化的JSON字符串 * @returns 反序列化后的值 */ private deserialize<T>(value: string | null): T | undefined { return isNil(value) ? UNDEFINED : (JSON.parse(value) as T); } /** * 带分布式锁的缓存获取方法 * 1. 先尝试获取缓存 * 2. 如果缓存不存在,则尝试获取分布式锁 * 3. 获取锁成功后执行回退函数获取数据并缓存 * 4. 释放锁并返回数据 */ public async getWithLock<T>( key: string, fallback: () => Promise<T>, ttl: number, lockOptions?: { timeout?: number; retryDelay?: number; maxRetries?: number; }, ): Promise<T> { // 1. 尝试获取缓存值 const cached = await this.get<T>(key); if (cached !== undefined) return cached; // 2. 配置锁参数 const { timeout = this.LOCK_TIMEOUT, retryDelay = this.LOCK_RETRY_DELAY, maxRetries = this.MAX_LOCK_RETRIES, } = lockOptions || {}; const lockKey = `${key}:lock`; let retryCount = 0; // 3. 尝试获取分布式锁 while (retryCount < maxRetries) { const locked = await this.redis.set( lockKey, 'LOCKED', 'EX', timeout, 'NX', ); if (locked) { try { // 4. 二次校验缓存(防止等待期间已有数据) const doubleCheck = await this.get<T>(key); if (doubleCheck !== undefined) return doubleCheck; // 5. 执行回退函数获取数据 const data = await fallback(); await this.set(key, data, ttl); return data; } finally { // 6. 释放分布式锁 await this.redis.del(lockKey); } } // 7. 未获取到锁时的处理 retryCount++; await new Promise((resolve) => setTimeout(resolve, retryDelay)); } throw new Error(`Failed to acquire lock after ${maxRetries} attempts`); } /** * 高性能管道批量操作方法 * 使用Redis管道技术批量执行操作,提高性能 */ public async pipelineExecute( operations: Array< | { type: 'SET'; key: string; value: any } | { type: 'SETEX'; key: string; value: any; ttl: number } >, ): Promise<void> { const pipeline = this.redis.pipeline(); operations.forEach((op) => { const serializedValue = this.serialize(op.value); if (op.type === 'SET') { pipeline.set(op.key, serializedValue); } else { pipeline.setex(op.key, op.ttl, serializedValue); } }); await pipeline.exec(); } /** * 设置键值对,可选TTL * @param key 键 * @param value 值 * @param ttl 过期时间(秒) */ public async set(key: string, value: any, ttl?: number): Promise<void> { const serialized = this.serialize(value); if (ttl) { await this.redis.setex(key, ttl, serialized); } else { await this.redis.set(key, serialized); } } /** * 获取键值,返回反序列化后的值 * @param key 键 * @returns 反序列化后的值 */ public async get<T>(key: string): Promise<T | undefined> { const value = await this.redis.get(key); return this.deserialize<T>(value); } /** * 批量设置键值对,可选TTL * @param kvList 键值对列表 * @param ttl 过期时间(秒) */ public async mset(kvList: [string, any][], ttl?: number): Promise<void> { if (ttl) { await this.pipelineExecute( kvList.map(([key, value]) => ({ type: 'SETEX', key, value, ttl, })), ); } else { await this.redis.mset( kvList.map(([key, value]) => [key, this.serialize(value)]), ); } } /** * 批量获取键值 * @param keys 键列表 * @returns 反序列化后的值列表 */ public mget(...keys: string[]): Promise<any[]> { return this.redis .mget(keys) .then((values) => values.map((v) => this.deserialize(v))); } /** * 批量删除键 * @param keys 键列表 */ public async mdel(...keys: string[]): Promise<void> { await this.redis.del(keys); } /** * 删除单个键 * @param key 键 * @returns 删除是否成功 */ public async del(key: string): Promise<boolean> { const result = await this.redis.del(key); return result > 0; } /** * 检查键是否存在 * @param key 键 * @returns 键是否存在 */ public async has(key: string): Promise<boolean> { const count = await this.redis.exists(key); return count !== 0; } /** * 获取键的剩余生存时间 * @param key 键 * @returns 剩余生存时间(秒) */ public async ttl(key: string): Promise<number> { return this.redis.ttl(key); } /** * 根据模式匹配获取键列表 * @param pattern 模式 * @returns 键列表 */ public async keys(pattern = '*'): Promise<string[]> { return this.redis.keys(pattern); } /** * 清空所有键 */ public async clean(): Promise<void> { const allKeys = await this.keys(); if (allKeys.length) { await this.redis.del(allKeys); } } }
通过代码开发,我们构建了一个高性能、高可用的 Redis 缓存服务模块。该模块支持分布式锁、批量操作、数据序列化等核心功能,能够有效提升系统的性能和可靠性
4.5 Session 会话
前面已经完成 Redis 连接和缓存的处理,现在要在这个基础上实现 Session 管理 。Session 可以用于维护用户的登录状态,避免用户每次访问都需要重新授权。以下是基于 Nest.js 和 Redis 实现 Session 管理的详细步骤
1. 安装依赖
npm install express-session connect-redis
express-session:用于在 Express 或 Nest.js 中管理 Session。
connect-redis:用于将 Session 存储到 Redis 中。
2. 配置 Session 中间件 在 AppModule
的 configure
方法中,通过 session
中间件配置了 Session,并将其应用到所有路由(forRoutes('*')
)
import { MiddlewareConsumer, Module } from '@nestjs/common'; import { AppService } from './app.service'; import { RedisCoreModule } from './processors/redis/redis.module'; import { CONFIG, SESSION } from '@app/configs'; import { RedisService } from './processors/redis/redis.service'; import session from 'express-session'; import { RedisStore } from 'connect-redis'; import { OriginMiddleware } from './middlewares/origin.middleware'; import { CorsMiddleware } from './middlewares/cors.middleware'; import { WechatAuthMiddleware } from './middlewares/wechat.middleware'; import { RouterController } from './modules/router/router.controller'; import { DatabaseModule } from './processors/database/database.module'; import modules from './modules'; /** * 应用程序主模块 * @export * @class AppModule */ @Module({ imports: [ // Redis核心模块,用于处理缓存 RedisCoreModule.forRoot(CONFIG.redis), DatabaseModule, ...modules, ], controllers: [], providers: [AppService], }) export class AppModule { constructor(private readonly redisService: RedisService) {} /** * 配置全局中间件 * @param {MiddlewareConsumer} consumer - 中间件消费者 */ configure(consumer: MiddlewareConsumer) { // 应用通用中间件到所有路由 consumer .apply( // 跨域资源共享中间件 CorsMiddleware, // 来源验证中间件 OriginMiddleware, // Session会话中间件 session({ // 使用Redis存储session store: new RedisStore({ client: this.redisService.client, }), ...SESSION, }), ) .forRoutes('*'); // 单独应用微信认证中间件到RouterModule consumer.apply(WechatAuthMiddleware).forRoutes(RouterController); // RouterModule中定义的路由 } }
扩展 Session 的相关配置
{ secret: 'wx-client-session-secret-das23-4241nsdf-%52132=-', // session密钥 name: 'sid', // cookie名称 saveUninitialized: false, // 是否自动保存未初始化的会话 resave: false, // 是否每次都重新保存会话 cookie: { sameSite: true, // 限制第三方Cookie httpOnly: true, // 仅允许服务端修改 maxAge: 7 * 24 * 60 * 60 * 1000, // cookie有效期为7天 }, rolling: true, // 每次请求时强制设置cookie,重置cookie过期时间 };
通过以上配置,Session 可以高效地存储用户登录状态,并通过 Redis 实现持久化和分布式支持。结合微信授权登录功能,可以实现一个完整的用户认证系统。
5、User 表和权限
在前文中我们已经获取微信授权信息(如用户 OpenID、昵称、头像等)的基础上,将这些信息保存到数据库中是一个非常重要的步骤。这样可以方便后续的用户信息维护、登录状态管理以及业务逻辑处理。以下是实现 将授权信息保存到数据库 的详细步骤。
5.1、创建用户实体
使用 @typegoose/typegoose
定义一个用户模型(User
模型),用于存储用户的基本信息。
Typegoose 使用:利用
@typegoose/typegoose
库定义 Mongoose 模型,以便与 MongoDB 进行交互。插件:
AutoIncrementID
:自动递增userId
字段,简化用户 ID 的管理。mongoose-paginate-v2
:提供分页功能,便于查询大量用户数据时进行分页。数据验证:使用
class-validator
库对用户输入进行验证,确保数据的完整性和合法性。字段定义:
userId
:唯一标识符。account
、password
、openid
等:存储用户的基本信息。create_at
和update_at
:记录用户信息的创建和更新时间。role
、privilege
:用于管理用户权限。
// 导入Typegoose的getProviderByTypegoose方法 import { getProviderByTypegoose } from '@app/transformers/model.transform'; // 导入Typegoose的AutoIncrementID插件 import { AutoIncrementID } from '@typegoose/auto-increment'; // 导入Typegoose的modelOptions, plugin, prop装饰器 import { modelOptions, plugin, prop } from '@typegoose/typegoose'; // 导入class-validator中的验证装饰器 import { IsDefined, IsOptional, IsString, IsNumber, IsNotEmpty, IsIn, IsArray, ValidateIf, } from 'class-validator'; // 导入mongoose的分页插件 import paginate from 'mongoose-paginate-v2'; // 应用AutoIncrementID插件,用于自动递增userId字段 @plugin(AutoIncrementID, { field: 'userId', incrementBy: 1, startAt: 1000000000, trackerCollection: 'identitycounters', trackerModelName: 'identitycounter', }) // 应用分页插件 @plugin(paginate) // 设置模型选项,包括转换为对象时的选项和时间戳配置 @modelOptions({ schemaOptions: { toObject: { getters: true }, timestamps: { createdAt: 'create_at', updatedAt: 'update_at', }, }, }) // 定义User类,表示用户模型 export class User { // 用户ID,设置唯一索引 @prop({ unique: true }) userId: number; // 用户账号,必填字段 @IsOptional() @IsString() @prop() account?: string; // 账号可选 @IsOptional() @IsString() @prop({ select: false }) password?: string; // 密码可选 @ValidateIf((o) => !o.openid) @IsNotEmpty({ message: '请输入您的账号或密码' }) @IsString() @IsDefined() @prop({ required: true }) openid?: string; // OpenID必填 // 用户头像,默认为null @ValidateIf((o) => o.avatar !== null) @IsString() @IsOptional() @prop({ default: null }) avatar?: string | null; // 用户角色,默认为[0] @ValidateIf((o) => o.role !== undefined) @IsArray() @IsNumber({}, { each: true }) @IsOptional() @prop({ type: [Number], default: [0] }) role?: number[]; // 创建时间,默认当前时间,索引且不可变 @prop({ default: Date.now, index: true, immutable: true }) create_at?: Date; // 更新时间,默认当前时间 @prop({ default: Date.now }) update_at?: Date; // 用户昵称,可选字段 @ValidateIf((o) => o.nickname !== undefined) @IsString() @IsOptional() @prop() nickname?: string; // 用户性别,默认为0 @IsNumber() @IsIn([0, 1, 2], { message: '性别只能是0, 1或2' }) @prop({ default: 0 }) sex: number; // 用户语言,可选字段 @ValidateIf((o) => o.language !== undefined) @IsString() @IsOptional() @prop() language?: string; // 用户所在城市,可选字段 @ValidateIf((o) => o.city !== undefined) @IsString() @IsOptional() @prop() city?: string; // 用户所在省份,可选字段 @ValidateIf((o) => o.province !== undefined) @IsString() @IsOptional() @prop() province?: string; // 用户所在国家,可选字段 @ValidateIf((o) => o.country !== undefined) @IsString() @IsOptional() @prop() country?: string; // 用户头像URL,可选字段 @ValidateIf((o) => o.headimgurl !== undefined) @IsString() @IsOptional() @prop() headimgurl?: string; // 用户特权信息,默认为空数组 @IsArray() @IsString({ each: true }) @prop({ type: () => [String], default: [] }) privilege: string[]; } // 获取User模型的提供者 export const UserProvider = getProviderByTypegoose(User);
5.3、创建用户服务
定义 UserService
,负责用户相关的业务逻辑,包括用户的登录、信息查询和验证
import { Injectable } from '@nestjs/common'; import { User } from './user.model'; import { Model } from 'mongoose'; import { InjectModel } from '@app/transformers/model.transform'; import { createLogger } from '@app/utils/logger'; import { AUTH } from '@app/configs'; import { JwtService } from '@nestjs/jwt'; import { AuthInfo } from '@app/interfaces/auth.interface'; const logger = createLogger({ scope: 'UserService', time: true, }); /** * 用户服务 * 该服务负责处理用户相关的业务逻辑 */ @Injectable() export class UserService { // 这里可以添加用户服务的相关方法 // 例如: 创建用户、获取用户信息、更新用户信息等 constructor( @InjectModel(User) private authModel: Model<User>, private readonly jwtService: JwtService, ) {} /** * 用户登录微信 * @param userData - 用户数据 * @returns 用户信息 */ public async loginWx(userData: any): Promise<AuthInfo> { // 根据用户的openId查找现有用户 let existingUser = await this.authModel.findOne({ openid: userData.openid, }); // 如果没有找到现有用户,则创建一个新用户 if (!existingUser) { existingUser = await this.authModel.create(userData); } // 如果用户创建失败,则抛出错误 if (!existingUser) { throw new Error('用户创建失败'); } // 根据用户userId、openId、nickname生成token const token = this.generateToken( existingUser.userId.toString() || '', existingUser.openid || '', existingUser.nickname || '', ); logger.info('loginWx', existingUser); return { user: { userId: existingUser.userId, openid: existingUser.openid || '', nickname: existingUser.nickname || '', account: existingUser.account || '', }, token, }; } /** * 生成token * @param userId - 用户ID * @param openId - 用户openId * @param nickname - 用户昵称 * @returns token */ private generateToken( userId: string, openId: string, nickname: string, ): { accessToken: string; expiresIn: number } { const token = { accessToken: this.jwtService.sign({ userId, openId, nickname }), expiresIn: AUTH.expiresIn as number, }; return token; } /** * 验证用户 * @param {ValidateUserRequest} { userId } * @return {*} * @memberof AuthService */ public async validateUser(userId: number) { return await this.getFindUserId(userId); } /** * 根据用户ID查找用户 * @param userId - 用户ID * @returns 用户信息 */ public async getFindUserId(userId: number) { return this.authModel.findOne({ userId }).exec(); } }
5.3 JwtStrategy
定义 JWT 策略,验证 JWT 并返回用户信息
import { AUTH } from '@app/configs'; import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-jwt'; import { UserService } from './user.service'; import { Request } from 'express'; import { get } from 'lodash'; import { createLogger } from '@app/utils/logger'; const logger = createLogger({ scope: 'JwtStrategy', time: true, }); /** * JWT策略 * 用于验证JWT并返回用户信息 */ @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { // 继承PassportStrategy constructor(private readonly userService: UserService) { // 构造函数注入UserService super({ jwtFromRequest: (req: Request) => { // 从请求中获取JWT // 从cookie中获取token const token = get(req, 'cookies.jwt'); logger.log(token, 'token'); return token || null; // 如果jwt为空,返回null以避免报错 }, secretOrKey: AUTH.jwtTokenSecret, // 设置JWT的密钥 }); } /** * 验证用户 * @param {*} payload - JWT载荷 * @return {*} * @memberof JwtStrategy */ async validate(payload: any) { // 验证JWT载荷 const res = await this.userService.validateUser(payload.data); // 调用用户服务验证用户 return res; // 返回验证结果 } }
6、微信SDK配置
配置和获取微信 JS SDK 的相关信息,以便于在前端实现微信功能
/** * 获取微信JS SDK配置 * @param {string} url - 当前页面的URL * @returns {Promise<Object>} - 返回微信JS SDK的配置对象 */ async getWxConfig(url: string): Promise<object> { try { // 尝试从缓存中获取access_token和ticket const cachedConfig = await this.cacheService.get<string>(WX_CONFIG_TOKEN); let accessToken: string; let ticket: string; if (cachedConfig) { const tokenConfig = JSON.parse(cachedConfig); // 如果缓存存在,直接返回 accessToken = tokenConfig.accessToken; ticket = tokenConfig.ticket; } else { // 获取access_token和JS API票据 accessToken = await this.getAccessConfigToken(); ticket = await this.getJsApiTicket(accessToken); await this.cacheService.set( WX_CONFIG_TOKEN, JSON.stringify({ accessToken, ticket }), 7200, ); } // 生成随机字符串和时间戳 const nonceStr = Math.random().toString(36).substring(2, 15); const timestamp = Math.floor(Date.now() / 1000); // 生成签名 const signature = this.generateSignature( ticket, nonceStr, timestamp, url, ); // 构建并返回配置对象 return { appId: this.appId, timestamp, nonceStr, signature }; } catch (error) { logger.error(error, '获取微信JS SDK配置失败'); throw new Error('获取微信JS SDK配置失败: ' + error.message); } } /** * 获取微信配置的access_token * 该方法通过调用微信API获取access_token,用于后续的API请求 * @returns {Promise<string>} - 返回获取到的access_token */ async getAccessConfigToken() { const tokenUrl = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`; const tokenResponse = await axios.get(tokenUrl); return tokenResponse.data.access_token; } /** * 获取微信JS API票据 * 该方法通过调用微信API获取JS API票据,用于后续的API请求 * @param {string} accessToken - 用于获取JS API票据的access_token * @returns {Promise<string>} - 返回获取到的JS API票据 */ async getJsApiTicket(accessToken: string) { const ticketUrl = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`; const ticketResponse = await axios.get(ticketUrl); return ticketResponse.data.ticket; } /** * 生成微信JS SDK签名 * @param {string} ticket - 微信JS API票据 * @param {string} nonceStr - 随机字符串 * @param {number} timestamp - 时间戳 * @param {string} url - 当前页面的URL * @returns {string} - 返回生成的签名 */ private generateSignature( ticket: string, nonceStr: string, timestamp: number, url: string, ): string { const stringToSign = `jsapi_ticket=${ticket}&noncestr=${nonceStr}×tamp=${timestamp}&url=${url}`; return crypto.createHash('sha1').update(stringToSign).digest('hex'); }
前端请求获取到微信JS SDK配置。
import http from '@src/services/http'; export function getWxConfig() { http.get('/api/wechat-auth/wx-config', {}).then((res) => { if (res.appId) { window.wx.config({ appId: res.appId, // 必填,公众号的唯一标识 timestamp: res.timestamp, // 必填,生成签名的时间戳 nonceStr: res.nonceStr, // 必填,生成签名的随机串 signature: res.signature, // 必填,签名 jsApiList: [ 'checkJsApi', 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareWeibo', 'onMenuShareQZone', 'hideMenuItems', 'showMenuItems', 'hideAllNonBaseMenuItem', 'showAllNonBaseMenuItem', 'translateVoice', 'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'playVoice', 'onVoicePlayEnd', 'pauseVoice', 'stopVoice', 'uploadVoice', 'downloadVoice', 'chooseImage', 'previewImage', 'uploadImage', 'downloadImage', 'getNetworkType', 'openLocation', 'getLocation', 'hideOptionMenu', 'showOptionMenu', 'closeWindow', 'scanQRCode', 'chooseWXPay', 'openProductSpecificView', 'addCard', 'chooseCard', 'openCard', ], // 必填,需要使用的JS接口列表 }); } }); }
通过本文的学习,读者将掌握如何基于 Nest.js 构建 BFF 层,并实现微信授权登录功能。希望本文能为开发者提供有价值的参考和启发!
本项目使用 Cursor 工具结合不同的大模型,可以根据提示生成相应代码、优化逻辑并添加注释,从而显著提升代码开发效率。通过自动化处理重复性任务,开发者能够更专注于核心功能的实现,减少手动编码的时间。这种智能化的工具不仅加快了开发进程,还提高了代码质量。 使用Cursor 感受: