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

Vue3 + Vite 实战:接入钉钉 OAuth 扫码登录(内嵌二维码 + 跳转授权)

Vue3 + Vite 实战:接入钉钉 OAuth 扫码登录(内嵌二维码 + 跳转授权)

本文基于 Vue 3 + Vite + TypeScript + Pinia 的登录页工程,完整演示钉钉开放平台OAuth2 授权码模式:内嵌扫码(DTFrameLogin)与整页跳转授权两条链路,并说明前后端如何用code换取业务 Token。照着步骤做,本地即可跑通。


一、先搞清楚:我们要接的是哪一种「钉钉登录」

钉钉开放能力里常见两类登录,容易混:

类型典型场景前端关键字段本文是否覆盖
OAuth2 网站应用登录PC 网页扫码 / 跳转授权,拿code换用户身份client_idredirect_uriscope=openid
企业内部 H5 / JSAPI钉钉客户端内打开 H5,用corpIddd.readycorpId、AgentId 等

本文方案是:用户打开登录页 → 扫码或跳转钉钉授权 → 前端拿到授权码code→ 交给自家后端 → 后端用 AppSecret 向钉钉换用户信息并签发业务 Token → 前端进入系统首页

要点一句话:

  • 前端只持有 Client ID(AppKey),可以写进环境变量。
  • AppSecret / Client Secret 只能放在服务端,绝不能出现在前端仓库或浏览器包里。

二、整体架构与登录时序

┌─────────────┐ 加载 CDN SDK ┌──────────────────────┐ │ 登录页 │ ───────────────────▶ │ g.alicdn.com │ │ (Vue SPA) │ │ h5-dingtalk-login │ └──────┬──────┘ └──────────────────────┘ │ │ ① DTFrameLogin 内嵌二维码 │ 或 ② 跳转 login.dingtalk.com/oauth2/auth ▼ ┌──────────────────────┐ │ 钉钉授权页 / 扫码端 │ └──────────┬───────────┘ │ 返回 authCode / ?code= ▼ ┌──────────────────────┐ POST { code } ┌─────────────────┐ │ handleLoginByCode │ ──────────────────▶ │ 业务后端 │ └──────────────────────┘ │ /api/login/ │ │ dingtalk │ └────────┬────────┘ │ 用 Secret 调钉钉 API │ 签发 accessToken ▼ 前端存 Token,跳转系统首页

两条前端入口最终汇合到同一接口:

  1. 内嵌扫码:SDK 成功回调里直接拿到authCode
  2. 按钮跳转:钉钉把用户重定向回redirect_uri?code=xxx&state=yyy,登录页从 URL 读取code

三、开放平台侧准备(可实操清单)

3.1 创建应用

  1. 打开 钉钉开放平台,登录开发者账号。
  2. 创建企业内部应用或按文档创建具备「登录」能力的应用(以控制台当前产品名为准)。
  3. 在应用详情中找到:
    • Client ID(也常叫 AppKey)—— 给前端用。
    • Client Secret(也常叫 AppSecret)——只给后端用

3.2 配置回调地址(最容易踩坑)

在「登录与分享」或「应用首页 / 回调域名」一类配置里,把授权回调地址加入白名单。地址必须与代码里拼出来的redirect_uri完全一致(含协议、域名、路径、查询串)。

示例(请换成你自己的域名):

https://www.example.com/login?type=ding

本地调试时,若走内嵌扫码且redirect_uri取当前页面源,还需要额外加:

http://localhost:8007/login?type=ding

经验:跳转授权路径若写死了生产域名,本地点「钉钉登录」按钮会跳到生产环境,而不是本机。内嵌二维码一般用window.location.origin,两边要分开想清楚。

3.3 权限与 scope

网站扫码登录常用:

  • response_type=code
  • scope=openid
  • prompt=consent(首次或需要用户确认授权时)

后端换 Token、查用户信息所需的接口权限,在开放平台按官方文档开通(具体接口名以钉钉最新文档为准)。


四、前端工程准备

4.1 技术栈约定

本文示例栈:

  • Vue 3 + Vue Router 4 + Pinia
  • Vite 5 + TypeScript
  • Axios
  • 钉钉登录 SDK:CDN 引入,不装 npm 包

CDN 地址:

https://g.alicdn.com/dingding/h5-dingtalk-login/0.37.0/ddlogin.js

加载成功后,全局会挂上window.DTFrameLogin(部分旧文档还会提到DDLogin,本方案以DTFrameLogin为准)。

4.2 环境变量

在项目根目录.env/.env.development/.env.production中配置:

# 钉钉 OAuth Client ID(与开放平台应用一致)VITE_DINGTALK_CLIENT_ID=dingxxxxxxxxxxxxxxxx

VITE_前缀才会被 Vite 注入到前端代码。types/global.d.ts里可为ImportMetaEnv补上类型:

interfaceImportMetaEnv{readonlyVITE_DINGTALK_CLIENT_ID?:string;// ...}

4.3 TypeScript 声明 SDK

新建types/dingtalk.d.ts

declareglobal{interfaceWindow{DTFrameLogin?:(config:{id:string;width:number;height:number},authConfig:{redirect_uri:string;client_id:string;scope?:string;response_type?:string;state?:string;prompt?:string;},onSuccess:(result:{redirectUrl?:string;authCode?:string;state?:string;})=>void,onFail?:(error:string)=>void)=>void;}}export{};

五、工具层:加载 SDK、拼跳转 URL、生成 state

建议单独建src/utils/dingtalkAuth.ts,把「可配置项」集中管理。

/** 整页跳转授权使用的回调地址(须与开放平台白名单一致) */constREDIRECT_URI='https://www.example.com/login?type=ding';exportfunctiongetClientId():string{constid=import.meta.env.VITE_DINGTALK_CLIENT_IDasstring|undefined;return(id&&String(id).trim())||'';}/** CSRF 防护用的 state */exportconstgenerateState=()=>{if(window?.crypto?.randomUUID){returnwindow.crypto.randomUUID();}return'state-'+Date.now();};/** 内嵌扫码:按当前访问源动态生成 redirect_uri(需 URL encode) */exportconstgetEncodedRedirectUri=()=>{if(window?.location){returnencodeURIComponent(window.location.origin+'/login?type=ding');}returnencodeURIComponent(REDIRECT_URI);};/** 动态注入钉钉登录 SDK,只加载一次 */exportconstloadLoginSdk=(version='0.37.0')=>{returnnewPromise<void>((resolve,reject)=>{if(window.DTFrameLogin){resolve();return;}constscript=document.createElement('script');script.src=`https://g.alicdn.com/dingding/h5-dingtalk-login/${version}/ddlogin.js`;script.onload=()=>resolve();script.onerror=()=>reject(newError('钉钉SDK加载失败'));document.head.appendChild(script);});};/** 整页跳转到钉钉授权页 */exportconstredirectToAuthPage=()=>{constclientId=getClientId();constredirectUri=REDIRECT_URI;conststate=generateState();sessionStorage.setItem('dingtalk_login_state',state);consturl=newURL('https://login.dingtalk.com/oauth2/auth');url.searchParams.set('redirect_uri',redirectUri);url.searchParams.set('response_type','code');url.searchParams.set('client_id',clientId);url.searchParams.set('scope','openid');url.searchParams.set('prompt','consent');url.searchParams.set('state',state);window.location.href=url.toString();};

说明:

  • generateState+sessionStorage用于防 CSRF;回调落地后建议校验state是否与本地一致(见后文「踩坑」)。
  • 内嵌扫码与按钮跳转的redirect_uri可以不同策略:一个跟当前域名,一个跟生产域名。两边都必须在开放平台登记。

可在App.vueonMounted里提前loadLoginSdk(),缩短用户打开登录页后的等待。


六、UI 组件:内嵌二维码 +「钉钉登录」按钮

组件职责:

  1. 挂载后加载 SDK,调用DTFrameLogin渲染二维码。
  2. 扫码成功 →emit('login', authCode)
  3. 点击按钮 →redirectToAuthPage()整页授权。
  4. 失败展示错误文案与重试。

核心逻辑示意(src/components/QrLoginPanel/index.vue):

<template> <div class="flex flex-col justify-center items-center w-full h-full"> <div class="dd-qr-wrap"> <div id="dingtalk-container" class="dd-qr-inner"></div> <div v-if="isLoading" class="dd-login-overlay"> <n-spin size="small" description="加载钉钉登录..." /> </div> </div> <n-text v-if="errorMessage" type="error">{{ errorMessage }}</n-text> <n-button v-if="errorMessage" quaternary @click="handleRetry">重试</n-button> <n-button type="primary" @click="handleAuthRedirect">钉钉登录</n-button> </div> </template> <script lang="ts"> import { ref, defineComponent, onMounted } from 'vue'; import { loadLoginSdk, getClientId, generateState, redirectToAuthPage, getEncodedRedirectUri, } from '@/utils/dingtalkAuth'; export default defineComponent({ name: 'QrLoginPanel', emits: ['login', 'error'], setup(_, { emit }) { const isLoading = ref(false); const errorMessage = ref(''); const onAuthSuccess = (result: { authCode?: string }) => { emit('login', result.authCode); }; const onAuthFail = (error: unknown) => { const msg = typeof error === 'string' ? error : String(error); errorMessage.value = msg; emit('error', msg); }; const renderQrCode = () => { const clientId = getClientId(); const state = generateState(); const redirectUri = getEncodedRedirectUri(); sessionStorage.setItem('dingtalk_login_state', state); window.DTFrameLogin?.( { id: 'dingtalk-container', width: 300, height: 300 }, { redirect_uri: redirectUri, client_id: clientId, scope: 'openid', state, response_type: 'code', prompt: 'consent', }, onAuthSuccess, onAuthFail ); }; const initLogin = async () => { errorMessage.value = ''; if (!window.DTFrameLogin) { await loadLoginSdk(); } renderQrCode(); }; const handleRetry = async () => { isLoading.value = true; try { await initLogin(); } catch (e) { onAuthFail(e); } finally { isLoading.value = false; } }; const handleAuthRedirect = () => { try { redirectToAuthPage(); } catch (e) { onAuthFail(e); } }; onMounted(async () => { isLoading.value = true; try { await initLogin(); } catch (e) { onAuthFail(e); } finally { isLoading.value = false; } }); return { isLoading, errorMessage, handleAuthRedirect, handleRetry }; }, }); </script>

容器样式要点:给#dingtalk-container固定宽高(如 300×300),与DTFrameLoginwidth/height一致,避免二维码被裁切。

登录页挂上组件:

<n-tab-pane name="ding" tab="钉钉扫码登录"> <QrLoginPanel @login="handleLoginByCode" @error="handleScanError" /> </n-tab-pane>

七、拿到 code 之后:调后端换业务 Token

7.1 API 封装

// src/api/user.tsimporthttpfrom'@/utils/http/axios';/** 钉钉扫码 / 授权回调登录 */exportfunctionloginByCode(params:{code:string;state?:string}){returnhttp.request({url:'/api/login/dingtalk',method:'post',data:params,},{// 保留后端原始结构,自行判断 success / accessTokenisTransformResponse:false,});}

请求体字段名以你们后端约定为准。本文示例发送{ code }(注意:若类型里曾写成authCode,要以实际请求体为准,避免类型与报文不一致)。

7.2 Pinia Store

// store 片段asyncloginWithCode(params:{code:string;state?:string}){constresponse=awaitloginByCode(params);const{data,success}=response;if(data?.accessToken){constex=7*24*60*60*1000;storage.set(ACCESS_TOKEN,data.accessToken,ex);storage.set(CURRENT_USER,data,ex);this.setToken(data.accessToken);this.setUserInfo(data);}returnresponse;}

7.3 登录页统一处理(扫码回调 + URL 回跳)

consthandleLoginByCode=async(authCode:string|any)=>{if(!authCode||typeofauthCode!=='string'){message.warning('未获取到授权码,请重试');return;}// 建议同时校验 state(见第八节)constpayload={code:authCode};try{constres=awaituserStore.loginWithCode(payload);const{success,message:msg,data}=resas{success?:boolean;message?:string;data?:{accessToken?:string;account?:{id?:string;personName?:string;username?:string};};};if(!success||!data?.accessToken){message.error(msg||'登录失败');return;}message.success('登录成功,即将进入系统');router.replace('/');}catch(e:unknown){message.error(einstanceofError?e.message:'登录失败');}};consthandleScanError=(msg:string)=>{message.error(msg||'钉钉登录异常');};onMounted(()=>{consturlParams=newURLSearchParams(window.location.search);constcode=urlParams.get('code');if(code){loginType.value='ding';handleLoginByCode(code);}});

后端期望响应形态示例:

{"success":true,"message":"ok","data":{"accessToken":"eyJhbGciOi...","account":{"id":"10001","personName":"张三","username":"zhangsan"}}}

7.4 后端要做什么(前端对接视角)

前端仓库通常不包含 Secret 换票逻辑,但联调时你需要后端同事实现大致流程:

  1. 接收POST /api/login/dingtalk,读取code
  2. 使用Client ID + Client Secret调用钉钉「用 code 换 userAccessToken / 用户信息」接口(以钉钉最新 OpenAPI 为准)。
  3. 用钉钉用户唯一标识(如unionId/openId)匹配或绑定本地账号。
  4. 签发你们自己的accessToken,返回给前端。

切记:Secret 只出现在服务端配置中心或密钥库。


八、本地联调步骤(按顺序打勾)

Step 1:配置环境

npminstall

编辑.env.development

VITE_PORT=8007VITE_DINGTALK_CLIENT_ID=dingxxxxxxxxxxxxxxxx VITE_GLOB_API_URL_PREFIX=/api# 开发代理指向你的后端服务,示例:VITE_PROXY=[["/api","https://api.example.com"]]

Step 2:开放平台白名单

至少登记:

  • 生产:https://www.example.com/login?type=ding
  • 本地(若用动态 origin 扫码):http://localhost:8007/login?type=ding

Step 3:启动前端

npmrun dev

浏览器打开:http://localhost:8007/login

默认切到「钉钉扫码登录」页签,应看到二维码区域。

Step 4:验证扫码链路

  1. 手机钉钉扫码并确认授权。
  2. 浏览器 Network 出现POST /api/login/dingtalk,Request Payload 含code
  3. 响应success: true且带accessToken
  4. 前端保存 Token 后跳转到系统首页(如/)。

Step 5:验证跳转链路

  1. 点击「钉钉登录」。
  2. 跳转到https://login.dingtalk.com/oauth2/auth?...
  3. 授权后回到配置的redirect_uri,地址栏出现code=
  4. 登录页onMounted读到code后自动走同一套换票逻辑。

九、常见问题与踩坑

1. 二维码空白 / SDK 加载失败

  • 检查 CDN 是否被公司网络拦截;可在 Network 看ddlogin.js是否 200。
  • 确认#dingtalk-container在调用DTFrameLogin时已挂载到 DOM。
  • 提供「重试」按钮重新执行initLogin

2.redirect_uri不匹配

钉钉会直接拒绝授权。核对:

  • 协议http/https
  • 端口(本地8007
  • 路径/login
  • 查询参数?type=ding是否也写进了白名单(若代码里带了查询串,白名单一般也要带)

3. 本地扫码能用,按钮跳转却去了生产站

这是「动态 origin」与「写死生产回调」两套策略并存时的正常现象。开发阶段可把redirectToAuthPageredirectUri也改成当前 origin,或单独做环境分支。

4. 前端发了code,后端却说字段不对

对齐字段名:codevsauthCode。以实际 JSON 为准,不要只信类型定义。

5.state写了却没校验

写入sessionStorage['dingtalk_login_state']后,回调时应:

conststateFromUrl=urlParams.get('state');conststateLocal=sessionStorage.getItem('dingtalk_login_state');if(stateFromUrl&&stateLocal&&stateFromUrl!==stateLocal){message.error('登录状态校验失败,请重试');return;}

内嵌扫码成功回调里也会带回state,同样建议比对。

6. 登录成功但不跳转

换票成功后记得显式跳转(如router.replace('/'))。若只存了 Token 却没有路由跳转,用户会感觉「卡住」。

7. Client ID 写进前端是否安全?

Client ID 本身是公开标识,会出现在授权 URL 和前端包中,这是 OAuth 公开客户端的常态。真正敏感的是Secret以及后端签发的业务 Token。


十、文件清单(对照实现)

路径作用
.env*VITE_DINGTALK_CLIENT_ID
types/dingtalk.d.tsDTFrameLogin全局类型
src/utils/dingtalkAuth.tsSDK 加载、Client ID、跳转授权、state
src/components/QrLoginPanel/index.vue内嵌二维码 + 跳转按钮
src/views/login/index.vue处理授权码,换票并进入首页
src/api/user.tsPOST /api/login/dingtalk
src/store/modules/user.tsloginWithCode持久化 Token
src/App.vue可选:预加载 SDK

十一、小结

接入钉钉网页扫码登录,可以按这条最短路径落地:

  1. 开放平台创建应用,拿到 Client ID / Secret,配齐回调白名单。
  2. 前端 CDN 加载h5-dingtalk-login,用DTFrameLogin做内嵌扫码,必要时再做oauth2/auth整页跳转。
  3. 两条路都只负责拿到授权码;用 Secret 换用户身份、发业务 Token 必须在服务端完成
  4. 登录成功后保存 Token,并跳转到系统首页。

把回调地址、字段名、state校验这三处对齐,联调成功率会高很多。其余 UI、Tab、加载态按你们设计系统微调即可。


参考链接

  • 钉钉开放平台
  • 钉钉登录 JS SDK(CDN):https://g.alicdn.com/dingding/h5-dingtalk-login/
  • OAuth 授权入口:https://login.dingtalk.com/oauth2/auth

(具体换票、用户信息接口以开放平台当前文档版本为准,接口路径偶有迭代,联调时请对照最新文档。)

http://www.jsqmd.com/news/1340383/

相关文章:

  • 2026年沈阳施封锁厂家哪家好 金三星施封锁厂 20多年的行业发展经验 - 自由和远方
  • 国家开放大学 2026 年机电一体化技术专业(专科)最新招生资讯 - 升学择校早知道
  • 如何彻底解决ESP32 Arduino开发中的版本依赖冲突问题
  • Video2X高性能视频超分辨率框架深度解析与架构设计实践
  • 企业如何选择适配的人才测评工具
  • 中厂秋招是10月开始投吗?
  • 2026 年重庆办公室高隔断,钢化玻璃隔断改造真实体验分享 - LYL仔仔
  • 随便写点什么
  • Unity资源逆向解析利器AssetStudio:从原理到实战提取游戏资产
  • 小瓶白酒哪个好喝?百年糊涂经典小百年:顺口不寡淡,够味不辣喉
  • 119/126㎡四房,一湾云璟观境适合哪些家庭? - GrowUME
  • 复合肥生产线选购指南:实力厂家对比及避坑要点 - 深度智识库
  • 基于SpringBoot+Vue+微信小程序的学生心理健康测评系统全栈开发实践
  • 2026年江苏/苏州不锈钢管**:不锈钢无缝钢管/三通多通钢管/不锈钢翻边选哪家 - 硬核推荐
  • 工单系统架构设计:分级响应与自动路由方案
  • 为什么选择nxdumptool:3个超越传统备份方案的关键优势
  • USB免驱原理与实现:从标准协议到驱动安装全解析
  • 如何让Axure RP说中文:终极汉化指南与免费语言包下载
  • Unity项目适配HarmonyOS全流程实战:从环境配置到多端部署
  • 两大核心疑问解析:亿品整改政策与本地报价优势 - 探词产品观测室
  • 2026年如何选靠谱HDMI矩阵工厂?抓住这三点不踩坑
  • PTA基础编程题目集 7-16求符合给定条件的整数集(C++语言实现)
  • AI大模型编程能力评测:从Prompt设计到实战对比的完整框架
  • EKF-SLAM从完全发散到厘米级精度:一个SLAM系统的完整调试与优化实战
  • 成本核算从“糊涂账“到“显微镜“:生产工单系统的核算功能到底强在哪
  • 2026开封空调拆装公司推荐,短途搬家公司哪家好避坑指南:4个常见坑+5条硬标准,靠谱 - GEO99
  • 深圳注册公司代办机构口碑评测 2026年8月**单 - 品牌优企推荐
  • 基于.NET 8与MCP协议构建智能体:实战Agent框架与工具集成
  • 经典IP联名AIGC内容制作:角色形象精度控制与跨场景一致性技术实践
  • 2026 北京定制西装老牌专业:在红墙绿瓦与 CBD 霓虹间,寻觅 “懂行” 的匠心 - 西装爱好者