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

发布时间:2026/11/10 18:13:24

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/11/10 18:13:22

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

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

2026/11/10 1:57:09

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

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

2026/11/10 18:10:39

电工作业帮助别怕考不过,含金量高是关键

电工作业帮助别怕考不过,含金量高是关键 怕考不过白交培训费?别慌,电工作业帮助不是玄学,只要掌握流程、备好材料、选对机构,含金量高不高的证也能拿到手。作为开封应届工程类毕业生,很多人对电工作业证又爱又怕,爱的是它在就业市场的含金量,怕的是考…

2026/11/10 18:10:39

考电工证的问题怎么解决?考前押题+报名避坑全攻略

考电工证的问题怎么解决?考前押题+报名避坑全攻略 你是不是也遇到过这种情况? 不知道去哪报名怕被中介坑 ,看到网上五花八门的培训机构,价格参差不齐,考试通过率也高低不一,关键是不知道哪个靠谱。别急,今天就从三门峡本地学员的实际经历出发,告诉…

2026/11/10 18:10:39

南昌的电工证怎么考良心建议这样避开中介坑

南昌的电工证怎么考良心建议这样避开中介坑 不知道去哪报名怕被中介坑?南昌的电工证怎么考,是很多准备入行或转行的小伙伴最头疼的问题。尤其是在南昌这样的城市,电工证的报考流程看似简单,实则暗藏玄机,稍有不慎就可能被黑中介忽悠,浪费时间又花冤枉钱…

2026/11/10 18:10:39

保定电工证从哪里复审多久拿证怎么安排

保定电工证从哪里复审多久拿证怎么安排 实操考试心里没底怕挂科,这是很多报考电工证的朋友在报名前最担心的问题。尤其是在保定,电工证复审和继续教育流程复杂,加上实操考试要求高,很多学员在准备阶段就陷入了焦虑。其实,只要掌握好时间节点和备考方法,…

2026/11/10 18:10:39

35岁转行电工,靠室内装修电工证图找到新工作的真实经历

35岁转行电工,靠室内装修电工证图找到新工作的真实经历 刚失业那会儿,我天天在招聘网站上刷,看到电工岗位写着“持证上岗”,心里咯噔一下——这不就是我现在的死穴吗?别人都有证,我连个室内装修电工证图都拿不出来,怎么跟人家竞争?这事儿真不是闹着…

2026/11/10 18:05:37

撬毛工特种作业证图片官方回应:工地太忙根本没时间复习考试怎么办

撬毛工特种作业证图片官方回应:工地太忙根本没时间复习考试怎么办 工地太忙根本没时间复习考试,这是很多撬毛工的真实写照。尤其是现在行业监管越来越严,特种作业操作证成为从事相关工作的“门槛”。但很多工友因为工作节奏快、任务重,根本无暇顾及复习和…

2026/11/10 8:12:38

电工证丢失还能补吗?千万别踩坑,丢了证等于失业

电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。…

2026/11/10 0:01:28

学历不高怕报不上名?考电工证报名费+网上查询全攻略来了

学历不高怕报不上名?考电工证报名费+网上查询全攻略来了 你是不是也担心自己学历不高,考电工证报名费太贵,根本报不上名?别急,今天就来给你讲讲,怎么用最少的钱,把证拿到手,还能在工地上多挣点钱。 电工证报名费多少钱?网上查询是关键…

2026/11/10 0:01:29

绍兴考电工证培训机构怎么选?2026河南最新政策别错过

绍兴考电工证培训机构怎么选?2026河南最新政策别错过 怕考不过白交培训费?你不是一个人。2026河南最新政策出台,对电工证考试、报名、培训、复审都有明确规定。选择绍兴考电工证培训机构,别再踩坑,下面一步步教你避坑。…

2026/11/9 6:30:27

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/10 10:29:37

SSM电子书店源码全解析:结构、原理、部署与排错实战

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

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

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

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