基于Nest.js的企业微信扫码登录全流程实战 目录一、扫码登录整体流程二、详细流程与代码1. 前端生成二维码链接2. 前端将 code 传给后端3. 后端 Controller 接收 code4. WechatService 处理扫码登录5. AuthService 生成 JWT Token 并存 Redis三、部门名称获取四、前端后续请求带上 Token企业微信提供了OAuth的扫码登录授权方式可以让企业的网站在浏览器内打开时引导成员使用企业微信扫码登录授权从而获取成员的身份信息免去登录的环节。本文结合实际项目详细讲解扫码登录的完整流程并给出前后端关键代码示例。一、扫码登录整体流程1. 前端生成二维码链接用户在前端页面看到企业微信扫码二维码扫码后企业微信会回调到指定地址并带上临时 code。2. 前端获取 code 并传给后端前端从回调 URL 拿到 codePOST 给后端接口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包含用户 IDwechatUserId、部门 IDdept_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?codeCODEstateSTATE2. 前端将 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 接收 codeimport { 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(): Promisestring { 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): Promisestring { 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): Promisenumber[] { 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 并存 Redisimport { 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_tokenACCESS_TOKEN四、前端后续请求带上 Tokenfunction 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() } });