当前位置: 首页 > 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.jsqmd.com/news/1046649/

相关文章:

  • CANN/GE RunGraph API文档
  • AspectMock与Codeception完美结合:构建全面的PHP测试套件
  • OpCore Simplify:3步快速创建黑苹果OpenCore EFI的终极指南
  • 告别抢票焦虑:biliTickerBuy 自动化工具的技术实现与应用指南
  • 2026贺州本地人必选防水补漏检测维修公司靠谱服务商TOP5推荐:房屋渗漏水检测维修/卫生间/厨房/天花板/阳台/外墙渗漏水检测补漏维修-暗管漏水检测专业仪器精准定位漏水点 - 即刻修防水
  • Presenton开源AI演示生成工具:企业级演示文稿创作的完整解决方案
  • GE 自定义算子架构设计
  • gh_mirrors/conf1/conf用户案例:打造高效Focused工作环境
  • 终极Raylib跨平台游戏开发指南:从零到专业级游戏引擎
  • CANN/GE获取Graph输出属性API
  • CANN/ops-math取余算子标量接口
  • IEC 60730标准下的MCU功能安全测试:从Class B到Class C的工程实践
  • CANN/ge图引擎字符串属性设置API
  • 深入解析MCF5282/MCF5216微控制器:架构、外设与低功耗设计实战
  • 告别抢票焦虑:大麦网自动化工具终极指南
  • (2026新)石家庄正规防水补漏公司口碑榜TOP5权威推荐!卫生间/厨房/阳台/屋顶/天花板/地下室渗漏水检测维修攻略-靠谱漏水检测维修师傅推荐 - 安佳防水
  • (2026新)福州正规防水补漏公司口碑榜TOP5权威推荐!卫生间/厨房/阳台/屋顶/天花板/地下室渗漏水检测维修攻略-靠谱漏水检测维修师傅推荐 - 安佳防水
  • 深度解析Maya权重平滑:如何用brSmoothWeights解决角色动画的5大技术难题
  • 如何5分钟快速上手GuoFeng3:古风AI绘画的终极完整指南
  • mal_unpack高级参数完全指南:/shellc、/hooks、/trigger等选项实战应用 [特殊字符]
  • 无线计算技术AirCPU框架:原理、优势与应用
  • Hermes Agent实战手册:轻量级AI智能体本地部署与调试指南
  • 2026赣州漏水检测维修精选优质服务商TOP5推荐!卫生间漏水/厨房漏水/屋顶天花板漏水/阳台漏水/地下室漏水防水补漏检测维修-正规防水补漏公司优选口碑榜测评推荐 - 即刻修防水
  • MC68HC(7)08KH12:经典USB HUB微控制器架构与嵌入式开发实战
  • Awesome-AI 开源仓库架构设计与技术学习路线工程化沉淀方案
  • Cursor AI版本管理完整指南:专业下载链接验证与安全降级策略
  • 2026赣州本地人必选防水补漏检测维修公司靠谱服务商TOP5推荐:房屋渗漏水检测维修/卫生间/厨房/天花板/阳台/外墙渗漏水检测补漏维修-暗管漏水检测专业仪器精准定位漏水点 - 即刻修防水
  • (2026新)珠海正规防水补漏公司口碑榜TOP5权威推荐!卫生间/厨房/阳台/屋顶/天花板/地下室渗漏水检测维修攻略-靠谱漏水检测维修师傅推荐 - 安佳防水
  • 深入解析CAN总线标识符过滤:原理、配置与MSCAN实战指南
  • 终极指南:跨平台获取macOS系统镜像的完整解决方案