尧图精选

10个前端安全关键操作:从路由守卫到API鉴权,构建全链路防御

🕒 发布时间:2026/9/13 2:20:50 📁 来源:尧图网络
管理后台权限只靠路由守卫拦截、接口地址写死在路由meta里、密钥直接放在前端环境变量中——这些做法在中小型项目里太常见了。在一个又一个安全评估和线上事故之后我越来越确信一件事前端安全不是“把页面藏起来”而是要让路由收敛入口、让API扛住真正的校验。这篇文章要聊的就是这套东西我把它拆成了10个可以直接落地操作覆盖路由层、API层和构建运行期对Vue/React都适用适合正在做中后台、CMS、交易类站点的前端开发者也适合后端同学拿来对照接口侧该怎么配合。1. 先说清楚前端到底在泄什么密1.1 前端安全的真实边界浏览器里的一切都等于公开要谈防护先得把认知摆正任何下发到浏览器的代码、配置、数据理论上都是公开的。你可以压缩代码、混淆变量名但混淆不是加密它只是提高了阅读成本。这就像你给一本日记本涂满涂鸦纸还是纸真想知道内容的人总能读懂。这个认知决定了很多工作该怎么分工。前端负责的是体验层安全比如按钮不显示、页面不跳转、请求带上正确的凭证后端负责的是信任层安全比如这个用户有没有权限、这条数据是否属于它的归属者、这个参数值合不合法。如果你把后端的信任判断强行放到前端来做结果非常直接攻击者根本不浏览你的页面只要在浏览器控制台里改一改代码、抓一把请求再复制出一条curl命令就能完全绕开页面访问后端接口。我在不少项目里都见过这种案例排查漏数据问题查了半天发现某个按钮虽然被前端隐藏了但背后的接口依然可以被任何人调用。这不是bug这是设计上默认了“前端藏起来就安全”。前端隐藏入口和禁止访问完全是两回事。1.2 泄密的主要路径不止是源码被别人下载通常聊到前端泄密大家第一反应是“源代码被下载了”。但实际上我把它归纳成四类每一类现在依然大量存在泄露类型典型表现责任方源码级泄露SourceMap外泄、注释里残留服务器地址或密钥、打包产物暴露内部API路径前端为主凭据级泄露Token明文存localStorage、URL带密钥、第三方API Key硬编码进JS前端后端数据越权泄露接口无对象级授权遍历一下ID就能读任意用户数据后端为最终防线逻辑泄露权限判断、加解密流程、业务规则全部写在客户端JS里前端为主源码级泄露好理解线上的.map文件、代码注释、打包产物里搜得到的一串likeAKIA、sk-开头的东西都属于这一类。凭据级泄露也很普遍很多人以为把token放进localStorage是安全的但localStorage里的数据对同一源下的任何JS脚本都是可读的只要你的页面被注入了脚本或者某个第三方脚本SDK被攻击者利用token就会被顺走。数据越权泄露值得多说两句。很多团队做接口开发时习惯性把前端传过来的参数当作可信输入比如用户想查详情前端传id1001后端直接返回记录。可一旦后端没有校验这个用户是否有权限访问该ID那攻击者从1001遍历到9999就能拖走整张表的数据。这类问题严格来说是后端缺陷但它往往是因为前端把参数设计得“太方便”了才让后端一直没有意识到接口已经裸奔。逻辑泄露则是前端把安全机制直接做成“客户端判定”。比如一个活动页面的抽奖次数、一个视频课程的试看逻辑、一个后台菜单的可见性全都写在JS里。攻击者不费吹灰之力就能打开Sources面板找到这段逻辑然后直接在内存里把判断条件改掉。1.3 为什么路由和API是重灾区路由和API恰好对应了应用里的“入口”和“通道”。路由管的是入口。哪个页面能访问、哪个菜单能显示、用户没登录时跳到哪个登录页这些是前端路由的核心职责。它也是前端项目里最容易被低估的地方——很多人把它当成权限系统在用这恰恰是根本性误判。路由守卫能控制页面跳转但它拦不住攻击者直接调后端接口因为接口请求根本不需要经过路由器。API管的是通道。页面上的所有数据交互都通过它传输它是连接前端与后端的主动脉也是攻击者最关注的攻击面。攻击者不会像普通用户那样乖乖点页面他们更倾向于把所有接口列出来逐个探测参数、遍历ID、尝试越权。所以后面谈的10个操作没有一个是在教你“把页面藏好”而是围绕两件事展开把入口收窄把通道加固。2. 路由层的3个核心操作把入口收窄而不是掩耳盗铃2.1 操作1路由守卫只做体验收敛真正的权限决策交给后端先看一段最常见也最危险的写法// Vue Router 示例只靠它做权限控制是最常见的坑 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.roles !to.meta.roles.includes(user.role)) { next(/403) return } next() })这段代码的作用是没有token就踢出登录页角色不匹配就跳到403页。看起来像权限控制实际上它只是“用户体验控制”。为什么这么说因为路由守卫只拦截了浏览器里的路由跳转拦不住对API的直接请求。攻击者完全可以在DevTools里执行一句fetch(/api/users/delete, { method: POST, body: ... })你的路由守卫根本不会执行如果后端接口没有再做角色和资源归属校验操作就直接成功了。所以正确的分工应该是这样前端路由守卫只负责判断“登录态是否正常”“前端菜单权限是否加载完成”然后决定要不要跳登录页或404页面。它让普通用户看到一个清晰的入口而不是用来做安全决策。后端接口在每次请求时都必须校验token是否有效、用户是否被禁用、角色能否访问该接口、这条数据是否属于该用户。这是不可省略的最后一公里。前端所有“菜单可不可见”“按钮可不可点”都应当基于后端返回的权限数据来推导而不是在前端代码里写死一份角色映射表。我见过很多团队把路由meta里的roles数组当成安全判断依据结果后端改了个角色编码前端菜单就乱掉更严重的是攻击者能直接从打包产物里看到这些判断逻辑然后照着绕过。路由守卫的定位是“让正常用户少走弯路”而不是“把攻击者挡在门外”。2.2 操作2从路由meta里剥离敏感资源信息别把内部权限结构泄露出去这个坑在中小型团队极其普遍。为了开发方便很多人会把接口路径、权限标识、角色名称直接挂到路由meta上{ path: user, name: UserManagement, component: () import(/views/user/UserManagement.vue), meta: { title: 用户管理, roles: [admin, super_admin], // 权限标识外泄 api: [/user/list, /user/delete] // 内部接口路径外泄 } }开发期确实爽组件里一读meta就知道调什么接口、需要什么角色。但它有两个非常隐蔽的副作用一是打包进JS之后等于把后端接口清单和权限层级直接公开了。任何人打开你的站点按下F12就能从chunk文件里搜到这些接口路径然后照着清单去探测后端。你本来希望隐藏的内部API变成了攻击者最省事的寻宝图。举个例子我在一次代码审计里直接在打包产物里搜到了/api/internal/syncUserData这类路径后面顺着路径再测接口完全不需要猜。二是角色名称被前端写死后很难跟随后端权限模型演进。比如后端用role1/2/3表示角色前端写死成admin/editor一旦后端调整了权限模型或者某个用户被追加了新角色前端菜单顺序就乱了。更危险的是如果攻击者发现前端把判断逻辑写成了if (user.role admin)他只需要篡改本地内存或网络请求里的用户信息就可能拿到管理员页面。我的建议是路由meta里只放展示层需要的信息。{ path: user, name: UserManagement, component: () import(/views/user/UserManagement.vue), meta: { title: 用户管理, icon: user, requiresAuth: true } }接口路径统一放到请求层管理角色权限和菜单数据由用户登录后从后端动态获取。这样即便攻击者把整个前端路由表翻个底朝天也得不到什么有价值的内部权限结构。2.3 操作3确认history模式下没有把敏感路径“回退”给前端单页应用普遍使用路由的history模式部署时通常会在Nginx配置一个通配回退location / { try_files $uri $uri/ /index.html; }这段配置本身没毛病但如果不加区分地把所有请求都回退到index.html会带来两类问题。第一类是错误路由与接口路径被混淆。当后端接口根本没有对应路径时也会被Nginx回退到前端入口HTML攻击者扫描时看到一片200判断不出哪些路径真的存在但如果某个路径下的接口返回了JSON工具就能立刻识别出这是有效后端接口。等于说通配回退并没有起到“隐藏接口”的作用反而让一些应该返回404的路径全部变成200给攻击者提供了判断应用的线索。第二类是静态资源泄露。很多团队把构建产物直接扔进任意目录同时Nginx又开着通配。此时如果.env、.git、backup.zip、*.map这类文件被误放到公开目录攻击者只要访问对应路径就能直接下载。自动扫描器最喜欢撞这种路径中招率非常高。更稳妥的部署方式是把前端页面、静态资源、API请求三段分开处理# 前端页面入口 location / { try_files $uri $uri/ /index.html; } # 静态资源单独设置缓存与安全头 location /static/ { expires 30d; add_header Cache-Control public, immutable; } # API请求统一交给后端网关处理绝对不交给 index.html 返回 location /api/ { proxy_pass http://backend_gateway; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }还要在服务器层面禁止敏感文件的直接访问location ~* \.(env|map|git|bak|sql|log)$ { deny all; return 404; }这一段看起来像服务器配置但它其实决定了前端路由能不能安全落地。我见过不少项目代码里的路由写得规规矩矩结果线上Nginx回退规则一团糟敏感文件直接暴露这属于“大家都盯着代码忘了部署边界”。3. API层的4个核心操作让接口不再裸奔3.1 操作4CORS的Origin白名单必须精确配置禁止反射任意来源CORS是浏览器层面的访问控制机制很多人觉得它只是一个“跨域配置”弄好了开发联调方便就行。但在安全视角下CORS配置不当等于给外部恶意站点发了自由通行证。最危险的两种配置add_header Access-Control-Allow-Origin *; # 或者后端从请求里拿到什么 Origin 就返回什么 Origin典型的反射式写法如果你的接口是完全公开的且不携带任何用户态信息用*问题不大。但如果是需要携带Cookie或Authorization的管理后台接口把Origin反射回去就非常危险。攻击者可以构造一个钓鱼页面用户在登录状态下访问这个页面页面里的脚本发起跨域请求到你的后台接口浏览器看到Access-Control-Allow-Origin: https://evil.com之后就会把接口的返回内容交给这段脚本管理员的数据就被远程读取了。正确做法是维护一个精确的域名白名单map $http_origin $cors_allow_origin { default ; ~^https://admin\.example\.com$ $http_origin; ~^https://open\.example\.com$ $http_origin; } server { location /api/ { add_header Access-Control-Allow-Origin $cors_allow_origin; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Allow-Headers Authorization, Content-Type, X-Request-Sign; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS; if ($cors_allow_origin ) { return 403; } proxy_pass http://backend_gateway; } }这里有几个细节非常容易踩不要使用allowedOrigins: *credentials: true的组合这在语义上就是矛盾的虽然多数框架会拒绝但也有一些实现会悄悄放行。要拒绝Origin: null。很多隐私模式、iframe沙箱场景下请求的Origin是null这个边界值如果被放行等于给某些隐蔽的跨域读取留了后门。开发环境尽量用本地代理解决跨域不要把CORS配置放开一大堆开发域名不然上线时很容易带着宽松配置直接发布。需要说明一点CORS是浏览器层面的防护它防的是“第三方页面里的脚本直接读取你的接口”防不了攻击者用curl或其他工具手工伪造请求。所以CORS必须和鉴权配合使用它属于减少暴露面不是最后防线。3.2 操作5前端提交参数必须“白名单化”别把操作透传给后端另一个高频泄密入口是参数设计。常见的安全隐患写法GET /api/download?path/var/data/report-2025-01.pdf POST /api/users/delete Content-Type: application/json { id: 123, role: admin }这种接口的问题很明显前端把文件路径、操作类型、权限等级作为参数直接传给后端。攻击者只要改一改参数就可能拿到路径下的任意文件或者在请求体里给自己伪造role: admin而后端如果直接信任参数越权就产生了。前端侧最基础的动作是参数白名单化用业务主键代替可猜测的系统路径。比如下载文件时传fileId5f8c2a7e-4b1c-4f63-9a1d-3f8e9b6c2c31后端根据fileId映射真实路径并校验这个用户是否有权限下载。不要传path/var/data/report.pdf这种可直接拼接的路径。历史上很多路径遍历类漏洞几乎都是因为前端把路径参数直接透传给后端文件操作接口导致的。前端对参数做类型与枚举校验。例如status只允许active | disabled、pageSize强制为1~100之间的整数、id必须符合UUID格式。这能拦掉一部分畸形请求但要注意这只能减少后端无谓的压力不能替代后端的参数校验。不在请求参数里携带任何“服务端该决定”的字段。比如role、permissionLevel这类字段应该由后端根据token解析出来而不是前端在请求体里替自己声明。前端传什么就信什么是越权漏洞最经典的温床。前端能做到的部分是“正常人不会传错”攻击者可以绕过前端直接构造请求所以后端必须对参数和权限做最终校验。前端的白名单化只是在第一层降低暴露面真正兜底的还是后端。3.3 操作6给前端API请求加上短时签名/一次性票据防止抓包重放很多团队对API的防护还停留在“带个token就完事”。但实际上token一旦被复制攻击者可以直接重放请求。比如一个普通用户A抓到了另一个用户B的某个查询请求包只要在脚本里替换一下凭据或者直接转发就可能读到B的数据。更常见的是脚本批量遍历ID模拟正常业务请求拉取数据。要在请求层加一道比较扎实的防线可以给每个请求加短时签名// 前端在拿到用户会话后生成请求签名示例 const timestamp Date.now().toString() const nonce crypto.randomUUID() const message [method, path, timestamp, nonce].join() // sessionToken 是登录接口返回的会话凭据只在本次会话有效 const sign computeHmacSha256(message, sessionToken) fetch(path, { method, headers: { X-Timestamp: timestamp, X-Nonce: nonce, X-Sign: sign } })后端要做的事情也比较标准检查timestamp与当前时间差不超过5分钟过期请求直接拒绝。检查nonce在缓存中是否已经存在如果存在说明是重放请求。用当前用户的会话密钥计算HMAC并对比X-Sign是否一致。对写操作强制校验对查询操作至少校验身份与资源归属权限。这套方案能挡住三类常见的攻击参数被篡改、请求被重放、脚本批量乱扫。攻击者拿到一个请求包后不能再反复使用他想改参数签名就对不上他想批量扫必须让每个请求都经过签名逻辑自动化门槛提高了不少。但它也不是银弹要清楚它的边界如果攻击者本身就是一个合法用户他完全可以打开页面利用前端逻辑生成合法签名去调用接口。签名证明的是“请求来自可信会话、没有被中间篡改”它不能证明“调用者是善良的”。所以签名方案属于纵深防御的一部分不能替代身份认证和权限校验。另外提醒一句签名用的“密钥”存在于前端内存里攻击者通过调试工具是有机会拿到的。它不能当服务器端密钥来用设计时必须假设它会泄露所以只用来做防篡改、防重放而不是用来做身份凭证。3.4 操作7敏感接口的鉴权不能只依赖Cookie/Session关键操作加强绑定前面提到过CORS但如果你的后端还是依赖Cookie/Session来识别身份那还得把CSRF的账算上。浏览器每次请求都会自动带上Cookie这意味着恶意页面发出跨站请求时后端很难从Cookie层面区分这个请求是用户主动操作还是被钓鱼页面被动触发。前端侧能做的有效动作有这么几个敏感操作二次验证。删除数据、修改密码、转账这类高风险操作强制要求输入密码或验证码或者使用一个短时有效的确认令牌。很多金融系统就是这么做的但中后台项目里经常被忽略等真出了批量越权就晚了。写操作统一使用POST/DELETE等非简单请求。GET请求天然会被img、script、a等标签跨站触发。如果删除操作放在GET上攻击者只需要诱导用户点开一个带图片的页面就可能触发删除。前端在请求头里加自定义标识。比如X-Requested-With: XMLHttpRequest后端校验它的存在。这个只能挡住非常初级的CSRF不能当最终防线。后端做对象级授权。前端传id123后端必须确认这个ID对应的数据属于当前用户或当前角色不是验证“登录没登录”就放行。对象级授权缺失是越权漏洞里最普遍的一种在安全圈子里叫法很多但道理很朴素你能看见自己家的门牌号不等于你能进别人家的门。我习惯的分工是前端负责给敏感请求带二次验证、自定义请求头、签名后端负责解析用户身份、校验角色权限、判断资源归属、记录操作日志。前端的每一项设计都假设它会被绕过真正的信任判断终点永远放在后端。4. 构建与运行期的3个收尾操作别把开发期的问题带进生产4.1 操作8环境变量不等于安全密钥不能进前端产物这个坑我见过太多次了。很多项目用Vite或Webpack会把第三方服务的密钥写进.envVITE_MAP_JS_KEYxxxxxxxxx VITE_OSS_SECRETyyyyyyyyy然后在代码里import.meta.env.VITE_MAP_JS_KEY开发的时候很爽构建也不报错直到某天打开线上JS搜关键词发现在产物里躺着各种VITE_开头的变量密钥直接曝光。原因非常简单前端环境变量在构建时会被直接替换进产物所有带有公开前缀的变量都会出现在打包后的JS里。Vite中VITE_前缀就是公开标记Webpack里如果用DefinePlugin注入也是同理。怎么解决把“会执行在浏览器里的代码”和“服务端代码”严格区分开。凡是最终会跑到浏览器里的字符串都默认等于公开。对真正的敏感凭据不要让前端直接持有。比如地图、短信、OSS、大模型API等场景把调用包装成后端代理接口前端只向后端申请一个短期有效的临时凭证后端拿着长期密钥去调用第三方最后把临时凭证返回给前端使用。如果只是“不希望被搜索到”的配置项比如内部接口地址可以通过一个带鉴权的后端配置接口下发给前端而不是写死在前端构建配置里。“最小权限 临时凭证”这个思路能保证即使攻击者从JS里抠出一个key也只能在极短的时效和很窄的权限内使用影响面可控。4.2 操作9从打包产物里抹掉SourceMap、调试注释和内部路由生产环境发布SourceMap是很多团队不知不觉就踩进去的坑。为了线上报错能定位到源码位置默认开个sourcemap: true结果就是任何人打开DevTools的Sources面板都能直接看到你的原始源码包括注释、变量名、业务逻辑、依赖写法后端接口路径更是一目了然。具体怎么关Vite项目在vite.config.ts里export default defineConfig({ build: { sourcemap: false } })Vue CLI项目在vue.config.js里module.exports { productionSourceMap: false }如果确实需要保留线上排错能力不要发布到静态服务器。可以考虑把SourceMap上传到内部错误监控平台平台只用来做源码映射不对外提供公开访问发布前在CI阶段扫描构建产物里的.map文件发现就中断构建这样能防止有人手滑误传。除了SourceMap还有两类东西也要处理生产环境关闭调试路由。很多项目会留下/mock、/test、/debug这类开发期路由菜单里看不见但路由表还在包里直接访问路径就能打开。建议用import.meta.env.DEV之类的条件判断动态注册开发路由。清理console日志和注释里的敏感信息。requests参数、响应体、token被直接打出来调试时无所谓上线后就成了信息外泄渠道。更离谱的是代码注释里写了数据库连接、服务器IP、临时密码虽然压缩后变量名变了但注释如果保留在产物里照样能被搜出来。4.3 操作10错误监控和日志上报要脱敏别让异常链路成为新泄密口前端安全里最容易漏掉的暗线是为排查问题接入的监控上报体系本身成了敏感信息集散地。很多团队接入了Sentry或者自己搭日志系统上报时把完整请求头和请求体都传了上去。于是用户的手机号、身份证、token、密码全都被搬到了第三方平台或自建日志服务器里。这本质上属于前端主动把数据交出去危害不亚于接口越权。前端侧至少要做上报前清洗数据。用Sentry举例Sentry.init({ dsn: https://xxxsentry.example.com/1, beforeSend(event) { if (event.request) { delete event.request.headers.Authorization delete event.request.data.password delete event.request.data.phone } return event } })对上报接口或DSN做白名单限制。DSN本身不是密码但把它暴露给任意第三方页面别人就能往你的监控平台塞海量假数据干扰你真正的告警通道。服务端日志同步脱敏。Nginx访问日志、后端业务日志里不要记录Authorization头、Cookie全文、请求体里的密码字段。敏感字段要做掩码处理比如手机号只保留前三位和后四位。脱敏的原则不是“数据完全不可见”而是“最小必要”。前端上报的数据只需要包含排错必需的信息比如错误类型、组件栈、当前路由不需要整包请求体和响应体。监控平台的安全水位往往决定了你出问题后能不能快速定位同时也决定了会不会引入新的数据泄露。5. 把10个操作落到团队执行优先级、责任边界与常踩的坑5.1 10个操作速查表编号操作所属层主要责任方落地成本优先级1路由守卫只做体验收敛权限决策交给后端路由前端后端低极高2从路由meta剥离敏感资源信息路由前端低高3history模式部署三段分离、屏蔽敏感文件路由/部署前端运维中高4CORS Origin精确白名单配置API前端后端/运维低高5参数白名单化业务主键代替路径API前端后端中高6API请求短时签名/一次性票据API前端后端中中7敏感操作二次鉴权、对象级授权API后端为主前端配合中高极高8密钥不进前端产物构建前端中极高9移除SourceMap、调试入口、敏感注释构建前端运维低极高10监控与日志脱敏运行期前端后端低高这张表可以直接拿去当团队的安全改造清单用。优先级那一列标的是“建议落地顺序”先做能立刻降低风险且成本低的再做需要前后端联调的。5.2 我踩过的几个坑第一个坑是权限判断全写在路由守卫里。有一段时间我维护的内部报表系统前端菜单根据用户角色渲染得很精细路由守卫也判断得很严格。结果安全测试的时候对方直接用脚本调用了下载报表的API后端竟然没有任何数据归属和角色校验直接把全量数据导走了。那次之后我评审代码时会特意确认每个数据请求接口的后端鉴权是否齐全而不只是看前端跳转做得好不好看。第二个坑是SourceMap上线图省事。当时为了线上排错方便团队把sourcemap开到了生产环境结果安全扫描报告直接标红“源码可还原”。后来改成SourceMap只上传到内部监控平台彻底不在静态资源目录里出现.map文件。这个改动成本极低但效果立竿见影。第三个坑是CORS配了*觉得没事。有一个内部工具接口返回的是业务统计数据不是个人隐私我就想着开Access-Control-Allow-Origin: *无所谓。结果被另一个站点直接跨域轮询数据全被第三方页面读走了。后来改成精确白名单并把数据维度按用户角色做了最小化。安全这事最怕“觉得没事”很多泄密事故都是在“觉得没事”的时候发生的。5.3 进一步可以做什么如果这10个操作都已经落地还想继续加固可以从这几个方向扩展前端页面水印给敏感页面加自定义水印降低无感截图外传的概率。反调试与代码混淆注意它只能提高分析成本不能当作安全边界。网关/WAF层防护针对API做限流、风控、异常行为识别。后端风控引擎通过设备指纹、IP频率、行为序列识别风险请求。这些属于锦上添花。对大多数项目来说先把路由层和API层的这10个基础操作不漏项地做完安全水平已经超过市面上相当一部分同类型系统了。最后再分享一个小习惯每次发版前我会用一两分钟在浏览器里直接搜索打包产物里的关键词比如.map、password、token、sk-、/api/internal。如果搜出了不该出现的东西说明构建配置或代码里还有没归位的敏感信息。这个习惯帮我拦下了好几次“带病上线”。前端安全永远是一个相对问题别指望前端做完就天下太平前端负责把入口收窄、把暴露面降低后端负责真正的信任判断。如果你现在正在维护一个中后台系统今天就可以先从第8条和第9条开始查一下线上打包产物里的SourceMap和API Key可能比你新写一个页面更有价值。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →