扫码登录的需求是:用户在电脑上打开管理后台,用手机 App 扫一下就登录,不用输密码。我负责手机端这一侧。第一版做得很顺——扫到码、调接口、电脑端就登录了。四个问题,其中一个是安全评审直接打回的。
一是没有确认环节,这是被安全评审打回的原因。我的实现是扫到码就直接完成登录。问题是用户可能扫到一个伪造的二维码——攻击者在自己的电脑上打开登录页、把二维码截图发给受害者(伪装成别的用途),受害者一扫,攻击者的浏览器就登录了受害者的账号。而受害者全程不知道自己做了什么。
二是二维码状态处理不全,报错笼统。二维码有有效期。过期之后我只是返回「登录失败」——用户不知道是网络问题、还是码不对、还是要去电脑上刷新。他的做法是反复扫同一个已过期的码。
三是令牌可以重复使用。我没有把扫码令牌设为一次性。理论上同一个码被确认之后还能再次提交——虽然实际很难利用,但这是明确的设计缺陷。
四是相机权限被拒之后是一个死胡同。用户之前拒绝过相机权限,点「扫一扫」没有任何反应(或者只弹一个「无法使用相机」)。他不知道要去系统设置里开,也不知道有没有别的登录方式。
所以四个改动:加必须的确认环节并展示登录目标信息、二维码状态收敛为五态并按状态给明确指引、令牌一次性且短有效期、各失败路径给出可操作的出路。
这个模块最想说的一句话是:扫码登录里那个「在手机上点一下确认」的步骤,看起来是多余的一步,实际上它是整个方案安全性的核心。因为「扫码」这个动作本身不代表用户理解了自己在授权什么——他可能是被骗着扫的。确认页的价值不在于「再点一次」,而在于它把「你正在授权某个设备登录你的账号」这件事明确告诉用户,让他有机会说不。我最初把它当成了一次多余的交互,想省掉它来让流程更顺。
保留确认环节,即使它让流程多了一步。产品最初希望「扫完直接登录」以追求流畅。但「扫码」这个动作本身不代表用户理解了自己在授权什么——他可能是被骗着扫的。确认页的价值不在于「再点一次」,而在于它把「你正在授权某个设备登录你的账号」明确告诉用户,让他有机会说不。判据是「省掉这一步之后,用户还有没有机会发现自己被骗」:没有,那这一步就不能省。而且我做了一件让这个取舍更站得住的事:确认页展示登录目标端、位置和时间——如果确认页什么信息都不给,那它确实只是多点一下,产品的质疑就是对的。
位置信息做粗粒度并标明估算,而不是精确定位。精确位置更有助于用户判断,但基于网络的位置估算本身误差很大,显示得太精确会造成误判(他看到一个陌生的街道名会以为被攻击了,实际只是估算偏差);而且这涉及隐私。判据是「这个信息的精度是否可靠」:不可靠就不要假装精确。
失败路径全部给「可操作的出路」而不是统一的错误提示。统一提示实现简单,但每种失败的正确应对完全不同:权限被拒要去系统设置、码过期要去电脑刷新、非本产品的码要换个 App 扫、无权限要联系管理员。给统一提示等于把「判断该怎么办」这件事推给了用户,而他判断不了。这一条的成本主要是要枚举失败情况,实现本身不复杂。
踩过的坑一:第一版没有确认环节,被安全评审打回。评审同学演示了攻击路径:他在自己的电脑上打开登录页,把二维码截图发给我,说「帮我看下这个码能不能扫」——我一扫,他的浏览器就登录了我的账号。而我全程不知道自己做了什么。教训是:我把「扫码」当成了「用户同意」,但这两者不是一回事——扫码只是一个物理动作,而同意需要用户理解自己在同意什么。这个认知偏差在很多授权类功能里都会出现(点了链接不等于同意授权)。
踩过的坑二:二维码过期只报「登录失败」,用户反复扫同一个码。客服收到的反馈是「你们的扫码登录一直失败」。而正确的做法只需要一句话:「请在电脑上刷新二维码」。教训是:错误提示的价值取决于它是否指向了正确的下一步动作——「失败」只是描述了状态,没有给出动作。
踩过的坑三:相机权限被拒之后是死胡同。用户点「扫一扫」没反应,他不知道要去系统设置开权限,也不知道有没有别的登录方式。教训是:任何依赖系统权限的入口,都要处理「权限已被永久拒绝」这个状态——它和「首次未授权」不同,重新请求不会再弹窗,只能引导用户去系统设置。而且必须同时给出不依赖该权限的替代路径。
没做的部分:没做「附近设备免扫码确认」这类更便捷的方案。它体验更好,但依赖蓝牙或声波等近场能力,兼容性与可靠性都不确定,而且「更便捷」的方向和这个模块的安全目标是有张力的。我的判断是在扫码登录这件事上,不该为了省一步而削弱确认环节——这也是我提这个建议时的保留意见。
安全性这一项不适合报数字,该报「通过了安全评审对授权确认的要求」以及具体做了什么。确认环节存在、确认页展示登录目标信息、令牌一次性且短有效期、二维码不含凭据——这四条是可核对的事实,比任何数字都有说服力。
状态机的正确性用断言型用例:五种状态各构造一次,断言手机端渲染对应界面且指引文案正确;过期状态断言提示的是「请在电脑上刷新二维码」而不是笼统失败。
令牌一次性:确认成功后再次提交同一令牌,断言被拒绝且不产生二次登录;并发提交两次确认,断言只成功一次。后者要真并发。
停留过久的重新校验:扫码后等待超过有效期再点确认,断言提示已过期而不是报未知错误。
失败路径要逐条验证并列成清单:相机权限被拒、非本产品二维码、网络失败、账号无权限——断言每一条都给出了可操作的指引。报法是「四条失败路径逐一验证并列出各自的指引文案」,列清单本身说明你想全了。
权限被永久拒绝这一条要单独测:它和「首次未授权」行为不同(重新请求不会弹窗),断言此时引导到系统设置并给出替代登录方式。
反馈及时性:手机端确认或取消后,断言电脑端在可接受时间内感知到状态变化(取决于轮询间隔,要报出这个间隔)。
求助工单:可报「扫码登录相关求助工单下降」,但要说明分类方式,并注意这类问题的绝对量本身不大。
不要报什么:不要报「扫码成功率」——它受相机、光线、屏幕反光影响,而且过期和取消都算「不成功」但那是正常行为。该报的是「四条安全要求的具体落实」「五种状态渲染与指引正确」「令牌一次性与并发只成功一次」「四条失败路径各有可操作指引」这几件可核对的事。
选人组件是审批、任务分派、通知范围这些功能的公共依赖。第一版我按最直觉的方式做:拉一棵组织树、渲染成可勾选的列表、勾完返回选中的人员标识数组。四个问题,其中第一个是业务语义错了。
一是勾选部门的语义我理解错了。用户勾选了「技术部」,我的实现是展开这个部门、把当时的成员全部加入选中列表。但业务上他的意思是「技术部的人都要审批」——包括以后新入职的。结果是一个月后新入职的成员不在审批范围内,流程漏了他。这不是 bug,是我把两种不同的语义合成了一种。
二是全量拉树在大企业里卡死。我一次性拉全部组织和成员。小客户几十人没问题,一个几千人的客户打开选人页要转好几秒,低端机上直接卡住。
三是跨层级选完之后看不到自己选了谁。用户在好几个部门下各勾了几个人,树折叠起来之后完全看不到已选的是谁。他要一层层展开去找,或者干脆全部取消重新选。
四是搜索结果只显示姓名,同名的分不清。几千人的组织里同名很常见。搜「张伟」出来三个,我只显示姓名和头像——他不知道该选哪个,只能猜。
所以四个改动:显式区分「选择该部门」与「选择其中若干人」两种语义并分别落库、按层级懒加载并缓存已展开节点、已选内容用浮层集中展示并与树双向联动、搜索结果带完整部门路径,另外补了可见范围过滤与失效成员的标注。
这个模块最想说的一句话是:选人组件最容易出错的地方不是交互,是「勾选一个部门到底是什么意思」这个语义问题——而它决定了几个月后业务会不会出漏洞。我把「选部门」实现成了「选当时那些人」,功能测试完全正常(选完确实是那些人),问题在一个月后新入职成员没被包含时才暴露。这类语义错误比代码 bug 危险,因为它不会报错,只会静默地给出不符合预期的结果。
两种语义都保留并显式区分,而不是只选一种。只做「选具体人」实现最简单,但审批范围、通知范围这类场景确实需要「随部门成员变动」的动态语义;只做「选部门」则无法指定具体的人。代价是落库结构要支持两种(部门标识 + 成员标识列表)、使用时要能展开、界面上要能区分。判据是「业务上这两种需求是否都真实存在」:都存在,那就不能用一种去覆盖另一种——而我最初的错误恰恰是用一种覆盖了另一种,还是用错的那一种。
按层级懒加载而不是全量拉取加前端虚拟列表。全量拉取加虚拟渲染也能解决卡顿(渲染层面),但拉取本身的耗时和流量省不掉——几千人的组织数据在移动网络下就是明显的等待。懒加载的代价是展开每一层都有一次请求(要处理加载态),而且本地搜索不可用(只能走服务端搜索)。判据是「瓶颈在渲染还是在传输」:在传输,那就必须减少一次要传的数据量。
可见范围过滤放在服务端,即使前端过滤更简单。前端过滤的问题不是性能,是数据已经发到客户端了——抓包就能看到全公司通讯录。这是越权问题不是显示问题。判据很明确:只要「不该看到」是一个安全要求而不是体验优化,过滤就必须在服务端。
失效成员在历史选择里标注而不是移除。移除更「干净」,但它会让审批人静默少一个,而没人知道发生了什么——流程可能卡住或者被跳过。标注失效的代价是界面上多了一种状态要处理。判据是「静默变化的后果是否可察觉」:不可察觉,就必须显式暴露。这一条和「估清冲突要提示而不是静默覆盖」是同一个原则。
踩过的坑一:把「选部门」实现成了「选当时那些人」。用户勾选「技术部」,我展开并把当时的成员加入选中列表。一个月后新入职的成员不在审批范围内,流程漏了他。而功能测试完全正常——选完确实是那些人。教训是:这类语义错误比代码 bug 危险,因为它不会报错,只会静默地给出不符合预期的结果,而且要等时间过去才暴露。我后来的习惯是遇到「选择一个集合」的交互,先问清楚「这个集合是快照还是动态的」——这个问题在选人、选商品范围、选门店范围时都要问。
踩过的坑二:全量拉组织树,大客户打开就卡。我用自己公司的测试数据(几十人)开发,完全没有暴露。是一个几千人的客户反馈「选人页打不开」。教训还是那条:性能问题必须用真实的极端数据验收——最大的组织、最深的层级、最差的机型。我现在会主动去问「我们最大的客户有多少人」再决定实现方式。
踩过的坑三:跨层级选完看不到已选,用户全部取消重选。这个反馈是「你们这个选人很难用」,说不出具体哪里不好。我是自己按真实场景操作了一遍(在四个部门下各选两个人)才体会到问题。教训是:组件类功能要自己按真实使用规模走一遍,而不是只验证「能选中」——选一个人和跨四层选八个人的体验完全不同。
没做的部分:没做「按角色或标签选人」(比如选中所有「部门负责人」)。客户提过,它比按部门选更灵活,但需要先有稳定的角色与标签体系,而当时这块本身还在演进。取舍依据是「依赖的基础能力还不稳定,先做会返工」;我在数据结构上把选择类型留成了可扩展的枚举,将来加一种选择类型不需要改表。
语义正确性是这个模块最重要的验证,用断言型用例:以「选择该部门」方式选中一个部门后新增一名成员,断言该成员被包含在范围内;以「选择其中若干人」方式选中同样的人后新增成员,断言不被包含。这两条一起才说明两种语义都对。
加载性能要报清组织规模:「某个 N 人、M 层的组织,首屏加载耗时从 X 降到 Y」,并说明在什么机型上测的。还要报「首屏请求的数据量对比」——这个数字解释了为什么快了。
懒加载与缓存:断言展开节点触发一次请求、二次展开不再请求;断言展开中的加载态可见。
搜索:断言走服务端、结果带完整部门路径、同名成员可区分;快速输入时断言过期响应被丢弃(只显示最新关键词的结果)。
已选联动:跨多层选择后,断言浮层数量与实际选中一致;在浮层移除一项,断言树上对应勾选被取消。
可见范围:用可见范围受限的账号请求,断言接口返回的数据本身已被过滤(而不是前端隐藏)。这条要看接口响应,不能只看界面。
失效成员:把历史选择中的一名成员停用,断言编辑时该成员显示为失效并有提示,而不是消失。
不要报什么:不要报「选人效率提升 N%」——没有可靠的度量方式。该报的是「两种语义在新增成员后的行为符合各自定义」「某规模组织的首屏耗时与数据量对比」「浮层与树的双向联动一致」「可见范围在服务端过滤」这几件可核对的事。
附件预览的需求听起来很小:审批单、任务、通知里都能带附件,用户点一下要能看。第一版我找了一个通用的预览方式,把全部格式都交给它。四个问题。
一是办公文档格式直接白屏。图片和部分格式能看,而客户最常上传的办公文档打开就是白屏——没有报错、没有提示,就是一片空白。用户以为文件坏了,去问上传的人;上传的人在电脑上能打开,双方都困惑。
二是失败之后没有任何出路。白屏或者报错之后,页面上没有「下载」入口。用户拿不到这个文件——而这个文件可能是他审批所必需的。他的做法是让对方通过别的方式(社交软件)再发一遍,那反而绕过了我们的权限控制。
三是大文档首屏很慢。一份几十页的文档,我是整份加载完才渲染。用户等很久,而他往往只想看第一页。
四是预览链接是长期有效的公开地址。企业文档里有合同、报表这类敏感内容。我拿到的是一个长期有效的地址,直接给了预览组件——这个地址被转发出去,任何人都能打开,完全绕过了权限控制。
所以四个改动:按格式分策略并在转换期间展示进度、任何失败都收敛到「下载原文件」这一个明确出路、大文档按页请求并预取相邻页、预览链接改为按需签发时效凭据并鉴权,另外补了敏感文档水印与转换结果按文件指纹缓存。
这个模块最想说的一句话是:预览是一个「大概率成功但注定有失败」的功能——格式千奇百怪、转换服务会失败、文件可能损坏——所以它的设计重点不是提高成功率,是保证「失败时用户仍然能拿到文件」。我第一版把全部精力放在了「让更多格式能预览」上,而真正让用户卡住的是「预览失败之后没有出路」。后者的实现成本远低于前者,价值却更高。
把「失败降级到下载」当成核心功能而不是兜底细节。我第一版把全部精力放在「让更多格式能预览」上,而真正让用户卡住的是「预览失败之后没有出路」。后者的实现成本远低于前者,价值却更高。判据是「这个功能有没有注定无法消除的失败率」:预览天然有(格式千奇百怪、转换服务会失败、文件可能损坏),那失败路径的设计就和成功路径一样重要。
提供下载入口,而不是「为了防泄露不给下载」。安全侧最初倾向于只允许预览不允许下载。但现实是预览失败时用户拿不到文件,他会让对方通过社交软件再发一遍——那份文件就完全脱离了我们的管控(没有鉴权、没有水印、没有记录)。所以提供受控的下载(鉴权 + 记录 + 水印)实际上比不提供下载更安全。这个论证我认为是这个模块里最重要的一次沟通——它把「安全」和「可用」从对立关系变成了一致的。
转换结果按内容指纹缓存而不是按文件标识。按文件标识缓存更简单,但同一份文件被多个人分别上传时会重复转换(浪费转换资源);而按文件名缓存则会出错——同名不同内容是很常见的情况。用内容摘要做指纹兼顾了两者。
水印做「可见水印」而不是隐式水印。隐式水印不影响阅读体验、能在泄露后定位来源,但它对使用者没有约束作用(他不知道文件被标记了)。可见水印的价值一半在事后追溯、一半在事前约束——他看到自己的名字在上面,转发的意愿会下降。我在说明这个功能时会明确「水印不能防截图」,不夸大它的作用。
踩过的坑一:办公文档直接白屏,没有任何提示。用户以为文件坏了去问上传的人,而对方在电脑上能打开,双方都困惑,最后来问客服。教训是:白屏是最糟的失败方式——它没有传达任何信息,用户只能猜。而「明确告诉他不支持」的实现成本几乎为零,我当时只是没想到要区分格式。
踩过的坑二:预览用的是长期有效的公开地址。是安全评审发现的——这个地址转发出去任何人都能打开,完全绕过了权限控制。而企业文档里有合同、报表这类内容。教训和资质材料那次一样:「难以猜到的长地址」不构成访问控制,含敏感内容的资源必须私有存储加时效凭据。我在同一类问题上踩过两次,说明这不是知识问题而是习惯问题——所以我后来把「这个资源的地址泄露了会怎样」加进了自检清单。
踩过的坑三:只校验了附件标识存在,没校验业务权限。知道附件标识就能预览,而附件标识在某些接口里是会返回的。教训是:鉴权要针对「业务对象」而不是「资源标识」——正确的问题是「这个用户能不能看这张审批单」,而不是「这个附件存在吗」。
没做的部分:没做在线编辑与批注。客户提过(希望能在附件上直接批注意见),但它需要接入完整的在线文档能力、处理并发编辑与版本,是一个独立方向。我做的是让批注意见走审批流程的评论区并支持引用页码——用现有能力覆盖了核心诉求。
核心指标是「附件打不开的反馈数」,并按原因分类。格式不支持、转换失败、权限问题各占多少。分类之后才知道该继续补格式支持还是该优化转换服务。
失败降级的覆盖要列成清单逐条验证:格式不支持、转换失败、转换超时、网络失败、文件损坏,断言每一条都展示了具体说明且下载入口可用、位置一致。报法是「五条失败路径逐一验证」并列出清单。
首屏时间要报清文档规模:「N 页文档的首屏渲染时间从 X 降到 Y」,并说明改造后首屏时间与总页数基本无关——这个「无关」才是按页加载的核心收益。
转换缓存效果报「同一文件二次预览的耗时对比」以及「转换任务的去重比例」。后者说明缓存真的在生效。
指纹正确性要专门测:两个同名但内容不同的文件,断言各自转换、不会串到同一份缓存。这条容易被忽略但一旦错就是很严重的数据串号。
鉴权:用无权访问该审批单的账号请求预览,断言被拒绝(即使他知道附件标识);断言时效凭据过期后失效。前一条是踩坑的直接回归。
内存:连续翻阅长文档若干页,断言内存占用不持续增长(缓存有上限并释放)。
水印:断言敏感文档的预览含操作人与时间水印;报告时要注明水印不能防截图,不要把它说成防泄露手段。
不要报什么:不要报「预览成功率」并把它当成质量指标——格式不支持是设计上的正常结果而不是失败。该报的是「按原因分类的打不开反馈数下降」「五条失败路径均有下载出路」「首屏时间与总页数解耦」「无权账号即使知道附件标识也被拒绝」这几件可核对的事。
没有匹配的内容,换个关键词试试。
项目拆解 · 企业移动端常用页(uni-app · 实习级)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据