当前位置: 首页 > news >正文

基于Nest.js的企业微信扫码登录全流程实战

目录

一、扫码登录整体流程

二、详细流程与代码

1. 前端生成二维码链接

2. 前端将 code 传给后端

3. 后端 Controller 接收 code

4. WechatService 处理扫码登录

5. AuthService 生成 JWT Token 并存 Redis

三、部门名称获取

四、前端后续请求带上 Token


企业微信提供了OAuth的扫码登录授权方式,可以让企业的网站在浏览器内打开时,引导成员使用企业微信扫码登录授权,从而获取成员的身份信息,免去登录的环节。本文结合实际项目,详细讲解扫码登录的完整流程,并给出前后端关键代码示例。

一、扫码登录整体流程

1. 前端生成二维码链接

用户在前端页面看到企业微信扫码二维码,扫码后企业微信会回调到指定地址并带上临时 code。

2. 前端获取 code 并传给后端

前端从回调 URL 拿到 code,POST 给后端接口(WechatController 控制器,@Post('wechat-login/get'))。

3. Controller 收到 code 后,交给 WechatService 处理

Controller 收到 code 后,交给专门处理企业微信业务的 WechatService 去处理。

4. WechatService 调用 getAccessToken() 方法

先检查 access_token 有没有过期,过期则用企业身份证明(固定的 corpId 和 corpSecret)去企业微信那里换 access_token。

5. 用 access_token 和 code 调用 getUserId() 方法

去企业微信服务器获得企业微信用户 ID (UserId)。

6. 获取用户部门信息

除了知道是谁,系统还需要知道"用户属于哪个部门",方便后续控制权限(比如技术部的用户只能看技术部的内容)。系统带着 accessToken 和 UserId,通过 WechatService.getUserDepartId 方法,调用企业微信接口,企业微信返回用户的部门 ID userDepatId。

7. AuthService.autoDepTokenByWechat 方法生成专属 Token

构造 payload:包含用户 ID(wechatUserId)、部门 ID(dept_id)、登录时间、角色等。用 jwtService.sign(payload) 生成 Token,并加上 Bearer 前缀,把 Token 存到 Redis 里,系统把 Token 返回给前端,前端存起来(如 localStorage 或 cookie)。以后访问系统的任何一个页面,前端都会在请求头上带着这个 Token。

二、详细流程与代码

1. 前端生成二维码链接

const corpId = '企业ID'; const agentId = '应用ID'; const redirectUri = encodeURIComponent('https://your-domain.com/wechat-callback'); const state = Math.random().toString(36).slice(2); const qrCodeUrl = `https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=${corpId}&agentid=${agentId}&redirect_uri=${redirectUri}&state=${state}`;

扫码后,企业微信会跳转到 redirect_uri,并带上 code 和 state 参数:

https://your-domain.com/wechat-callback?code=CODE&state=STATE

2. 前端将 code 传给后端

// 在回调页面获取 code const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); // 发送给后端 fetch('/api/v1/wechat-login/get', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) }) .then(res => res.json()) .then(data => { // 保存 token localStorage.setItem('token', data.token); // 跳转到系统首页 window.location.href = '/'; });

3. 后端 Controller 接收 code

import { Controller, Post, Body } from '@nestjs/common'; import { WechatService } from './wechat-login.service'; @Controller('wechat-login') export class WechatController { constructor(private readonly wechatService: WechatService) {} @Post('get') async getUserId1(@Body('code') code: string) { return await this.wechatService.getToken(code); } }

4. WechatService 处理扫码登录

