Skip to Content
🎉 探索 Shopify 的无限可能 结构化知识 + 实战案例,持续更新中...
Liquid 开发安全最佳实践

安全最佳实践

安全是 Shopify 主题开发中至关重要的方面。本指南将介绍如何在主题开发中实施安全最佳实践,保护用户数据和店铺信息。

数据验证和清理

1. 用户输入验证

<!-- 表单输入验证 --> <form action="/contact" method="post" class="contact-form" data-form-validation> <div class="form-group"> <label for="contact-name">姓名 *</label> <input type="text" id="contact-name" name="contact[name]" required maxlength="100" pattern="[a-zA-Z\s\u4e00-\u9fa5]+" data-validate="name"> <span class="error-message" data-error="name"></span> </div> <div class="form-group"> <label for="contact-email">邮箱 *</label> <input type="email" id="contact-email" name="contact[email]" required maxlength="254" data-validate="email"> <span class="error-message" data-error="email"></span> </div> <div class="form-group"> <label for="contact-message">消息 *</label> <textarea id="contact-message" name="contact[body]" required maxlength="1000" data-validate="message"></textarea> <span class="error-message" data-error="message"></span> </div> <!-- 防止机器人提交 --> <div class="form-group hidden"> <input type="text" name="contact[honey_pot]" tabindex="-1" autocomplete="off"> </div> <button type="submit" class="btn btn--primary">发送消息</button> </form> <script> class FormValidator { constructor(form) { this.form = form; this.init(); } init() { this.form.addEventListener('submit', this.handleSubmit.bind(this)); this.bindInputValidation(); } bindInputValidation() { const inputs = this.form.querySelectorAll('[data-validate]'); inputs.forEach(input => { input.addEventListener('blur', () => this.validateField(input)); input.addEventListener('input', () => this.clearError(input)); }); } handleSubmit(event) { event.preventDefault(); if (this.validateForm()) { this.submitForm(); } } validateForm() { const inputs = this.form.querySelectorAll('[data-validate]'); let isValid = true; inputs.forEach(input => { if (!this.validateField(input)) { isValid = false; } }); // 检查蜜罐字段 const honeyPot = this.form.querySelector('input[name="contact[honey_pot]"]'); if (honeyPot && honeyPot.value) { console.warn('Bot detected'); return false; } return isValid; } validateField(input) { const type = input.dataset.validate; const value = input.value.trim(); // 清理输入 const cleanValue = this.sanitizeInput(value); if (cleanValue !== value) { input.value = cleanValue; } let isValid = true; let errorMessage = ''; switch (type) { case 'name': if (!cleanValue) { errorMessage = '请输入姓名'; isValid = false; } else if (!/^[a-zA-Z\s\u4e00-\u9fa5]+$/.test(cleanValue)) { errorMessage = '姓名只能包含字母和中文字符'; isValid = false; } break; case 'email': if (!cleanValue) { errorMessage = '请输入邮箱地址'; isValid = false; } else if (!this.isValidEmail(cleanValue)) { errorMessage = '请输入有效的邮箱地址'; isValid = false; } break; case 'message': if (!cleanValue) { errorMessage = '请输入消息内容'; isValid = false; } else if (cleanValue.length < 10) { errorMessage = '消息内容至少10个字符'; isValid = false; } break; } this.showError(input, errorMessage); return isValid; } sanitizeInput(input) { // 移除潜在的危险字符 return input .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') .replace(/<[^>]*>/g, '') .replace(/javascript:/gi, '') .replace(/on\w+=/gi, '') .trim(); } isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } showError(input, message) { const errorElement = this.form.querySelector(`[data-error="${input.dataset.validate}"]`); if (errorElement) { errorElement.textContent = message; errorElement.style.display = message ? 'block' : 'none'; } input.classList.toggle('error', !!message); } clearError(input) { this.showError(input, ''); } async submitForm() { try { const formData = new FormData(this.form); const response = await fetch(this.form.action, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (response.ok) { this.showSuccess('消息发送成功!'); this.form.reset(); } else { throw new Error('发送失败'); } } catch (error) { this.showError(null, '发送失败,请稍后重试'); } } showSuccess(message) { const successElement = document.createElement('div'); successElement.className = 'success-message'; successElement.textContent = message; this.form.insertBefore(successElement, this.form.firstChild); setTimeout(() => { successElement.remove(); }, 5000); } } // 初始化表单验证 document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('[data-form-validation]').forEach(form => { new FormValidator(form); }); }); </script>

2. 输出转义

<!-- 安全的内容输出 --> {% comment %} 始终对用户生成的内容进行转义 {% endcomment %} <!-- 产品标题 - 自动转义 --> <h1>{{ product.title }}</h1> <!-- 产品描述 - 移除脚本标签 --> <div class="product-description"> {{ product.description | strip_html: 'script,style,iframe,object,embed' }} </div> <!-- 用户评论 - 严格清理 --> {% for comment in blog.comments %} <div class="comment"> <h4>{{ comment.author | escape }}</h4> <p>{{ comment.content | strip_html | escape }}</p> <time>{{ comment.created_at | date: '%Y-%m-%d' }}</time> </div> {% endfor %} <!-- 搜索查询 - 转义并限制长度 --> {% if search.terms %} <p>搜索结果: "{{ search.terms | escape | truncate: 100 }}"</p> {% endif %} <!-- 自定义字段 - 安全输出 --> {% if product.metafields.custom.description %} <div class="custom-description"> {{ product.metafields.custom.description | metafield_tag }} </div> {% endif %}

CSRF防护

1. 表单令牌保护

<!-- 添加CSRF令牌到表单 --> <form action="/cart/add" method="post" class="product-form"> {{ form.authenticity_token }} <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}"> <div class="form-group"> <label for="quantity">数量:</label> <input type="number" id="quantity" name="quantity" value="1" min="1" max="10"> </div> <button type="submit" class="btn btn--primary"> 加入购物车 </button> </form>

2. Ajax请求安全

// 安全的Ajax请求处理 class SecureAjax { constructor() { this.csrfToken = this.getCSRFToken(); } getCSRFToken() { const tokenMeta = document.querySelector('meta[name="csrf-token"]'); return tokenMeta ? tokenMeta.getAttribute('content') : null; } async request(url, options = {}) { const defaultOptions = { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin' }; // 添加CSRF令牌到POST请求 if (options.method === 'POST' && this.csrfToken) { defaultOptions.headers['X-CSRF-Token'] = this.csrfToken; } const finalOptions = { ...defaultOptions, ...options }; try { const response = await fetch(url, finalOptions); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response; } catch (error) { console.error('Request failed:', error); throw error; } } async addToCart(variantId, quantity = 1) { return this.request('/cart/add.js', { method: 'POST', body: JSON.stringify({ id: variantId, quantity: quantity }) }); } async updateCart(updates) { return this.request('/cart/update.js', { method: 'POST', body: JSON.stringify({ updates }) }); } } // 全局安全Ajax实例 window.secureAjax = new SecureAjax();

内容安全策略 (CSP)

1. CSP头部设置

<!-- layout/theme.liquid --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.shopify.com https://www.google-analytics.com https://www.googletagmanager.com https://connect.facebook.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com https://cdn.shopify.com; img-src 'self' data: https://cdn.shopify.com https://www.google-analytics.com; connect-src 'self' https://monorail-edge.shopifysvc.com;">

2. 内联脚本安全

<!-- 避免内联脚本,使用外部文件 --> <!-- 不推荐 --> <script> var productData = {{ product | json }}; </script> <!-- 推荐做法 --> <script id="product-data" type="application/json"> {{ product | json }} </script> <script> // 在外部文件中安全获取数据 class ProductDataManager { static getData() { const dataElement = document.getElementById('product-data'); if (dataElement) { try { return JSON.parse(dataElement.textContent); } catch (error) { console.error('Failed to parse product data:', error); return null; } } return null; } } </script>

敏感信息保护

1. API密钥管理

<!-- 环境变量使用 --> {% comment %} 永远不要在前端代码中暴露敏感信息 使用环境变量或Shopify的设置系统 {% endcomment %} <!-- 错误做法 --> <script> const API_KEY = 'sk_live_abcd1234...'; // 危险! </script> <!-- 正确做法 --> <script> // 使用公开的可配置标识符 const ANALYTICS_ID = '{{ settings.google_analytics_id }}'; const PUBLIC_KEY = '{{ settings.stripe_public_key }}'; </script>

2. 客户信息保护

<!-- 安全处理客户信息 --> {% if customer %} <div class="customer-info"> <h3>欢迎回来,{{ customer.first_name | escape }}!</h3> <!-- 不要显示完整邮箱 --> <p>账户邮箱: {{ customer.email | replace: '@', '***@' | escape }}</p> <!-- 不要显示敏感的订单信息 --> <p>您有 {{ customer.orders_count }} 个历史订单</p> </div> {% endif %} <!-- 客户地址信息脱敏 --> {% for address in customer.addresses %} <div class="address"> <h4>{{ address.name | escape }}</h4> <p>{{ address.address1 | escape }}</p> {% if address.address2 != blank %} <p>{{ address.address2 | escape }}</p> {% endif %} <p>{{ address.city | escape }}, {{ address.province | escape }}</p> <!-- 不显示完整邮编 --> <p>{{ address.zip | slice: 0, 3 }}***</p> </div> {% endfor %}

防范常见攻击

1. 防止点击劫持

<!-- layout/theme.liquid --> <meta http-equiv="X-Frame-Options" content="SAMEORIGIN"> <script> // 防止页面被嵌入到不受信任的iframe中 if (window.top !== window.self) { // 检查是否在合法的iframe中 const allowedDomains = [ '{{ shop.permanent_domain }}', 'checkout.shopify.com' ]; const parentDomain = document.referrer.match(/^https?:\/\/([^\/]+)/); if (!parentDomain || !allowedDomains.some(domain => parentDomain[1].includes(domain))) { // 破坏潜在的点击劫持 window.top.location = window.self.location; } } </script>

2. 防止Session劫持

// 会话安全管理 class SessionSecurity { constructor() { this.init(); } init() { this.setupSessionValidation(); this.setupIdleTimeout(); } setupSessionValidation() { // 定期验证会话有效性 setInterval(async () => { try { const response = await fetch('/account', { method: 'HEAD', credentials: 'same-origin' }); if (response.status === 401) { this.handleSessionExpired(); } } catch (error) { console.warn('Session validation failed:', error); } }, 5 * 60 * 1000); // 每5分钟检查一次 } setupIdleTimeout() { let idleTime = 0; const idleLimit = 30; // 30分钟 // 重置空闲计时器 const resetIdleTime = () => { idleTime = 0; }; // 监听用户活动 ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'].forEach(event => { document.addEventListener(event, resetIdleTime, { passive: true }); }); // 每分钟检查空闲时间 setInterval(() => { idleTime++; if (idleTime >= idleLimit) { this.handleIdleTimeout(); } }, 60 * 1000); } handleSessionExpired() { // 清除本地数据 localStorage.clear(); sessionStorage.clear(); // 显示会话过期提示 this.showSessionExpiredModal(); } handleIdleTimeout() { if (window.customer) { this.showIdleWarning(); } } showSessionExpiredModal() { const modal = document.createElement('div'); modal.className = 'session-expired-modal'; modal.innerHTML = ` <div class="modal-content"> <h3>会话已过期</h3> <p>为了您的安全,会话已过期。请重新登录。</p> <button onclick="window.location.href='/account/login'">重新登录</button> </div> `; document.body.appendChild(modal); } showIdleWarning() { const warning = document.createElement('div'); warning.className = 'idle-warning'; warning.innerHTML = ` <div class="warning-content"> <p>您已经空闲很长时间,为了安全考虑建议重新登录。</p> <button onclick="this.parentElement.parentElement.remove()">继续使用</button> <button onclick="window.location.href='/account/logout'">退出登录</button> </div> `; document.body.appendChild(warning); // 10秒后自动移除警告 setTimeout(() => { if (warning.parentElement) { warning.remove(); } }, 10000); } } // 初始化会话安全 if (window.customer) { new SessionSecurity(); }

第三方集成安全

1. 安全的外部脚本加载

<!-- 安全加载第三方脚本 --> {% if settings.google_analytics_id %} <script> // 动态加载并验证 function loadGoogleAnalytics() { const script = document.createElement('script'); script.src = 'https://www.googletagmanager.com/gtag/js?id={{ settings.google_analytics_id }}'; script.async = true; script.onload = initializeGA; script.onerror = () => console.warn('Failed to load Google Analytics'); document.head.appendChild(script); } function initializeGA() { window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '{{ settings.google_analytics_id }}'); } // 在页面加载后延迟加载 window.addEventListener('load', () => { setTimeout(loadGoogleAnalytics, 1000); }); </script> {% endif %}

2. API请求安全

// 安全的API请求处理 class SecureAPIClient { constructor(baseURL, options = {}) { this.baseURL = baseURL; this.options = { timeout: 10000, retries: 3, ...options }; } async request(endpoint, options = {}) { const url = new URL(endpoint, this.baseURL); // 验证URL安全性 if (!this.isSecureURL(url)) { throw new Error('Insecure URL detected'); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.options.timeout); try { const response = await fetch(url.toString(), { ...options, signal: controller.signal, credentials: 'same-origin', headers: { 'Content-Type': 'application/json', ...options.headers } }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error('Request timeout'); } throw error; } } isSecureURL(url) { // 只允许HTTPS协议 if (url.protocol !== 'https:') { return false; } // 检查域名白名单 const allowedDomains = [ '{{ shop.permanent_domain }}', 'api.shopify.com', 'cdn.shopify.com' ]; return allowedDomains.some(domain => url.hostname === domain || url.hostname.endsWith('.' + domain) ); } }

通过实施这些安全最佳实践,您可以有效保护 Shopify 主题免受常见的安全威胁!

下一步学习

掌握安全最佳实践后,建议继续学习:

  1. 实际应用示例 - 实战案例分析
  2. 常见问题解决 - 问题排查指南
最后更新时间: