news 2026/8/6 15:10:08

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/6 15:05:47

Windows恢复环境丢失的三种修复方法:从ReAgentC到BCD重建

1. 问题本质&#xff1a;为什么Windows会“找不到恢复环境”&#xff1f;如果你在尝试进入Windows恢复环境&#xff08;WinRE&#xff09;时&#xff0c;屏幕上赫然出现了“找不到恢复环境”或类似的错误提示&#xff0c;先别急着重装系统。这个看似棘手的问题&#xff0c;背后…

作者头像 李华
网站建设 2026/8/6 15:05:45

NMOS与PMOS实战解析:从核心原理到防反接、电平转换电路设计

1. 从两个符号到电路基石&#xff1a;NMOS与PMOS的深度解析 在电子设计的江湖里&#xff0c;无论你是刚拿起烙铁的新手&#xff0c;还是已经画了十几年板子的老鸟&#xff0c;有两个名字你绝对绕不开&#xff1a;NMOS和PMOS。它们就像电路世界里的“阴”与“阳”&#xff0c;一…

作者头像 李华
网站建设 2026/8/6 15:02:07

不只是发稿,更是建信任:朝闻通如何让品牌故事在海外生根?

当 “不出海&#xff0c;就出局” 成为中国企业的共识&#xff0c;亚太市场凭借地缘与文化邻近性&#xff0c;自然成为多数品牌全球化征程的第一站。然而&#xff0c;一个普遍的困境是&#xff1a;许多企业完成了 “发稿” 动作&#xff0c;稿件却如石沉大海&#xff0c;未能转…

作者头像 李华
网站建设 2026/8/6 14:59:44

5大核心功能解析:d2s-editor暗黑破坏神2存档编辑器深度体验指南

5大核心功能解析&#xff1a;d2s-editor暗黑破坏神2存档编辑器深度体验指南 【免费下载链接】d2s-editor 项目地址: https://gitcode.com/gh_mirrors/d2/d2s-editor 你是否厌倦了反复刷装备的枯燥过程&#xff1f;是否想体验不同角色build的乐趣却不想重新练级&#xf…

作者头像 李华
网站建设 2026/8/6 14:57:49

ChoccyIDE:轻量级IDE的极速开发体验

1. ChoccyIDE是什么&#xff1f;为什么开发者都在关注它ChoccyIDE是近期在开发者社区中热议的一款轻量级集成开发环境&#xff08;IDE&#xff09;。作为一个专注于快速编码体验的工具&#xff0c;它特别适合需要频繁切换项目或进行原型开发的程序员。我第一次接触ChoccyIDE是在…

作者头像 李华