
做 Azure AD B2C 身份接入久了你会发现一个特别有意思的现象登录、注册两个页面大家还愿意花力气美化等到了“忘记密码”这种边缘流程基本就是默认界面直接上。但密码重置恰恰是用户最容易在深夜情绪崩溃时打开的页面界面如果太敷衍用户的第一反应不是“这系统有点丑”而是“我的账号是不是要丢了”。这篇文章就专门聊聊 Azure AD B2C 的密码重置界面怎么定制从方案选型、模板编写到 CORS 配置、用户流与自定义策略绑定再到我一路上踩过的坑完整过一遍。适合正在做 B2C 租户品牌化、打算把密码找回流程做像样的开发者和 IT 管理员参考。1. 为什么要把默认密码重置界面换掉1.1 默认界面到底差在哪Azure AD B2C 自带的密码重置页面技术上没什么毛病但它有几个天然短板放在真实业务环境里非常扎眼。第一视觉完全不统一。企业的登录页通常已经做了品牌化有专属 logo、配色和字体结果用户点“忘记密码”之后一下子跳到一版“微软默认风”的页面这种割裂感在客户演示时尤其尴尬。第二文案没法随心改。默认页面上的提示语是微软写好的你只能整体换模板想在输入框下面加一句“密码至少 8 位需包含大小写字母和数字”这种业务说明默认界面根本不给入口。第三交互有点僵硬。验证码按钮点完之后没有任何反馈倒计时用户手一快就连点好几次反而把验证码消费掉了。第四错误提示不友好。用户输错验证码页面显示一串像“AADB2C90080”的编号普通用户看了只会更慌。所以所谓“定制密码重置界面”不只是换个皮肤而是要把这个高压力、低容错的流程在产品视觉和引导文案上都拉回到自家产品的体验水准。这也是我把这件事单独拎出来写一篇的原因。1.2 定制的三条路线怎么选Azure AD B2C 里做界面定制大致有三条路线按侵入程度从低到高排列路线做法定制能力改造成本适合场景用户流 页面布局模板在现有密码重置用户流里给“本地帐户密码重置页面”配置自定义 HTML/CSS中可换视觉、改提示、加简单脚本低门户操作即可项目本身用用户流且不需要改后端逻辑自定义策略 ContentDefinition修改 TrustFrameworkBase.xml 里的 LoadUri把页面协定指向自己的模板高页面、文案、用户旅程都可以控制中需要熟悉 IEF 策略语法项目已经用自定义策略或需要精细控制验证流程完全脱离标准页面做前置系统用 API 连接器 / REST API 自己实现密码重置前端B2C 只做后端认证极高前后端完全自定义高相当于自建一个小型重置系统有特殊合规要求或前端团队想把所有身份流程都收口我这次的项目最终选了第二条路线因为客户早就从用户流迁移到了自定义策略登录、注册、资料编辑全都跑在 Identity Experience Framework 上密码重置如果单独用用户流会多出一套配置要维护。但如果你的项目只是用用户流那第一条路完全够用我后面会两条路都讲到。提示不管你走哪条路线底层渲染原理和 CORS 要求基本一致。先把原理搞懂后面配置就不会出奇奇怪怪的问题。2. 先搞懂密码重置页面背后的渲染机制2.1 密码重置用户旅程大致长什么样在进行定制之前得先搞清楚一个完整的密码重置流程在 B2C 里到底分几步。以最常见的“本地账户验证码重置”为例完整链路是用户点击“忘记密码”进入密码重置用户流。页面要求输入邮箱地址。B2C 发送一封含验证码的邮件到该邮箱。用户把邮件里的验证码填到页面上。验证通过后进入设置新密码的页面。新密码提交成功流程结束可以跳转回登录页。在这个流程里第 2、4、5 步都是需要渲染页面的节点。B2C 不是刷新整个网页而是在同一个 HTML 模板容器里根据流程进度切换页面内容。这就引出了页面协定Page Contract的概念也是做界面定制时必须理解的核心机制。2.2 页面协定Page Contract是什么你可以把页面协定理解成一份“B2C 和前端模板之间的渲染协议”。B2C 官方要求自定义模板里必须留出一个特定 ID 的容器通常是div idapi>!DOCTYPE html html langzh-CN head meta charsetutf-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / title重置密码/title style :root { --brand-primary: #0067b8; --brand-bg: #f5f7fa; } body { margin: 0; font-family: Segoe UI, Microsoft YaHei, sans-serif; background: var(--brand-bg); } .login-container { max-width: 420px; margin: 60px auto; padding: 32px; background: #fff; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,.08); } .brand-logo { text-align: center; margin-bottom: 24px; } .brand-logo img { max-height: 48px; } #api { width: 100%; } .footer { text-align: center; margin-top: 24px; color: #888; font-size: 13px; } /style /head body div classlogin-container div classbrand-logo img srchttps://your-cdn.example.com/logo.png alt公司 Logo / /div div idapi>az storage account create --name b2cuiassets --resource-group rg-b2cui --location eastasia --sku Standard_LRS --kind StorageV2 az storage blob service-properties update --account-name b2cuiassets --static-website true --index-document index.html --404-document error.html上传文件可以用az storage blob upload --account-name b2cuiassets --container-name $web --name index.html --file ./password-reset.html上传完成后先直接在浏览器里打开静态网站地址确认模板能正常显示。这里有一个容易忽略的细节静态网站的 URL 是根路径https://账户.z13.web.core.windows.net/如果你把模板文件命名为password-reset.html那完整地址就是对应的子路径绑定到 B2C 时一定要写全。3.3 配置 CORS 让 B2C 能加载模板这一步是决定成败的关键也最容易被漏掉。回到存储账户左侧找到“设置”下面的“资源分享CORS”针对 Blob 服务添加一条规则。配置项推荐值允许的源https://你的租户名.b2clogin.com允许的方法GET, OPTIONS允许的标头*公开的标头*最大期限3600如果你还在使用旧的login.microsoftonline.com域名或者你所在的环境自定义了域名需要把对应的来源也加进去。允许的来源可以填多个但不建议写成*因为 B2C 页面承载的是身份认证流程CORS 开得越窄越安全。规则保存后一般一两分钟内就会生效。怎么确认 CORS 生效了打开浏览器开发者工具访问一次 B2C 的密码重置页面在 Network 面板里看模板请求如果 Response Headers 里出现了Access-Control-Allow-Origin就说明配置成功。3.4 绑定用户流并预览模板托管好、CORS 配好后就可以绑定到用户流了。如果你用的是用户流方式操作路径是在 Azure AD B2C 租户里找到“用户流”选择你的密码重置用户流。点击左侧“页面布局预览”找到“本地帐户密码重置页面”。把“使用自定义页面内容”开关打开在 URI 或 HTML 内容里填入你的模板地址。保存后点击“运行用户流”进行预览或者直接发起一次真实的密码重置流程测试。这里有个技巧同一个用户流里通常还有“登录页面”等其他布局如果只想改密码重置就只在这个入口配置别的不用动。如果想让视觉统一可以把登录、注册页面的模板也指向同一套 CSS 风格。预览时如果发现模板没生效优先检查 CORS 和 URL 是否填写完整不要急着怀疑代码。4. 进阶在自定义策略里接入自定义模板4.1 自定义策略和用户流的区别如果你的项目已经切换到自定义策略也就是 Identity Experience Framework那么用户流那条路就基本不用了。策略文件本质上是一堆 XML定义了用户旅程、技术配置文件和页面协定。定制界面的方式是在 XML 里找到对应的ContentDefinition把LoadUri改成你的模板地址。自定义策略的优点是控制力强密码重置这种多步骤流程每个步骤对应的页面都可以单独指定模板甚至可以针对不同语言、不同品牌加载不同版本。缺点也很明显学习曲线陡一个语法错误整个流程就会挂掉。所以我的建议是如果还在用用户流能用页面布局解决就别动策略文件只有当你确实需要细粒度控制时再上手自定义策略。4.2 改 ContentDefinition 把 LoadUri 指向模板在实际项目中密码重置相关的内容定义通常会在基础策略文件TrustFrameworkBase.xml里。你可以找一个类似的 ContentDefinition然后做如下修改ContentDefinition Idapi.localaccountpasswordreset LoadUrihttps://b2cuiassets.z13.web.core.windows.net/password-reset.html/LoadUri RecoveryUri~/common/default_page_template.html/RecoveryUri DataUriurn:com:microsoft:aad:b2c:elements:contract:selfasserted:2.1.7/DataUri Metadata Item KeyDisplayName本地账户密码重置页面/Item /Metadata /ContentDefinition这里有三个属性需要留意LoadUri你的自定义模板地址。建议不要直接指向index.html根路径而是指向明确的模板文件名方便后续多页面管理和版本回退。RecoveryUri回退模板地址。当自定义模板加载失败时B2C 会使用这个默认模板它是保命底牌不要删。DataUri页面协定的版本标识它决定 B2C 注入表单的脚本行为。如果后面要开 JavaScript需要把版本号升到支持脚本的版本这一点我在第 5 部分详细说。修改完 XML 后通过“Identity Experience Framework - 自定义策略 - 上传”把策略包上传然后重新运行密码重置用户旅程即可。注意自定义策略通常分基础文件和扩展文件界面定制相关改动建议放在扩展文件里避免升级基础策略时被覆盖。4.3 多步骤密码重置是怎么复用模板的很多人在自定义策略里容易疑惑密码重置明明有“输入邮箱”、“输入验证码”、“设置新密码”三个页面为什么我只看到一个 ContentDefinition 就把界面改了实际情况是这三个步骤确实会生成多个页面但它们默认可以复用同一个模板文件。B2C 的页面协定会根据当前步骤动态切换div#api里的内容你的模板不用为每个步骤单独准备一份 HTML只要把>#api input[typetext], #api input[typepassword], #api input[typeemail] { width: 100%; padding: 10px 12px; border: 1px solid var(--brand-primary); border-radius: 6px; box-sizing: border-box; }这段样式的作用是无论 B2C 注入什么结构输入框至少能保持统一的宽度和间距不会出现按钮和输入框“各长各的”的尴尬情况。5.2 JavaScript 能用但有限制很多第一次接触 B2C 模板的人以为模板里不能写 JavaScript其实是可以的只是默认关闭而且只在特定版本下可用。如果你在用用户流需要在用户流的“属性”里手动打开“启用 JavaScript”开关如果你在用自定义策略需要把 ContentDefinition 的DataUri版本号提高到支持 JS 的版本比如2.1.7或更高。打开后模板里的script标签就能正常执行。一个常见需求是给“发送验证码”按钮加倒计时。B2C 不会自动保存你的倒计时状态所以刷新页面后会重新开始。我一般这样处理script window.onload function () { var sendBtn document.querySelector([data-rolesend-code]) || document.querySelector(input[typebutton]); if (sendBtn) { sendBtn.addEventListener(click, function () { countdown(sendBtn, 60); }); } }; function countdown(btn, seconds) { var origin btn.value; btn.disabled true; var timer setInterval(function () { btn.value seconds s 后可重发; if (seconds 0) { clearInterval(timer); btn.value origin; btn.disabled false; } seconds--; }, 1000); } /script注意B2C 的表单元素类名和属性在不同版本里可能变化写脚本前一定要先打开开发者工具实际确认一下按钮的选择器是什么不要照抄网上的旧代码。脚本尽量做容错用document.querySelector找不到目标时直接 return不要抛错误影响整个页面。5.3 多语言和错误提示优化如果你的产品面向多个地区模板需要支持多语言。B2C 在加载页面时会在 URL 上带ui_locales参数模板可以直接从location.search里解析这个参数然后切换对应语言的文案。错误提示优化是我觉得性价比最高的定制点。默认的验证码错误提示虽然可用但文案偏“官方化”。我通常会在脚本里监听错误消息容器把特定错误码转换成更容易理解的话script window.onload function () { var errorBox document.querySelector(#api .error, #api .alert); if (errorBox) { var text errorBox.innerText || ; if (text.indexOf(AADB2C90080) ! -1) { errorBox.innerText 验证码不正确请重新输入。; } } }; /script这个方法不一定覆盖所有场景因为错误信息是在 B2C 脚本执行后异步注入的直接在onload里获取可能太早。稳妥的做法是用MutationObserver监听div#api的内容变化看到错误消息出现后再替换文案。这里我只提供思路实际实现要以你抓到的 DOM 结构为准。6. 我踩过的坑常见问题与排查实录6.1 模板不加载、样式丢失先查 CORS这个问题出现的频率最高。现象是模板文件在浏览器地址栏里能打开但接到 B2C 页面后一片空白或者样式全部丢失。排查顺序打开浏览器开发者工具切到 Network 面板过滤 Doc / Fetch 请求。找到加载模板的请求看 Response Headers 里有没有Access-Control-Allow-Origin。如果没有就是存储端的 CORS 没配好去存储账户的“资源分享(CORS)”里补充规则。如果 CORS 已经配置了还不生效检查允许的源是否写对了域名。注意b2clogin.com的租户名是 B2C 租户的完整名称比如contoso.b2clogin.com不要只写contoso。还有CORS 规则保存后需要一点时间生效刚改完立刻刷新测试有可能还是旧的等一两分钟再试。6.2 页面出来了但按钮没反应模板加载正常页面也显示了 logo 和文案但用户点击“发送验证码”或“验证”按钮没有任何反应。这种情况多半是模板结构有问题。最常见的元凶是模板里自己写了form标签。B2C 在div#api里注入的表单有自己的提交逻辑你再在外层包一个 form会导致事件绑定错乱。解决办法很简单删掉模板里所有的form只保留div#api作为容器。另一个原因是div#api里预先放了东西。有些人为了让页面看起来“丰富”在容器里写死了按钮结果 B2C 注入内容时和静态内容冲突。记住div#api必须是一个空容器页面上的静态元素都应该放在它的外面。6.3 修改模板后看不到变化这是一个老生常谈但每次都有人踩的坑包括我自己也吃过亏。第一次上传模板后调试改了几个样式刷新 B2C 页面发现还是老样子于是开始怀疑是不是策略文件没生效。排查要点先按 F12 打开开发者工具看 Network确认模板请求是不是命中了你的托管地址。如果请求地址正确但内容没变那就是缓存问题。浏览器、CDN、B2C 端都可能缓存模板文件。最简单的办法是在模板 URL 后面加查询参数版本号比如password-reset.html?v20250401强制绕过缓存。我给存储服务设置 Cache-Control 时的经验是开发阶段用no-cache生产环境再默认缓存。6.4 错误码很“官方”怎么换成大白话用户输入错误验证码时B2C 默认会显示一条包含技术错误码的提示比如 “AADB2C90080” 之类的文字。这种提示在测试阶段还好真正上线后很容易被用户截图吐槽。除了前面提到的在前端用 MutationObserver 替换文案之外还有一种更干净的做法是处理 B2C 服务端的本地化资源。在自定义策略里可以针对不同错误码配置本地化消息把默认文案替换成“验证码不正确请检查后重新输入”。前端替换适合快速迭代服务端本地化适合正式交付两种方式可以结合使用。另外排查时注意区分两类错误码一类是“交互失败”比如验证码错、账户不存在这类适合做友好提示另一类是“系统配置错误”比如策略文件写错这类应该直接暴露出来方便技术人员定位不要强行转换文案否则会掩盖真正的配置问题。6.5 验证码邮件迟迟不到可以先自查这几个环节界面定制完成后最后一步往往是联调邮件发送。邮件收不到的原因可能很多我常用的排查顺序是确认邮箱地址没有输错且该用户确实存在于当前 B2C 租户。查看 B2C 的“运行日志”或 Application Insights确认验证码邮件是否已经触发发送。检查邮件是否进了垃圾箱。B2C 默认发送的邮件发件人比较通用很多邮箱服务可能拦截。如果项目使用的是第三方邮件连接器比如 SendGrid去第三方后台看投递记录确认是否被退信。这方面有个经验验证码邮件本身不属于“界面定制”的范畴但用户感知里它和密码重置界面是同一个流程如果邮件样式和语气都很突兀前面做的界面定制分数会大打折扣。有条件的话建议用 API 连接器或自定义邮件模板把邮件发件人和内容也品牌化整套体验才完整。6.6 登录和密码重置页面像两套系统这是品牌化过程中最容易出现的“局部最优”问题密码重置模板做得再漂亮如果登录页还停留在默认风格整体品牌体验还是割裂的。我处理客户项目时会先建一套统一的 CSS 变量和几个基础组件logo 容器、输入框、按钮、页脚然后把它复用到登录、注册、密码重置等所有 B2C 页面里。每个页面的模板只做差异化调整不重写整套样式。这样后续改品牌色时只需要改一个地方即可不至于在维护中迷失方向。提示在用模板前可以先在本地把 HTML 用浏览器静态打开快速确认布局是否正常。虽然 B2C 注入的内容在静态预览时看不到但外层样式、响应式布局这些基础项是可以提前检查的。最后再分享一点体会界面定制这件事平时没人夸但一出问题大家都会骂。密码重置又是一个用户在慌乱中进行的流程一套看起来“安心”的界面能明显减少用户的恐慌感。我在做这类定制时优先把三件事做扎实验证码重发有倒计时、错误提示说人话、按钮点击有状态反馈。这三件事做到位比任何花哨动画都管用。另外还有一个后端层面的建议虽然和界面无关但容易被前端同事忽略密码规则这类校验一定要用自定义策略在服务端做兜底不要只依赖前端脚本。界面定制是体验层的事安全校验是服务端的事两条线分开推进页面才会既好看又稳靠。