diff --git a/app/common/controller/Backend.php b/app/common/controller/Backend.php index e2d8a4d..6fad10d 100644 --- a/app/common/controller/Backend.php +++ b/app/common/controller/Backend.php @@ -142,9 +142,14 @@ class Backend extends Api if ($needLogin) { if (!$this->auth->isLogin()) { + if ($request->method() === 'GET' && !$this->expectsApiJsonResponse($request)) { + $location = $this->adminSpaLoginUrl($request); + return redirect($location); + } + // 必须使用 HTTP 200 返回 JSON:若用 HTTP 303,axios 会跟随重定向,拿不到 JSON,前端无法跳转登录 return $this->error(__('Please login first'), [ 'type' => Auth::NEED_LOGIN, - ], 0, ['statusCode' => Auth::LOGIN_RESPONSE_CODE]); + ], 0); } if ($needPermission) { $controllerPath = $this->getControllerPath($request); @@ -167,6 +172,37 @@ class Backend extends Api return null; } + /** + * 是否应按 API 返回 JSON(前端 axios 会带 server: true;纯浏览器地址栏访问多为 HTML Accept) + */ + protected function expectsApiJsonResponse(WebmanRequest $request): bool + { + $server = $request->header('server', ''); + if ($server === 'true' || $server === '1') { + return true; + } + if (strtolower($request->header('x-requested-with', '')) === 'xmlhttprequest') { + return true; + } + $accept = strtolower($request->header('accept', '')); + if (str_contains($accept, 'application/json')) { + return true; + } + // 浏览器地址栏/点击链接触发的主文档请求,优先 302 到前端登录(避免误判为 API) + if (strtolower((string) $request->header('sec-fetch-mode', '')) === 'navigate') { + return false; + } + return false; + } + + /** + * 后台 Vue 为 hash 路由时的登录页(相对路径,与 web/src/router 一致) + */ + protected function adminSpaLoginUrl(WebmanRequest $request): string + { + return '/#/admin/login'; + } + /** * 子类可覆盖,用于初始化 model 等(替代原 initialize) * @return Response|null 需直接返回时返回 Response,否则 null diff --git a/web/src/utils/axios.ts b/web/src/utils/axios.ts index 19d6fa6..5f88c3b 100644 --- a/web/src/utils/axios.ts +++ b/web/src/utils/axios.ts @@ -1,6 +1,7 @@ import type { AxiosRequestConfig, Method } from 'axios' import axios from 'axios' import { ElLoading, ElNotification, type LoadingOptions } from 'element-plus' +import { nextTick } from 'vue' import { refreshToken } from '/@/api/common' import { i18n } from '/@/lang/index' import router from '/@/router/index' @@ -20,6 +21,12 @@ const loadingInstance: LoadingInstance = { count: 0, } +/** 请求是否后台 /admin/ 接口(不依赖当前路由,避免 loading 等场景误判为前台) */ +function isAdminBackendRequest(config: AxiosRequestConfig): boolean { + const u = `${config.baseURL ?? ''}${config.url ?? ''}` + return /\/admin\//i.test(u) +} + /** * 根据运行环境获取基础请求URL */ @@ -112,6 +119,25 @@ function createAxios>(axiosConfig: AxiosRequest if (response.config.responseType == 'json') { if (response.data && response.data.code !== 1) { + const needLogin = + response.data.data && + typeof response.data.data === 'object' && + response.data.data.type === 'need login' + if (needLogin) { + const isAdminAppFlag = isAdminApp() || isAdminBackendRequest(response.config) + if (isAdminAppFlag) { + adminInfo.removeToken() + } else { + userInfo.removeToken() + } + const loginRouteName = isAdminAppFlag ? 'adminLogin' : 'userLogin' + if (router.currentRoute.value.name !== loginRouteName) { + nextTick(() => { + void router.replace({ name: loginRouteName }) + }) + } + return Promise.reject(response.data) + } if (response.data.code == 409) { if (!window.tokenRefreshing) { window.tokenRefreshing = true