资讯详情

资讯详情

前端敏感数据脱敏实战:手机号身份证号正则替换与Vue组件实现

1. 整体设计与思路拆解1.1 先搞清楚脱敏到底在解决什么问题手机号、身份证号这类个人敏感信息在真实项目中几乎天天碰到。管理后台的用户列表、客服平台的工单详情、运营看板里的用户画像只要界面上会出现用户的真实手机号和身份证号就有数据泄露的风险。测试人员截个图发到群里、前端同事开着DevTools调接口、甚至只是领导从你工位旁边路过瞄了一眼屏幕敏感信息就这么不经意地流出去了。脱敏就是干这个事的把关键字段的中间几位用星号替换掉让数据看起来像真的但已经不是完整的真实数据。比如手机号138****5678身份证号110***********1234既保留了必要的辨识度比如知道是哪个号段、哪个人又不会暴露完整的个人隐私。这里要重点强调一个容易被忽略的需求点不改变源数据。也就是说你的脱敏逻辑只能作用在展示层不能把接口返回的原始数据对象给改掉。这个约束看起来简单但实际操作中很多人会踩坑比如直接对数组里的某个字段做替换结果污染了源数据后面提交表单、二次操作时拿到的是已经脱敏过的值排查半天找不到原因。1.2 为什么选择在前端做而不是交给后端有一种声音认为脱敏应该在后端做后端返回给前端的数据就应该已经是脱敏后的。这话对也不全对。对于需要严格合规的场景涉及刑法规定的“侵犯公民个人信息罪”那种后端脱敏并且控制权限确实更安全。但现实项目里有很多场景后端没办法替你处理旧接口历史遗留问题返回的就是完整数据你改不动后端后端返回同一份数据但用户在详情页需要看完整手机号比如客服工单列表页只需要脱敏展示同一字段不同场景要求不同联调阶段后端还没适配脱敏逻辑前端需要先行处理甚至有些项目压根没有后端纯前端模拟数据或对接第三方服务。所以我的建议是前端展示层脱敏必须做而且要做得规范、做得统一。前端脱敏的价值在于兜底——不依赖后端任何时候拿到数据都能安全展示。至于更极致的合规要求那是后端和架构层面的问题两者不冲突。在动手写代码之前先明确三个设计原则第一脱敏函数必须是纯函数。输入一个原始字符串输出脱敏后的字符串不修改入参不产生副作用。这样可以保证同一条数据在任何地方调用都得到一致结果也方便单元测试。第二脱敏逻辑要集中管理。不要每个页面复制粘贴一套正则最好抽成一个工具模块通过导入方式复用。后续如果要调整脱敏规则比如星号数量、保留位数只改一处全局生效。第三区分“脱敏”和“隐藏”。脱敏是中间打星号隐藏是整个字段都不显示。有些页面需求可能是“用户无权限看手机号统一显示——”这种情况不要用脱敏函数硬套单独处理就行。1.3 方案选型对比正则替换 VS 字符串截取实现脱敏的核心思路就两种正则替换和字符串截取拼接。我列个对比表方便你根据项目情况选方案优点缺点适用场景字符串截取substring/slice 拼接逻辑直观性能极好对长度敏感号码格式变化时容易出错规范定长的手机号11位、身份证号18位正则替换replace 捕获组灵活强大能处理变长和复杂格式正则写错容易误替换可读性稍差带区号、带空格、格式不稳定的场景两种方案结合先做格式规整再脱敏兼容性强代码量稍多需要处理边界生产环境建议兼容脏数据先看字符串截取的思路。手机号是定长的11位数字截取前3位和后4位中间用4个星号连接function maskMobile(phone) { return phone.slice(0, 3) **** phone.slice(7); }这个方案简单粗暴但有个隐形bug如果传入的手机号不是11位比如有人存了带区号86slice(7)拿到的就不是后4位结果完全不对。所以在生产环境我更推荐用正则来做。手机号的正则脱敏function maskMobile(phone) { return String(phone).replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2); }这里的核心逻辑是用捕获组把前3位和后4位先“框”出来中间的\d{4}匹配要脱敏的4位替换时用$1和$2引用两个捕获组中间拼上4个星号。注意正则里的^和$锚定符它强制要求整个字符串必须刚好是3位数字4位数字4位数字多一位少一位都不匹配不匹配就原样返回这样反而起到了“格式校验”的作用。这个正则方案还有个优势即便以后运营商手机号段增加比如新增了某些罕见号段正则的规则依然适用。因为手机号的前3位决定了运营商和号段归属中间4位是地区编码随机位后4位是用户编号只要前3后4的保留逻辑不变脱敏规则就可以一直用。2. 核心细节解析与实操要点2.1 手机号脱敏的几种写法与坑点从最简单的开始我们一行一行拆解。写法一直接对数字字符串操作const maskMobile (mobile) { if (!mobile) return ; const str String(mobile); return str.replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2); };这个版本适用于标准的11位手机号。有一个很容易踩的坑如果数据源里的手机号是number类型比如JSON里没加引号直接用replace会报错因为number没有replace方法。所以我在函数第一行先把入参转成String再做处理这是前端脱敏函数必须具备的健壮性。写法二兼容“带区号”和“带空格”很多用户在填手机号的时候会手滑输入空格比如“138 1234 5678”或者某些业务场景存了“86 13812345678”这种带国际区号的值。这种脏数据直接套上面的正则匹配不上就原样返回等于脱了个寂寞。所以在脱敏之前可以先做一次数据清洗把非数字字符统一剔除只保留纯数字再走脱敏逻辑const maskMobileSmart (mobile) { if (!mobile) return ; const digits String(mobile).replace(/\D/g, ); if (digits.length ! 11) return String(mobile); return digits.replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2); };注意这里的策略先去掉所有非数字字符\D等价于[^0-9]如果去掉之后恰好是11位才认为它是个有效的手机号并做脱敏如果长度不对说明这个数据有问题那就“老实”地把原始值原样返回。这样设计有个好处你不会因为格式不合法就把用户数据误解成脱敏后的数据出了问题好排查。写法三处理“非11位”数据的兜底策略有些业务场景手机号可能是座机号“010-88886666”或者短号“10086”此时上面两个版本都会原样返回但这会造成一个问题明明该脱敏的字段却没脱敏数据泄露风险依然在。正确的做法是无论什么格式只要不是合法的11位手机号就统一只保留前3位和后3位中间全部星号覆盖const maskMobileFallback (mobile) { if (!mobile) return ; const str String(mobile); const digits str.replace(/\D/g, ); if (digits.length 11) { return digits.replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2); } if (str.length 7) { return str.slice(0, 3) ****; } return str.slice(0, 3) **** str.slice(-3); };这种兜底策略可以在“不能完全脱敏”和“完全不能脱敏”之间做一个折中至少保证敏感信息的核心部分不完整暴露。2.2 身份证号脱敏的正则细节身份证号是18位最后一位可能是X脱敏的常见策略是把出生年月日那8位第7-14位用星号代替这样既能保留省份和性别信息又能起到基本的保护作用。另一种更严格的策略是只保留前1位代表地区和后1位校验位中间16位全部打星号。具体用哪种取决于你业务对“辨识度”的需求。先看最常见的“保留前6后4”方案const maskIdCard (idCard) { if (!idCard) return ; return String(idCard).replace(/^(\d{6})\d{8}(\d{4})$/, $1********$2); };这里用\d{8}匹配了8位出生日期YYYYMMDD替换成8个星号所以结果是类似110101********1234的格式。这里有一个新手很容易搞错的地方捕获组和替换字符串中星号的数量必须严格对应。如果你把\d{6}和\d{8}都“框”进捕获组但替换时只写3个星号那么脱敏后的长度就和原来不一致了看起来会很奇怪。另外脱敏后的数据长度必须和原数据一致这是一个隐含的校验——如果长度变了说明你的正则写错了。再提一个容易被忽视的细节身份证号末位可能是X大小写不确定。如果直接用上面的正则\d{4}匹配后4位时会要求这4位全是数字一旦末位是X整个正则就匹配不上了。这就需要单独处理const maskIdCardSafe (idCard) { if (!idCard) return ; const str String(idCard).toUpperCase(); return str.replace(/^(\d{6})\d{8}(\d{3}[\dX])$/, $1********$2); };把最后的\d{4}改成\d{3}[\dX]意思是后4位中的最后一位允许是0-9或X。同时把输入统一toUpperCase防止小写x影响匹配。2.3 “不改变源数据”的落地策略这是整个需求里最容易被忽略、但最容易出bug的点。直接对源数据做替换是最典型的错误// 错误示例直接修改了源数据 userList.forEach(item { item.phone item.phone.replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2); });这行代码执行完userList里的phone就变成脱敏后的了。如果后续要把这个userList传给下一个页面、提交给后端、或者做一个搜索匹配那么拿到的全都是带星号的数据整个链路就崩了。正确的做法是什么两个方向方向一展示时用函数处理不修改原值。!-- Vue 2 中 -- span{{ maskPhone(user.phone) }}/span !-- Vue 3 中同样适用 -- span{{ maskIdCard(user.idCard) }}/span每次渲染都调用脱敏函数但user对象里的phone和idCard字段永远是原始值。Vue的响应式系统会保证在user.phone变化时重新渲染脱敏结果也会同步更新。这个方案最无脑也最安全适合绝大多数场景。方向二创建一个新字段存脱敏值。const displayList userList.map(item ({ ...item, phoneDisplay: maskPhone(item.phone), idCardDisplay: maskIdCard(item.idCard) }));通过展开运算符浅拷贝出一个新对象把脱敏结果挂在新字段上原对象的字段不受影响。页面渲染时只用phoneDisplay和idCardDisplay提交操作时用phone和idCard。这种方案的好处是计算一次后续渲染不需要重复调用函数性能更好坏处是如果原数据更新了你必须同步刷新display字段否则会显示旧值。成年人全都要我一般两种方案结合列表页用“显示字段”方案一次性计算渲染快详情页用“函数调用”方案每次实时计算不会出现脏缓存。3. 实操过程与核心环节实现3.1 封装一个健壮的脱敏工具模块不建议在每个组件里写正则太散且难维护。我习惯新建一个utils/mask.js把所有脱敏逻辑集中管理。/** * 手机号脱敏保留前3后4中间用*替换 * param {string|number} phone - 手机号 * returns {string} 脱敏后的手机号 */ export function maskPhone(phone) { if (phone null || phone undefined || phone ) return ; const str String(phone); const digits str.replace(/\D/g, ); if (digits.length 11) { return digits.replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2); } // 非11位数字时保留首尾各3位中间用*覆盖 if (str.length 7) { return str.slice(0, 3) ****; } return str.slice(0, 3) **** str.slice(-3); } /** * 身份证号脱敏保留前6后4出生日期8位用*替换 * param {string} idCard - 身份证号 * returns {string} 脱敏后的身份证号 */ export function maskIdCard(idCard) { if (idCard null || idCard undefined || idCard ) return ; const str String(idCard).toUpperCase(); const match str.match(/^(\d{6})\d{8}(\d{3}[\dX])$/); if (match) { return match[1] ******** match[2]; } // 不规则数据保留前1后1其余全部打码 if (str.length 2) { return str[0] *.repeat(str.length - 2) str[str.length - 1]; } return str; } /** * 通用脱敏工具根据类型自动选择规则 * param {string} value - 原始值 * param {string} type - phone | idCard | name | email */ export function maskValue(value, type) { switch (type) { case phone: return maskPhone(value); case idCard: return maskIdCard(value); case name: return maskName(value); case email: return maskEmail(value); default: return value; } }这个模块还顺带加了姓名脱敏和邮箱脱敏两个示例。姓名脱敏一般保留姓氏后面的字用*代替邮箱脱敏一般是前面只保留第一个字符export function maskName(name) { if (!name) return ; if (name.length 1) return name; if (name.length 2) return name[0] *; return name[0] *.repeat(name.length - 2) name[name.length - 1]; } export function maskEmail(email) { if (!email) return ; const atIndex email.indexOf(); if (atIndex 0) return email; const prefix email[0] ***; return prefix email.substring(atIndex); }3.2 Vue 2 与 Vue 3 中的接入实践环境不同接入方式差异还挺大。先看Vue 2传统Options API写法。在Vue 2的组件中使用import { maskPhone, maskIdCard } from /utils/mask; export default { name: UserList, data() { return { userList: [] }; }, methods: { maskPhone, maskIdCard } };模板里直接用el-table :datauserList el-table-column label手机号 propphone template slot-scopescope span{{ maskPhone(scope.row.phone) }}/span /template /el-table-column el-table-column label身份证号 propidCard template slot-scopescope span{{ maskIdCard(scope.row.idCard) }}/span /template /el-table-column /el-table在Vue 2中注册为全局过滤器如果多个页面都要用建议注册成Vue过滤器这样模板里可以少写一行方法调用// main.js import Vue from vue; import { maskPhone, maskIdCard } from /utils/mask; Vue.filter(maskPhone, maskPhone); Vue.filter(maskIdCard, maskIdCard);模板里写span{{ user.phone | maskPhone }}/span span{{ user.idCard | maskIdCard }}/spanVue 2的过滤器很好用但一个坑是过滤器里的this不是组件实例所以不要试图在过滤器内部访问this.$route或者this.someData。虽然脱敏函数一般不会用到组件上下文但如果你有类似需求就得改成方法调用。Vue 3 Composition API 的方案Vue 3 的setup函数里封装一个组合式函数是最舒服的。我把脱敏逻辑封装成一个useMask hook组件里引入就能用// hooks/useMask.js import { maskPhone, maskIdCard, maskValue } from /utils/mask; export function useMask() { const maskPhoneValue (value) maskPhone(value); const maskIdCardValue (value) maskIdCard(value); const maskField (value, type) maskValue(value, type); return { maskPhoneValue, maskIdCardValue, maskField }; }组件里这样用template div p手机号{{ maskPhoneValue(user.phone) }}/p p身份证号{{ maskIdCardValue(user.idCard) }}/p /div /template script setup import { reactive } from vue; import { useMask } from /hooks/useMask; const { maskPhoneValue, maskIdCardValue } useMask(); const user reactive({ phone: 13812345678, idCard: 110101199001011234 }); /script另外在表格组件里Element Plus / Ant Design Vue有一种更优雅的玩法——利用列配置的customRender或者scopedSlots// Element Plus 的列配置式写法 const columns [ { prop: phone, label: 手机号, formatter: (row) maskPhone(row.phone) }, { prop: idCard, label: 身份证号, formatter: (row) maskIdCard(row.idCard) } ];这样表格列渲染时自动脱敏组件模板里不需要写多余的插槽代码更清爽。3.3 列表多个字段批量脱敏的优雅循环实际项目中经常是一个表格里同时有手机号、身份证号、邮箱等多个敏感字段如果每列都写一遍方法调用模板会显得很啰嗦。我一般会在拿到接口数据后循环处理一次生成一个“展示用副本”const sensitiveFieldsMap { phone: phone, idCard: idCard, email: email }; const getDisplayList (rawList) { return rawList.map(item { const displayItem { ...item }; for (const field in sensitiveFieldsMap) { if (item[field] ! undefined item[field] ! null) { displayItem[${field}Display] maskValue(item[field], sensitiveFieldsMap[field]); } } return displayItem; }); };模板直接用phoneDisplay这种带后缀的字段名即可el-table-column label手机号 propphoneDisplay/el-table-column这么做的好处有三点源数据完全不动展示字段是独立的新变量不存在脏数据每个字段只计算一次没有重复的运行时开销。坏处是如果表格行数据发生更新需要重新跑一遍getDisplayList否则展示字段不会变。不过通常表格数据都是从接口统一拉取的全量替换即可不存在这个问题。3.4 脱敏后的操作限制与旁路处理脱敏做完不代表万事大吉。一个经典问题列表页对手机号脱敏了那么“复制”操作怎么办用户看到138****5678想复制这个号码去加微信结果复制了一串星号。针对这种场景合理的做法是列表页默认脱敏展示但提供“点击显隐”的能力。点击眼睛图标或复制按钮时用源数据非脱敏字段做复制操作。在Element Plus里可以用Popover做一个“悬停显示完整信息点击复制”的交互el-popover triggerclick width200 div p classfull-info{{ row.phone }}/p el-button sizemini clickcopyText(row.phone)复制/el-button /div span slotreference{{ maskPhone(row.phone) }}/span /el-popover这里的关键点是只有用户主动触发点击、悬停时才显示完整信息这个交互过程算是一次“临时授权”比直接把完整数据铺在页面上安全得多。同时产品层面要加上权限控制不是所有人都能看到完整信息那就需要和后端配合让有权限的接口返回源数据无权限的接口直接返回脱敏数据前端不需要操心这个。4. 常见问题与排查技巧实录4.1 为什么脱敏后数据长度对不上之前有同事反馈身份证脱敏后长度变成了17位排查了半天发现他写的替换字符串里星号数量少了。这是一个非常典型的低级错误但发生频率极高。我的排查习惯是先对脱敏前后的字符串长度做一次断言校验。写单元测试时这行代码能救你一命// 单元测试示例Jest test(掩码后长度与原始长度一致, () { const raw 110101199001011234; const masked maskIdCard(raw); expect(masked).toHaveLength(raw.length); });只要长度校验不通过赶紧去数正则里的星号个数和\d{n}里的n值是不是对得上。4.2 正则没匹配上数据原样返回这种情况多见于“数据源带格式”的场景。比如手机号存的是“138-1234-5678”这种带横杠的或者身份证号有前导空格。正则严格匹配会直接失败返回原样数据表面上看是“没脱敏成功”实际上是“格式校验失败按原值返回”了。解决办法就是我在3.1节中写的方案先剔除所有非数字字符用纯数字做匹配。但是要注意这种“清洗后脱敏”的策略也有风险如果数据源是“11010119900101123X”这种合法身份证号清洗后反而会误删末位X导致匹配失败。所以清洗逻辑要区分场景手机号可以大胆剔除所有\D身份证号只能剔除空格和中划线不能剔除字母X。4.3 列表页卡顿脱敏函数性能是不是拖后腿脱敏函数的逻辑本身是O(n)的字符串正则匹配性能损耗微乎其微。真正导致卡顿的原因是在Vue模板里写方法调用每次渲染都会执行一次函数。如果列表有10000行每一行有2个脱敏字段那一次渲染就要执行20000次正则匹配。实测下来这个量级的匹配在普通pc上耗时大概几十毫秒单个操作不卡但如果是大列表滚动、频繁排序、筛选操作时累积起来确实会感知到卡顿。我推荐的优化手段就是上面提到的“先批量算好display字段再渲染”把重复计算变成一次计算之后渲染全是读缓存字段。这一步做完哪怕几万行的表格也不会因为脱敏逻辑卡顿。4.4 表格排序和筛选时应该用原始值还是脱敏值这个坑特别隐蔽。如果你按脱敏后的字段排序比如按手机号排序敏感的号段信息本来在源数据里是连续的脱敏后排序顺序就完全乱了。原因很简单星号的ASCII码是42数字的ASCII码是48-57两者混在一起排序结果不可预测。所以排序和筛选的逻辑必须基于源数据字段脱敏计算只影响展示字段。在Element Plus里实现时表格的sortable应该绑定原始字段或者你对数据做一次“先排序再脱敏”的预处理const sortedList rawList .sort((a, b) a.phone.localeCompare(b.phone)) .map(item ({ ...item, phoneDisplay: maskPhone(item.phone) }));先排完序再打入脱敏展示字段这样就同时兼顾了排序正确性和展示安全性。4.5 后端接口返回的数据本身就是脱敏后的前端再脱敏一次会怎样这个问题经常出现在联调阶段。如果后端已经做了脱敏返回的就是“1385678”前端拿到后再次调用脱敏函数正则根本匹配不上因为它中间是星号不是数字最终原样返回。所以“双重脱敏”并不会报错也不会变成“138**68”这种奇怪结果整体是安全的。但这里隐含的风险是如果后端把脱敏后的数据直接存到了缓存或者库里下次查询出来再给前端时数据就永远“脏”了。所以必须明确前后端的分工后端负责“存储安全”前端负责“展示安全”。后端的接口如果已经脱敏了前端就不要重复调用脱敏函数不然以后后端调整脱敏规则比如从保留前3后4改成保留前3后3前端这边风格就对不上了。4.6 一个容易被忽视的边界空字符串和null脱敏函数最基础但也最容易被忽视的边界就是空值处理。在真实接口数据中有些用户的手机号是null有些是空字符串有些干脆没这个字段。如果脱敏函数不处理这些情况页面就会显示[object Object]、undefined这类内容测试看板瞬间变车祸现场。我的习惯是函数入口一句if (!value) return 把null、undefined、空字符串统一输出为空字符串。注意这里不能用value null做判断因为空字符串也是有效输入要原样返回空串而不是undefined。另外如果读取字段时直接用了user.phone.mask()这种链式写法一旦phone为null就会抛TypeError整个组件都崩了。这种场景建议改用可选链user.phone?.slice(0, 3)或者解构时给默认值const { phone } user。4.7 面试官常问字符串包含、正则替换、响应式数据修改这些脱敏相关技术点也经常出现在前端面试题里比如面试官可能问“JS中如何判断字符串是否包含某个子串”——这里可以用String.prototype.includes方法。再比如“从一个比价项目中如何把脱敏功能复用”答案就是抽离成纯工具函数。如果面试官让你手写一个正则脱敏一定要展示你对边界条件的思考比如非11位手机号怎么处理身份证末位是X匹配不上怎么办是否修改了源数据是否考虑过大写X与小写x的区别这些点基本就是考察一个前端工程师的“工程思维”——你写代码不只是为了跑通还要考虑全面、考虑健壮性。把这些边角聊出来面试表现基本就稳了。5. 从脱敏到数据安全的扩展思考5.1 脱敏逻辑也适用于姓名、邮箱、地址等字段脱敏并不只局限于手机号和身份证号。姓名、邮箱、家庭住址、银行卡号都可能是敏感信息。设计师和产品经理有时候只提了手机号和身份证号的脱敏需求但作为前端我们可以在工具模块里把常用脱敏函数都准备好后续其他字段要加脱敏直接调用即可。我之前在项目里还遇到过“银行卡号脱敏”的需求银行卡号一般是16-19位脱敏规则一般是保留前4位和后4位中间全部用星号export function maskBankCard(cardNo) { if (!cardNo) return ; const str String(cardNo); if (str.length 8) { return str.slice(0, 4) *.repeat(str.length - 8) str.slice(-4); } return str; }这类“中间打星号”的函数写多了之后你会发现它们内核都是同一个模式保留头部N位保留尾部M位中间用星号填充。可以抽象一个通用的maskMiddle(str, headCount, tailCount, maskChar *)函数出来一行代码搞定各种变体。不过实用性上单独的命名函数可读性更好我不太推荐过度抽象。5.2 前端脱敏不是终点尽量和后端统一规则前端脱敏本质上属于“展示层安全”只能防君子不能防黑客。因为任何认真看网络请求的人都能在DevTools里看到源数据——前端拿到的本来就是完整数据。真正要保护数据安全必须靠后端接口层面控制权限、日志层面不记录完整身份证号、返回数据默认脱敏、内部系统才返回源数据。前端脱敏是“即使后端没控制好也要在界面上把好关”的最后一道防线。所以项目中最好不要出现“后端不脱敏、全靠前端脱敏”的情况。理想状态是后端在接口层根据用户权限决定返回“源数据”还是“脱敏数据”前端根据返回字段标记做展示。如果后端还没支持前端用工具函数兜底同时跟进后端同事尽快把这个逻辑加上去。我在实际项目中是这样推进的先在前端把脱敏工具写好铺到所有页面上然后和后端约定好规则后端再逐步改造接口。两层都做了才敢说数据展示环节是安全的。5.3 脱敏后的内容禁止回传最后一个特别容易犯的错误把脱敏后的数据提交给后端。比如表单里有个“确认手机号”的输入框用户没有重新填写前端读的是脱敏后的展示值直接提交了。后端收到的就是“138****5678”这种带有星号的脏数据后续短信发送、号码校验全都会出问题。怎么避免核心原则提交的数据永远用源数据脱敏只用于展示。如果某个表单需要“默认带入手机号”再允许用户修改那么初始值应该设置成源数据内部字段展示时用脱敏值用户不修改则提交源数据修改了则提交用户输入的新值。我一般这样处理// 组件内 const formData reactive({ phone: user.phone, // 源数据只用于提交 phoneDisplay: maskPhone(user.phone) // 展示数据只用于页面回显 }); // 用户修改了输入框时 watch(() formData.phoneDisplay, (newVal) { // 展示值不等于源数据脱敏结果说明用户改了内容 if (newVal ! maskPhone(formData.phone)) { formData.phone newVal.replace(/\D/g, ); } });这样写下来用户看到的是脱敏后的手机号但提交给后端的永远是完整的真实数据。用户不改源数据原样提交用户改了用新的输入值覆盖。6. 实操心得写脱敏函数时我养成的几个习惯脱敏这个功能技术含量不算高但特别容易写得毛糙。几次踩坑之后我给自己定了几条规矩分享给你参考。第一入口先做类型归一化。任何脱敏函数第一行永远是判空 转字符串。因为接口数据经常不按套路出牌number、undefined、null都可能出现先把类型统一了再谈后面的匹配逻辑。第二严格区分“展示值”和“源值”。后端返回的数据结构尽量保持原封不动要脱敏展示就单独加display字段或者模板里调用函数。永远不要让脱敏逻辑去修改后端给的数据对象。第三每个脱敏函数都必须有兜底分支。正则匹配不上时的处理逻辑不是简单地return原值而是要判定“这个值是不是合法数据”如果合法但不匹配规则说明规则不兼容如果不合法则返回空或按通用规则脱敏。宁可展示不太美观的兜底结果也不要让敏感信息原样裸奔。第四写单元测试重点测边界。脱敏函数是纯函数最好测了。把空值、11位、非11位、带横杠、带空格、身份证末位X、大写X、小写x全测一遍。这些测试代码不需要很多但能保证你重构时不改坏逻辑。第五在代码里留注释说明规则来源。比如手机号脱敏为什么保留前3后4身份证号为什么保留前6后4这些规则通常来自产品需求不写注释的话三个月后你自己都忘了当初为什么这么设计。写一行注释后面的人维护起来会轻松很多。回到最初的问题——手机号、身份证号脱敏“中间显示星号不改变源数据”这件事的本质是前端在展示层给敏感信息加了一道滤网这道滤网不阻碍业务正常流转但能让不该看到信息的人看到“打码版本”。应对面试也好、应对真实需求也好真正重要的不是你背了多少行代码而是你清不清楚边界在哪里、数据流怎么走、安全性怎么保障。把这些想明白任何字段的脱敏需求到你手上都能快速落地。最后再分享一个小技巧兼容性检查时不妨把脱敏函数扔进浏览器控制台跑一把看输入各种脏数据的结果是否符合预期。前端这种跟正则、字符串打交道的小工具最快的验证方式就是打开DevTools直接玩比写一堆mock再跑测试要直观得多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →