fix(android): cross-origin Bearer auth in Tauri shell
Inject a Tauri shell script during CI prep to disable service workers and attach Bearer tokens on cross-origin API requests. Enable Android third-party cookies in MainActivity and eval the same script from Rust on page load.
This commit is contained in:
32
.github/scripts/inject-tauri-shell.mjs
vendored
Normal file
32
.github/scripts/inject-tauri-shell.mjs
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const root = path.resolve(__dirname, "../..");
|
||||||
|
const MARKER = "data-web2app-tauri-shell";
|
||||||
|
const scriptPath = path.join(root, "template", "scripts", "tauri-shell.js");
|
||||||
|
|
||||||
|
export function injectTauriShell(distDir) {
|
||||||
|
const indexPath = path.join(distDir, "index.html");
|
||||||
|
if (!fs.existsSync(indexPath)) return;
|
||||||
|
|
||||||
|
let html = fs.readFileSync(indexPath, "utf8");
|
||||||
|
if (html.includes(MARKER)) return;
|
||||||
|
|
||||||
|
const script = fs.readFileSync(scriptPath, "utf8").trim();
|
||||||
|
const tag = `<script ${MARKER}>${script}</script>`;
|
||||||
|
|
||||||
|
if (html.includes("<head>")) {
|
||||||
|
html = html.replace("<head>", `<head>\n${tag}`);
|
||||||
|
} else if (html.includes("</head>")) {
|
||||||
|
html = html.replace("</head>", `${tag}\n</head>`);
|
||||||
|
} else if (html.includes("</body>")) {
|
||||||
|
html = html.replace("</body>", `${tag}\n</body>`);
|
||||||
|
} else {
|
||||||
|
html += `\n${tag}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(indexPath, html);
|
||||||
|
console.log("Injected Tauri shell script into index.html");
|
||||||
|
}
|
||||||
@@ -32,6 +32,8 @@ function patchMainActivity(filePath) {
|
|||||||
const content = `package ${pkg}
|
const content = `package ${pkg}
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebView
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
class MainActivity : TauriActivity() {
|
class MainActivity : TauriActivity() {
|
||||||
@@ -39,6 +41,13 @@ class MainActivity : TauriActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
// 内容不延伸到状态栏 / 导航栏区域
|
// 内容不延伸到状态栏 / 导航栏区域
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||||
|
CookieManager.getInstance().setAcceptCookie(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWebViewCreate(webView: WebView) {
|
||||||
|
super.onWebViewCreate(webView)
|
||||||
|
// Android WebView 默认拦截跨站 Cookie;桌面壳请求远程 API 时需放行
|
||||||
|
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
2
.github/scripts/prepare-template.mjs
vendored
2
.github/scripts/prepare-template.mjs
vendored
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { BUNDLE_ICONS, generateAppIcons } from "./generate-app-icons.mjs";
|
import { BUNDLE_ICONS, generateAppIcons } from "./generate-app-icons.mjs";
|
||||||
import { injectInAppNav } from "./inject-in-app-nav.mjs";
|
import { injectInAppNav } from "./inject-in-app-nav.mjs";
|
||||||
|
import { injectTauriShell } from "./inject-tauri-shell.mjs";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const root = path.resolve(__dirname, "../..");
|
const root = path.resolve(__dirname, "../..");
|
||||||
@@ -109,6 +110,7 @@ function patchTauriConfig(filePath) {
|
|||||||
fs.writeFileSync(filePath, `${JSON.stringify(conf, null, 2)}\n`);
|
fs.writeFileSync(filePath, `${JSON.stringify(conf, null, 2)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
injectTauriShell(distDir);
|
||||||
injectInAppNav(distDir);
|
injectInAppNav(distDir);
|
||||||
|
|
||||||
patchTauriConfig(confPath);
|
patchTauriConfig(confPath);
|
||||||
|
|||||||
126
template/scripts/tauri-shell.js
Normal file
126
template/scripts/tauri-shell.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
(function () {
|
||||||
|
if (window.__web2appShellInstalled) return;
|
||||||
|
window.__web2appShellInstalled = true;
|
||||||
|
|
||||||
|
function isTauriShell() {
|
||||||
|
var protocol = location.protocol;
|
||||||
|
var host = location.hostname;
|
||||||
|
return (
|
||||||
|
protocol === "tauri:" ||
|
||||||
|
protocol === "file:" ||
|
||||||
|
host === "asset.localhost" ||
|
||||||
|
host === "tauri.localhost"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTauriShell()) return;
|
||||||
|
|
||||||
|
var TOKEN_KEYS = ["sprout2fa_session", "__web2app_bearer_token"];
|
||||||
|
|
||||||
|
function readToken() {
|
||||||
|
try {
|
||||||
|
for (var i = 0; i < TOKEN_KEYS.length; i++) {
|
||||||
|
var value = localStorage.getItem(TOKEN_KEYS[i]);
|
||||||
|
if (value) return value;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeToken(token) {
|
||||||
|
if (!token) return;
|
||||||
|
try {
|
||||||
|
for (var i = 0; i < TOKEN_KEYS.length; i++) {
|
||||||
|
localStorage.setItem(TOKEN_KEYS[i], token);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearToken() {
|
||||||
|
try {
|
||||||
|
for (var i = 0; i < TOKEN_KEYS.length; i++) {
|
||||||
|
localStorage.removeItem(TOKEN_KEYS[i]);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.getRegistrations().then(function (regs) {
|
||||||
|
for (var i = 0; i < regs.length; i++) regs[i].unregister();
|
||||||
|
});
|
||||||
|
navigator.serviceWorker.register = function () {
|
||||||
|
return Promise.resolve({
|
||||||
|
active: null,
|
||||||
|
installing: null,
|
||||||
|
waiting: null,
|
||||||
|
unregister: function () {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
},
|
||||||
|
update: function () {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalFetch = window.fetch;
|
||||||
|
window.fetch = function (input, init) {
|
||||||
|
init = init || {};
|
||||||
|
var url =
|
||||||
|
typeof input === "string"
|
||||||
|
? input
|
||||||
|
: input instanceof URL
|
||||||
|
? input.href
|
||||||
|
: input && input.url
|
||||||
|
? input.url
|
||||||
|
: "";
|
||||||
|
|
||||||
|
var requestUrl;
|
||||||
|
try {
|
||||||
|
requestUrl = new URL(url, location.href);
|
||||||
|
} catch (_) {
|
||||||
|
return originalFetch.call(this, input, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
var crossOrigin = requestUrl.origin !== location.origin;
|
||||||
|
if (crossOrigin) {
|
||||||
|
var headers = new Headers(init.headers || {});
|
||||||
|
if (!headers.has("Authorization")) {
|
||||||
|
var token = readToken();
|
||||||
|
if (token) headers.set("Authorization", "Bearer " + token);
|
||||||
|
}
|
||||||
|
init = Object.assign({}, init, { headers: headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalFetch.call(this, input, init).then(function (response) {
|
||||||
|
if (!response.ok) return response;
|
||||||
|
|
||||||
|
var path = requestUrl.pathname.replace(/\/$/, "");
|
||||||
|
if (/\/auth\/login$/i.test(path)) {
|
||||||
|
response
|
||||||
|
.clone()
|
||||||
|
.json()
|
||||||
|
.then(function (data) {
|
||||||
|
if (data && typeof data.token === "string" && data.token) {
|
||||||
|
writeToken(data.token);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
/* ignore */
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\/auth\/logout$/i.test(path)) {
|
||||||
|
clearToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
|
const TAURI_SHELL_SCRIPT: &str = include_str!("../../scripts/tauri-shell.js");
|
||||||
|
|
||||||
const IN_APP_NAV_SCRIPT: &str = r##"
|
const IN_APP_NAV_SCRIPT: &str = r##"
|
||||||
(function () {
|
(function () {
|
||||||
if (window.__web2appNavInstalled) return;
|
if (window.__web2appNavInstalled) return;
|
||||||
@@ -68,12 +70,14 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.eval(TAURI_SHELL_SCRIPT);
|
||||||
let _ = window.eval(IN_APP_NAV_SCRIPT);
|
let _ = window.eval(IN_APP_NAV_SCRIPT);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.on_page_load(|webview, payload| {
|
.on_page_load(|webview, payload| {
|
||||||
if payload.event() == tauri::webview::PageLoadEvent::Finished {
|
if payload.event() == tauri::webview::PageLoadEvent::Finished {
|
||||||
|
let _ = webview.eval(TAURI_SHELL_SCRIPT);
|
||||||
let _ = webview.eval(IN_APP_NAV_SCRIPT);
|
let _ = webview.eval(IN_APP_NAV_SCRIPT);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user