Pārlūkot izejas kodu

feat: 实现C大脑单点登录

ai0187 2 mēneši atpakaļ
vecāks
revīzija
4e857fb8b8

+ 3 - 0
public/app.config.js

@@ -13,6 +13,9 @@ window.__PRODUCTION__SKYEYEADMIN__CONF__ = {
   VITE_GLOB_TIANSUO_PLATFORM: "/tiansuo/",
   VITE_GLOB_SHOW_CAPTCHA: true,
 
+  // C大脑 OAuth2.0 单点登录(请按实际环境替换)
+  VITE_GLOB_CBRAIN_LOGIN_APP: "",
+  VITE_GLOB_CBRAIN_LOGIN_CLIENT_ID: "",
 };
 
 Object.freeze(window.__PRODUCTION__SKYEYEADMIN__CONF__);

+ 12 - 0
src/api/system/user.ts

@@ -122,6 +122,18 @@ export function editUserInfo(params) {
   });
 }
 
+/**
+ * 单点登录 OAuth2.0授权码校验
+ * @param code
+ * @returns
+ */
+export const checkOAuthCode = (code: string) => {
+  return http.request({
+    url: `/login/cauth2?code=${code}`,
+    method: 'get',
+  });
+};
+
 /**
  * @description: 用户登出
  */

+ 4 - 0
src/hooks/setting/index.ts

@@ -14,6 +14,8 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
     VITE_GLOB_SKYEYE_H5_URL,
     VITE_GLOB_TIANSUO_PLATFORM,
     VITE_GLOB_SHOW_CAPTCHA,
+    VITE_GLOB_CBRAIN_LOGIN_APP,
+    VITE_GLOB_CBRAIN_LOGIN_CLIENT_ID,
   } = getAppEnvConfig();
 
   // Take global configuration
@@ -33,6 +35,8 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
 
     tiansuoHost: VITE_GLOB_TIANSUO_PLATFORM,
     showCaptcha: VITE_GLOB_SHOW_CAPTCHA,
+    cbrainLoginApp: VITE_GLOB_CBRAIN_LOGIN_APP,
+    cbrainLoginClientId: VITE_GLOB_CBRAIN_LOGIN_CLIENT_ID,
   };
   return glob as Readonly<GlobConfig>;
 };

+ 14 - 0
src/router/base.ts

@@ -1,5 +1,6 @@
 import type { AppRouteRecordRaw } from '@/router/types';
 import { ErrorPage, RedirectName, Layout } from '@/router/constant';
+import { checkCBrainOAuthToken } from '@/utils/checkCBrainOAuth';
 
 // 404 on a page
 export const ErrorPageRoute: AppRouteRecordRaw = {
@@ -43,3 +44,16 @@ export const RedirectRoute: AppRouteRecordRaw = {
     },
   ],
 };
+
+/** C大脑 OAuth2.0 单点登录入口(无 code 会重定向到授权页,有 code 会换 token 后跳首页) */
+export const CbrainEntryRoute: AppRouteRecordRaw = {
+  path: '/cbrain-entry',
+  name: 'cbrain-entry',
+  component: () => import('@/views/entry/Entry.vue'),
+  beforeEnter: (to, from, next) => {
+    checkCBrainOAuthToken(to, from, next);
+  },
+  meta: {
+    title: 'C大脑登录',
+  },
+};

+ 2 - 2
src/router/index.ts

@@ -1,6 +1,6 @@
 import { App } from 'vue';
 import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
-import { RedirectRoute } from '@/router/base';
+import { RedirectRoute, CbrainEntryRoute } from '@/router/base';
 import { createRouterGuards } from './router-guards';
 import { exceptionRouters } from './routers';
 import { HOME_PAGE, RootRoute } from './full-routes';
