Browse Source

feat: 登录修改以及token无感刷新

曾坤森 3 months ago
parent
commit
4abb3d20c2

+ 80 - 22
src/api/interceptor.ts

@@ -4,13 +4,55 @@ import type {
   AxiosResponse,
   AxiosRequestHeaders,
 } from 'axios';
+import router from '@/router';
 import { Message, Modal } from '@arco-design/web-vue';
 import { useUserStore } from '@/store';
-import { getToken } from '@/utils/auth';
+import useAuth from '@/utils/auth';
+import { refreshToken } from '@/api/user';
+const { getToken, getRefreshToken, setToken, clearToken } = useAuth();
+
+let isRefreshing = false;
+let subscribers: ((token: string) => void)[] = [];
+
+function subscribeTokenRefresh(cb: (token: string) => void) {
+  subscribers.push(cb);
+}
+
+function onRefreshed(token: string) {
+  subscribers.forEach(cb => cb(token));
+  subscribers = [];
+}
 
 const instance = axios.create({
   baseURL: import.meta.env.VITE_API_BASE_URL,
 });
+// 新增一个独立的函数来处理 Token 刷新
+async function handleTokenRefresh(
+  refreshTokenValue: string,
+  originalConfig: InternalAxiosRequestConfig
+) {
+  isRefreshing = true;
+  try {
+    const res = await refreshToken({ token: refreshTokenValue });
+    if (res && res.data && res.success) {
+      const { token: newAccessToken, refreshToken: newRefreshToken } = res.data;
+      setToken(newAccessToken, newRefreshToken);
+      onRefreshed(newAccessToken);
+      // 重试原始请求
+      originalConfig.headers.Authorization = `${newAccessToken}`;
+      return instance(originalConfig);
+    } else {
+      throw new Error('Invalid token refresh response');
+    }
+  } catch (error) {
+    Message.error('登录状态已过期,请重新登录');
+    clearToken();
+    router.push({ name: 'login' });
+    throw error; // 重新抛出错误,让外部拦截器可以继续处理
+  } finally {
+    isRefreshing = false;
+  }
+}
 
 instance.interceptors.request.use(
   (config: InternalAxiosRequestConfig) => {
@@ -23,7 +65,7 @@ instance.interceptors.request.use(
       if (!config.headers) {
         config.headers = {} as AxiosRequestHeaders;
       }
-      config.headers.Authorization = `Bearer ${token}`;
+      config.headers.Authorization = `${token}`;
     }
     return config;
   },
@@ -35,28 +77,44 @@ instance.interceptors.request.use(
 // 响应拦截器修复版本
 instance.interceptors.response.use(
   (response: AxiosResponse) => {
-    const res = response.data;
-    console.log('res', res);
-    if (response.status === 401) {
-      Modal.error({
-        title: 'Confirm logout',
-        content:
-          'You have been logged out, you can cancel to stay on this page, or log in again',
-        okText: 'Re-Login',
-        async onOk() {
-          const userStore = useUserStore();
-          await userStore.logout();
-          window.location.reload();
-        },
-      });
-    }
     return response;
   },
-  error => {
-    Message.error({
-      content: error.message || 'Request Error',
-      duration: 5 * 1000,
-    });
+  async error => {
+    const { config, response } = error;
+    const refreshTokenValue = getRefreshToken();
+    if (!response) {
+      // 网络错误处理
+      Message.error('网络连接异常');
+      return Promise.reject(error);
+    }
+    if (response.status === 401) {
+      if (!isRefreshing) {
+        isRefreshing = true;
+        config._retry = true;
+        if (refreshTokenValue) {
+          return handleTokenRefresh(refreshTokenValue, config);
+        }
+      } else {
+        return new Promise(resolve => {
+          subscribeTokenRefresh((token: string) => {
+            config.headers.Authorization = `${token}`;
+            resolve(axios(config));
+          });
+        });
+      }
+    } else if (response.status === 403) {
+      Message.error('没有权限执行此操作');
+      // 可选:跳转到权限不足页面或首页
+      // router.push({ name: 'forbidden' });
+    } else if (response.status === 404) {
+      Message.error('请求的资源不存在');
+    } else if (response.status >= 500) {
+      Message.error('服务器内部错误,请稍后再试');
+    } else {
+      Message.error(
+        error.response?.data?.message || `请求错误: ${response.status}`
+      );
+    }
     return Promise.reject(error);
   }
 );

+ 17 - 1
src/api/user.ts

@@ -12,6 +12,8 @@ export interface LogoutRes {
   token: string;
 }
 export interface Data {
+  token: string;
+  refreshToken: string;
   id: number;
   name: string;
   password: string;
@@ -25,11 +27,25 @@ export interface LoginRes {
   message: string;
   code: string;
 }
+export interface RefreshData {
+  token?: any;
+  refreshToken?: any;
+}
+
+export interface RefreshToken {
+  data: RefreshData;
+  success: boolean;
+  message: string;
+  code: string;
+}
 export async function login(params: object): Promise<LoginRes> {
   const res = await instance.post('/api/Author/Login', params);
   return res.data;
 }
-
+export async function refreshToken(params: object): Promise<RefreshToken> {
+  const res = await instance.post('/api/Author/RefreshToken', params);
+  return res.data;
+}
 export function logout() {
   return axios.post<LoginRes>('/api/user/logout');
 }

+ 1 - 1
src/hooks/locale.ts

@@ -12,7 +12,7 @@ export default function useLocale() {
       return;
     }
     i18.locale.value = value;
-    localStorage.setItem('arco-locale', value);
+    localStorage.setItem('smms-locale', value);
     Message.success(i18.t('navbar.action.locale'));
   };
   return {

+ 1 - 1
src/locale/index.ts

@@ -6,7 +6,7 @@ export const LOCALE_OPTIONS = [
   { label: '中文', value: 'zh-CN' },
   { label: 'English', value: 'en-US' },
 ];
-const defaultLocale = localStorage.getItem('arco-locale') || 'zh-US';
+const defaultLocale = localStorage.getItem('smms-locale') || 'en-US';
 
 const i18n = createI18n({
   locale: defaultLocale,

+ 2 - 2
src/mock/user.ts

@@ -5,8 +5,8 @@ import setupMock, {
 } from '@/utils/setup-mock';
 
 import { MockParams } from '@/types/mock';
-import { isLogin } from '@/utils/auth';
-
+import useAuth from '@/utils/auth';
+const { isLogin } = useAuth();
 setupMock({
   mock: false,
 

+ 2 - 1
src/router/guard/userLoginInfo.ts

@@ -2,7 +2,8 @@ import type { Router, LocationQueryRaw } from 'vue-router';
 import NProgress from 'nprogress'; // progress bar
 
 import { useUserStore } from '@/store';
-import { isLogin } from '@/utils/auth';
+import useAuth from '@/utils/auth';
+const { isLogin } = useAuth();
 
 export default function setupUserLoginInfoGuard(router: Router) {
   router.beforeEach(async (to, from, next) => {

+ 1 - 1
src/router/index.ts

@@ -17,7 +17,7 @@ const router = createRouter({
   routes: [
     {
       path: '/',
-      redirect: 'login',
+      redirect: '/dashboard/workplace',
     },
     {
       path: '/login',

+ 46 - 46
src/router/routes/modules/exception.ts

@@ -1,48 +1,48 @@
-import { DEFAULT_LAYOUT } from '../base';
-import { AppRouteRecordRaw } from '../types';
+// import { DEFAULT_LAYOUT } from '../base';
+// import { AppRouteRecordRaw } from '../types';
 
-const EXCEPTION: AppRouteRecordRaw = {
-  path: '/exception',
-  name: 'exception',
-  component: DEFAULT_LAYOUT,
-  meta: {
-    locale: 'menu.exception',
-    requiresAuth: true,
-    icon: 'icon-exclamation-circle',
-    order: 6,
-  },
-  children: [
-    {
-      path: '403',
-      name: '403',
-      component: () => import('@/views/exception/403/index.vue'),
-      meta: {
-        locale: 'menu.exception.403',
-        requiresAuth: true,
-        roles: ['admin'],
-      },
-    },
-    {
-      path: '404',
-      name: '404',
-      component: () => import('@/views/exception/404/index.vue'),
-      meta: {
-        locale: 'menu.exception.404',
-        requiresAuth: true,
-        roles: ['*'],
-      },
-    },
-    {
-      path: '500',
-      name: '500',
-      component: () => import('@/views/exception/500/index.vue'),
-      meta: {
-        locale: 'menu.exception.500',
-        requiresAuth: true,
-        roles: ['*'],
-      },
-    },
-  ],
-};
+// const EXCEPTION: AppRouteRecordRaw = {
+//   path: '/exception',
+//   name: 'exception',
+//   component: DEFAULT_LAYOUT,
+//   meta: {
+//     locale: 'menu.exception',
+//     requiresAuth: true,
+//     icon: 'icon-exclamation-circle',
+//     order: 6,
+//   },
+//   children: [
+//     {
+//       path: '403',
+//       name: '403',
+//       component: () => import('@/views/exception/403/index.vue'),
+//       meta: {
+//         locale: 'menu.exception.403',
+//         requiresAuth: true,
+//         roles: ['admin'],
+//       },
+//     },
+//     {
+//       path: '404',
+//       name: '404',
+//       component: () => import('@/views/exception/404/index.vue'),
+//       meta: {
+//         locale: 'menu.exception.404',
+//         requiresAuth: true,
+//         roles: ['*'],
+//       },
+//     },
+//     {
+//       path: '500',
+//       name: '500',
+//       component: () => import('@/views/exception/500/index.vue'),
+//       meta: {
+//         locale: 'menu.exception.500',
+//         requiresAuth: true,
+//         roles: ['*'],
+//       },
+//     },
+//   ],
+// };
 
-export default EXCEPTION;
+// export default EXCEPTION;

+ 4 - 3
src/store/modules/user/index.ts

@@ -6,10 +6,11 @@ import {
   LoginData,
   Data,
 } from '@/api/user';
-import { setToken, clearToken } from '@/utils/auth';
+import useAuth from '@/utils/auth';
 import { removeRouteListener } from '@/utils/route-listener';
 import { UserState } from './types';
 import useAppStore from '../app';
+const { setToken, clearToken } = useAuth();
 const useUserStore = defineStore('user', {
   state: (): UserState => ({
     name: undefined,
@@ -64,8 +65,8 @@ const useUserStore = defineStore('user', {
           password: loginForm.password,
         };
         const res = await userLogin(params);
-        setToken('test-token');
-        console.log('dddd', res.data);
+        setToken(res.data.token, res.data.refreshToken);
+        console.log('aaaddd', res.data);
         this.info(res.data);
 
         return res;

+ 28 - 14
src/utils/auth.ts

@@ -1,19 +1,33 @@
-const TOKEN_KEY = 'token';
+import { useStorage } from '@vueuse/core';
+const TOKEN_KEY = 'smms-token';
+const useAuth = () => {
+  const valueObj = useStorage(TOKEN_KEY, { token: '', refreshToken: '' });
+  const isLogin = () => {
+    return !!valueObj.value.token;
+  };
 
-const isLogin = () => {
-  return !!localStorage.getItem(TOKEN_KEY);
-};
-
-const getToken = () => {
-  return localStorage.getItem(TOKEN_KEY);
-};
+  const getToken = () => {
+    return valueObj.value.token;
+  };
+  const getRefreshToken = () => {
+    return valueObj.value.refreshToken;
+  };
 
-const setToken = (token: string) => {
-  localStorage.setItem(TOKEN_KEY, token);
-};
+  const setToken = (token: string, refreshToken: string) => {
+    valueObj.value.token = token;
+    valueObj.value.refreshToken = refreshToken;
+  };
 
-const clearToken = () => {
-  localStorage.removeItem(TOKEN_KEY);
+  const clearToken = () => {
+    valueObj.value = { token: '', refreshToken: '' };
+  };
+  return {
+    isLogin,
+    getToken,
+    getRefreshToken,
+    setToken,
+    clearToken,
+  };
 };
 
-export { isLogin, getToken, setToken, clearToken };
+export default useAuth;

+ 2 - 1
src/views/dashboard/workplace/index.vue

@@ -155,7 +155,7 @@
       title-align="start"
       @cancel="handleCancel"
     >
-      <template #title>Device Information</template>
+      <template #title>{{ t('dashboard.dialog.title') }}</template>
       <div>
         <a-descriptions :data="deviceInfo" bordered :column="2" />
       </div>
@@ -305,6 +305,7 @@ const onPageChange = (current: number) => {
 
 const handleClick = (value: Data) => {
   if (value.data) {
+    deviceInfo.value.length = 0;
     const obj = JSON.parse(value.data);
     for (const key in obj) {
       deviceInfo.value.push({

+ 1 - 0
src/views/dashboard/workplace/locale/en-US.ts

@@ -1,6 +1,7 @@
 export default {
   'menu.dashboard.workplace': 'DeviceInfo',
   'workplace.welcome': 'Welcome!',
+  'dashboard.dialog.title': 'Device Information',
   'dashboard.form.address': 'Address',
   'dashboard.form.name': 'Name',
   'dashboard.form.entityType': 'EntityType',

+ 1 - 0
src/views/dashboard/workplace/locale/zh-CN.ts

@@ -1,6 +1,7 @@
 export default {
   'menu.dashboard.workplace': '设备信息',
   'workplace.welcome': '欢迎回来!',
+  'dashboard.dialog.title': '设备信息',
   'dashboard.form.address': 'IP地址',
   'dashboard.form.name': '名称',
   'dashboard.form.entityType': '设备类型',

+ 13 - 15
src/views/login/components/login-form.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="login-form-wrapper">
-    <div class="login-form-title">{{ $t('login.form.title') }}</div>
-    <div class="login-form-sub-title">{{ $t('login.form.title') }}</div>
+    <div class="login-form-title">{{ t('login.form.title') }}</div>
+    <div class="login-form-sub-title">{{ t('login.form.title') }}</div>
     <div class="login-form-error-msg">{{ errorMessage }}</div>
     <a-form
       ref="loginForm"
@@ -12,13 +12,13 @@
     >
       <a-form-item
         field="username"
-        :rules="[{ required: true, message: $t('login.form.userName.errMsg') }]"
+        :rules="[{ required: true, message: t('login.form.userName.errMsg') }]"
         :validate-trigger="['change', 'blur']"
         hide-label
       >
         <a-input
           v-model="userInfo.username"
-          :placeholder="$t('login.form.userName.placeholder')"
+          :placeholder="t('login.form.userName.placeholder')"
         >
           <template #prefix>
             <icon-user />
@@ -27,13 +27,13 @@
       </a-form-item>
       <a-form-item
         field="password"
-        :rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
+        :rules="[{ required: true, message: t('login.form.password.errMsg') }]"
         :validate-trigger="['change', 'blur']"
         hide-label
       >
         <a-input-password
           v-model="userInfo.password"
-          :placeholder="$t('login.form.password.placeholder')"
+          :placeholder="t('login.form.password.placeholder')"
           allow-clear
         >
           <template #prefix>
@@ -48,15 +48,15 @@
             :model-value="loginConfig.rememberPassword"
             @change="setRememberPassword as any"
           >
-            {{ $t('login.form.rememberPassword') }}
+            {{ t('login.form.rememberPassword') }}
           </a-checkbox>
-          <a-link>{{ $t('login.form.forgetPassword') }}</a-link>
+          <a-link>{{ t('login.form.forgetPassword') }}</a-link>
         </div>
         <a-button type="primary" html-type="submit" long :loading="loading">
-          {{ $t('login.form.login') }}
+          {{ t('login.form.login') }}
         </a-button>
         <a-button type="text" long class="login-form-register-btn">
-          {{ $t('login.form.register') }}
+          {{ t('login.form.register') }}
         </a-button>
       </a-space>
     </a-form>
@@ -80,14 +80,13 @@ const errorMessage = ref('');
 const { loading, setLoading } = useLoading();
 const userStore = useUserStore();
 
-const loginConfig = useStorage('login-config', {
+const loginConfig = useStorage('smms-login', {
   rememberPassword: true,
   username: 'admin', // 演示默认值
-  password: 'admin', // demo default value
 });
 const userInfo = reactive({
   username: loginConfig.value.username,
-  password: loginConfig.value.password,
+  password: null,
 });
 
 const handleSubmit = async ({
@@ -104,7 +103,7 @@ const handleSubmit = async ({
       const res = await userStore.login(values as LoginData);
       const { redirect, ...othersQuery } = router.currentRoute.value.query;
       if (res.success) {
-        router.push({
+        await router.push({
           name: (redirect as string) || 'Workplace',
           query: {
             ...othersQuery,
@@ -116,7 +115,6 @@ const handleSubmit = async ({
         // 实际生产环境需要进行加密存储。
         // The actual production environment requires encrypted storage.
         loginConfig.value.username = rememberPassword ? username : '';
-        loginConfig.value.password = rememberPassword ? password : '';
       } else {
         Message.error(res.message);
       }