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}`;
}
架构特点:
- 双Token机制:access token + refresh token
- 自动过期检查:提前5分钟检测token过期
- 统一管理:TokenManager类封装所有token操作
- 安全存储:使用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);
}
);
刷新机制特点:
- 队列机制:多个请求同时401时只刷新一次token
- 自动重试:刷新成功后自动重试原始请求
- 错误处理:刷新失败时统一处理错误
- 无缝体验:用户无感知的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 = ''
}
防爆破机制特点:
- 失败计数:记录登录失败次数
- 验证码触发:3次失败后显示验证码
- 账户锁定:5次失败后锁定账户5分钟
- 自动重置:登录成功后重置所有状态
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>
指令权限控制特点:
- DOM级控制:直接移除无权限的元素
- 实时生效:用户角色变化时自动更新
- 简洁易用:只需添加v-admin指令
- 性能优化:避免渲染无权限的内容
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);
}
清理机制特点:
- 服务端通知:调用后端退出接口,使token失效
- 本地清理:清除localStorage中的所有认证信息
- 请求头清理:清除axios默认请求头
- 页面跳转:自动跳转到登录页
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()
}
滑动验证码特点:
- 拼图验证:使用拼图形式的滑动验证
- 实时反馈:拖动时实时更新滑块位置
- 服务端验证:滑块位置发送到服务端验证
- 防机器人:增加登录安全性
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;
}
}
}
安全保证机制:
- 自动过期检查:提前5分钟检测token过期
- 格式验证:设置token时验证格式
- 错误处理:解析失败时自动清理
- HTTPS传输:生产环境使用HTTPS传输token
- 短期有效: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'],
},
});
状态管理特点:
- 响应式状态:使用Pinia管理用户状态
- 持久化存储:用户信息持久化到localStorage
- 状态同步:登录时同步用户信息到store
- 状态清理:退出时清理用户状态
登录时的状态更新:
typescript
// 登录成功后更新用户信息
const res = await API.user.login(loginForm)
if (res.code === 0) {
handleLoginSuccess()
useTool.changeUserInfo(res.data) // 更新用户信息到store
router.push('/index')
}
这些机制确保了项目的登录权限设置既安全又用户友好,提供了完整的认证和授权解决方案。