1. 需求拆解:用户要的是“分享”,不是“公开”
1.1 先说清楚这个需求从哪来
3月中旬接了一个需求,原型页面上写得很简单:用户A可以把手机号分享给用户B,B能看到A的联系方式并主动联系。产品经理的原话是“就一个分享功能,开发量不大”。但做过这类需求的人都知道,手机号在数据合规和用户隐私里的敏感级别是整个用户体系里最高的一档,越简单的描述背后往往藏着越多的隐性要求。
我花了大概半天时间把需求重新梳理了一遍,核心问题其实只有三个:分享给谁、能看多久、能不能取消。现场原型只定义了“用户A主动分享给用户B”,但“能看多久”没写,“B能不能再转发给别人”没写,“A想撤回怎么办”也没写。这些都是必须第一时间向产品确认的边界条件,否则到了开发后期再改,接口设计、页面状态、权限校验全部要动,成本完全不一样。
这类需求本质上是在解决一个信任传递的问题:A信任B,想把联系方式留给B,但A不信任平台以外的任何渠道。所以产品上要做的不是简单展示一个明文手机号,而是让A能控制这个联系方式的生效窗口和可见范围。需求评审会上我建议加上了三个默认策略:分享有效期默认为24小时、B查看联系方式需要点击“获取号码”动作、A可以随时主动撤回。产品经理一开始觉得多余,但实际用下来这三个点恰好是后期被用户反馈最多的正向体验。
1.2 直接明文展示的问题在哪里
很多产品直觉上认为,既然用户都愿意分享了,直接给B展示明文号码是最顺滑的交互。这个思路没有从数据生命周期的角度想问题:手机号一旦以明文形式出现在用户聊天记录、通知栏或者订单备注里,它就不受你控制了。
我在这个项目里给团队做了一个简单的数据暴露面分析:手机号明文状态每多停留一秒,就多一层被截图、转发、爬取的风险。就算A信任B,也没法保证B的手机不被偷看,B不会手滑转发给群聊。从业务设计角度,手机号这种高价值、强身份关联的数据,必须走“按需可见”的路线,在用户主动执行某个操作(比如点击“查看号码”)时才把完整明文暴露出来,并且最好给它加一层短时效的上下文限制。
另外还有一个容易被忽略的问题:明文展示也会导致平台侧的存储风险。如果接口返回了完整手机号,那么后端日志、数据库binlog、监控系统的error body里都可能被动带上这份明文数据,这些链路往往不在业务开发的掌控范围内。所以从源头做限制——能不返回明文就不返回明文——是性价比最高的隐私保护措施。这个原则后来也直接用在了接口设计里:默认字段永远返回脱敏号码,只有带有效token的请求才返回明文。
1.3 先想清楚“分享”和“公开”的差异
产品文案里把功能叫做“分享手机号”,但我坚持在需求文档里把它描述成“有条件地授权展示联系方式”。这两个叫法的区别不是咬文嚼字,而是决定了你要不要做“取消授权”和“有效期控制”。
公开的意思是没有反向操作,数据一旦放出去就收不回来。分享则是用户主观行为,天然具备撤回的心理预期。用户列表里有个“我分享出去的号码”入口,A随时可以点进去看到自己给哪些人开过联系方式权限,一键停用。这个能力在实现上不复杂,本质上就是维护一个分享关系的状态字段,但它对用户安全感的建立非常关键。
我比较推荐的做法是:每次分享生成一条独立的授权记录,记录里包含授权码、被授权人ID、创建时间、过期时间、状态(有效/撤回/已过期)。所有查询接口都必须带着这条授权的状态做过滤,而不是只靠手机号本身去查。这样即便将来账号体系发生变化,授权数据依然可以独立审计,出问题的时候能快速定位是谁、什么时候、通过什么方式看到了谁的号码。
2. 技术选型:脱敏、虚拟号、授权后查看,到底选哪个
2.1 三种常见方案的对比
日常开发里处理隐私手机号分享,不外乎三条技术路线:脱敏显示后置查看、虚拟中间号(AXB)、授权后查看明文。我把它们的差异整理成了一张对比表,方便做方案决策时直接参考:
| 方案 | 实现成本 | 用户体验 | 隐私保护强度 | 适用场景 |
|---|---|---|---|---|
| 脱敏显示(138****8000) | 低,前后端各处理一字段 | 一般,用户需要额外操作查看完整号码 | 中,脱敏隐藏了大部分信息但剩余位数仍有被猜测风险 | 列表展示、好友申请、通知卡片 |
| AXB虚拟中间号 | 高,需要引入语音/短信能力 | 较好,双方通过中间号通话,互相看不到真实号 | 高,真实号码完全不暴露 | 交易撮合、打车、外卖等实时沟通场景 |
| 授权后查看明文 | 中,核心是令牌与有效期控制 | 好,用户主动点击后立即看到完整号码 | 高,只要授权记录可撤销、令牌有过期时间 | 熟人分享、订单联系、客服回拨 |
这个项目选的是第三种,原因是业务本身以异步联系为主,分享人和接收人不需要实时通话。AXB方案虽然保护强度最高,但接入成本也在那摆着:如果你没有现成的虚拟号供应商,商务对接、号码池配置、通话录音合规这些环节够你忙一个季度的。脱敏显示方案倒是便宜,但你做的是一个用户主动分享的场景,最终总要给B看完整号码,脱敏只解决展示过程的中间态问题,解决不了最终披露的问题。
2.2 授权后查看的权限模型
授权后查看的关键在于把“你能看”和“你正在看”这两个动作分开。系统初始只告诉B“A为你分享了一个联系方式”,此时B看到的是掩码。B必须点击一次“获取号码”按钮,带着点击时生成的短期令牌去请求明细接口,才拿到明文。
这个模型里我踩过最重要的一个坑:令牌必须是短期的、一次性使用的。项目第一版图省事,明文号码接口用的是一个长期有效的分享链接作为凭证,结果用户把链接转发给第三人,第三人也能打开。后来改成二段式:分享关系ID加上一次性token,token有效期两分钟,使用后立即作废,同一个token不能重复请求明文。实测下来副作用很小,用户从点击到看到号码基本在两秒内完成,几乎感知不到有效期限制的存在,而安全性却提高了一个量级。
权限模型具体设计我会在后面接口章节详细展开,这里先提一个容易漏的字段:授权记录必须保留“撤回时间”和“撤回触发者”。因为产品计划做一个“我分享的号码”管理页,列表上所有记录都是动态状态,A可以随时撤回。如果撤回状态没有落库,前端只能靠过期时间推断,用户看到的往往是过时信息。
2.3 什么时候才需要引入AXB虚拟号
如果你的业务场景是双方要在不见面的情况下完成实时沟通——典型场景比如二手交易、拼车、上门服务——那脱敏加授权查看的玩法就不够了。因为用户直接把自己真实号码交给陌生人,一旦后续产生纠纷,号码被拿去怎么用你完全无法约束。这时候该上AXB虚拟号。
AXB的原理很好理解:运营商提供一个中间号码X,A打电话给X,X再转接给B。A和B彼此看到的都是X,永远不会触及对方的真实号码。每次通话行为都在后台留有记录,订单结束或者用户投诉后,平台可以随时解绑X与AB的关系,双方就再也联系不上了。
从成本角度,AXB适合订单生命周期短、交流次数有限、且沟通双方信任基础薄的业务。如果你做的产品是陌生人社交或者闲置交易,别犹豫,直接规划虚拟号能力。但如果你做的只是熟人场景,比如用户给好友留个联系方式方便后续当面碰头,用授权查看就够了,不必为了“专业感”硬上虚拟号,然后每个月为号码池账单头疼。
2.4 方案选型时容易被忽略的合规维度
除了技术成本和用户体验,还有一个维度必须在选型阶段就拉进来:数据合规要求。手机号在国内属于个人敏感信息,处理起来需要遵循“告知-同意”的基本链条。A主动分享这个动作本身就是第一层授权,但作为平台方,你还得考虑要不要在A提交分享时弹一次确认告知,说明“你的号码将对B可见,且仅在X小时内有效”。这个确认动作在合规视角看不是摆设,它从根本上改变了平台的数据处理依据——不再是平台自行使用数据,而是用户主动发起的数据转移。
我做这个需求的时候专门拉了一次合规评审,结论是两点:其一,分享页面上必须明示有效期和可撤回的能力;其二,后台保存的明文号码日志需要按需保留,默认30天后自动清理。这两条加进去之后,整个功能的合规链路才算闭环。很多开发者在做技术规划时觉得法务参与是走形式,但隐私设计这个东西,等到用户投诉或者监管抽查时再补救,成本是天壤之别。
3. 开发实战中的Battle实录:产品、后端、测试怎么过招
3.1 和产品经理Battle:有效期到底设多长
需求会议上最激烈的一轮交锋发生在有效期设置上。产品经理拿竞品截图说“人家是永久有效的”,理由是用户愿意分享就说明信任对方,设置有效期会让用户觉得限制太多。我的意见是默认24小时,理由有两条:第一,用户当前在聊天窗口里发起分享,目标是让对方“现在”能够联系到自己,而不是让对方囤积一个号码作为长期资产;第二,从App活跃度看,用户主动发起分享和对方实际查看之间的时间差通常集中在几小时内,24小时足够覆盖绝大多数场景。
后来我们用了一个折中方案,默认24小时但允许用户在分享时手动切到7天,绝不提供永久选项。这个细节在灰度期被验证是合理的:大约90%的用户留在默认24小时,7天选项的使用率约为7%,剩余3%的用户压根没主动修改过设置。产品上对永久授权的执念,本质上还是没想清楚一个朴素的心理事实——人愿意分享一件事,不等于愿意承担这件事的全部后果。
3.2 和后端Battle:明文号码能不能过日志
这是我这个项目里最较真的一轮沟通。后端同事的常规做法是把接口出入参全量打印到日志,排障方便。但明文手机号一旦出现在日志里,就进入了另一个存储系统,后续日志平台的访问权限、保留期限都变得不可控。我坚持让后端把手机号字段单独标记为脱敏字段,并要求日志链路里统一过滤。
具体落地了三件事:接口层统一返回脱敏后的手机号字段作为默认值;明细查询接口虽然返回明文,但在日志AOP切面里对该字段进行了正则替换;数据库表设计里加密存储,只允许指定服务账号解密。这三项改动不复杂,但把“按需暴露”从接口层面延伸到了系统链路的每个角落。后端同事一开始觉得麻烦,直到测试阶段做了一次数据泄漏演练,发现链路里所有日志都没有明文痕迹,才承认这套设计是值的。
3.3 和测试Battle:用例不能只覆盖“正常分享”
隐私需求最怕测试也只测“正常路径”。我主动拉着测试同学过了一遍异常路径矩阵,这轮讨论很有价值,因为很多边界问题是需求文档里不会写的:
- 用户A分享给B,B一直不查看,24小时后链接失效,B再点“获取号码”应该看到什么提示
- 用户A分享给B后,A反悔并撤回,此时B已经在查看页停留但未点击获取
- 同一个A,把号码分享给多个用户,每个用户的授权记录是否独立
- B拿到了明文号码后,截图转发给C,平台是否保留风险识别手段
其中第二点特别容易出bug:页面上展示的还是有效状态,接口已经判了撤回。第一版就出现了状态不同步,后来加了前端定时轮询和接口二次校验双保险,才把这个问题按下去。这个案例后来被我写进团队的测试手册里,作为“前端状态必须是后端状态的投影”的典型示例。
3.4 和定需求的人Battle:用户文案的态度边界
还有一轮容易忽略的battle在文案上。最初页面上的按钮文案是“获取号码”,被分享人点击后系统直接展示明文。我建议改成“联系TA”,点击后先弹一个半屏确认页,写明“该号码由对方主动分享,请勿转发”,确认后再展示。
理由是:按钮动作越模糊,用户在系统上留下的操作意图越不容易被追溯。如果按钮直接叫“获取号码”,一旦用户转发号码造成纠纷,用户可以说“我就是随手点的”,平台缺乏明确的二次确认证据。改成“联系TA”+确认页,既保留了操作流畅性,又在关键动作前增加了一层告知,后续如果有投诉、争议,这层确认可以成为平台免责和处置的依据。产品经理当时说“你们开发想得真多”,但这个文案上线后,用户侧反馈非常自然,没有人觉得多了一步很烦,反而因为明确提示了不要转发,接收方对号码的珍视程度更高了。
4. 接口设计与数据隐私的最小化实践
4.1 字段设计:明文和脱敏必须分开
接口设计上我用了一个通俗的比喻:把脱敏字段当成默认菜,明文字段当成隐藏菜单。列表接口、卡片接口、通知推送,一律只返回脱敏号码;只有带有效凭证访问明细接口时,才返回明文号码。
核心的结构长这样,分享关系创建后返回给前端的字段只有:
{ "share_id": "share_uuid_12345", "masked_mobile": "138****8000", "expired_at": 1710172800, "status": "active", "owner_action_required": false }点击“联系TA”后,前端带着share_id去请求一个换取凭证的接口,拿到短期token后,再调明文号码接口:
{ "share_id": "share_uuid_12345", "token": "tk_8f3ab5c2", "token_expired_at": 1710169500, "full_mobile": "13812348000" }明文接口只有这一个,而且强制要求POST、要求登录态、要求token校验。有人问为什么不用GET,因为GET的参数会暴露在浏览器历史、代理日志和CDN访问日志里,share_id和token一旦出现在这些链路中,就相当于再次把凭证暴露给了第三方。POST在后端日志层面更容易做字段过滤,这是一个日常开发里特别便宜但特别有效的选择。
4.2 最小化原则不是只指字段
数据最小化不只是接口里少给一个字段,还包括业务系统里少存一份数据、日志里少打一个参数、告警信息里少带一条上下文。
开发过程中我不止一次遇到同事为了排查方便把整个request body打进告警消息里的事情。隐私项目里这条路坚决不能开。正确的做法是排查时统一用share_id定位,手机号字段只能通过脱敏后的方式出现在工单和告警里。团队内部定了一条规矩:所有隐私敏感字段在排查工具、监控看板、日志平台里显示前必须过同一套脱敏函数,谁改了脱敏逻辑,谁就要重新走一次code review。
这条规矩的有效性在后续一次线上问题排查里得到了验证。当时有一个用户反馈“我分享出去后对方一直说看不到号码”,排查链路涉及到前端日志、后端接口日志和数据库查询结果,你猜怎么着?所有环节我都是用share_id和掩码去筛的,整个排查过程没有暴露任何一位用户的真实号码给临时拉进来的值班同事。这才叫最小化不只在设计里,而是在日常操作习惯里。
4.3 分享链接的防滥用设计
有了接口和权限模型,还得防着一件事:用户把分享凭证复制粘贴到第三方工具,或者接收方用户把链接转发给其他人。理论上授权记录是强绑定接收人ID的,但如果对方用同一个账号在另一个设备登录,或者接收人压根就是未登录游客,凭证就要承担一部分风险控制职能。
我做的防滥用手段按成本从低到高总计四层。第一层是基础参数带上时间戳,服务端拒绝超过48小时的分享创建请求;第二层是凭证与设备指纹绑定,同一凭证不允许在两个设备上同时请求明文;第三层是配合业务行为模型,单账号单日获取明文号码超过一定次数自动触发风控审核;第四层是接收方举报按钮,明文展示页下方附带“号码被冒用”的反馈入口。这四层里前两层是开发兜底,后两层是运营手段,组合使用下来灰度期的滥用率基本可以忽略。
4.4 幂等与并发:分享关系创建和撤回的边界
分享记录的创建和撤回都要考虑重复操作。创建接口如果被前端重复点击,可能产生多条有效授权记录。后端设计上我用了一个简单的幂等键——分享发起方ID加上业务场景ID,同一个业务场景只能创建一条有效分享记录,重复请求返回同一条记录,而不是新建。
撤回接口也是同理。用户A在管理页疯狂点击“撤回”,后端需要保证最后一次操作的结果一致,不能出现撤回后又因为旧请求被重新置为有效的情况。这里我用的是状态机思想:有效态、撤回态、过期态这三种状态之间是单向流转的,撤回态只能由有效态进入一次,重复撤回直接返回当前状态而不是报错。状态流转的代码不复杂,但在这个过程中写清楚状态机的变更条件,比任何文档都更能防止团队后续扩展时改出逻辑漏洞。
5. 测试、灰度与上线后的坑:常见问题排查实录
5.1 最容易翻车的边界情况
隐私分享功能的测试重点不在功能实现,而在各种“时间窗口”和“权限变更”的边界组合。我让测试同学重点覆盖了下面五类场景,每一类都确实抓到了问题:
- 分享有效期内,被分享人一直未查看,有效期边界上发起请求,有概率命中缓存里的旧数据
- 分享撤回后,被分享人点击“联系TA”,应提示已撤回,但前端页面残留了旧的手机号
- 用户A分享给B后,B的账号被冻结,此时B还能不能看到明文号码
- 两个不同用户同时对A的手机号发起分享请求,后端是否保持多次幂等
- iOS和Android端本地缓存策略不一致,安卓端退出账号后明文号码是否还残留在内存页面栈里
其中账号冻结这个场景特别有意思:按业务逻辑,账号冻结意味着所有权限冻结,但分享记录表里如果只判断分享关系状态、不判断接收方账号状态,明文接口依然可以被请求到。后来在查询逻辑里补了一层接收方账号状态校验,才把这条链路堵住。
5.2 上线后的实战踩坑记录
灰度期遇到最折腾的问题是“部分用户反馈分享后对方看到的号码是另一个网段的”。排查下来,原因是老用户升级App时本地缓存了旧版的脱敏规则,新版的掩码算法改了保留位数,导致同一手机号在不同端渲染出来的脱敏结果不一样,用户自然以为号码错了。办法是客户端版本升级时清零本地缓存,同时脱敏函数统一收敛到后端返回为准,前端不做二次脱敏。这个坑在联调阶段很难发现,因为测试环境都是新安装,不会模拟跨版本升级,建议大家在灰度计划里加上一步“老版本客户端请求新接口”的兼容性验证。
第二个坑是明文接口的高峰期耗时波动。因为明文查询要校验token、查授权关系、查账号状态,多重校验叠加后偶发超时。后来做了两步优化:第一,token校验结果加五秒钟本地缓存,幂等请求直接放行;第二,授权关系用唯一的share_id做索引查询,避免嵌套子查询。优化后接口P99从800多毫秒降到了200毫秒以内,效果很明显。
第三个坑更有代表性:推送卡片里带了脱敏号码,用户点推送进到详情页后,详情页自己做了一次明文获取展示,这时候前面做的一切隐私控制就全被打破了。问题根源是客户端拿到推送参数后,直接拿share_id去请求了明文接口,而没有重新触发一次获取动作。后来约束了所有明文展示必须经过统一的曝光组件,组件内部校验会话内是否已完成过“联系TA”确认,否则一律展示掩码。这个设计原则现在写进了团队的组件规范。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 处理方案 |
|---|---|---|
| 被分享人提示号码无效 | 授权已过期或撤回,前端状态未同步 | 前端轮询授权状态,接口二次校验,提示文案区分“已过期”和“已撤回” |
| 同一号码分享多次后管理页列表错乱 | 创建接口缺乏幂等控制 | 引入业务场景幂等键,保证同一场景同一用户只保留一条有效记录 |
| 明文接口偶发超时 | 多重校验嵌套查询 | token缓存、单索引查询、去掉不必要的事务 |
| 新旧版本脱敏显示不一致 | 客户端本地缓存旧脱敏规则 | 脱敏统一收敛到后端,客户端升级清缓存 |
| 用户投诉号码被第三方看到 | 截图转发或链接复用 | 接收人绑定、token一次性、风控阈值、举报入口 |
| 撤回后对方仍然能看到明文 | 前端页面栈保留旧数据 | 统一曝光组件,退出页面时清理已暴露的隐私数据 |
6. 最后再分享几条实操心得
隐私手机号分享这个功能,单独看每个模块都不复杂,但把它在整个系统链路里做扎实,需要的不只是写代码的能力,而是对数据流向的敏感度。我自己的体会是:凡是和敏感数据沾边的需求,做的时候把“少暴露、短暴露、可撤回”当成三条默认准则,后面出的问题会少一大半。
少暴露说的是接口字段、日志、缓存、推送这些环节能不给就不给明文。短暴露说的是任何明文展示都要带时间窗口,哪怕窗口只有一个小时,也比永久强。可撤回说的是给用户一条反悔的路,这条路不只是产品按钮,而是背后完整的授权状态机。
如果你接下来也要做类似的需求,建议先把产品讨论的重心从“UI怎么展示”拉到“数据从哪里来、到哪里去、多久失效”这组问题上。花半天时间把这些问题聊透,比开发完再返工改接口要划算得多。项目本身不大,但这些围绕隐私的细节点,才是日常开发最值钱的沉淀。