安全最佳实践
安全是 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 主题免受常见的安全威胁!
下一步学习
掌握安全最佳实践后,建议继续学习:
最后更新时间: