界定买家账户 API 与店铺 API
先确认数据主体和调用边界
Customer Account API 面向已认证买家自己的账户数据,不是把 Admin API 的店铺权限搬到浏览器。开始设计时先画出买家、店铺、应用、认证服务和数据存储的边界,列出每个请求代表谁、能读什么、能写什么。购物车、商品目录和公开内容仍可由 Storefront API 处理;地址、订单历史和个人资料则必须按买家会话和受保护数据要求处理。
边界表要包含调用端、令牌类型、GraphQL 端点、客户身份、允许字段、错误处理和回退体验。不要用一个全局店铺令牌代表所有买家,也不要把客户邮箱当作稳定身份。Customer Account API 的响应可能包含 data 和 errors,HTTP 成功不等于业务成功。把身份、权限、数据状态和页面展示分别记录,避免前端把错误对象当成空账户。 相关背景可参阅 Shopify API 总览。
失败案例是前端把 Admin API access token 放进浏览器,用它读取客户资料;风险不只在泄露,还在于无法证明请求由哪个买家授权。回退时撤销暴露令牌,切换到无账户的目录和购物车路径,订单与地址区域显示重新认证提示。不要用隐藏字段或 cookie 伪造买家身份。
官方Customer Account API 概览强调客户范围和安全认证。将其作为架构起点,再用具体对象和 mutation 文档核对字段。对于不能提供明确客户会话的请求,默认返回最小公开内容,并把需要账户的动作留在受保护流程内。
发现认证与 GraphQL 端点
从发现文档获取端点而不是硬编码
Customer Account API 提供 OpenID 配置和账户 API 发现端点,应用应在初始化或受控缓存刷新时读取它们,获得授权、令牌和 GraphQL 地址。端点属于店铺账户上下文,不能从一个商店复制到另一个商店。发现结果要记录来源、读取时间、协议版本和缓存过期原因;请求失败时不要悄悄回退到旧店铺地址。
创建一个端点解析器,输入规范化店铺域名和运行环境,输出授权地址、令牌地址、GraphQL 地址和 issuer 校验结果。对 HTTPS、主机匹配、证书错误、重定向和发现文档缺字段定义明确失败。GraphQL 客户端只接受通过 issuer 和店铺上下文检查的地址,不能接受页面参数直接提供的 URL。 相关背景可参阅 Shopify Liquid 主题开发指南。
失败案例是把测试商店的账户端点写进生产配置,买家登录后收到无账户或跨店错误。回退时清空错误缓存,重新读取当前店铺发现文档,只保留公开目录体验,并让会话重新建立。不要让客户端任意替换 issuer,也不要把发现失败当作匿名成功。
Customer Account API 参考列出发现路径、客户端类型和 GraphQL 约束。验收时保存发现响应的非敏感摘要,测试正常、过期、结构不完整和网络不可达四种情况。端点发现是会话的前置条件,不是一次性配置文件;商店迁移或域名变化时要再次核对。
| 层次 | 可持有内容 | 主要校验 | 不应持有 |
|---|---|---|---|
| 浏览器 | 会话引用、展示状态 | Secure、HttpOnly、SameSite | client secret、原始 token |
| 服务端 | 受保护会话与令牌状态 | 店铺、subject、过期时间 | 无用途的完整客户副本 |
| Customer API | 买家范围 GraphQL 数据 | scope、schema、错误 | Admin 全店权限 |
| 匿名路径 | 商品、购物车等公开内容 | 无客户身份 | 订单、地址、个人资料 |
选择公开或机密客户端与 PKCE
让客户端能力与秘密保存位置相匹配
浏览器和移动应用属于公开客户端,无法安全保存 client secret,应使用授权码流程与 PKCE;能保护秘密的服务器应用才适合机密客户端。选择前记录代码运行位置、是否可安全保存秘密、回调地址、允许的响应类型和令牌存储策略。不要因为项目有一个后端就让前端直接携带机密客户端秘密。
PKCE 的 verifier 只在授权事务中使用,challenge 在授权请求中传递,回调时先核对 state、nonce、issuer、client_id 和 redirect URI,再交换 code。每个浏览器标签页有自己的事务记录,不能用一个全局 verifier 覆盖并发登录。授权请求中的 scope 与应用实际需要一致,拒绝未经说明的额外范围。 相关背景可参阅 Shopify 店铺速度优化手册。
失败案例是回调端只检查 code,不检查 state,攻击者把另一浏览器的授权结果注入当前会话。回退时丢弃 code、清除临时事务并要求重新登录,不把错误 code 换成令牌。若 PKCE verifier 丢失,不能退回无 PKCE 的交换,也不能把 code 写入日志。
认证流程的每个输入都要有过期时间和一次性语义。使用客户认证指南核对官方示例的边界,再对自己的跨域 cookie、移动深链和浏览器返回路径做测试。公钥配置可公开,secret、verifier、code 和 token 只能进入受限存储。
设计授权回调状态
把回调绑定到一次明确的浏览器事务
state 不是装饰字段,而是把授权回应绑定到发起请求的浏览器、店铺、语言区域、客户端和回调路径的关联值。服务端或受保护的会话存储保存 state、PKCE verifier、创建时刻和预期 issuer;回调收到后做常量时间比较并立即作废。state 不应包含邮箱、订单号或可读个人资料。
多标签页登录、返回键、网络重试和移动端应用切换都会产生旧回调。每个事务独立保存,旧 state 只能得到可解释的过期错误。回调地址采用固定 allow-list,不从 return_to 参数拼接任意主机。成功交换后只把最小身份结果放进应用会话,原始 code 和 verifier 立即删除。 相关背景可参阅 Shopify 页面构建工具选型。
失败案例是把 state 放在可被第三方脚本读取的长生命周期 cookie,并允许任意 return_to;攻击者能窃取关联值并引导用户离开可信域。回退时清除 cookie 和临时状态,回到固定账户入口,不继续消费可疑 code。不要为了减少登录步骤而延长 state 有效期。
回调验收表至少覆盖正常返回、用户取消、code 重复、state 不匹配、issuer 不匹配、PKCE 不匹配和授权服务器超时。每个分支都应告诉买家下一步,同时不泄漏内部令牌或账户是否存在。错误日志记录事务摘要和失败分类,不记录完整查询字符串或 authorization code。
| 流程输入 | 必须保存 | 成功条件 | 失败回退 |
|---|---|---|---|
| 发现 | issuer、端点摘要、读取时刻 | 主机与店铺匹配 | 清缓存并重新发现 |
| 授权 | state、PKCE verifier、nonce | 一次性匹配并交换 | 丢弃 code,重新认证 |
| 刷新 | 过期时间、锁、结果摘要 | 一次受控刷新 | 清会话并提示登录 |
| 登出 | 会话、令牌、缓存引用 | 当前与其他标签页失效 | 回到匿名状态 |
管理 access token 生命周期
把短期访问和长期恢复分开
access token 是给 Customer Account API 使用的短期凭证,应用应读取 expires_in 或等价的过期信息并提前安排刷新。公开客户端是否获得 refresh token、刷新方式和轮换规则必须以当前官方参考为准,不能套用另一个 OAuth 服务的假设。令牌存储要绑定客户端、店铺和买家会话,不能放进页面 HTML、分析事件或普通日志。
请求层在发送前检查过期时间,遇到令牌失效只允许一次受控刷新,然后重试原始 GraphQL 请求;刷新失败就清除会话并要求重新认证。并发请求要有单飞锁,防止多个标签页同时刷新并互相覆盖新令牌。刷新记录保存 token family 的非敏感摘要、结果和时间,不保存原文。 相关背景可参阅 Shopify Flow 自动化教程。
失败案例是把 access token 当成永久登录凭证,页面缓存一夜后继续请求,服务器返回未授权,前端却显示旧的客户订单。回退时清除令牌和本地账户状态,只保留公开目录与购物车;不要用旧 token 重试多次或把它换成 Admin token。若怀疑令牌泄露,按会话和客户端撤销并检查访问日志。
官方参考中的 confidential/public client 差异、expires_in 和 refresh-token 行为要写进自己的会话契约。用时间向前跳、刷新响应缺字段、刷新令牌轮换和两个标签页并发四组夹具验证。过期不是异常噪声,而是正常状态;界面应能保留不敏感购物上下文,同时明确提示账户操作需要重新认证。
会话 cookie、登出与多标签页
只让当前浏览器和当前店铺看到账户状态
应用会话 cookie 只保存不可读的会话引用或经过保护的最小状态,设置合适的 Secure、HttpOnly、SameSite 和路径范围。它不能代替 Customer Account API 令牌,也不能跨店铺复用。服务端从会话引用加载店铺、客户 subject、令牌状态和创建时间,前端只得到页面所需的非敏感结果。
登出需要撤销或清理令牌、服务端会话、刷新锁和本地账户缓存,并通知其他标签页刷新显示。多标签页同时刷新时,旧会话收到未授权应回到匿名状态,不得把另一客户的结果留在内存。缓存键至少区分店铺、客户 subject、语言区域和资源类型,不能只用 URL。
失败案例是共享 cookie 的 domain 和缓存键覆盖多个商店,买家从 A 店切到 B 店后看到 A 店的账户菜单。回退时删除跨店 cookie 和缓存,重新走当前店铺发现与认证;在确认隔离前只显示公开商品。不要用前端隐藏元素掩盖仍在网络响应中的个人数据。
用登录、登出、过期、返回键、重复标签页、隐身窗口和店铺切换演练。检查浏览器开发者工具、服务端会话表和 GraphQL 请求是否一致。涉及主题交互时,可参考Shopify 主题性能与实现实践,但账户隔离必须在服务端和缓存层都成立。
| GraphQL 结果 | 解释 | 页面动作 | 记录内容 |
|---|---|---|---|
| 网络失败 | 未知是否到达 | 稍后重试 | trace、时间、重试次数 |
| HTTP 错误 | 传输失败 | 按错误类型提示 | 状态与端点摘要 |
| 200 + errors | 业务未必成功 | 按路径处理 | operation、错误路径 |
| data + userErrors | 部分结果 | 保留旧值并修正 | 非敏感摘要、版本 |
最小权限与受保护客户数据
先证明用途,再请求字段
Customer Account API 的账户数据属于买家身份范围,客户对象还可能受保护客户数据要求约束。为每个页面写数据用途、字段、保存期限和删除路径,只请求完成当前动作所需的选择集。订单历史、地址、姓名和联系方式不要因为“以后可能有用”就一起读取;减少字段也能降低日志、缓存和错误响应的暴露面。
权限评审应从匿名页面、已认证页面、账户管理和支持人员工具分别开始。GraphQL fragment 要跟着用途命名,禁止一个巨型 fragment 被所有组件复用。服务端过滤字段、日志和缓存,不能只依赖前端不展示。用客户对象参考核对所需权限和字段状态,保存评审依据与查询版本。
失败案例是账户页面查询了完整 Customer 对象并把响应写入分析平台,买家关闭页面后资料仍在第三方日志。回退时停止宽查询,删除不必要缓存和日志,改用最小字段;需要历史资料的支持流程走受控工具。不要通过复制数据到新的表来规避权限审计。
Customer 对象参考说明客户字段和权限边界。验收应包含无权限、部分权限、字段为空、买家已删除和请求被拒绝,且页面仍能显示清晰的匿名状态。把隐私删除、访问请求和保留期作为账户功能的一部分,而不是事后补丁。
GraphQL 查询契约与错误分类
同时检查 HTTP、GraphQL 和业务状态
Customer Account API 只提供 GraphQL 语义,客户端必须区分网络错误、HTTP 错误、GraphQL errors、data 为空和业务字段状态。响应 200 只能说明传输层完成,不能说明 mutation 成功。每个 query 和 mutation 定义必需字段、可空字段、错误码映射和页面回退;不要把任何 errors 数组都显示成“没有订单”。
请求层保留 operation name、变量摘要、响应状态、错误路径和 trace reference,不保留令牌或个人字段。schema 更新后先检查 fragment、enum、输入校验和 mutation payload,再运行旧会话和过期会话夹具。遇到未知字段或类型错误时停止该功能分支,显示当前账户仍可使用的安全部分。
失败案例是 mutation 返回 200,同时 userErrors 表示地址校验失败,前端只判断 HTTP 状态并显示保存成功。回退时读取 payload 与 userErrors,保持旧地址可见并要求用户修正输入,不重复提交。若 data 和 errors 同时存在,按字段路径决定哪些结果可采用,不能把部分成功扩大成全局成功。
把错误分类映射到行动:重新认证、修正输入、稍后重试、联系支持、匿名继续或停止写入。对 schema 版本和 API 版本都记录在请求上下文中,方便把文档差异与运行错误区分。页面链接到Shopify 页面构建实践时,仍要让账户状态来自真实 GraphQL 结果。
| 隐私事件 | 立即动作 | 待确认内容 | 禁止做法 |
|---|---|---|---|
| 撤回 | 停止非必要读写 | 外部服务结果 | 复制到新表 |
| 删除 | 清令牌、会话、缓存 | 备份保留期 | 用空对象冒充完成 |
| 访问请求 | 最小导出 | 目的与期限 | 导出完整日志 |
| 应用注销 | 盘点所有存储 | 审计证明 | 保留无用途副本 |
变更、幂等与冲突恢复
让账户 mutation 能安全面对重复和并发
账户资料更新、地址变更和偏好保存都要先定义幂等键、版本条件和重复提交结果。客户端生成一次动作引用,服务端把它绑定到客户 subject、店铺和资源;同一引用再次到达时返回已有结果或明确冲突。不要用浏览器时间戳单独作为幂等键,也不要让一次刷新重复创建同一资源。
两个标签页同时修改资料时,服务端需要以版本、更新时间或业务冲突规则决定胜者,并把被拒绝的字段返回给用户重新确认。读到旧缓存后再写入,可能覆盖另一处更新;写前可读取当前资源,写后再查询确认。幂等记录只保存必要摘要,既能查重复,又不会成为客户数据副本。
失败案例是保存按钮被点击两次,第一次 mutation 成功但响应超时,第二次没有幂等键又创建了第二条地址。回退时停止自动重试,查询当前地址并让用户选择保留项;不要按创建时间盲删,因为两个地址可能都有效。对无法判定的冲突进入人工核对。
冲突夹具应覆盖超时后重试、相同动作引用、不同引用同资源、版本过期、字段校验失败和权限变化。记录 mutation payload 的非敏感摘要、响应 userErrors 和最终资源版本。账户变更不是 webhook 可靠性问题的复制品;它需要买家可理解的确认和可恢复的当前状态。
市场与语言上下文
让区域上下文改变展示而不改变身份
买家账户的身份由当前店铺和认证 subject 确定,语言、货币、市场或地区只是展示和业务上下文。发现授权地址时按官方支持的市场上下文生成请求,不能把 locale 拼进账户端点主机。会话键和令牌始终绑定店铺与客户 subject;区域变化应更新文案和可用字段,而不是切换到另一客户。
登录前后的语言切换、多域名、国家选择和浏览器自动翻译都可能改变页面路径。把 locale 作为安全的显示参数保存,重新构造授权 URL 时仍验证固定 issuer、client_id 和回调。对地址格式、可用字段和时间格式设定区域规则,错误时保留账户身份并提示修正,不把数据复制到新的区域账户。
失败案例是缓存只按客户 ID 建键,客户从英文路径切换到中文路径后拿到另一市场的旧页面片段,甚至混入另一店铺的地址标签。回退时清空语言与区域缓存,重新读取当前店铺和 subject 的数据;身份不明时只显示公开内容。不要把市场名称当作唯一客户身份。
区域验收覆盖授权回调、会话刷新、登出、账户菜单、订单列表、地址表单和错误提示。将Shopify Flow 自动化实践作为店铺运营背景即可,账户 API 的权限和令牌仍按 Customer Account API 官方契约校验。
| 回退层 | 可缩小到 | 保留证据 | 触发条件 |
|---|---|---|---|
| 端点 | 重新发现当前店铺 | issuer 摘要 | 硬编码或主机不匹配 |
| 会话 | 清理并重新认证 | 审计与匿名购物 | 跨店或过期循环 |
| GraphQL | 停止单个 mutation | 当前资源版本 | 200 但有错误 |
| 页面 | 匿名目录与购物车 | 不敏感上下文 | 客户主体不明 |
隐私删除、撤回与审计
让账户数据的生命周期可证明
账户功能不仅要能读取和修改,还要能处理买家撤回同意、删除账户、数据访问和应用注销。先列出 Customer Account API 返回的数据、应用本地会话、缓存、日志和备份,再为每一类定义删除或去标识动作。审计记录只保留证明动作所需的摘要、时间、主体引用和结果,不复制完整个人资料。
撤回或删除开始后,阻止新的非必要读取和写入,清除访问令牌、会话 cookie、缓存和待处理 mutation。正在执行的请求要检查会话状态,已排队任务按客户 subject 过滤。删除结果要区分已清理、等待备份周期、外部服务需确认和无法确认;支持人员不能用导出文件绕过删除流程。
失败案例是清理任务按客户邮箱匹配,邮箱改变后留下旧会话和地址缓存;任务还把空值写回账户,覆盖了仍需保留的业务审计。回退时停止自动清理,重新用稳定 subject 和会话索引盘点,保留审计证据并逐项确认。不要用一个空对象代表已删除全部数据。
审计验收包括正常删除、令牌已过期、网络中断、重复请求、部分服务成功、备份等待和买家再次登录。每一项都要能说明当前可读范围和下一步。官方API 版本说明提醒 Customer Account API 也遵循版本节奏,因此隐私流程要记录 API 版本和 schema 依据。
失败案例与可用性回退
在身份不明时仍保护公开购物路径
案例一:应用硬编码 GraphQL 端点,店铺迁移后发现文档返回新地址,查询全部失败。先验证 issuer 和发现文档,再清空过期缓存;恢复前账户区显示重新认证或稍后再试,目录和购物车仍按匿名路径可用。案例二:公开客户端假定一定有 refresh token,令牌过期后一直循环刷新;清除会话并回到认证入口。
案例三:共享 cookie 让两个店铺的账户菜单串联,先按店铺和 subject 失效会话与缓存,再核对日志中是否有跨店响应。案例四:GraphQL 返回 200 和 userErrors,界面却显示保存成功;保留旧值,展示可修正字段并避免重写。案例五:清理任务把空资料写回账户,先暂停任务,使用审计和当前 API 状态区分真正删除与写入错误。 前端组件不应直接拼授权 URL、读 token 或解释全部 GraphQL 响应。服务端提供窄接口,例如 anonymous、authenticated、reauth_required、forbidden、retryable 和 conflict,并附带页面需要的最小字段。账户数据的获取、令牌刷新、错误映射和审计留在受保护服务,主题只负责显示状态、提交用户输入和保持可访问的导航。 接口契约写出缓存控制、响应字段、错误码、会话变化和重试上限。任何返回账户资料的响应都带上店铺和 subject 的内部关联,避免通用缓存误复用。表单提交先取得动作引用,服务端验证当前会话和字段,再返回可渲染的结果。不要让一个万能 JSON 让不同组件自行猜测权限。 失败案例是一个主题 section 在浏览器端保存账户 token,为了显示问候语又把完整 customer 对象写入 data 属性。回退时移除 token 和完整对象,只保留服务端返回的显示名片段或匿名问候;重新认证通过固定入口完成。若窄接口缺字段,扩展接口契约并做权限评审,不把原始响应直接透出。 把Shopify 主题二次开发与 Liquid 指南用于页面层协作,把账户 API 作为独立的身份边界。验收接口时同时看网络响应、缓存头、日志关联和键盘可达性,确保安全收敛没有变成无法使用的空白区域。 每次 Customer Account API 版本、schema、发现文档、客户端配置、cookie 策略、令牌存储或页面组件变化,都重跑端点发现、PKCE、state、过期、刷新、登出、跨店隔离、权限、GraphQL errors、mutation 幂等和隐私删除。固定商店、浏览器、语言和账户夹具,保存输入摘要、响应状态和当前账户事实。 清单的证据包括发现响应摘要、issuer 校验、回调事务、令牌生命周期记录、会话 cookie 属性、GraphQL operation、schema 错误分类、缓存键、删除审计和回退截图。不要只看登录按钮变绿;需要证明失效时停在哪里、恢复后保留什么、匿名路径是否仍清晰。使用Shopify 页面性能手册可补充页面证据,但不能替代身份和隐私证据。 失败案例是只在一个浏览器完成登录,就假定多标签页、移动回调和过期 token 都正确;问题直到买家看见旧账户资料才暴露。回退时关闭账户私有区域,保留匿名目录和购物车,重新跑会话隔离夹具。若版本行为与文档不一致,记录 API 版本和发现结果,暂停扩展字段而不是复制旧 schema。 日常巡检抽查匿名、刚认证、即将过期、已过期、登出、跨店和删除中的账户,确认每个状态都有清晰页面和服务端日志。按店铺和 subject 做关联,不按邮箱或页面标题做身份。最终目标是任何账户数据都能回答“谁、通过哪一会话、按哪个版本、在何时、以什么权限读取或改变”。
可用性回退不是绕过身份,而是缩小功能面:匿名浏览、公开商品、购物车和明确的登录入口可以继续;账户订单、地址、个人资料和 mutation 在没有有效会话时停止。缓存层、前端组件和服务端接口要使用同一个匿名状态,不能只隐藏按钮却继续发送受保护请求。
恢复条件包括发现端点可信、issuer 匹配、token 生命周期可读、GraphQL 错误可分类、缓存已隔离、删除队列已盘点和抽样账户结果正确。用固定会话夹具验证过期、登出、跨店切换和部分响应,再逐步恢复单个账户功能。任何无法证明主体的响应都回到匿名体验。
常见问题
Customer Account API 能否直接使用 Admin API 令牌?
不能。Customer Account API 的调用应绑定买家会话和相应客户范围,Admin API 令牌代表店铺级权限,放入浏览器会造成严重边界错误。公开目录等无账户内容可走公开接口,账户数据必须走受保护认证。
发现端点为什么不能写死?
账户端点属于店铺上下文,发现文档提供授权、令牌和 GraphQL 地址。店铺迁移、域名或版本变化都可能使旧地址失效;应用应验证 issuer 与店铺匹配,并在失败时清缓存、重新发现或回到匿名体验。
access token 过期时是否一直刷新?
不应无限刷新。请求层可执行一次受控刷新,刷新失败就清除会话并提示重新认证。并发标签页要使用单飞锁,避免互相覆盖令牌;旧令牌不能改作店铺令牌。
GraphQL 返回 200 是否代表保存成功?
不是。还要检查 errors、userErrors、data、字段路径和业务状态。保存失败时保留当前值、指出可修正字段,并避免重复 mutation;只有 payload 明确表示成功才更新页面状态。
账户功能故障时如何保持可用?
缩小到匿名商品、购物车和固定登录入口,暂停订单、地址、个人资料和 mutation。清除过期会话与私有缓存,重新验证发现、issuer、令牌生命周期和 GraphQL 错误分类后,再按功能恢复。