跳到主要内容前端路由权限拦截:3 种方案与常见坑点 | 极客日志Python
前端路由权限拦截:3 种方案与常见坑点
前端路由权限拦截:3 种方案与常见坑点 引言:前端权限的常见隐患 每次处理前端路由权限问题时,核心痛点往往在于如何确保未授权用户无法访问敏感页面。很多开发者误以为后端校验接口权限即可,但前端若直接暴露敏感页面链接,即使用户拿不到数据,也会暴露系统结构,甚至因页面骨架渲染导致信息泄露。 例如,某些系统退出登录后,用户点击浏览器后退键,仪表盘页面可能再次出现,这是因为路由状态未正确清除。此外,若前端先…
刀狂44K 浏览 前端路由权限拦截:3 种方案与常见坑点
引言:前端权限的常见隐患
每次处理前端路由权限问题时,核心痛点往往在于如何确保未授权用户无法访问敏感页面。很多开发者误以为后端校验接口权限即可,但前端若直接暴露敏感页面链接,即使用户拿不到数据,也会暴露系统结构,甚至因页面骨架渲染导致信息泄露。
例如,某些系统退出登录后,用户点击浏览器后退键,仪表盘页面可能再次出现,这是因为路由状态未正确清除。此外,若前端先渲染页面再调接口,接口返回 401 时的 Loading 状态会严重影响用户体验。因此,前端路由拦截是保障安全与体验的关键环节。
路由保护原理与守卫机制
前端路由保护依赖于路由守卫(Guard)。想象前端应用是一个小区,路由是大门,守卫则是门岗。根据权限级别不同,守卫分为三种:
- 全局前置守卫(Global Before Guards):级别最高,监听所有路由变化。Vue Router 中为
router.beforeEach,适合检查登录态、Token 等通用逻辑。
- 独享守卫(Per-Route Guard):针对特定路由配置。Vue Router 中为
beforeEnter,适合特定页面的权限校验。
- 组件内守卫(In-Component Guards):级别最低,在组件内部执行。Vue 中为
beforeRouteEnter 等钩子,适合页面内细粒度权限控制。
执行顺序为:全局前置 → 独享 → 组件内。
关于 Token 存储,常见方案有 LocalStorage、Cookie 和 SessionStorage。LocalStorage 方便但易受 XSS 攻击;Cookie 可设 HttpOnly 防 XSS,但有 CSRF 风险且体积限制;SessionStorage 关闭即清。推荐方案:Access Token 放内存,Refresh Token 放 HttpOnly Cookie。小项目 LocalStorage 配合路由守卫校验通常足够。
三种实现方案
方案一:全局拦截
适合 90% 的项目。思路是在路由跳转前统一检查登录状态。
Vue Router 3.x 实现:
import Vue from 'vue'
import Router from 'vue-router'
import routes from './routes'
import { getToken } from '@/utils/auth'
Vue.use(Router)
const router = new Router({
mode: 'history',
routes
})
router.beforeEach(() => {
hasToken = ()
(to. === ) {
()
}
(hasToken) {
()
} {
()
}
})
router
to, from, next
const
getToken
if
path
'/login'
next
return
if
next
else
next
`/login?redirect=${encodeURIComponent(to.fullPath)}`
export
default
const TokenKey = 'Admin-Token'
export function getToken() {
return localStorage.getItem(TokenKey)
}
export function setToken(token) {
return localStorage.setItem(TokenKey, token)
}
export function removeToken() {
return localStorage.removeItem(TokenKey)
}
React Router v6 没有直接的全局守卫,可通过自定义组件包装。
import { Navigate, useLocation } from 'react-router-dom'
import { getToken } from '../utils/auth'
const AuthRoute = ({ children }) => {
const location = useLocation()
const hasToken = getToken()
if (!hasToken) {
return <Navigate to="/login" state={{ from: location }} replace />
}
return children
}
export default AuthRoute
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import AuthRoute from './components/AuthRoute'
import Login from './pages/Login'
import Dashboard from './pages/Dashboard'
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<AuthRoute><Dashboard /></AuthRoute>} />
<Route path="/" element={<Navigate to="/dashboard" />} />
</Routes>
</BrowserRouter>
)
}
export default App
方案二:路由元信息配置
适合需要区分公开页面与敏感页面的场景。通过路由 meta 字段配置权限要求。
export const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/home/index.vue'),
meta: { title: '首页', requiresAuth: false }
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/admin/index.vue'),
meta: { title: '管理后台', requiresAuth: true, requiredRoles: ['admin', 'super_admin'] }
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { title: '登录', requiresAuth: false, hideInMenu: true }
}
]
import router from './router'
import { getToken, getUserInfo } from '@/utils/auth'
router.beforeEach(async (to, from, next) => {
document.title = to.meta.title || '默认标题'
const hasToken = getToken()
if (!to.meta.requiresAuth) {
next()
return
}
if (!hasToken) {
next(`/login?redirect=${to.path}`)
return
}
if (to.meta.requiredRoles) {
const userInfo = getUserInfo()
const hasRole = to.meta.requiredRoles.some(role => userInfo.roles.includes(role))
if (!hasRole) {
next('/403')
return
}
}
next()
})
import { Navigate, useLocation } from 'react-router-dom'
import { getToken, getUserInfo } from '../utils/auth'
const ProtectedRoute = ({ route, children }) => {
const location = useLocation()
const hasToken = getToken()
const meta = route.meta || {}
if (!meta.requiresAuth) {
return children
}
if (!hasToken) {
return <Navigate to="/login" state={{ from: location }} replace />
}
if (meta.requiredRoles) {
const userInfo = getUserInfo()
const hasRole = meta.requiredRoles.some(role => userInfo?.roles?.includes(role))
if (!hasRole) {
return <Navigate to="/403" replace />
}
}
return children
}
方案三:高阶组件包装
适合 React 项目,通过 HOC 抽离权限逻辑,保持组件纯净。
import { Component } from 'react'
import { Navigate } from 'react-router-dom'
import { getToken, getUserInfo } from '../utils/auth'
const withAuth = (options = {}) => (WrappedComponent) => {
return class AuthComponent extends Component {
render() {
const hasToken = getToken()
if (!hasToken) {
return <Navigate to="/login" replace />
}
if (options.requiredRoles) {
const userInfo = getUserInfo()
const hasRole = options.requiredRoles.some(role => userInfo?.roles?.includes(role))
if (!hasRole) {
return <div>403 Forbidden - 你没有权限访问此页面</div>
}
}
const userInfo = getUserInfo()
return <WrappedComponent {...this.props} userInfo={userInfo} />
}
}
}
export default withAuth
import withAuth from '../hocs/withAuth'
const AdminDashboard = ({ userInfo }) => {
return (
<div>
<h1>欢迎回来,{userInfo.name}</h1>
<p>你的角色:{userInfo.roles.join(', ')}</p>
</div>
)
}
export default withAuth({ requiredRoles: ['admin', 'super_admin'] })(AdminDashboard)
潜在风险与注意事项
动态菜单与异步权限
后台系统常需根据权限动态生成菜单。若前端硬编码路由表,无权限用户虽进不去页面,但菜单入口可见,体验不佳。解决方案是后端返回菜单配置,前端动态生成路由。
import store from '@/store'
router.beforeEach(async (to, from, next) => {
const hasToken = getToken()
if (to.path === '/login') {
next()
return
}
if (!hasToken) {
next('/login')
return
}
if (!store.state.user.hasFetchedInfo) {
try {
await store.dispatch('user/getInfo')
next({ ...to, replace: true })
} catch (error) {
await store.dispatch('user/logout')
next(`/login?redirect=${to.path}`)
}
return
}
if (to.meta.requiredRoles) {
const hasRole = to.meta.requiredRoles.some(role => store.state.user.roles.includes(role))
if (!hasRole) {
next('/403')
return
}
}
next()
})
死循环陷阱
守卫中跳转登录页时,若未排除登录页本身,会导致无限循环。务必在守卫中判断 to.path === '/login' 时直接放行。
微前端场景
微前端中主子应用路由协调复杂。建议权限校验统一在主应用,子应用仅负责渲染,或子应用保留简单校验逻辑。
生产环境实践
Token 无感刷新
使用双 Token 机制:Access Token(短期)+ Refresh Token(长期)。Access Token 过期时,通过 Refresh Token 换取新 Token,避免用户被强制登出。
import axios from 'axios'
import { getToken, setToken, removeToken, getRefreshToken } from './auth'
import router from '@/router'
let isRefreshing = false
let requestsQueue = []
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
service.interceptors.request.use(config => {
const token = getToken()
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
})
service.interceptors.response.use(response => response.data, async error => {
const { response, config } = error
if (response?.status === 401 && !config._retry) {
const refreshToken = getRefreshToken()
if (!refreshToken) {
removeToken()
router.push('/login')
return Promise.reject(error)
}
if (isRefreshing) {
return new Promise(resolve => {
requestsQueue.push(() => {
config.headers['Authorization'] = 'Bearer ' + getToken()
resolve(service(config))
})
})
}
isRefreshing = true
config._retry = true
try {
const { data } = await axios.post('/auth/refresh', { refreshToken })
setToken(data.accessToken)
if (data.refreshToken) {
setRefreshToken(data.refreshToken)
}
config.headers['Authorization'] = 'Bearer ' + data.accessToken
requestsQueue.forEach(callback => callback())
requestsQueue = []
return service(config)
} catch (refreshError) {
removeToken()
removeRefreshToken()
router.push('/login')
return Promise.reject(refreshError)
} finally {
isRefreshing = false
}
}
return Promise.reject(error)
})
export default service
按钮级权限控制
页面内按钮也需权限控制。Vue 可使用自定义指令,React 可使用 Hook。
import { getUserPermissions } from '@/utils/auth'
export default {
mounted(el, binding) {
const { value } = binding
const permissions = getUserPermissions()
if (!permissions.includes(value)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
}
import { getUserPermissions } from '../utils/auth'
export const usePermission = () => {
const permissions = getUserPermissions() || []
const hasPermission = (permission) => permissions.includes(permission)
return { hasPermission }
}
问题排查思路
- 检查 Network:确认请求头是否携带
Authorization 字段。
- 检查路由配置:确认
meta 字段拼写是否正确(如 requiresAuth)。
- 留意异步时机:确保用户信息获取完成后再执行路由跳转或接口请求。
- 清理缓存:强制刷新或清除 Service Worker 缓存,排除浏览器缓存干扰。
进阶优化建议
- 拦截器与守卫联动:HTTP 拦截器处理 401 状态,触发全局事件通知路由守卫跳转。
- SEO 处理:后台系统无需 SEO,前台付费内容可结合 SSR 或预渲染处理。
- 降级策略:权限服务挂掉时,根据业务场景选择安全优先(拒绝访问)或可用性优先(默认放行)。
- 避免过度设计:小项目使用简单逻辑即可,无需引入复杂 RBAC 模型。
总结
权限保护旨在防止未授权访问,保障数据安全。前端拦截虽不能替代后端校验,但能显著提升用户体验并增加攻击成本。合理选择方案,注意异步处理与边界条件,是构建安全前端应用的关键。
相关免费在线工具
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online