整理钉钉原生免登流程

This commit is contained in:
kx_guo 2026-09-05 18:18:48 +08:00
parent 551c68c456
commit 598a2cbc29
3 changed files with 67 additions and 28 deletions

View File

@ -9,6 +9,7 @@ import secrets
import time
import urllib.parse
import urllib.request
import urllib.error
import uuid
import io
import zipfile
@ -333,8 +334,11 @@ def _dingtalk_workbench_user(code: str) -> dict:
try:
with urllib.request.urlopen(token_request, timeout=10) as response:
token_data = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")[:500]
raise HTTPException(status_code=502, detail=f"钉钉应用凭证校验失败:{detail}") from exc
except Exception as exc:
raise HTTPException(status_code=502, detail="钉钉应用凭证校验失败,请稍后重试") from exc
raise HTTPException(status_code=502, detail=f"钉钉应用凭证校验失败:{exc}") from exc
access_token = token_data.get("access_token")
if not access_token:
raise HTTPException(status_code=502, detail="钉钉未返回应用访问令牌,请检查应用权限")
@ -347,8 +351,11 @@ def _dingtalk_workbench_user(code: str) -> dict:
try:
with urllib.request.urlopen(user_request, timeout=10) as response:
user_data = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")[:500]
raise HTTPException(status_code=502, detail=f"无法读取钉钉用户信息:{detail}") from exc
except Exception as exc:
raise HTTPException(status_code=502, detail="无法读取钉钉用户信息,请检查应用免登权限") from exc
raise HTTPException(status_code=502, detail=f"无法读取钉钉用户信息:{exc}") from exc
if user_data.get("errcode", 0) not in (0, None):
raise HTTPException(status_code=502, detail=f"钉钉用户信息读取失败:{user_data.get('errmsg', '请检查应用权限')}")
return user_data.get("result") or user_data

View File

