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

发布时间:2026/9/24 13:52:16

Vue3 + Vite 实战:接入钉钉 OAuth 扫码登录(内嵌二维码 + 跳转授权) Vue3 Vite 实战接入钉钉 OAuth 扫码登录内嵌二维码 跳转授权本文基于 Vue 3 Vite TypeScript Pinia 的登录页工程完整演示钉钉开放平台OAuth2 授权码模式内嵌扫码DTFrameLogin与整页跳转授权两条链路并说明前后端如何用code换取业务 Token。照着步骤做本地即可跑通。一、先搞清楚我们要接的是哪一种「钉钉登录」钉钉开放能力里常见两类登录容易混类型典型场景前端关键字段本文是否覆盖OAuth2 网站应用登录PC 网页扫码 / 跳转授权拿code换用户身份client_id、redirect_uri、scopeopenid是企业内部 H5 / JSAPI钉钉客户端内打开 H5用corpId、dd.readycorpId、AgentId 等否本文方案是用户打开登录页 → 扫码或跳转钉钉授权 → 前端拿到授权码code→ 交给自家后端 → 后端用 AppSecret 向钉钉换用户信息并签发业务 Token → 前端进入系统首页。要点一句话前端只持有 Client IDAppKey可以写进环境变量。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跳转系统首页两条前端入口最终汇合到同一接口内嵌扫码SDK 成功回调里直接拿到authCode。按钮跳转钉钉把用户重定向回redirect_uri?codexxxstateyyy登录页从 URL 读取code。三、开放平台侧准备可实操清单3.1 创建应用打开 钉钉开放平台登录开发者账号。创建企业内部应用或按文档创建具备「登录」能力的应用以控制台当前产品名为准。在应用详情中找到Client ID也常叫 AppKey—— 给前端用。Client Secret也常叫 AppSecret——只给后端用。3.2 配置回调地址最容易踩坑在「登录与分享」或「应用首页 / 回调域名」一类配置里把授权回调地址加入白名单。地址必须与代码里拼出来的redirect_uri完全一致含协议、域名、路径、查询串。示例请换成你自己的域名https://www.example.com/login?typeding本地调试时若走内嵌扫码且redirect_uri取当前页面源还需要额外加http://localhost:8007/login?typeding经验跳转授权路径若写死了生产域名本地点「钉钉登录」按钮会跳到生产环境而不是本机。内嵌二维码一般用window.location.origin两边要分开想清楚。3.3 权限与 scope网站扫码登录常用response_typecodescopeopenidpromptconsent首次或需要用户确认授权时后端换 Token、查用户信息所需的接口权限在开放平台按官方文档开通具体接口名以钉钉最新文档为准。四、前端工程准备4.1 技术栈约定本文示例栈Vue 3 Vue Router 4 PiniaVite 5 TypeScriptAxios钉钉登录 SDKCDN 引入不装 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_IDdingxxxxxxxxxxxxxxxxVITE_前缀才会被 Vite 注入到前端代码。types/global.d.ts里可为ImportMetaEnv补上类型interfaceImportMetaEnv{readonlyVITE_DINGTALK_CLIENT_ID?:string;// ...}4.3 TypeScript 声明 SDK新建types/dingtalk.d.tsdeclareglobal{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_URIhttps://www.example.com/login?typeding;exportfunctiongetClientId():string{constidimport.meta.env.VITE_DINGTALK_CLIENT_IDasstring|undefined;return(idString(id).trim())||;}/** CSRF 防护用的 state */exportconstgenerateState(){if(window?.crypto?.randomUUID){returnwindow.crypto.randomUUID();}returnstate-Date.now();};/** 内嵌扫码按当前访问源动态生成 redirect_uri需 URL encode */exportconstgetEncodedRedirectUri(){if(window?.location){returnencodeURIComponent(window.location.origin/login?typeding);}returnencodeURIComponent(REDIRECT_URI);};/** 动态注入钉钉登录 SDK只加载一次 */exportconstloadLoginSdk(version0.37.0){returnnewPromisevoid((resolve,reject){if(window.DTFrameLogin){resolve();return;}constscriptdocument.createElement(script);script.srchttps://g.alicdn.com/dingding/h5-dingtalk-login/${version}/ddlogin.js;script.onload()resolve();script.onerror()reject(newError(钉钉SDK加载失败));document.head.appendChild(script);});};/** 整页跳转到钉钉授权页 */exportconstredirectToAuthPage(){constclientIdgetClientId();constredirectUriREDIRECT_URI;conststategenerateState();sessionStorage.setItem(dingtalk_login_state,state);consturlnewURL(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.hrefurl.toString();};说明generateStatesessionStorage用于防 CSRF回调落地后建议校验state是否与本地一致见后文「踩坑」。内嵌扫码与按钮跳转的redirect_uri可以不同策略一个跟当前域名一个跟生产域名。两边都必须在开放平台登记。可在App.vue的onMounted里提前loadLoginSdk()缩短用户打开登录页后的等待。六、UI 组件内嵌二维码 「钉钉登录」按钮组件职责挂载后加载 SDK调用DTFrameLogin渲染二维码。扫码成功 →emit(login, authCode)。点击按钮 →redirectToAuthPage()整页授权。失败展示错误文案与重试。核心逻辑示意src/components/QrLoginPanel/index.vuetemplate div classflex flex-col justify-center items-center w-full h-full div classdd-qr-wrap div iddingtalk-container classdd-qr-inner/div div v-ifisLoading classdd-login-overlay n-spin sizesmall description加载钉钉登录... / /div /div n-text v-iferrorMessage typeerror{{ errorMessage }}/n-text n-button v-iferrorMessage quaternary clickhandleRetry重试/n-button n-button typeprimary clickhandleAuthRedirect钉钉登录/n-button /div /template script langts 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与DTFrameLogin的width/height一致避免二维码被裁切。登录页挂上组件n-tab-pane nameding tab钉钉扫码登录 QrLoginPanel loginhandleLoginByCode errorhandleScanError / /n-tab-pane七、拿到 code 之后调后端换业务 Token7.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}){constresponseawaitloginByCode(params);const{data,success}response;if(data?.accessToken){constex7*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 回跳consthandleLoginByCodeasync(authCode:string|any){if(!authCode||typeofauthCode!string){message.warning(未获取到授权码请重试);return;}// 建议同时校验 state见第八节constpayload{code:authCode};try{constresawaituserStore.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((){consturlParamsnewURLSearchParams(window.location.search);constcodeurlParams.get(code);if(code){loginType.valueding;handleLoginByCode(code);}});后端期望响应形态示例{success:true,message:ok,data:{accessToken:eyJhbGciOi...,account:{id:10001,personName:张三,username:zhangsan}}}7.4 后端要做什么前端对接视角前端仓库通常不包含 Secret 换票逻辑但联调时你需要后端同事实现大致流程接收POST /api/login/dingtalk读取code。使用Client ID Client Secret调用钉钉「用 code 换 userAccessToken / 用户信息」接口以钉钉最新 OpenAPI 为准。用钉钉用户唯一标识如unionId/openId匹配或绑定本地账号。签发你们自己的accessToken返回给前端。切记Secret 只出现在服务端配置中心或密钥库。八、本地联调步骤按顺序打勾Step 1配置环境npminstall编辑.env.developmentVITE_PORT8007VITE_DINGTALK_CLIENT_IDdingxxxxxxxxxxxxxxxx VITE_GLOB_API_URL_PREFIX/api# 开发代理指向你的后端服务示例VITE_PROXY[[/api,https://api.example.com]]Step 2开放平台白名单至少登记生产https://www.example.com/login?typeding本地若用动态 origin 扫码http://localhost:8007/login?typedingStep 3启动前端npmrun dev浏览器打开http://localhost:8007/login默认切到「钉钉扫码登录」页签应看到二维码区域。Step 4验证扫码链路手机钉钉扫码并确认授权。浏览器 Network 出现POST /api/login/dingtalkRequest Payload 含code。响应success: true且带accessToken。前端保存 Token 后跳转到系统首页如/。Step 5验证跳转链路点击「钉钉登录」。跳转到https://login.dingtalk.com/oauth2/auth?...授权后回到配置的redirect_uri地址栏出现code。登录页onMounted读到code后自动走同一套换票逻辑。九、常见问题与踩坑1. 二维码空白 / SDK 加载失败检查 CDN 是否被公司网络拦截可在 Network 看ddlogin.js是否 200。确认#dingtalk-container在调用DTFrameLogin时已挂载到 DOM。提供「重试」按钮重新执行initLogin。2.redirect_uri不匹配钉钉会直接拒绝授权。核对协议http/https端口本地8007路径/login查询参数?typeding是否也写进了白名单若代码里带了查询串白名单一般也要带3. 本地扫码能用按钮跳转却去了生产站这是「动态 origin」与「写死生产回调」两套策略并存时的正常现象。开发阶段可把redirectToAuthPage的redirectUri也改成当前 origin或单独做环境分支。4. 前端发了code后端却说字段不对对齐字段名codevsauthCode。以实际 JSON 为准不要只信类型定义。5.state写了却没校验写入sessionStorage[dingtalk_login_state]后回调时应conststateFromUrlurlParams.get(state);conststateLocalsessionStorage.getItem(dingtalk_login_state);if(stateFromUrlstateLocalstateFromUrl!stateLocal){message.error(登录状态校验失败请重试);return;}内嵌扫码成功回调里也会带回state同样建议比对。6. 登录成功但不跳转换票成功后记得显式跳转如router.replace(/)。若只存了 Token 却没有路由跳转用户会感觉「卡住」。7. Client ID 写进前端是否安全Client ID 本身是公开标识会出现在授权 URL 和前端包中这是 OAuth 公开客户端的常态。真正敏感的是Secret以及后端签发的业务 Token。十、文件清单对照实现路径作用.env*VITE_DINGTALK_CLIENT_IDtypes/dingtalk.d.tsDTFrameLogin全局类型src/utils/dingtalkAuth.tsSDK 加载、Client ID、跳转授权、statesrc/components/QrLoginPanel/index.vue内嵌二维码 跳转按钮src/views/login/index.vue处理授权码换票并进入首页src/api/user.tsPOST /api/login/dingtalksrc/store/modules/user.tsloginWithCode持久化 Tokensrc/App.vue可选预加载 SDK十一、小结接入钉钉网页扫码登录可以按这条最短路径落地开放平台创建应用拿到 Client ID / Secret配齐回调白名单。前端 CDN 加载h5-dingtalk-login用DTFrameLogin做内嵌扫码必要时再做oauth2/auth整页跳转。两条路都只负责拿到授权码用 Secret 换用户身份、发业务 Token 必须在服务端完成。登录成功后保存 Token并跳转到系统首页。把回调地址、字段名、state校验这三处对齐联调成功率会高很多。其余 UI、Tab、加载态按你们设计系统微调即可。参考链接钉钉开放平台钉钉登录 JS SDKCDNhttps://g.alicdn.com/dingding/h5-dingtalk-login/OAuth 授权入口https://login.dingtalk.com/oauth2/auth具体换票、用户信息接口以开放平台当前文档版本为准接口路径偶有迭代联调时请对照最新文档。
延伸阅读

更多相关文章

2026/9/24 13:51:04

FPGA时序优化:Valid/Ready握手信号打拍原理与实战

1. 项目概述:从“握手打拍”聊起最近在调试一个基于FPGA的数据采集模块时,又遇到了那个熟悉的老朋友——时序违例。问题的表象是数据偶尔会错位,深究下去,根源往往出在两个模块间数据交换的“握手”环节没处理好。这让我想起&…

2026/9/19 23:30:29

搜广推算法面试实战指南:从基础理论到系统设计

1. 项目概述:一份面向搜广推领域的面试实战指南又到了招聘季,看着身边不少朋友和团队里的同学开始为面试做准备,特别是那些瞄准搜索、广告、推荐(业内常合称为“搜广推”)方向的算法工程师们。我发现一个挺普遍的现象&…

2026/9/24 13:51:15

【企业智能体开发】部署可用的企业服务台智能体

系列开始时,小林在培训前遇到投屏故障,我们用一张任务卡、一条执行循环和几项受控工具,逐步把她的求助从模糊描述推进到真实工单。演示代码能跑,并不等于企业服务台已经可用。真正上线后,员工会在不同时间、不同页面重复提问;知识文档会更新,模型接口会超时,工单系统会…

2026/9/24 13:51:15

NLDM、CCS、ECSM时序模型选型指南:从原理到先进工艺签核实操

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/23 12:07:00

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/23 12:06:55

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/24 0:00:21

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:21

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:21

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/22 16:34:32

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/22 20:01:30

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/22 13:25:41

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码