import { Injectable, Logger } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; import { AuthService } from '../auth/auth.service'; import { Conf } from 'src/config/conf'; @Injectable() export class WechatService { private readonly logger = new Logger(WechatService.name); private tokenCache: { token: string; expiresAt: number } = { token: '', expiresAt: 0 }; constructor( private readonly httpService: HttpService, private readonly authService: AuthService, ) {} // 获取企业微信 access_token,带缓存 async getAccessToken1(): Promise<string> { const now = Date.now() / 1000; if (this.tokenCache.token && this.tokenCache.expiresAt - now > 300) { return this.tokenCache.token; } const { data } = await firstValueFrom( this.httpService.get( `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${Conf.wework.corpId}&corpsecret=${Conf.wework.corpSecret}`, ), ); if (data.errcode !== 0) throw new Error(data.errmsg); this.tokenCache = { token: data.access_token, expiresAt: now + data.expires_in, }; return this.tokenCache.token; } // 用 code 换取用户ID async getUserId(code: string, accessToken: string): Promise<string> { const url = `https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=${accessToken}&code=${code}`; const { data } = await firstValueFrom(this.httpService.get(url)); if (data.errcode !== 0) throw new Error(data.errmsg); return data.UserId; } // 获取用户部门ID async getUserDepartId(userid: string, accessToken: string): Promise<number[]> { const url = `https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=${accessToken}&userid=${userid}`; const { data } = await firstValueFrom(this.httpService.get(url)); if (data.errcode !== 0) throw new Error(data.errmsg); return data.department; // 数组 } // 主流程 async getToken(code: string): Promise<{ token: string; user_id: string; dept_id: number[] }> { const accessToken = await this.getAccessToken1(); const userId = await this.getUserId(code, accessToken); const deptIds = await this.getUserDepartId(userId, accessToken); const user = await this.authService.autoDepTokenByWechat(userId, deptIds); return { token: user.token, user_id: userId, dept_id: deptIds }; } }

5. AuthService 生成 JWT Token 并存 Redis

import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { RedisService } from '../redis/redis.service'; import { Conf } from 'src/config/conf'; @Injectable() export class AuthService { constructor( private readonly jwtService: JwtService, private readonly redisService: RedisService, ) {} async autoDepTokenByWechat(wechatUserId: string, dept_id: number[]) { const payload = { user_id: wechatUserId, role: 2, distributor_id: null, login_time: Date.now(), login_type: 3, super_admin: 1, dept_id, }; const token = 'Bearer ' + this.jwtService.sign(payload); await this.redisService.set(token, JSON.stringify(payload), Conf.expiresIn); return { token }; } }

三、部门名称获取

如果需要部门名称,可用 access_token 调用:

GET https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=ACCESS_TOKEN

四、前端后续请求带上 Token

function getAuthHeader() { const raw = localStorage.getItem('token'); if (!raw) throw new Error('未登录或无 token'); return raw.startsWith('Bearer ') ? raw : `Bearer ${raw}`; } fetch('/api/your-protected-api', { method: 'GET', headers: { Authorization: getAuthHeader() } });
http://www.gsyq.cn/news/1558557.html

相关文章:

  • CANN/GE RunGraph API文档
  • OpCore Simplify:3步快速创建黑苹果OpenCore EFI的终极指南
  • 告别抢票焦虑:biliTickerBuy 自动化工具的技术实现与应用指南
  • gh_mirrors/conf1/conf用户案例:打造高效Focused工作环境
  • 深入解析MCF5282/MCF5216微控制器:架构、外设与低功耗设计实战
  • 告别抢票焦虑:大麦网自动化工具终极指南
  • 如何5分钟快速上手GuoFeng3:古风AI绘画的终极完整指南
  • 无线计算技术AirCPU框架:原理、优势与应用
  • Hermes Agent实战手册:轻量级AI智能体本地部署与调试指南
  • Cursor AI版本管理完整指南:专业下载链接验证与安全降级策略
  • 2026赣州本地人必选防水补漏检测维修公司靠谱服务商TOP5推荐:房屋渗漏水检测维修/卫生间/厨房/天花板/阳台/外墙渗漏水检测补漏维修-暗管漏水检测专业仪器精准定位漏水点 - 即刻修防水
  • MC68HC(7)08KH12:经典USB HUB微控制器架构与嵌入式开发实战
  • 2026贵阳漏水检测维修精选优质服务商TOP5推荐!卫生间漏水/厨房漏水/屋顶天花板漏水/阳台漏水/地下室漏水防水补漏检测维修-正规防水补漏公司优选口碑榜测评推荐 - 即刻修防水
  • xtb:当传统量子化学计算让你束手无策时,这个半经验扩展紧束缚程序包如何成为你的科研加速器?
  • GPT-SoVITS v4深度解析:三阶段架构如何实现少样本语音合成的革命性突破
  • 掌握AI写专著技巧,20万字专著轻松撰写不是梦
  • 终极Markdown浏览器插件指南:30+主题+数学公式+流程图一站式解决方案
  • 5分钟掌握TestSigma:AI驱动的跨平台测试自动化实战指南
  • H100与DeepSeek-V4-Flash软硬协同推理实战
  • 三步轻松备份微信聊天记录:WechatBakTool让珍贵对话永不丢失
  • 如何用StemRoller一键分离歌曲人声和伴奏?3分钟上手教程
  • WaveTools:为现代游戏开发者打造的智能性能分析与优化套件
  • Umi-OCR终极指南:三步实现免费离线文字识别与数字提取
  • S12XS MCU端口复用与电源管理:嵌入式硬件设计核心解析
  • Tailwind CSS快速开发技巧:Instagram界面组件从零到一实现
  • 5步实现大麦抢票自动化:双端API集成与扩展指南
  • 昇腾GE性能分析初始化函数
  • Vssue性能优化技巧:提升评论系统加载速度的7个方法
  • Qwen音频与多模态模型本地部署实战指南
  • CANN/ge图引擎替换API