Front-End-Checklist 实战:按 GDPR 第 17 条实现用户数据删除机制(Right to Erasure)
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
本文以 Front-End-Checklist 仓库中的 right-to-erasure 规则文档(references/rule.md 与 SKILL.md)为主体,系统讲解面向用户的数据删除机制应如何设计:从账户设置里的删除入口、两步确认交互,到
localStorage/sessionStorage/IndexedDB/Cookie/Service Worker 缓存的全面清理,再到服务端删除 API 与 30 天期限的落地。读完本文,你将能依据 GDPR Article 17 在自己的前端应用中实现一套完整、可审计、可通过验证清单的自助删除流程。
一、什么是"被遗忘权":GDPR 第 17 条为什么关乎前端
被遗忘权(Right to erasure / Right to be forgotten)是 GDPR(《通用数据保护条例》)第 17 条赋予欧盟境内用户的核心权利:当个人数据不再为收集目的所必需,或用户撤回同意时,数据控制者(controller)必须应请求删除其个人数据,除非存在继续处理的法定理由。
对前端应用而言,这一义务天然地分为两个维度:
- 清除全部客户端存储——浏览器里散落的
localStorage、sessionStorage、IndexedDB、Cookie、Service Worker 缓存; - 触发服务端删除——向删除 API 发起请求,让服务器在 30 天内完成数据清除。
原文档特别强调了一个合规动机:不履行这一权利可能招致最高2000 万欧元或全球年营业额 4%的监管罚款。与此同时,一个清晰、可发现的删除流程本身就是建立用户信任的关键体验,属于隐私质量检查清单中的核心项。
在仓库中,这条规则被编排在 packages/content/rules/en/privacy/right-to-erasure.mdx,归属于privacy分类下的data-rights子类,优先级 medium、难度 intermediate、预估耗时 60 分钟;同时它也是 Privacy & Consent 检查清单 的组成规则之一,并在 README.md 的整体清单中作为可勾选项出现。
二、入口设计与两步确认:删除选项必须可被发现
删除机制的第一步是"用户能找到它"。原文档给出的设计原则是:把删除入口放在账户设置(Account Settings)中,并置于诸如 "Privacy" 或 "Your data" 这样语义清晰的分区之下;同时,为避免误触导致数据被意外清除,必须采用两步确认模式——用户先点击入口进入确认态,再显式点击确认按钮才真正执行。
// DeletionRequestButton.tsx import { useState } from 'react'; import { clearUserData } from '@/lib/privacy'; type DeletionState = 'idle' | 'confirming' | 'pending' | 'done' | 'error'; export function DeletionRequestButton() { const [state, setState] = useState<DeletionState>('idle'); async function handleConfirm() { setState('pending'); try { // 1. Clear all client-side storage immediately clearUserData(); // 2. Send deletion request to the server const response = await fetch('/api/account/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); if (!response.ok) throw new Error('Server deletion request failed'); setState('done'); } catch { setState('error'); } } if (state === 'done') { return ( <p role="status"> Your deletion request has been received. Your data will be removed within 30 days. You have been signed out. </p> ); } if (state === 'confirming') { return ( <div role="alertdialog" aria-labelledby="delete-confirm-title"> <h2 id="delete-confirm-title">Delete your account and all data?</h2> <p>This action cannot be undone. You will be signed out immediately.</p> <button onClick={handleConfirm} disabled={state === 'pending'}> {state === 'pending' ? 'Deleting…' : 'Yes, delete my data'} </button> <button onClick={() => setState('idle')}>Cancel</button> </div> ); } return ( <button onClick={() => setState('confirming')}> Delete my account and data </button> ); }这个组件用状态机(idle → confirming → pending → done / error)管理整个删除流程,有三处值得注意的细节:
- 确认对话框使用
role="alertdialog"与aria-labelledby:这不是普通的弹窗,而是语义化的模态告警,屏幕阅读器用户能明确感知"这是需要决断的删除操作"; - 完成态使用
role="status":删除请求受理后,辅助技术可以播报这一状态变化; pending状态禁用按钮:防止重复提交,同时按钮文案切换为 "Deleting…" 给予即时反馈。
从源码结构看,原文档在 right-to-erasure.mdx 中为该示例补全了import与组件导出,删除了引用文档中的残缺片段,使其成为可直接编译运行的完整组件。
三、必须删除什么:数据存放位置的完整清单
一次合规的删除必须覆盖所有可能存放个人数据的位置。原文档给出了一张"存放位置 → 清理 API"的对照表,这是实施时逐项核对的地图:
| 存放位置 | 清理 API |
|---|---|
localStorage | localStorage.clear()或按 key 调用removeItem |
sessionStorage | sessionStorage.clear() |
| IndexedDB | indexedDB.deleteDatabase(name) |
| Cookies | 将每个 Cookie 设置为Max-Age=0; expires=Thu, 01 Jan 1970 |
| Service Worker 缓存 | caches.delete(cacheName) |
| 服务端数据 | 向删除 API 发起 DELETE / POST 请求 |
值得注意的是,这张表把"服务端数据"也并列纳入——客户端清理只是删除机制的一半,任何只做了前端清理、却把个人数据留在服务器上的实现,都不能算作满足 GDPR 第 17 条。
四、clearUserData():把客户端清理收敛为一个可审计函数
原文档强烈建议把客户端所有存储机制的清理逻辑集中到一个clearUserData()函数中。集中化的收益是可审计、可扩展:审查者只需查看这一个函数,就能确认所有存储位置都被覆盖,未来新增存储类型时也只需在此扩展。
// lib/privacy.ts /** * Removes all personal data from client-side storage. * Call this immediately when a deletion request is confirmed — do not wait * for the server response, as the user has already expressed intent to delete. */ export async function clearUserData(): Promise<void> { // 1. localStorage localStorage.clear(); // 2. sessionStorage sessionStorage.clear(); // 3. IndexedDB — delete every database the application has created const databases = await indexedDB.databases(); await Promise.all( databases.map((db) => { if (!db.name) return Promise.resolve(); return new Promise<void>((resolve, reject) => { const req = indexedDB.deleteDatabase(db.name!); req.onsuccess = () => resolve(); req.onerror = () => reject(req.error); }); }) ); // 4. Cookies — clear known application cookies const cookiesToDelete = ['session', 'refresh_token', 'user_prefs', '__stripe_mid']; for (const name of cookiesToDelete) { document.cookie = `${name}=; Max-Age=0; path=/; SameSite=Lax`; } // 5. Cache Storage (Service Worker caches) if ('caches' in window) { const cacheNames = await caches.keys(); await Promise.all(cacheNames.map((name) => caches.delete(name))); } }几个需要展开的实现细节:
- IndexedDB 的删除是异步且逐个进行的:先通过
indexedDB.databases()枚举所有数据库(注意该 API 可能返回空name的占位项,需要过滤),再用Promise包装deleteDatabase的成功/失败回调,最后以Promise.all并行等待全部完成。deleteDatabase返回的是IDBOpenDBRequest,其onsuccess/onerror是回调式 API,必须手动转成 Promise 才能与async/await协作。 - Cookie 无法枚举,只能按已知名单逐个清除:
document.cookie只能写入而不能读取所有 Cookie 的完整列表(HttpOnly的 Cookie 更是不可见),因此需要一个cookiesToDelete应用白名单。示例中给出的session、refresh_token、user_prefs、__stripe_mid覆盖了会话、令牌、偏好与第三方支付标记四类典型场景,实际项目中应替换为应用真实使用的 Cookie 名单。 - Service Worker 缓存需要能力检测:
'caches' in window的守卫确保在不支持 Cache Storage API 的环境中不抛错;caches.keys()返回所有缓存名,逐个caches.delete()后同样用Promise.all并行处理。
原文档还给出了一条重要的时序纪律(在 right-to-erasure.mdx 中以 Warning 形式强调):
不要等服务端响应后再清理客户端数据。应在确认删除意图后立即清除本地数据。即使网络请求失败,用户的本地数据也已消失——这是正确的行为。服务端调用的重试与本地清理是两件独立的事。
五、服务端删除 API:接受请求并纳入 30 天窗口
客户端清理只解决了浏览器侧,服务端必须接收删除请求,并在 GDPR 规定的 30 天内异步完成处理。原文档给出的 Next.js App Router 路由实现如下:
// app/api/account/delete/route.ts (Next.js App Router) import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; export async function POST() { const session = await getServerSession(); if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthenticated' }, { status: 401 }); } // Queue deletion — process asynchronously within 30 days await scheduleDeletion({ userId: session.user.id, requestedAt: new Date().toISOString(), // GDPR deadline: 30 calendar days from request deadline: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), }); // Send confirmation email to the address on record before it is deleted await sendDeletionConfirmationEmail(session.user.email); return NextResponse.json({ received: true }); }实现要点拆解:
- 身份校验先行:
getServerSession()拿不到会话(未登录)时立即返回401,删除接口必须拒绝匿名调用,否则会成为任意用户的删除攻击面; - 采用"排队 + 异步处理"而非同步删除:
scheduleDeletion()把删除任务连同requestedAt与计算好的deadline(Date.now() + 30 * 24 * 60 * 60 * 1000,即 30 个日历日)写入队列,由后台任务在窗口内完成删除。这样即便数据量大、涉及多个系统,也不会阻塞用户请求; - 在数据被删前发送确认邮件:
sendDeletionConfirmationEmail(session.user.email)必须在删除发生之前调用——这是唯一仍能联系到用户的时机,同时也为用户留存了受理凭证; - 幂等的受理响应:
{ received: true }表示"已受理"而非"已删除完成",语义上准确地区分了请求接收与最终执行。
六、确认与时间线沟通:让用户知道接下来会发生什么
删除请求提交后,原文档要求向用户明确传达四类信息:
- 请求已被受理(request has been received);
- 数据将在什么期限内被移除(例如 "within 30 days");
- 用户已从所有设备登出(signed out of all devices);
- 一个可供留存记录的参考号(reference number,如果系统支持)。
这四项沟通内容其实也是前文DeletionRequestButton完成态文案("Your deletion request has been received. Your data will be removed within 30 days. You have been signed out.")的落点:受理确认 + 30 天期限 + 登出提示,三者缺一不可。
第三方数据处理者:删除请求必须转发
原文档以 Warning 形式单独强调了第三方处理器问题:应用很可能把个人数据传递给分析(analytics)、CRM、邮件工具等第三方处理器,删除请求必须同步转发给这些处理器。每个处理器的 API 都应检查其删除端点,并把相应调用纳入删除工作流。常见做法是维护一张"处理器 → 删除端点"的清单,在scheduleDeletion的后台任务中逐一遍历调用——这与前文"每个处理器 API 检查删除端点"的建议互为表里。
七、与关联规则的协作:数据最小化是删除的"减负"
在规则体系中,right-to-erasure 并非孤立存在。right-to-erasure.mdx 的 frontmatter 声明了三条关联规则,其中与删除机制最互补的是 contenteditable="false">【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考