整理钉钉原生免登流程
This commit is contained in:
parent
551c68c456
commit
598a2cbc29
@ -9,6 +9,7 @@ import secrets
|
|||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
import uuid
|
import uuid
|
||||||
import io
|
import io
|
||||||
import zipfile
|
import zipfile
|
||||||
@ -333,8 +334,11 @@ def _dingtalk_workbench_user(code: str) -> dict:
|
|||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(token_request, timeout=10) as response:
|
with urllib.request.urlopen(token_request, timeout=10) as response:
|
||||||
token_data = json.loads(response.read().decode("utf-8"))
|
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:
|
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")
|
access_token = token_data.get("access_token")
|
||||||
if not access_token:
|
if not access_token:
|
||||||
raise HTTPException(status_code=502, detail="钉钉未返回应用访问令牌,请检查应用权限")
|
raise HTTPException(status_code=502, detail="钉钉未返回应用访问令牌,请检查应用权限")
|
||||||
@ -347,8 +351,11 @@ def _dingtalk_workbench_user(code: str) -> dict:
|
|||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(user_request, timeout=10) as response:
|
with urllib.request.urlopen(user_request, timeout=10) as response:
|
||||||
user_data = json.loads(response.read().decode("utf-8"))
|
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:
|
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):
|
if user_data.get("errcode", 0) not in (0, None):
|
||||||
raise HTTPException(status_code=502, detail=f"钉钉用户信息读取失败:{user_data.get('errmsg', '请检查应用权限')}")
|
raise HTTPException(status_code=502, detail=f"钉钉用户信息读取失败:{user_data.get('errmsg', '请检查应用权限')}")
|
||||||
return user_data.get("result") or user_data
|
return user_data.get("result") or user_data
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -86,16 +86,37 @@ function LoginPage({ onLogin }) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [dingtalkLoading, setDingtalkLoading] = useState(false);
|
const [dingtalkLoading, setDingtalkLoading] = useState(false);
|
||||||
const [autoDingtalkLogin, setAutoDingtalkLogin] = useState(false);
|
const [autoDingtalkLogin, setAutoDingtalkLogin] = useState(false);
|
||||||
|
const [dingtalkLoginFailed, setDingtalkLoginFailed] = useState(false);
|
||||||
const autoLoginStarted = useRef(false);
|
const autoLoginStarted = useRef(false);
|
||||||
const query = new URLSearchParams(window.location.search);
|
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() {
|
function loadDingTalkSdk() {
|
||||||
if (window.dd) return Promise.resolve(window.dd);
|
if (getDingTalkBridge()) return Promise.resolve(getDingTalkBridge());
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const existing = document.querySelector('script[data-dingtalk-sdk="true"]');
|
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 = () => {
|
const finish = () => {
|
||||||
if (window.dd) resolve(window.dd);
|
if (settled) return;
|
||||||
else reject(new Error("钉钉JSAPI未加载,请确认工作台应用类型为H5微应用,并将 ask.dieya.net.cn 加入应用安全域名"));
|
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) {
|
if (existing) {
|
||||||
existing.addEventListener("load", finish, { once: true });
|
existing.addEventListener("load", finish, { once: true });
|
||||||
@ -104,7 +125,7 @@ function LoginPage({ onLogin }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const script = document.createElement("script");
|
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.async = true;
|
||||||
script.dataset.dingtalkSdk = "true";
|
script.dataset.dingtalkSdk = "true";
|
||||||
script.onload = finish;
|
script.onload = finish;
|
||||||
@ -114,7 +135,7 @@ function LoginPage({ onLogin }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function requestDingTalkAuthCode(corpId) {
|
async function requestDingTalkAuthCode(corpId) {
|
||||||
const ding = (await loadDingTalkSdk()) || window.dd || window.DingTalkPC;
|
const ding = (await loadDingTalkSdk()) || getDingTalkBridge();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!ding) return reject(new Error("未检测到钉钉JSAPI,请将当前地址配置为钉钉H5微应用首页,并加入应用安全域名"));
|
if (!ding) return reject(new Error("未检测到钉钉JSAPI,请将当前地址配置为钉钉H5微应用首页,并加入应用安全域名"));
|
||||||
let settled = false;
|
let settled = false;
|
||||||
@ -131,8 +152,15 @@ function LoginPage({ onLogin }) {
|
|||||||
const invoke = () => {
|
const invoke = () => {
|
||||||
try {
|
try {
|
||||||
const requestAuthCode = ding.runtime?.permission?.requestAuthCode || ding.permission?.requestAuthCode;
|
const requestAuthCode = ding.runtime?.permission?.requestAuthCode || ding.permission?.requestAuthCode;
|
||||||
if (!requestAuthCode) throw new Error("当前钉钉版本不支持工作台免登JSAPI");
|
if (requestAuthCode) {
|
||||||
requestAuthCode.call(ding.runtime?.permission || ding.permission, { corpId, onSuccess: success, onFail: failure });
|
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) {
|
} catch (error) {
|
||||||
failure(error);
|
failure(error);
|
||||||
}
|
}
|
||||||
@ -142,21 +170,10 @@ function LoginPage({ onLogin }) {
|
|||||||
window.setTimeout(() => failure(new Error("钉钉免登请求超时,请确认应用已发布并开通免登权限")), 10000);
|
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() {
|
async function loginWithDingTalkWorkbench() {
|
||||||
setDingtalkLoading(true);
|
setDingtalkLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
|
setDingtalkLoginFailed(false);
|
||||||
try {
|
try {
|
||||||
const config = await api("/api/admin/auth/dingtalk/config");
|
const config = await api("/api/admin/auth/dingtalk/config");
|
||||||
if (!config.data?.enabled || !config.data?.corp_id) throw new Error("钉钉工作台免登尚未配置,请联系管理员");
|
if (!config.data?.enabled || !config.data?.corp_id) throw new Error("钉钉工作台免登尚未配置,请联系管理员");
|
||||||
@ -165,6 +182,7 @@ function LoginPage({ onLogin }) {
|
|||||||
onLogin(result.data.user);
|
onLogin(result.data.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
setDingtalkLoginFailed(true);
|
||||||
setAutoDingtalkLogin(false);
|
setAutoDingtalkLogin(false);
|
||||||
setDingtalkLoading(false);
|
setDingtalkLoading(false);
|
||||||
}
|
}
|
||||||
@ -188,7 +206,7 @@ function LoginPage({ onLogin }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inDingtalkWorkbench || query.get("dingtalk") === "success" || autoLoginStarted.current) return;
|
if (!inDingtalkWorkbench || autoLoginStarted.current) return;
|
||||||
autoLoginStarted.current = true;
|
autoLoginStarted.current = true;
|
||||||
setAutoDingtalkLogin(true);
|
setAutoDingtalkLogin(true);
|
||||||
loginWithDingTalkWorkbench();
|
loginWithDingTalkWorkbench();
|
||||||
@ -196,6 +214,23 @@ function LoginPage({ onLogin }) {
|
|||||||
if (autoDingtalkLogin) {
|
if (autoDingtalkLogin) {
|
||||||
return <div className="loading-screen">正在验证钉钉身份…</div>;
|
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 (
|
return (
|
||||||
<main className="login-shell">
|
<main className="login-shell">
|
||||||
<div className="login-decoration">
|
<div className="login-decoration">
|
||||||
@ -210,10 +245,7 @@ function LoginPage({ onLogin }) {
|
|||||||
<div className="brand-title">{Z.brand}</div>
|
<div className="brand-title">{Z.brand}</div>
|
||||||
<h1>{Z.backend}</h1>
|
<h1>{Z.backend}</h1>
|
||||||
<p className="login-subtitle">从钉钉工作台进入,使用钉钉身份安全登录</p>
|
<p className="login-subtitle">从钉钉工作台进入,使用钉钉身份安全登录</p>
|
||||||
<button className="dingtalk-login-button" type="button" onClick={inDingtalkWorkbench ? loginWithDingTalkWorkbench : loginWithDingTalk} disabled={dingtalkLoading}>
|
{!inDingtalkWorkbench && <div className="login-divider"><span>运营后台账号登录</span></div>}
|
||||||
{dingtalkLoading ? "正在验证钉钉身份…" : "使用钉钉登录"}
|
|
||||||
</button>
|
|
||||||
<div className="login-divider"><span>或使用后台账号</span></div>
|
|
||||||
<form onSubmit={submit} className="login-form">
|
<form onSubmit={submit} className="login-form">
|
||||||
<label>
|
<label>
|
||||||
{Z.username}
|
{Z.username}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user