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