基于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=STATE2. 前端将 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() } });