@@ -20,7 +20,7 @@ function sortRoute(a, b) {
 
 // routeModuleList.sort(sortRoute);
 
-const constantRoutes = [RootRoute, HOME_PAGE, RedirectRoute, exceptionRouters];
+const constantRoutes = [RootRoute, HOME_PAGE, RedirectRoute, CbrainEntryRoute, exceptionRouters];
 
 //需要验证权限
 export const asyncRoutes = [...routeModuleList];

+ 47 - 0
src/utils/checkCBrainOAuth.ts

@@ -0,0 +1,47 @@
+import { ElMessage } from 'element-plus';
+import { useUserStore } from '@/store/modules/user';
+import { getCBrainRedirectUrl } from '@/utils/getRedirectUrl';
+import { handleTokenFromUrl } from '@/utils/handleTokenFromUrl';
+import { checkOAuthCode } from '@/api/system/user';
+import { PageEnum } from '@/enums/pageEnum';
+import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
+
+/**
+ * C大脑 OAuth2.0 单点登录路由前置守卫
+ * - 无 code:重定向到 C大脑授权服务器
+ * - 有 code:用 code 换 token,写存储,拉用户信息,再跳转首页
+ */
+export async function checkCBrainOAuthToken(
+  to: RouteLocationNormalized,
+  _from: RouteLocationNormalized,
+  next: NavigationGuardNext,
+) {
+  const userStore = useUserStore();
+  const codeFromUrl = to.query.code as string | undefined;
+
+  if (!codeFromUrl) {
+    window.location.href = getCBrainRedirectUrl();
+    return;
+  }
+
+  // 有code:换取token
+  try {
+    // 单点登录OAuth2.0 通过code请求接口,获取token
+    const { satoken } = await checkOAuthCode(codeFromUrl);
+    userStore.setToken(satoken);
+    handleTokenFromUrl(satoken);
+
+    try {
+      await userStore.GetInfo();
+    } catch (err: any) {
+      console.log('catch getInfo error', err);
+      // c大脑登录失败的时候不需要跳转到登录页面
+      ElMessage.error(err.message);
+      return;
+    }
+
+    next(PageEnum.BASE_HOME);
+  } catch {
+    ElMessage.error('获取token失败');
+  }
+}

+ 19 - 15
src/utils/getRedirectUrl.ts

@@ -1,15 +1,19 @@
-import { useGlobSetting } from '@/hooks/setting';
-import urlJoin from 'url-join';
-
-const { loginApp } = useGlobSetting();
-
-export const getRedirectUrl = () => {
-  const encodeUrl = encodeURIComponent(window.location.href);
-  return loginApp + '?redirect=' + encodeUrl;
-};
-
-export const getLogoutUrl = () => {
-  if (!loginApp) return '';
-  const encodeUrl = encodeURIComponent(location.pathname);
-  return loginApp + `logout?redirect=${encodeUrl}`;
-};
+import { useGlobSetting } from '@/hooks/setting';
+
+const { loginApp, cbrainLoginApp, cbrainLoginClientId } = useGlobSetting();
+
+export const getRedirectUrl = () => {
+  const encodeUrl = encodeURIComponent(window.location.href);
+  return loginApp + '?redirect=' + encodeUrl;
+};
+
+export const getLogoutUrl = () => {
+  if (!loginApp) return '';
+  const encodeUrl = encodeURIComponent(location.pathname);
+  return loginApp + `logout?redirect=${encodeUrl}`;
+};
+
+export const getCBrainRedirectUrl = (path?: string) => {
+  const encodeUrl = encodeURIComponent(path || window.location.href);
+  return cbrainLoginApp + '&client_id=' + cbrainLoginClientId + '&redirect_uri=' + encodeUrl;
+};

+ 15 - 0
src/utils/handleTokenFromUrl.ts

@@ -0,0 +1,15 @@
+import { storage } from '@/utils/Storage';
+import { ACCESS_TOKEN, TENANT_TOKEN } from '@/store/mutation-types';
+
+/**
+ * 处理从URL获取的token并存储
+ * @param token 从OAuth回调获取的token
+ */
+
+export const handleTokenFromUrl = (token: string) => {
+  const ex = 30 * 24 * 60 * 60 * 1000;
+  storage.set(ACCESS_TOKEN, token, ex);
+  storage.set(TENANT_TOKEN, ex);
+  storage.setCookie('satoken', token);
+};
+export default handleTokenFromUrl;

+ 38 - 0
src/views/entry/Entry.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="entry-container">
+    <div class="loading-wrapper">
+      <el-icon class="is-loading" :size="24">
+        <Loading />
+      </el-icon>
+      <p>正在跳转中...</p>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { Loading } from '@element-plus/icons-vue';
+</script>
+
+<style lang="scss" scoped>
+  .entry-container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 100vh;
+    width: 100vw;
+    background-color: #f5f5f5;
+  }
+
+  .loading-wrapper {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 16px;
+
+    p {
+      margin: 0;
+      color: #666;
+      font-size: 14px;
+    }
+  }
+</style>

+ 8 - 0
types/config.d.ts

@@ -69,6 +69,10 @@ export interface GlobConfig {
   // 天梭外链地址
   tiansuoHost: string | undefined;
   showCaptcha: boolean;
+
+  // C大脑 OAuth2.0 单点登录
+  cbrainLoginApp: string | undefined;
+  cbrainLoginClientId: string | undefined;
 }
 
 export interface GlobEnvConfig {
@@ -91,4 +95,8 @@ export interface GlobEnvConfig {
   VITE_GLOB_TIANSUO_PLATFORM: string;
 
   VITE_GLOB_SHOW_CAPTCHA: boolean;
+
+  // C大脑 OAuth2.0
+  VITE_GLOB_CBRAIN_LOGIN_APP: string;
+  VITE_GLOB_CBRAIN_LOGIN_CLIENT_ID: string;
 }