登录权限设置

1. 问题:请介绍一下你们项目中Token管理的整体架构?(高频)

答案: 我们项目中的Token管理架构分为两个系统:

用户端Token管理(web-company):

typescript 复制代码
// TokenManager类 - 统一的Token管理
export class TokenManager {
  private static readonly TOKEN_KEY = 'token';
  private static readonly REFRESH_TOKEN_KEY = 'refreshToken';
  private static readonly USER_INFO_KEY = 'userInfo';

  // 获取访问令牌
  static getToken(): string | null {
    return localStorage.getItem(this.TOKEN_KEY);
  }

  // 获取刷新令牌
  static getRefreshToken(): string | null {
    return localStorage.getItem(this.REFRESH_TOKEN_KEY);
  }

  // 设置令牌
  static setTokens(token: string, refreshToken: string): void {
    localStorage.setItem(this.TOKEN_KEY, token);
    localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
  }

  // 检查是否已登录
  static isLoggedIn(): boolean {
    return !!this.getRefreshToken();
  }

  // 检查token是否即将过期(提前5分钟刷新)
  static isTokenExpiringSoon(): boolean {
    const token = this.getToken();
    if (!token) return false;

    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      const expirationTime = payload.exp * 1000;
      const currentTime = Date.now();
      const fiveMinutes = 5 * 60 * 1000;

      return (expirationTime - currentTime) < fiveMinutes;
    } catch (error) {
      return false;
    }
  }
}

管理端Token管理(web-admin):

typescript 复制代码
// 简单的localStorage管理
const token = localStorage.getItem('token');
if (token) {
  config.headers['Authorization'] = `Bearer ${token}`;
}

架构特点:

  1. 双Token机制:access token + refresh token
  2. 自动过期检查:提前5分钟检测token过期
  3. 统一管理:TokenManager类封装所有token操作
  4. 安全存储:使用localStorage存储,支持持久化

2. 问题:路由守卫是如何实现权限控制的?(高频)

答案: 我们项目中的路由守卫权限控制:

用户端路由守卫(web-company):

typescript 复制代码
// router/index.ts
router.beforeEach(async (to, from, next) => {
  if (to.meta.requiresAuth) {
    const isLoggedIn = TokenManager.isLoggedIn()
    
    if (isLoggedIn) {
      // 检查是否需要刷新token
      const currentToken = TokenManager.getToken()
      const needsRefresh = !currentToken || TokenManager.isTokenExpiringSoon()
      
      if (needsRefresh) {
        try {
          const refreshToken = TokenManager.getRefreshToken()
          if (refreshToken) {
            const response = await fetch('/webapi/users/refresh-token', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ refreshToken }),
            })

            if (response.ok) {
              const data = await response.json()
              if (data.code === 0) {
                TokenManager.setTokens(data.token, data.refreshToken)
                next()
                return
              }
            }
          }
          
          TokenManager.clearAuth()
          next('/login')
          return
        } catch (error) {
          TokenManager.clearAuth()
          next('/login')
          return
        }
      }
      
      next()
    } else {
      next('/login')
    }
  } else {
    next()
  }
})

管理端路由守卫(web-admin):

typescript 复制代码
// router/index.ts
router.beforeEach((to, from, next) => {
  const useTool = useToolStore();
  if (to.name === 'login') {
    next();
  } else {
    if (!localStorage.getItem('token')) {
      next({ path: '/login' });
    } else {
      if (!useTool.isGetterRouter) {
        ConfigRouter();
        next({ path: to.fullPath });
      } else {
        next();
      }
    }
  }
});

3. 问题:Token自动刷新机制是如何实现的?(高频)

答案: 我们项目中的Token自动刷新机制:

用户端自动刷新(web-company):

typescript 复制代码
// api/config.ts
let isRefreshing = false;
let failedQueue: Array<{ resolve: (value?: any) => void; reject: (reason?: any) => void; }> = [];

// 响应拦截器
instance.interceptors.response.use(
  response => {
    return response.data;
  },
  async error => {
    const originalRequest = error.config;

    // 如果是401错误且不是刷新token的请求
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // 如果正在刷新token,将请求加入等待队列
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then(token => {
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return instance(originalRequest);
        }).catch(err => {
          return Promise.reject(err);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      const refreshToken = TokenManager.getRefreshToken();
      
      if (!refreshToken) {
        TokenManager.clearAuth();
        window.location.href = '/login';
        return Promise.reject(error);
      }

      try {
        // 调用刷新token接口
        const response = await axios.post('/webapi/users/refresh-token', {
          refreshToken: refreshToken
        });

        if (response.data.code === 0) {
          const { token: newToken, refreshToken: newRefreshToken } = response.data;
          
          // 更新本地存储
          TokenManager.setTokens(newToken, newRefreshToken);
          
          // 更新axios默认请求头
          instance.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
          
          // 处理等待队列中的请求
          processQueue(null, newToken);
          
          // 重试原始请求
          originalRequest.headers.Authorization = `Bearer ${newToken}`;
          return instance(originalRequest);
        } else {
          TokenManager.clearAuth();
          window.location.href = '/login';
          return Promise.reject(error);
        }
      } catch (refreshError) {
        TokenManager.clearAuth();
        processQueue(refreshError, null);
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

刷新机制特点:

  1. 队列机制:多个请求同时401时只刷新一次token
  2. 自动重试:刷新成功后自动重试原始请求
  3. 错误处理:刷新失败时统一处理错误
  4. 无缝体验:用户无感知的token刷新

4. 问题:登录防爆破机制是如何实现的?(中频)

答案: 我们项目中的登录防爆破机制:

typescript 复制代码
// web-admin/src/views/Login.vue
// 防爆破相关状态
const loginAttempts = ref(0)
const isLocked = ref(false)
const lockEndTime = ref(0)
const showCaptcha = ref(false)
const captchaText = ref('')

// 防爆破配置
const MAX_LOGIN_ATTEMPTS = 5
const LOCK_DURATION = 5 * 60 * 1000 // 5分钟
const CAPTCHA_THRESHOLD = 3 // 3次失败后显示验证码

// 检查是否被锁定
const checkLockStatus = () => {
    const now = Date.now()
    if (lockEndTime.value > now) {
        isLocked.value = true
        return true
    } else {
        isLocked.value = false
        return false
    }
}

// 处理登录失败
const handleLoginFailure = () => {
    loginAttempts.value++
    
    if (loginAttempts.value >= CAPTCHA_THRESHOLD) {
        showCaptcha.value = true
        generateCaptcha()
    }

    if (loginAttempts.value >= MAX_LOGIN_ATTEMPTS) {
        lockEndTime.value = Date.now() + LOCK_DURATION
        isLocked.value = true
        ElMessage.error(`登录失败次数过多,账户已锁定 ${LOCK_DURATION / 1000 / 60} 分钟`)
    }
}

// 处理登录成功
const handleLoginSuccess = () => {
    loginAttempts.value = 0
    showCaptcha.value = false
    isLocked.value = false
    lockEndTime.value = 0
    loginForm.captcha = ''
}

防爆破机制特点:

  1. 失败计数:记录登录失败次数
  2. 验证码触发:3次失败后显示验证码
  3. 账户锁定:5次失败后锁定账户5分钟
  4. 自动重置:登录成功后重置所有状态

5. 问题:权限控制是如何与菜单系统结合的?(中频)

答案: 我们项目中的权限控制与菜单系统结合:

typescript 复制代码
// store/modules/menu.ts
const checkPermission = (item: any) => {
  const useTool = useToolStore() as any;
  if (item.meta?.requireAdmin) {
    return useTool.userInfo.role == 1; // 1为管理员
  }
  return true;
};

// 动态路由配置
const routes: Array<RouteRecordRaw> = [
  {
    path: '/user-manage/adduser',
    component: () => import('@/views/user-manage/UserAdd.vue'),
    meta: {
      requireAdmin: true, // 需要管理员权限
    },
  },
  {
    path: '/admin/login-attempts',
    component: () => import('@/views/admin/LoginAttempts.vue'),
    meta: {
      requireAdmin: true,
    },
  },
  {
    path: '/gis',
    component: () => import('@/views/gis/Gis.vue'),
    meta: {
      title: 'GIS地图',
      requireAdmin: false // 普通用户可访问
    }
  },
];

// 动态添加路由时的权限检查
const ConfigRouter = () => {
  const useTool = useToolStore();
  dynamicRoutes.forEach(item => {
    checkPermission(item) && router.addRoute('mainbox', item);
  });
  useTool.changeGetterRouter(true);
};

菜单权限过滤:

typescript 复制代码
// SideMenu.vue
const filteredMenuItems = computed(() => {
    const userRole = useTool.userInfo.role
    return menuStore.getMenuTree.filter((menu) => {
        if (menu.requireAdmin && userRole !== 1) {
            return false
        }
        return true
    })
})

6. 问题:自定义指令是如何实现权限控制的?(中频)

答案: 我们项目中的自定义指令权限控制:

typescript 复制代码
// directive/index.ts
import { useToolStore } from '@/store';
import { App } from 'vue';

export const setupDirective = (app: App<Element>) => {
  const useTool = useToolStore() as any;
  app.directive('admin', {
    mounted(el) {
      if (useTool.userInfo.role !== 1) {
        el.parentNode?.removeChild(el);
      }
    },
  });
};

使用示例:

vue 复制代码
<template>
  <div>
    <el-button v-admin>管理员专用按钮</el-button>
    <el-button>普通用户按钮</el-button>
  </div>
</template>

指令权限控制特点:

  1. DOM级控制:直接移除无权限的元素
  2. 实时生效:用户角色变化时自动更新
  3. 简洁易用:只需添加v-admin指令
  4. 性能优化:避免渲染无权限的内容

7. 问题:退出登录是如何清理认证信息的?(中频)

答案: 我们项目中的退出登录清理机制:

typescript 复制代码
// web-company/src/utils/auth.ts
export async function logout() {
  try {
    // 调用后端退出登录接口
    const token = TokenManager.getToken();
    if (token) {
      await axios.post('/webapi/users/logout', {}, {
        headers: {
          'Authorization': `Bearer ${token}`
        }
      });
    }
  } catch (error) {
    console.error('退出登录失败:', error);
  } finally {
    // 清除本地存储的认证信息
    TokenManager.clearAuth();
    
    // 清除axios默认请求头
    delete axios.defaults.headers.common['Authorization'];
    
    // 跳转到登录页
    window.location.href = '/login';
  }
}

// TokenManager中的清理方法
static clearAuth(): void {
  localStorage.removeItem(this.TOKEN_KEY);
  localStorage.removeItem(this.REFRESH_TOKEN_KEY);
  localStorage.removeItem(this.USER_INFO_KEY);
}

清理机制特点:

  1. 服务端通知:调用后端退出接口,使token失效
  2. 本地清理:清除localStorage中的所有认证信息
  3. 请求头清理:清除axios默认请求头
  4. 页面跳转:自动跳转到登录页

8. 问题:滑动验证码是如何实现的?(低频)

答案: 我们项目中的滑动验证码实现:

typescript 复制代码
// web-company/src/views/Login.vue
const sliderLeft = ref(0)
const isVerified = ref(false)
const isDragging = ref(false)
const startX = ref(0)
const captchaText = ref('请将滑块拖到拼图缺口')
const captchaBgImage = ref('')
const captchaBlockImage = ref('')
const puzzleTop = ref(0)
const puzzleSize = ref(40)
const captchaSessionId = ref('')

// 初始化验证码
const initCaptcha = async () => {
    try {
        const response = await generateCaptcha()
        if (response.success) {
            captchaBgImage.value = response.data.bgImage
            captchaBlockImage.value = response.data.blockImage
            puzzleTop.value = response.data.puzzleTop
            captchaSessionId.value = response.data.sessionId
        }
    } catch (error) {
        console.error('初始化验证码失败:', error)
    }
}

// 滑块拖动处理
const handleMouseDown = (e: MouseEvent) => {
    if (isVerified.value) return
    isDragging.value = true
    startX.value = e.clientX - sliderLeft.value
    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mouseup', handleMouseUp)
}

const handleMouseMove = (e: MouseEvent) => {
    if (!isDragging.value) return
    const newLeft = e.clientX - startX.value
    sliderLeft.value = Math.max(0, Math.min(newLeft, maxLeft.value))
}

const handleMouseUp = async () => {
    if (!isDragging.value) return
    isDragging.value = false
    document.removeEventListener('mousemove', handleMouseMove)
    document.removeEventListener('mouseup', handleMouseUp)
    
    // 验证滑块位置
    await verifyCaptcha()
}

滑动验证码特点:

  1. 拼图验证:使用拼图形式的滑动验证
  2. 实时反馈:拖动时实时更新滑块位置
  3. 服务端验证:滑块位置发送到服务端验证
  4. 防机器人:增加登录安全性

9. 问题:如何保证Token的安全性?(低频)

答案: 我们项目中的Token安全保证机制:

typescript 复制代码
// TokenManager中的安全检查
export class TokenManager {
  // 检查token是否即将过期(提前5分钟刷新)
  static isTokenExpiringSoon(): boolean {
    const token = this.getToken();
    if (!token) return false;

    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      const expirationTime = payload.exp * 1000; // 转换为毫秒
      const currentTime = Date.now();
      const fiveMinutes = 5 * 60 * 1000; // 5分钟

      return (expirationTime - currentTime) < fiveMinutes;
    } catch (error) {
      return false;
    }
  }

  // 设置令牌时的安全处理
  static setTokens(token: string, refreshToken: string): void {
    // 验证token格式
    if (!token || !refreshToken) {
      throw new Error('Invalid token format');
    }
    
    localStorage.setItem(this.TOKEN_KEY, token);
    localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
  }

  // 获取用户信息时的安全检查
  static getUserInfo(): any {
    const userInfo = localStorage.getItem(this.USER_INFO_KEY);
    if (!userInfo) return null;
    
    try {
      return JSON.parse(userInfo);
    } catch (error) {
      console.error('解析用户信息失败:', error);
      this.clearAuth(); // 清除可能损坏的数据
      return null;
    }
  }
}

安全保证机制:

  1. 自动过期检查:提前5分钟检测token过期
  2. 格式验证:设置token时验证格式
  3. 错误处理:解析失败时自动清理
  4. HTTPS传输:生产环境使用HTTPS传输token
  5. 短期有效:access token短期有效,降低泄露风险

10. 问题:用户状态管理是如何实现的?(低频)

答案: 我们项目中的用户状态管理:

typescript 复制代码
// store/index.ts
export const useToolStore = defineStore('tool', {
  state: () => {
    return {
      isGetterRouter: false,
      isCollapsed: false,
      userInfo: {
        username: '',
        role: 0,
        avatar: '',
        gender: 0,
        introduction: '',
      },
    };
  },
  actions: {
    changeUserInfo(value: Object) {
      this.userInfo = {
        ...this.userInfo,
        ...value,
      };
    },
    clearUserInfo() {
      this.userInfo = {
        username: '',
        role: 0,
        avatar: '',
        gender: 0,
        introduction: '',
      };
    },
  },
  persist: {
    key: 'tool',
    storage: localStorage,
    paths: ['isCollapsed', 'userInfo'],
  },
});

状态管理特点:

  1. 响应式状态:使用Pinia管理用户状态
  2. 持久化存储:用户信息持久化到localStorage
  3. 状态同步:登录时同步用户信息到store
  4. 状态清理:退出时清理用户状态

登录时的状态更新:

typescript 复制代码
// 登录成功后更新用户信息
const res = await API.user.login(loginForm)
if (res.code === 0) {
    handleLoginSuccess()
    useTool.changeUserInfo(res.data) // 更新用户信息到store
    router.push('/index')
}

这些机制确保了项目的登录权限设置既安全又用户友好,提供了完整的认证和授权解决方案。

相关推荐
shoubepatien14 分钟前
JavaWeb_Web基础
java·开发语言·前端·数据库·intellij-idea
WordPress学习笔记21 分钟前
wordpress外贸主题Google地图添加(替换)方案
前端·wordpress·wordpress地图
码农秋1 小时前
Element Plus DatePicker 日期少一天问题:时区解析陷阱与解决方案
前端·vue.js·elementui·dayjs
未来之窗软件服务1 小时前
未来之窗昭和仙君(五十六)页面_预览模式——东方仙盟筑基期
前端·仙盟创梦ide·东方仙盟·昭和仙君·东方仙盟架构
top_designer1 小时前
Illustrato:钢笔工具“退休”了?Text to Vector 零基础矢量生成流
前端·ui·aigc·交互·ux·设计师·平面设计
星哥说事1 小时前
星哥带你玩飞牛NAS-13:自动追番、订阅下载 + 刮削,动漫党彻底解放双手!
前端
donecoding1 小时前
前端AI开发:为什么选择SSE,它与分块传输编码有何不同?axios能处理SSE吗?
前端·人工智能
安_1 小时前
<style scoped>跟<style>有什么区别
前端·vue
姝然_95271 小时前
Claude Code 命令完整文档
前端
wjcroom1 小时前
web版进销存的设计到实现一
前端