ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Entra External ID自定义属性为何不能改:更新策略与替换方案

Entra External ID自定义属性为何不能改:更新策略与替换方案 最近有个客户找到我说他们在Microsoft Entra External ID里建了一个自定义属性用来存用户的会员等级。业务上线三个月后运营那边要求把这个属性从字符串改成整数顺便改个更规范的名字。客户在管理后台点了半天发现属性名称和数据类型都是灰的根本动不了整个人都懵了。这个场景我见过太多次了。很多人把Entra External ID里的自定义属性当成普通数据库字段觉得随时可以改类型、改名字甚至删掉重建。但实际上自定义属性一旦创建就会和目录扩展机制、用户流、令牌签发链条深度绑定牵一发而动全身。这篇文章我会把Entra External ID自定义属性的更新策略拆开讲清楚它为什么改不了、哪些东西其实还能改、业务需求变了之后怎么在不动旧属性的前提下完成替换以及怎么在规划阶段就把更新成本降到最低。内容偏实操适合正在用或者准备用Entra External ID做客户身份管理的架构师、运维和全栈开发。1. 为什么一个属性改不了先看它背后的数据模型很多人把自定义属性理解成在用户表里加了一列改属性名就是改一下列名改数据类型就是执行一条ALTER TABLE。这个理解放在普通业务数据库里没错但在Entra External ID里事情完全不一样。1.1 自定义属性其实是目录扩展Entra External ID的自定义属性本质上是Microsoft Entra目录里的extension attribute扩展属性。它和你平时看到的displayName、city、jobTitle这些内置属性完全不同。内置属性是目录Schema里预先定义好的固定列有微软统一管理的数据类型和存储逻辑。而自定义属性是租户在运行时动态追加的扩展列。打个比方内置属性就像商场统一装修好的标准铺位地板、墙、水电都是固定格局。自定义属性则是你在商场里临时搭的展台搭的时候可以自由设计但一旦搭好并开始使用再想改展台的承重结构就要考虑整栋楼的消防、电路和消防通道。在实际的Entra External ID租户里这些扩展属性由微软的一个第一方应用承载。注册流程收集的值会写到用户对象的扩展属性里身份验证事件可以通过API连接器读取需要的话还能以自定义声明claim的形式放进ID Token里返回给应用。每一个使用场景都会引用这个属性的键值这就是为什么它不能像数据库字段一样随便改。1.2 属性在底层的样子extension_命名空间在Entra管理中心的界面上你看到的是memberTier这样的友好名称。但到了数据层通过Microsoft Graph API读用户数据时这个属性长这样extension_b0c1d2e3f4a546789abcdeff_memberTier前面那段b0c1d2e3f4a546789abcdeff是你租户里B2C扩展应用的应用ID去掉连字符之后的结果。也就是说同一个属性名在不同租户里生成的完整键是不同的因为不同租户的扩展应用ID不一样。这个细节非常重要。当你用Graph API去读或者写这个属性时必须使用完整的扩展键。很多项目后期出问题就是因为应用代码里硬编码了开发环境的扩展键部署到生产环境后全部失配。一旦属性名改了整个扩展键也会跟着变。这意味着所有通过Graph API读写该属性的脚本、所有从Token里取该声明的应用逻辑、所有用户流里对该属性的引用全部需要同步修改。这不是改一个字段名那么简单是一次跨系统的接口变更。1.3 “创建即锁定”的源由类型、名称与删除限制的底层逻辑微软在这块的设计逻辑其实很清晰数据一致性优先于操作便利性。为什么属性名不能改因为扩展属性一旦发布就会被注册页面、认证事件、令牌发布规则等组件引用。这些组件在运行时是直接按属性键去匹配的属于编译期静态依赖而不是运行期动态查找。为什么数据类型不能改因为类型变了属性对应的存储结构就变了。字符串改整数看着简单但存量用户里可能有空值、有格式错误的数据、有超长字符串。迁移数据时任何一条脏数据都会导致同步任务失败。与其让管理员在数据迁移的坑里挣扎不如从行头上就锁死。删除受限也是同一个道理。属性一旦被用户对象的数据引用或者被某个用户流挂在了注册表单里系统就认为它处于“正在使用”状态。这种状态下你从管理界面点删除按钮基本是灰的。就算强行调Graph API去删也会收到一个引用冲突的报错。2. 更新策略的边界什么能改什么碰不得搞清楚底层机制之后你就能理解更新策略的真正边界了。我不会一棒子打死说“所有东西都不能改”那也不准确。关键在于你要知道哪些是安全的操作哪些是雷区。2.1 管理中心里能编辑的内容描述与令牌返回开关我实际在Entra管理中心操作下来进入自定义属性的编辑界面后属性名称和数据类型确实是锁定的灰色不可点。但有两个字段是开放的Description描述和令牌返回相关设置。这里要提醒一句描述字段别当摆设。我见过太多租户的描述栏是空的过了半年运维换人谁都不知道这个属性当初是干什么用的。我在实操中会约定俗成地写上属性用途、维护人、来源系统、有效时间比如“会员等级用于电商小程序展示来源于CRM系统2025年Q1上线”。这样做的好处是排查问题时能快速定位责任人。令牌返回开关控制的是这个自定义属性要不要作为声明出现在Token里。默认是关闭的关闭状态下属性只存在目录里应用侧通过Graph读取打开之后属性会出现在ID Token或者Access Token的自定义声明里。启动令牌返回后要注意一个副作用Token体积会膨胀。属性值越大、数量越多Token就越大影响API调用的网络开销和性能。2.2 用户流中的属性编排注册表单、API连接器与身份验证事件自定义属性的更新策略还要考虑到它在用户流里的存在。一个属性可能同时被多个场景引用改起来的影响面是辐射状的。注册登录用户流里属性可以作为“User attribute”挂在注册表单上用户填写后写入目录。同一个属性可能出现在多个用户流里比如面向C端用户的注册流、面向管理员创建用户的后台流程。如果你只改了其中一个流程的引用另一个流程还在用旧属性就会出现同一个业务字段在不同入口收集到的值类型不一致的情况。API连接器是另一个容易被忽略的引用点。Entra External ID允许在注册流程中调用你自己的API做业务校验比如检查会员编号是否存在于CRM系统。这个API收到的请求体里就可能包含自定义属性的键值。如果你改了属性名API连接器的请求体结构也会变后端接口解析字段的那段代码如果不更新整个注册流程就会断掉。身份验证事件这块自定义属性经常被用来做条件访问或者风险判断。比如读取用户的accountStatus属性如果是disabled就直接阻断登录。这种情况下属性值的约定就变成了业务逻辑的一部分你不仅要管属性的Schema还要管属性值的枚举标准。2.3 边界之外的硬性约束删除限制与跨租户复制限制删除限制我已经提过这里补充一个我亲测过的细节即使你清空了所有用户对象上的该属性值即使你已经把属性从所有用户流里移除系统依然可能拒绝删除。因为后台策略或审计日志里可能还留存着对它的引用。遇到这种情况我的建议是直接放弃删除的念头把属性留着在管理上标记为“已废弃”就行。跨租户复制限制也要提前知道。开发环境、测试环境、生产环境如果分属不同租户自定义属性不能通过界面直接复制。你在生产租户创建了ext_memberTierV1测试租户里得手动再建一次。而且由于扩展应用ID不同两边最终的属性键还不一样。在写自动化脚本时一定要把属性键作为配置项参数化否则换个环境就抓瞎。3. 业务需求变了怎么办不删属性也能完成属性替换既然创建即锁定那业务需求变了就只能等死当然不是。正确思路是用新属性替换旧属性而不是修改旧属性。3.1 属性替换的标准操作流程我把这个过程总结成四步新建目标属性、历史数据迁移、引用切换、旧属性下线。第一步新建一个符合新需求的属性。这一步的关键是类型和命名一次到位。比如旧属性memberTier是String新需求要求整数那就建一个ext_memberTierV2数据类型选Int描述里注明“替代memberTier值为整数等级”。第二步把存量用户的数据从旧属性迁移到新属性。这一步通常用Graph API批量处理。第三步切换引用。把用户流里的注册表单属性从旧属性换成新属性把令牌返回的声明映射也切到新属性同时通知应用侧改读取键。第四步下线旧属性。因为删不掉能做的就是把旧属性从所有用户流里摘下来在属性字典里标记废弃新用户也不再往旧属性里写入数据。老数据保留在旧属性上给历史报表查询留一条后路。这里要特别强调切换引用要在非生产环境完整演练一遍确认注册、登录、令牌返回、API连接器四个环节全部正常后再在生产环境执行。这个习惯救过我很多次。3.2 用户数据回填的几种路径Graph批量处理数据迁移是方案里最需要谨慎对待的环节。存量用户少比如几十个在管理后台手动复制粘贴也就算了。存量用户上千上万的必须用Graph API写脚本批量处理。这里给一个真实场景里的迁移脚本示例作用是把所有用户extension_xxx_memberTier的值转成整数写入extension_xxx_memberTierV2# 示例将 memberTier 迁移到 memberTierV2 # 注意{appId} 必须是当前租户B2C扩展应用的ID去掉连字符 $headers { Authorization Bearer $accessToken } $uri https://graph.microsoft.com/v1.0/users?$selectid,displayName,extension_{appId}_memberTier$top100 while ($uri) { $resp Invoke-RestMethod -Uri $uri -Headers $headers -Method Get foreach ($user in $resp.value) { $oldValue $user.extension_{appId}_memberTier if ($null -eq $oldValue) { continue } # 这里按业务规则做类型转换转换失败要单独记录 $newValue 0 if (-not [int]::TryParse($oldValue, [ref]$newValue)) { Write-Warning 用户 $($user.displayName) 的 memberTier 数据无法转换为整数: $oldValue continue } $body { extension_{appId}_memberTierV2 $newValue } | ConvertTo-Json Invoke-RestMethod -Uri https://graph.microsoft.com/v1.0/users/$($user.id) -Headers $headers -Method Patch -ContentType application/json -Body $body } $uri $resp.odata.nextLink }几个实操注意点调Graph API需要Directory.ReadWrite.All权限。用Application权限时要在应用注册里提前配好并完成管理员同意否则会一直报权限不足。大批量更新时Graph的节流策略会返回429状态码。生产脚本里一定要加指数退避重试逻辑不然跑到一半任务就断了。这个脚本里用了TryParse做字符串到整数的安全转换转换失败的用户单独记录日志不要直接跳过或者中断整个任务。数据错的用户后面再人工处理不能因为一两条坏数据影响全量迁移。3.3 令牌输出与应用侧适配数据迁移完成不代表切换结束。应用侧如果一直在读Token里的自定义声明切换到V2属性后声明的键名也会跟着变。这一层最容易出问题。在Entra External ID里用户流会决定哪些属性以声明形式出现在Token里。切属性时你要把用户流里的“声明”配置一并切换让V2属性出现在Token里。如果应用侧同时需要V1和V2的值做过渡期兼容可以让两个属性在短期内同时出现在Token里等应用侧切完再摘掉旧声明。如果应用是通过Graph API读取用户资料而不是读Token那就需要改应用代码里的属性键。这里强烈建议不要硬编码在应用配置中心维护一个“属性键映射表”代码里统一通过配置读取。这样以后再有V3、V4改一下配置就行不用发版。4. 把更新策略前置属性规划阶段就该做的三件事讲了这么多补救方案其实最省事的策略是把工作做在前面。属性创建的当天就决定了未来几年你要不要为它折腾。4.1 从需求倒推属性类型创建属性前先问一句这个值未来会被用来做什么这决定了你选什么数据类型。业务需求场景推荐类型为什么不推荐其他类型显示文本、名称、地址、描述String如果是数值型但不需要计算String也可以但要注意排序不是数字序年龄、数量、积分、等级编码Int用String存整数的后果是排序错乱、范围比较困难后续迁移成本高开关、是否、启用禁用Boolean用String存true/false容易混入其他值校验麻烦多选标签、列表、一组值拆分成多个属性或JSON字符串目前类型没有数组型多值场景要么拆属性要么约定JSON格式但JSON会让Token膨胀时间戳String或Int没有专门日期类型存Unix时间戳或ISO字符串要提前约定格式并写进描述我见过最典型的反面案例是把所有东西都定义成String理由是“省得以后换类型麻烦”。结果业务方要按会员等级做数值排序时发现排序结果永远不对。更坑的是这种问题往往在数据量大了之后才暴露到时候再迁移成本比一开始选对类型高出一个量级。4.2 命名空间治理统一前缀、语义化命名与保留字段清单自定义属性的命名规范必须从第一个属性开始定。我的建议是统一加业务前缀比如ext_然后跟业务域再跟语义名最后带上版本号ext_member_tier_v1。这种命名的好处是看一眼就知道它是扩展属性、属于哪个业务域、是第几代版本。不要用tier、city、email这种听起来和内置属性撞车的名字。就算系统允许建也会给后续维护的人造成困扰这个email到底是自定义的还是内置的在用户流里配置时也会让人犹豫不决。这里推荐每个团队建立一份“属性字典”至少包含以下字段属性键含扩展前缀业务含义数据类型允许取值/格式约定创建日期关联用户流状态启用/废弃责任人属性字典建议放到团队共享文档里创建属性前先查字典确认没有重复再动手。这个流程很轻量但能有效避免命名混乱和重复建设。4.3 环境一致性几套租户如何同步扩展属性大多数团队都有开发、测试、生产至少三套环境。没有统一管理手段的话属性Schema很容易漂移开发环境建了ext_member_tier_v2测试环境还是ext_member_tier_v1生产环境压根没建。用户流一发布全链路测试直接原地爆炸。我现在的做法是写一个属性同步脚本。脚本内容就是用Graph API从模板环境读取所有customUserAttribute定义再在目标环境里批量重建。每套租户执行一遍保证属性键一致。注意由于不同租户的扩展应用ID不同脚本里的属性键不能写死。把扩展应用ID做成参数通过环境变量或配置文件传入每个环境跑一遍就行。属性同步完成后在目标环境里拉一份用户对象检查扩展键是否正确生成这是最基本的验证。5. 我实际踩过的坑和现在的操作习惯最后这部分我整理几个真实踩过的坑以及这些坑怎么改变了我现在的操作习惯。很多经验不是从文档里来的是项目上线之后用业务代价买来的。5.1 多语言显示名与最终用户界面的坑第一次在Entra External ID里做注册用户流时我把自定义属性命名为preferred_language觉得挺清晰。结果用户流发布后注册页面上直接显示了这个下划线字段名连个中文标签都没有。用户注册时看到的是一个看起来像程序变量的东西转化率受了很大影响。后来才明白用户在注册表单上看到的文字和属性名是两回事。用户流中配置属性时可以设置向最终用户展示的显示名称。现在我的习惯是属性名保持英文下划线风格用户流里的显示标签单独配置成多语言版本。不同语言的用户看到不同语言的标签底层存的还是同一个属性值这一层的坑就不会再踩了。5.2 API调用中的权限与延迟还有一次做属性自动化时我通过Graph API批量创建自定义属性脚本跑完就去配置用户流结果用户流里怎么都加载不出新属性排查了半天一度以为是代码问题。后来才发现自定义属性创建后并不是即刻在所有组件里生效目录Schema的同步有几个秒到几十秒的延迟。系统是Eventual Consistency最终一致性模型不是强一致模型。从那以后我的自动化脚本里都会在创建属性之后加一个等待和重试逻辑每隔几秒检查一次属性是否可见最多等一分钟。这个改动看起来很小却让自动化流程的稳定性提升了一个档次。另外Graph API的权限作用域也要提前摸清楚。创建和管理自定义属性需要对应的Directory权限很多人一开始用的是User.ReadWrite.All死活调不通换成Directory.ReadWrite.All之后一切正常。建议在项目初始化时就把权限清单一次性申请到位避免后续反复加权限、重新做管理员同意拖慢整个进度。5.3 我现在的属性更新习惯版本号写进属性名经历了那么多次迁移和兼容性问题之后我现在已经不太纠缠于“怎么修改属性”了。在团队里我定了一个更新策略属性Schema不可变业务语义变化时不做原地修改直接新建带版本号的新属性旧属性保留数据并标记废弃。比如业务方说“会员等级以后不光有数字还要带子分类”我不会去修改ext_member_tier_v1而是直接新建ext_member_tier_v2类型设置为String约定存储格式为数字_子类。V1里所有的历史数据原样保留报表系统继续读V1新业务系统读V2两边互不干扰。等V1的数据消费者全部迁走再把V1从所有用户流里摘掉彻底进入只读状态。这套策略的本质是把“数据迁移”拆成了可控的小步骤让每一步都有独立的验证点。属性本身不让改我们就让属性的生命周期更规范规划、创建、使用、冻结、废弃。Entra External ID的自定义属性虽然看着死板但摸清它的脾气之后你会发现这套规则反而逼着你养成了更好的数据治理习惯。希望这篇分享能帮你少走一些弯路。
返回列表