前端安全面试核心考点:XSS、CSRF、CSP与JWT防御实战
1. 前端安全面试的底层逻辑拆解1.1 为什么前端安全成了面试必考题早些年面前端安全这块顶多问一句“你知道XSS吗”候选人回一句“跨站脚本攻击要转义”面试官点点头就过去了。现在完全不是这个节奏。我最近帮团队筛简历、做技术面安全相关的追问能占到整个面试时长的四分之一尤其是中高级岗位安全答不上来基本就挂了。原因很直接前端能碰到的敏感数据越来越多。Token存在哪、用户输入怎么渲染、第三方脚本怎么加载、跨域请求怎么带凭证这些决策一旦做错轻则数据泄露重则整个账号体系被击穿。面试官考安全考的不是你背了多少名词而是你有没有“攻击者视角”——你写代码的时候脑子里有没有一个假想敌。这一章我打算把XSS、CSRF、CSP、JWT这四个高频考点拆开讲透。每个点我都会按“面试官想听什么→实际原理是什么→代码层面怎么防→常见追问怎么答”这个顺序来展开。你如果正在准备面试可以把它当成一份带答案的复习提纲你如果是已经工作的前端也可以拿它对照检查自己项目里的安全水位。1.2 面试官到底在考察什么先把这个说清楚不然你背再多知识点也是散的。面试官问安全通常想验证三件事。第一你知不知道攻击面在哪。比如一个搜索框用户输入的内容直接innerHTML到页面上这就是一个典型的DOM型XSS入口。你能不能一眼看出来决定了你写代码时会不会主动规避。第二你懂不懂防御的层次。安全从来不是单点方案。防XSS你需要输入校验、输出编码、CSP兜底防CSRF你需要SameSite、Token、Referer校验。面试官喜欢听你说“我会从几个层面来做”而不是“我用了一个库就搞定了”。第三你有没有实战排查经验。比如线上突然报了一个XSS告警你怎么定位是哪个参数、哪个页面、什么类型的注入这种问题没有标准答案但能区分出“看过书”和“干过活”的人。提示面试时遇到安全题先别急着背定义。用一句话说清楚“这个漏洞的本质是什么”再展开防御手段节奏会稳很多。1.3 四个考点的关联关系XSS、CSRF、CSP、JWT不是四个孤立的知识点它们之间有清晰的逻辑链条。XSS是“攻击者往你页面里塞了恶意脚本”CSRF是“攻击者借你的手发了请求”两者经常组合使用。CSP是防XSS的兜底手段相当于给浏览器立规矩只准加载我白名单里的脚本。JWT则是身份凭证的载体一旦泄露或者校验不严攻击者就能直接冒充用户绕过前面所有的防线。理解了这个链条你在回答时就能串起来讲。比如面试官问“JWT存在localStorage里安全吗”你可以答“如果页面存在XSS漏洞localStorage里的JWT可以被脚本直接读取所以JWT的安全性强依赖于XSS的防御。这也是为什么很多方案会把JWT放在HttpOnly Cookie里同时配合CSRF Token使用。”这样答层次就出来了。2. XSS考点全解析从反射型到DOM型2.1 XSS的三种类型与本质区别XSS全称Cross-Site Scripting为了和CSS区分才缩写成了XSS。它的本质就一句话攻击者让浏览器执行了本不该执行的脚本。根据恶意脚本的“注入路径”和“触发方式”分成三类。反射型XSS是最常见的一种。攻击者构造一个带恶意参数的URL诱导用户点击。服务端把参数原样拼到HTML里返回浏览器一渲染脚本就执行了。比如https://example.com/search?qscriptalert(1)/script如果搜索页直接把q的值输出到页面就中招了。反射型的特点是“一次性的”不存库需要诱导点击。存储型XSS危害最大。恶意内容被存进了数据库比如评论区、用户昵称、留言板。任何访问这个页面的用户都会触发脚本。CTF里常见的“留言板打管理员Cookie”就是典型场景。存储型的排查难度也最高因为注入点可能在几个月前就埋下了。DOM型XSS比较特殊它不经过服务端。恶意数据直接在浏览器端被JavaScript处理并插入DOM。比如document.getElementById(output).innerHTML location.hash.slice(1)如果URL的hash里带了img srcx onerroralert(1)脚本就执行了。DOM型XSS的隐蔽性很强因为服务端日志里看不到异常传统的WAF也拦不住。类型注入位置是否经过服务端触发条件危害等级反射型URL参数是诱导点击中存储型数据库是访问页面高DOM型浏览器端JS否特定DOM操作中高2.2 面试高频追问DOM型XSS怎么防DOM型XSS是这两年面试的宠儿因为它能区分出真正理解原理的人。面试官常问“服务端已经做了转义为什么还有DOM型XSS”答案在于服务端的转义只对服务端输出的内容生效。如果前端JS从location、document.referrer、window.name这些地方取数据然后直接塞进innerHTML服务端的转义根本管不到。数据从头到尾没经过服务端。防御DOM型XSS核心是避免把不可信数据当HTML解析。具体做法能用textContent就不用innerHTML。textContent会把内容当纯文本不会解析标签。如果必须用innerHTML先做转义。可以用DOMPurify这类库它专门做HTML清洗比手写正则靠谱得多。避免eval、setTimeout(string)、new Function这类动态执行字符串的API。对location.hash、location.search、document.referrer这些来源的数据保持警惕使用前先校验。// 危险写法 const userInput location.hash.slice(1); document.getElementById(content).innerHTML userInput; // 安全写法一用textContent document.getElementById(content).textContent userInput; // 安全写法二用DOMPurify清洗 import DOMPurify from dompurify; document.getElementById(content).innerHTML DOMPurify.sanitize(userInput);注意DOMPurify的默认配置已经能挡住绝大多数XSS向量但如果你的业务允许富文本需要仔细配置白名单别图省事直接放开script和on*事件。2.3 存储型XSS的实战排查思路面试官如果问你“线上发现存储型XSS你怎么排查”这是在考实战能力。我分享一下自己的排查流程。第一步确认注入点。看告警里的Payload出现在哪个页面、哪个字段。常见的高危字段有用户昵称、评论内容、个人签名、文件名、订单备注。第二步判断存储位置。是存进了MySQL还是Redis还是直接写进了静态文件不同存储位置的清理方式不一样。第三步追溯输入源头。这个字段是从哪个接口进来的有没有做输入校验是前端直接提交的还是经过服务端处理的第四步评估影响范围。有多少条记录被污染有没有触发过有没有Cookie或Token泄露的迹象第五步修复与清理。修复分两层一是清理已存储的恶意数据二是修补输入输出逻辑。输出层一定要做编码输入层做校验。如果字段允许HTML用白名单清洗如果不允许直接转义。这里有个坑很多团队只修了输出层没清理数据库里的脏数据。结果老数据一被访问还是弹窗。所以清理和修复要同步做。2.4 XSS防御的编码细节输出编码不是简单地把换成lt;就完事了。不同的输出位置编码规则不一样。HTML正文编码、、、、HTML属性除了上述字符还要注意属性值必须加引号JavaScript代码块不能直接输出用户数据必须用JSON.stringify序列化并且转义、、URL参数用encodeURIComponentCSS尽量避免用户数据进入CSS如果必须做严格的白名单校验// HTML实体编码函数 function escapeHtml(str) { const map { : amp;, : lt;, : gt;, : quot;, : #x27; }; return str.replace(/[]/g, m map[m]); }面试时如果能说出“不同上下文用不同编码方式”面试官会知道你确实踩过坑。因为很多XSS漏洞就是因为编码方式用错了比如在JS上下文里用了HTML编码照样能被绕过。3. CSRF考点全解析从原理到绕过3.1 CSRF的本质借刀杀人CSRF全称Cross-Site Request Forgery跨站请求伪造。它的本质是攻击者诱导用户在已登录的网站上执行非预期的操作。关键点在于“已登录”。用户在某银行网站登录了Cookie还在有效期内。攻击者做一个恶意页面里面藏一个表单自动向银行网站提交转账请求。浏览器会自动带上银行的Cookie服务端一看Cookie有效就执行了转账。用户全程无感知。CSRF和XSS的区别XSS是“攻击者在你页面里执行脚本”CSRF是“攻击者借你的浏览器发请求”。XSS能拿到CookieCSRF拿不到Cookie但能利用Cookie。面试官常问“CSRF能拿到用户数据吗”答案是单纯的CSRF拿不到数据因为它只能发请求读不到响应。但如果配合XSS就能读响应了。所以CSRF通常用来做“写操作”比如转账、改密码、发消息。3.2 CSRF防御的三道防线防御CSRF业界公认的方案有三层面试时按这个顺序答基本满分。第一层SameSite Cookie属性。这是最简单也最有效的方案。设置SameSiteLax或SameSiteStrict浏览器在跨站请求时就不会带上Cookie。Lax允许顶级导航的GET请求带CookieStrict完全禁止。现在主流浏览器默认就是Lax所以很多CSRF攻击已经天然失效了。第二层CSRF Token。服务端生成一个随机Token嵌入表单或请求头。提交时服务端校验Token是否匹配。攻击者因为同源策略拿不到Token所以伪造的请求会被拒绝。Token要满足几个条件随机、一次性或有时效、和用户会话绑定。第三层Referer/Origin校验。检查请求的来源域名是否在白名单内。这个方案有局限性因为Referer可能被隐私设置屏蔽但Origin头在POST请求里比较可靠。// 服务端校验CSRF Token的伪代码 function verifyCsrfToken(req) { const tokenFromRequest req.headers[x-csrf-token] || req.body._csrf; const tokenFromSession req.session.csrfToken; if (!tokenFromRequest || tokenFromRequest ! tokenFromSession) { throw new Error(CSRF token mismatch); } }提示CSRF Token不要放在Cookie里否则就失去了意义。放在表单隐藏字段或自定义请求头里攻击者跨域拿不到。3.3 CSRF绕过的常见姿势面试官如果问“CSRF Token能不能被绕过”这是在考深度。答案是能但需要特定条件。场景一Token泄露。如果页面存在XSS漏洞攻击者可以直接读取Token。所以CSRF防御的前提是XSS已经防住了。场景二Token未绑定会话。有些实现只校验Token存在不校验Token属于哪个用户。攻击者用自己的账号获取一个合法Token然后诱导受害者提交服务端只检查Token格式就绕过了。场景三GET请求未防护。很多团队只对POST做了Token校验忽略了GET。如果敏感操作支持GET比如/transfer?toxxxamount100攻击者直接用一个img标签就能触发。场景四子域名问题。如果a.example.com存在XSS攻击者可以往example.com写Cookie因为同源策略对子域名的限制较松。这种情况下主域的CSRF防御可能被绕过。DVWA靶场的CSRF High级别就是通过结合XSS来绕过Token校验的。CTF里常见的“打管理员”题目也是这个思路。3.4 前后端分离下的CSRF新问题现在很多项目是前后端分离的前端用React/Vue后端用Spring Boot。这种架构下CSRF的形态变了。如果认证用的是JWT并且JWT存在localStorage里通过Authorization头传递那CSRF的风险其实降低了。因为攻击者跨域发请求时浏览器不会自动带上Authorization头。但前提是你不把JWT放在Cookie里。如果JWT放在Cookie里那CSRF风险依然存在。这时候需要配合CSRF Token或者SameSite。还有一种情况SPA项目用JWT做登录验证Token续签逻辑没做好。攻击者可以伪造一个续签请求让用户的Token一直有效。这种攻击比较隐蔽面试时如果提到会加分。// Spring Boot中配置CSRF防护的示例 Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class); } }4. CSP考点全解析内容安全策略4.1 CSP是什么为什么需要它CSP全称Content Security Policy内容安全策略。它的作用是告诉浏览器哪些资源可以加载、哪些脚本可以执行。你可以把它理解成一份白名单浏览器只认白名单里的东西。CSP最大的价值在于兜底。你不可能保证代码里100%没有XSS漏洞但CSP可以在漏洞被利用时阻止恶意脚本执行。比如你设置了script-src self那么即使攻击者注入了script srchttps://evil.com/x.js浏览器也会拒绝加载。CSP通过HTTP响应头或meta标签来设置。响应头的方式更推荐因为meta标签有一些限制比如不支持frame-ancestors。Content-Security-Policy: default-src self; script-src self https://trusted.cdn.com; style-src self unsafe-inline; img-src self data: https:;面试官问CSP通常想看你有没有实际配置过。因为CSP的坑很多配不好会把正常功能搞挂。4.2 CSP指令详解与配置策略CSP的指令很多面试常考的有这几个。default-src兜底指令其他指令没设置时用它。建议设为self。script-src控制脚本来源。这是最重要的指令。self表示只允许同源脚本unsafe-inline允许内联脚本不建议unsafe-eval允许eval不建议nonce-xxx允许特定nonce的脚本。style-src控制样式来源。unsafe-inline在样式里很常见因为很多框架会动态插入样式。但这也带来了风险攻击者可能通过CSS注入来窃取数据。img-src控制图片来源。通常设为self data: https:。connect-src控制XHR、WebSocket、fetch的请求目标。这个指令经常被忽略但很重要。如果设得太松攻击者可以通过fetch把数据外传。frame-ancestors控制页面能被哪些网站嵌入。用来防点击劫持。none表示不允许任何网站嵌入。指令作用推荐值风险点default-src兜底self设太松等于没设script-src脚本来源self nonceunsafe-inline风险高style-src样式来源self unsafe-inlineCSS注入可窃取数据connect-src请求目标self API域名设太松可外传数据frame-ancestors防点击劫持nonemeta标签不支持4.3 CSP的nonce和hash机制unsafe-inline是CSP配置里最常见的妥协。很多项目因为用了内联脚本不得不开这个选项。但开了之后CSP对XSS的防护就大打折扣了。更好的方案是nonce或hash。nonce是服务端每次请求生成一个随机字符串嵌入到script noncexxx里同时在CSP头里声明script-src nonce-xxx。浏览器只执行nonce匹配的脚本。攻击者因为猜不到nonce注入的脚本会被拒绝。!-- 服务端生成的nonce -- script nonced8f7a6b5c4e3 console.log(this will run); /script !-- 攻击者注入的脚本没有正确的nonce -- scriptalert(xss)/scripthash是直接计算脚本内容的哈希值在CSP头里声明。浏览器只执行哈希匹配的脚本。适合静态内联脚本。Content-Security-Policy: script-src sha256-abc123...nonce的缺点是每次请求都要生成对缓存不友好。hash的缺点是脚本内容一变哈希就要重算。实际项目中nonce更常用。4.4 CSP的常见绕过与局限CSP不是万能的面试官如果问“CSP能不能被绕过”你要能说出几种情况。情况一配置了unsafe-inline。这等于给内联脚本开了后门攻击者直接注入scriptalert(1)/script就能执行。情况二白名单域名存在JSONP接口。如果script-src允许了某个CDN而这个CDN上有JSONP接口攻击者可以利用JSONP来执行任意代码。这种绕过在CTF里很常见。情况三base-uri未设置。如果页面用了相对路径加载脚本攻击者可以注入base hrefhttps://evil.com把脚本请求劫持到恶意服务器。情况四script-src允许了data:。攻击者可以构造script srcdata:text/javascript,alert(1)。情况五CSP报告模式。如果只设置了Content-Security-Policy-Report-Only策略不会生效只会上报违规。有些团队误以为已经开启了防护。注意CSP的部署建议先用Report-Only模式跑一段时间收集违规报告确认不影响正常功能后再切换到强制模式。直接上强制模式很容易把线上搞挂。5. JWT考点全解析从结构到漏洞5.1 JWT的结构与验证流程JWT全称JSON Web Token是目前最流行的无状态认证方案。它的结构分三部分用点号分隔Header.Payload.Signature。Header是Base64Url编码的JSON包含算法类型和Token类型。比如{alg:HS256,typ:JWT}。Payload也是Base64Url编码的JSON包含声明Claims。标准声明有iss签发者、exp过期时间、sub主题、aud受众。自定义声明可以放用户ID、角色等。Signature是对前两部分的签名用来防篡改。服务端用密钥对Header.Payload进行签名客户端拿到Token后服务端验证签名是否匹配。// JWT的结构示例 const header { alg: HS256, typ: JWT }; const payload { userId: 123, role: admin, exp: 1735689600 }; // 签名过程伪代码 const signature HMACSHA256(base64UrlEncode(header) . base64UrlEncode(payload), secret); const token base64UrlEncode(header) . base64UrlEncode(payload) . signature;面试官问JWT第一层是考结构第二层是考验证流程第三层是考漏洞。很多人只知道JWT长什么样但不知道验证时要注意什么。5.2 JWT的常见漏洞与攻击手法JWT的漏洞主要集中在算法混淆和密钥管理上。漏洞一algnone。JWT规范里允许alg为none表示不签名。如果服务端没有严格校验算法攻击者可以把alg改成none去掉签名伪造任意Payload。防御方法很简单服务端强制指定算法拒绝none。漏洞二HS256与RS256混淆。HS256是对称加密用同一个密钥签名和验证。RS256是非对称加密用私钥签名、公钥验证。如果服务端用的是RS256但验证时没限制算法攻击者可以把alg改成HS256然后用公钥作为HMAC密钥来签名。因为公钥是公开的攻击者能拿到所以能伪造Token。防御方法是服务端明确指定算法不信任Header里的alg。漏洞三弱密钥。如果HS256的密钥太短或太常见攻击者可以暴力破解。比如密钥是secret、123456用hashcat几分钟就能跑出来。防御方法是使用足够长、足够随机的密钥。漏洞四kid注入。kid是Header里的一个字段表示密钥ID。如果服务端用kid来查密钥文件攻击者可以构造kid为../../etc/passwd导致路径穿越。或者构造kid为SQL注入语句如果服务端用kid查数据库就可能被注入。漏洞五Token未校验过期。有些实现只验证签名不检查exp。攻击者可以拿一个过期的Token继续用。防御方法是验证时检查exp、nbf、iat等时间声明。漏洞类型攻击手法防御措施algnone去掉签名强制指定算法算法混淆HS256冒充RS256服务端固定算法弱密钥暴力破解使用强密钥kid注入路径穿越/SQL注入校验kid格式过期未校验重用旧Token检查exp声明5.3 JWT的存储位置与安全性面试官常问“JWT应该存在哪里”这个问题没有标准答案但你要能说出权衡。localStorage方便前端可以直接读取。但XSS能直接偷走。如果页面有XSS漏洞Token就没了。sessionStorage和localStorage类似但关闭标签页就清空。安全性略好但XSS依然能偷。HttpOnly CookieJavaScript读不到XSS偷不走。但会带来CSRF风险需要配合CSRF Token或SameSite。另外Cookie有大小限制JWT如果太大可能放不下。内存存在JS变量里刷新页面就丢。安全性最高但用户体验差需要配合Refresh Token。实际项目中比较常见的方案是Access Token存在内存或sessionStorageRefresh Token存在HttpOnly Cookie。Access Token短期有效Refresh Token长期有效但只能用来换新Token。// 前端存储JWT的示例 // 不推荐localStorage localStorage.setItem(token, jwt); // 推荐内存 Refresh Token let accessToken null; function setToken(token) { accessToken token; } // Refresh Token由服务端通过HttpOnly Cookie下发提示不管存在哪里JWT的安全性强依赖于XSS的防御。如果XSS防不住存哪都不安全。所以安全是一个整体不能只盯着一个点。5.4 JWT续签与注销的工程实践JWT是无状态的服务端不存Session。这带来了一个工程难题怎么注销Token用户点了退出登录但Token还在有效期内攻击者拿到旧Token还能用。常见的解决方案有几种。方案一短期Access Token 长期Refresh Token。Access Token有效期设短一点比如15分钟。Refresh Token有效期长比如7天。退出登录时服务端把Refresh Token加入黑名单。Access Token过期后攻击者就用不了了。这个方案牺牲了一点实时性但工程上最常用。方案二Token黑名单。服务端维护一个黑名单存已注销的Token。每次验证时查黑名单。缺点是失去了无状态的优势黑名单本身要存储和查询。方案三版本号机制。用户表里存一个tokenVersion签发Token时把版本号放进去。注销时版本号加一。验证时对比版本号不匹配就拒绝。这个方案比较优雅但需要查库。// Spring Security整合JWT的续签逻辑伪代码 public String refreshToken(String refreshToken) { if (isBlacklisted(refreshToken)) { throw new RuntimeException(Token已注销); } Claims claims parseToken(refreshToken); String newAccessToken generateAccessToken(claims.getSubject()); return newAccessToken; }面试时如果能把续签和注销讲清楚说明你确实在项目里用过JWT而不是只背了概念。6. 安全防御的工程化落地6.1 全局过滤器处理XSS的实践在实际项目里防XSS不能靠每个开发手动转义那样迟早会漏。比较靠谱的做法是全局过滤器。以Spring Boot项目为例可以写一个XssFilter对所有请求参数做清洗。对于普通表单字段直接转义HTML特殊字符。对于富文本字段用白名单清洗保留允许的标签。// Spring Boot全局XSS过滤器示例 Component public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; XssHttpServletRequestWrapper wrapper new XssHttpServletRequestWrapper(req); chain.doFilter(wrapper, response); } } public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { Override public String[] getParameterValues(String name) { String[] values super.getParameterValues(name); if (values null) return null; return Arrays.stream(values).map(this::cleanXss).toArray(String[]::new); } private String cleanXss(String value) { // 转义HTML特殊字符 return value.replaceAll(, lt;) .replaceAll(, gt;) .replaceAll(\, quot;) .replaceAll(, #x27;); } }这里有个坑上传PDF文件时的XSS。如果PDF文件名或内容里带了恶意脚本而前端直接渲染可能触发XSS。处理方式是上传时校验文件类型和内容下载时设置Content-Disposition: attachment强制下载而不是预览。如果必须预览用PDF.js这类库并且设置CSP。6.2 CSP与富文本白名单的配合富文本是XSS的重灾区。用户需要加粗、斜体、链接、图片这些都需要HTML标签。但一旦允许HTMLXSS的风险就来了。比较稳妥的方案是服务端白名单清洗 CSP兜底。服务端用jsoup或OWASP Java HTML Sanitizer做白名单清洗只保留允许的标签和属性。比如只允许b、i、a、img并且a的href必须是http或https开头img的src必须是白名单域名。CSP层面设置script-src self禁止内联脚本。这样即使清洗漏了恶意脚本也执行不了。Content-Security-Policy: default-src self; script-src self; style-src self unsafe-inline; img-src self https://trusted-images.com; object-src none;注意object-src none可以禁止Flash等插件减少攻击面。base-uri self可以防止base标签劫持。6.3 安全开发的检查清单最后分享一份我在项目中常用的安全检查清单面试时如果被问到“你怎么保证前端安全”可以按这个思路答。所有用户输入都做校验前端校验为了体验服务端校验为了安全。所有输出都做编码根据上下文选择HTML编码、JS编码、URL编码。富文本用白名单清洗不用黑名单。设置CSP至少script-src self禁用unsafe-inline。Cookie设置HttpOnly、Secure、SameSite。敏感操作加CSRF Token校验Referer/Origin。JWT设置合理的过期时间验证时检查exp强制指定算法。第三方脚本用integrity属性做SRI校验。定期做安全扫描关注依赖库的漏洞公告。安全不是一次性的工作而是持续的过程。每次代码评审时多问一句“这里有没有注入风险”比事后补救强得多。6.4 面试中的安全场景题应对面试官有时候会出场景题比如“你负责一个电商网站的前端用户可以在商品详情页留言。你怎么设计安全方案”这种题没有标准答案但你可以按层次来答。第一层输入校验。留言内容限制长度过滤敏感词服务端校验用户身份。第二层输出编码。留言渲染时如果允许富文本用白名单清洗如果不允许直接转义。第三层CSP兜底。设置script-src self防止漏网的XSS。第四层CSRF防护。留言接口加CSRF Token校验来源。第五层监控与告警。记录异常输入设置告警规则发现攻击及时响应。这样答下来面试官会觉得你有体系化的思维而不是零散地背知识点。我在实际面试中最怕遇到那种“一问XSS就背定义一问防御就说转义”的候选人。安全考的是思维是你在写代码时有没有把攻击者放在心里。你不需要成为安全专家但你需要知道常见的坑在哪以及怎么用工程手段把坑填上。