@ -7,7 +7,7 @@
</head>
<body>
<div id="root"></div>
<script data-dingtalk-sdk="true" src="https://g.alicdn.com/dingding/dingtalk-jsapi/2.15.15/dingtalk.open.js"></script>
<script data-dingtalk-sdk="true" src="https://g.alicdn.com/dingding/dingtalk-jsapi/2.10.3/dingtalk.open.js"></script>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -86,16 +86,37 @@ function LoginPage({ onLogin }) {
const [loading, setLoading] = useState(false);
const [dingtalkLoading, setDingtalkLoading] = useState(false);
const [autoDingtalkLogin, setAutoDingtalkLogin] = useState(false);
const [dingtalkLoginFailed, setDingtalkLoginFailed] = useState(false);
const autoLoginStarted = useRef(false);
const query = new URLSearchParams(window.location.search);
const inDingtalkWorkbench = /DingTalk|AliApp\(DT/i.test(navigator.userAgent || "") || query.get("from") === "dingtalk" || Boolean(window.dd || window.DingTalkPC);
const getDingTalkBridge = () => window.dd || window.DingTalkPC || window.dingtalk;
const inDingtalkWorkbench = /DingTalk|AliApp\(DT/i.test(navigator.userAgent || "") || query.get("from") === "dingtalk" || Boolean(getDingTalkBridge());
function loadDingTalkSdk() {
if (window.dd) return Promise.resolve(window.dd);
if (getDingTalkBridge()) return Promise.resolve(getDingTalkBridge());
return new Promise((resolve, reject) => {
const existing = document.querySelector('script[data-dingtalk-sdk="true"]');
const urls = [
existing?.src || "https://g.alicdn.com/dingding/dingtalk-jsapi/2.10.3/dingtalk.open.js",
"https://g.alicdn.com/dingding/dingtalk-jsapi/2.15.15/dingtalk.open.js",
];
let urlIndex = 0;
let settled = false;
const finish = () => {
if (window.dd) resolve(window.dd);
else reject(new Error("钉钉JSAPI未加载,请确认工作台应用类型为H5微应用,并将 ask.dieya.net.cn 加入应用安全域名"));
if (settled) return;
if (getDingTalkBridge()) resolve(getDingTalkBridge());
else if (urlIndex < urls.length - 1) {
urlIndex += 1;
const fallback = document.createElement("script");
fallback.src = urls[urlIndex];
fallback.async = true;
fallback.dataset.dingtalkSdk = "true";
fallback.onload = finish;
fallback.onerror = finish;
document.head.appendChild(fallback);
} else {
settled = true;
reject(new Error("钉钉JSAPI未加载,请确认应用类型为H5微应用,并将 ask.dieya.net.cn 加入应用安全域名"));
}
};
if (existing) {
existing.addEventListener("load", finish, { once: true });
@ -104,7 +125,7 @@ function LoginPage({ onLogin }) {
return;
}
const script = document.createElement("script");
script.src = "https://g.alicdn.com/dingding/dingtalk-jsapi/2.15.15/dingtalk.open.js";
script.src = urls[0];
script.async = true;
script.dataset.dingtalkSdk = "true";
script.onload = finish;
@ -114,7 +135,7 @@ function LoginPage({ onLogin }) {
});
}
async function requestDingTalkAuthCode(corpId) {
const ding = (await loadDingTalkSdk()) || window.dd || window.DingTalkPC;
const ding = (await loadDingTalkSdk()) || getDingTalkBridge();
return new Promise((resolve, reject) => {
if (!ding) return reject(new Error("未检测到钉钉JSAPI,请将当前地址配置为钉钉H5微应用首页,并加入应用安全域名"));
let settled = false;
@ -131,8 +152,15 @@ function LoginPage({ onLogin }) {
const invoke = () => {
try {
const requestAuthCode = ding.runtime?.permission?.requestAuthCode || ding.permission?.requestAuthCode;
if (!requestAuthCode) throw new Error("当前钉钉版本不支持工作台免登JSAPI");
requestAuthCode.call(ding.runtime?.permission || ding.permission, { corpId, onSuccess: success, onFail: failure });
if (requestAuthCode) {
requestAuthCode.call(ding.runtime?.permission || ding.permission, { corpId, onSuccess: success, onFail: failure });
return;
}
if (typeof ding.getAuthCode === "function") {
ding.getAuthCode({ onSuccess: success, onFail: failure });
return;
}
throw new Error("当前钉钉版本不支持工作台免登JSAPI");
} catch (error) {
failure(error);
}
@ -142,21 +170,10 @@ function LoginPage({ onLogin }) {
window.setTimeout(() => failure(new Error("钉钉免登请求超时,请确认应用已发布并开通免登权限")), 10000);
});
}
async function loginWithDingTalk() {
setDingtalkLoading(true);
setError("");
try {
const result = await api("/api/admin/auth/dingtalk/start");
window.location.assign(result.data.url);
} catch (err) {
setError(err.message);
setDingtalkLoading(false);
setAutoDingtalkLogin(false);
}
}
async function loginWithDingTalkWorkbench() {
setDingtalkLoading(true);
setError("");
setDingtalkLoginFailed(false);
try {
const config = await api("/api/admin/auth/dingtalk/config");
if (!config.data?.enabled || !config.data?.corp_id) throw new Error("钉钉工作台免登尚未配置,请联系管理员");
@ -165,6 +182,7 @@ function LoginPage({ onLogin }) {
onLogin(result.data.user);
} catch (err) {
setError(err.message);
setDingtalkLoginFailed(true);
setAutoDingtalkLogin(false);
setDingtalkLoading(false);
}
@ -188,7 +206,7 @@ function LoginPage({ onLogin }) {
}
}
useEffect(() => {
if (!inDingtalkWorkbench || query.get("dingtalk") === "success" || autoLoginStarted.current) return;
if (!inDingtalkWorkbench || autoLoginStarted.current) return;
autoLoginStarted.current = true;
setAutoDingtalkLogin(true);
loginWithDingTalkWorkbench();
@ -196,6 +214,23 @@ function LoginPage({ onLogin }) {
if (autoDingtalkLogin) {
return <div className="loading-screen">正在验证钉钉身份…</div>;
}
if (inDingtalkWorkbench && dingtalkLoginFailed) {
return (
<main className="login-shell">
<section className="login-card dingtalk-workbench-card">
<div className="brand-mark"><MessageCircle size={28} /></div>
<div className="brand-title">{Z.brand}</div>
<h1>钉钉工作台免登</h1>
<p className="login-subtitle">正在使用当前钉钉账号验证身份,不需要输入后台账号密码。</p>
<div className="form-error">{error || "钉钉免登失败,请稍后重试"}</div>
<button className="primary-button login-button" type="button" onClick={() => { setAutoDingtalkLogin(true); loginWithDingTalkWorkbench(); }} disabled={dingtalkLoading}>
{dingtalkLoading ? "正在验证钉钉身份…" : "重新验证"}
</button>
<div className="login-hint">请从钉钉工作台打开此应用,并确认应用已发布、配置安全域名和免登权限。</div>
</section>
</main>
);
}
return (
<main className="login-shell">
<div className="login-decoration">
@ -210,10 +245,7 @@ function LoginPage({ onLogin }) {
<div className="brand-title">{Z.brand}</div>
<h1>{Z.backend}</h1>
<p className="login-subtitle">从钉钉工作台进入,使用钉钉身份安全登录</p>
<button className="dingtalk-login-button" type="button" onClick={inDingtalkWorkbench ? loginWithDingTalkWorkbench : loginWithDingTalk} disabled={dingtalkLoading}>
{dingtalkLoading ? "正在验证钉钉身份…" : "使用钉钉登录"}
</button>
<div className="login-divider"><span>或使用后台账号</span></div>
{!inDingtalkWorkbench && <div className="login-divider"><span>运营后台账号登录</span></div>}
<form onSubmit={submit} className="login-form">
<label>
{Z.username}