1. 多租户建站为什么卡在二级域名这一步做 SaaS 或者多用户建站平台绕不开一个需求每个注册用户拿到一个属于自己的二级域名比如abc.yourdomain.com访问这个地址直接进到他的专属页面。听起来简单真动手会发现三个坑叠在一起DNS 要支持泛解析、IIS 要把不同子域的请求路由到同一套程序、跨子域的登录态还得保持一致。我见过不少团队的做法是给每个用户单独建一个 IIS 站点用户量一上来站点数量爆炸运维直接崩溃。正确思路是一个站点目录 泛解析 URL 重写 Session 域共享用户注册时只写一条数据库记录不需要动 IIS 配置。这套方案适合谁适合用 ASP.NET含 ASP.NET Core 的兼容思路在 Windows Server IIS 上做多租户站点的开发者尤其是需要「注册即开通二级域名」这种自助式体验的产品。核心检索词就是二级域名泛解析怎么配、IIS URL 重写怎么把子域路由到用户目录、跨子域 Session 怎么共享。下面按落地顺序拆开讲先配 DNS 泛解析再配 IIS 绑定和 web.config 重写规则然后解决 Session 跨子域最后给验证步骤和常见报错排查。每一步都给可复制的配置你照着改域名就能跑。2. 泛解析与 IIS 站点绑定的前置配置2.1 DNS 泛解析记录怎么加泛解析的意思是一条记录覆盖所有子域。在域名管理后台加一条 A 记录主机记录填*记录值填你服务器的公网 IP。以devin.cn为例记录类型主机记录记录值说明A*203.0.113.10所有子域指向同一 IPA203.0.113.10主域Awww203.0.113.10主站 www加完之后abc.devin.cn、xyz.devin.cn都会解析到同一台服务器不需要为每个用户单独加记录。这一步是整套方案的地基DNS 没生效后面全白搭。验证方法命令行执行nslookup abc.devin.cn看返回的 IP 是不是你的服务器。注意泛解析生效有 TTL 缓存改完别急着测等几分钟或者用nslookup abc.devin.cn 8.8.8.8指定公共 DNS 查询。2.2 IIS 站点绑定主机头留空打开 IIS 管理器选中你的站点右侧点「绑定」添加一个 HTTP 绑定主机名留空IP 填服务器 IP 或「全部未分配」。主机头留空意味着只要请求打到这台服务器、且没有更精确的绑定匹配就落到这个站点。这里有个关键设计建两个站点指向同一个物理目录。主站站点绑定主机头www.devin.cn和devin.cn默认文档设为Default.aspx。二级域站点主机头留空默认文档设为Index.aspx。为什么两个站点指同一目录因为主站和用户区共享同一套程序集、同一份数据库连接配置只是入口文件不同。主站走Default.aspx做门户二级域走Index.aspx做用户区入口。IIS 匹配绑定时精确主机头优先所以www.devin.cn会进主站abc.devin.cn因为没精确匹配就落到主机头为空的二级域站点。2.3 应用程序池与目录权限两个站点可以共用一个应用程序池也可以分开。共用的好处是 Session 状态天然在同一进程内InProc 模式下跨站点读取更方便。但要注意如果分开应用程序池InProc Session 就不通了必须换成 StateServer 或 SQLServer 模式这个后面 Session 章节细说。目录权限给IIS_IUSRS读权限用户上传目录给写权限。别图省事给 Everyone 完全控制这是安全底线。3. 可复制的 web.config 重写与 Session 配置3.1 安装 URL Rewrite 模块IIS 默认不带 URL 重写需要先装URL Rewrite Module 2.0微软官方下载搜 IIS URL Rewrite 即可。装完后 IIS 管理器里站点会多出「URL 重写」图标。也可以用微软早期的URLRewriter.NET组件但推荐用官方模块配置写在web.config的rewrite节点里更规范。3.2 web.config 完整重写规则把下面这段放进站点根目录的web.config。核心逻辑把对Index.aspx的请求重写到/User/Index.aspx同时把用户区其他页面也做映射保证地址栏始终显示二级域名。?xml version1.0 encodingUTF-8? configuration system.webServer rewrite rules !-- 用户区首页abc.devin.cn/ 重写到 /User/Index.aspx -- rule nameUserHome stopProcessingtrue match url^$ / conditions add input{HTTP_HOST} pattern^(?!www\.)(?!devin\.cn$)(.)\.devin\.cn$ / /conditions action typeRewrite url/User/Index.aspx / /rule !-- 用户区子页面abc.devin.cn/test.aspx 重写到 /User/test.aspx -- rule nameUserSubPage stopProcessingtrue match url^([a-zA-Z0-9_\-])\.aspx$ / conditions add input{HTTP_HOST} pattern^(?!www\.)(?!devin\.cn$)(.)\.devin\.cn$ / add input{REQUEST_FILENAME} matchTypeIsFile negatetrue / /conditions action typeRewrite url/User/{R:1}.aspx / /rule /rules /rewrite httpProtocol customHeaders add nameX-Tenant-Host value{HTTP_HOST} / /customHeaders /httpProtocol /system.webServer system.web sessionState modeStateServer stateConnectionStringtcpip127.0.0.1:42424 cookielessfalse timeout60 / httpCookies domain.devin.cn / /system.web /configuration几个要点解释一下。match url^$ /匹配根路径条件里用正则排除www和裸域只对真正的二级域生效。{R:1}是反向引用把匹配到的页面名回填到重写目标。httpCookies domain.devin.cn /是 Session 跨子域的关键它让 Cookie 的作用域覆盖所有子域。3.3 Session 跨子域共享的两种模式模式一InProc Cookie 域。两个站点共用一个应用程序池Session 存在进程内存里配合httpCookies domain.devin.cnabc.devin.cn和www.devin.cn就能读到同一个 SessionID。这是最省事的做法但应用池回收 Session 就丢且不能多台服务器负载均衡。模式二StateServer。上面配置里用的就是这种。开启 ASP.NET State Service服务里找 ASP.NET State Service 启动Session 存在独立的 state server 进程里多个站点、多个应用池都能共享。生产环境推荐这种配合stateConnectionString指向本机或独立的状态服务器。注意StateServer 模式下存进 Session 的对象必须可序列化自定义类记得加[Serializable]否则运行时报序列化异常。3.4 在代码里取二级域名并写入 Session在/User/Index.aspx.cs的Page_Load里取主机头protected void Page_Load(object sender, EventArgs e) { string host Request.Url.Host; // abc.devin.cn string[] parts host.Split(.); // 校验格式必须是 xxx.devin.cn长度恰好 3 if (parts.Length ! 3 || parts[1] ! devin || parts[2] ! cn) { Response.Redirect(https://www.devin.cn/Error.aspx); return; } string tenant parts[0].ToLower(); // abc // 排除保留前缀 if (tenant www || tenant api || tenant admin) { Response.Redirect(https://www.devin.cn/Error.aspx); return; } Session[UserDomainName] tenant; // 存入 Session // 后续页面直接从 Session 取无需重复解析 }这样abc.devin.cn访问时Session[UserDomainName]就是abc用户区所有页面都能拿到当前租户标识去数据库查对应数据。4. 验证请求与成功结果确认4.1 本地 hosts 模拟测试正式 DNS 生效前先在本地C:\Windows\System32\drivers\etc\hosts加一行模拟127.0.0.1 abc.devin.cn 127.0.0.1 www.devin.cn然后浏览器访问http://abc.devin.cn/应该看到用户区首页地址栏保持abc.devin.cn不变。再访问http://abc.devin.cn/test.aspx页面正常加载且地址栏不变说明重写规则生效。4.2 验证 Session 跨子域一致在www.devin.cn登录后手动跳转到abc.devin.cn检查登录态是否还在。可以在两个站点都输出Session.SessionID对比Response.Write(SessionID: Session.SessionID); Response.Write(Tenant: Session[UserDomainName]);如果两个子域输出的 SessionID 一致说明 Cookie 域配置正确。如果不一致八成是httpCookies domain没配对或者两个站点用了不同的应用程序池且 Session 模式是 InProc。4.3 注册新用户后的完整链路验证模拟真实流程在www.devin.cn注册一个用户用户名testuser数据库写入记录。然后直接访问http://testuser.devin.cn/预期结果页面正确加载显示该用户的专属内容地址栏保持testuser.devin.cn如果之前在www.devin.cn已登录此时登录态延续点击用户区内的任意链接地址栏仍是二级域形式。实测下来只要 DNS 泛解析、IIS 空主机头绑定、web.config 重写、Cookie 域这四样都配对整条链路是通的。5. 常见报错与排查对照5.1 HTTP 404.4 - 未找到与请求匹配的绑定访问二级域返回 404错误码 404.4。原因通常是 IIS 里没有主机头为空的绑定或者泛解析没生效。排查nslookup abc.devin.cn确认解析到服务器 IPIIS 绑定里确认存在主机名为空的 HTTP 绑定。5.2 HTTP 500.19 - 配置错误无法读取 web.config多半是 URL Rewrite 模块没装或者web.config里rewrite节点不被识别。解决确认已安装 URL Rewrite Module 2.0IIS 管理器里能看到「URL 重写」图标。如果用的是旧版URLRewriter.NET配置节点名不一样别混用。5.3 Session 丢失 / 跨子域登录态不一致现象在www.devin.cn登录跳到abc.devin.cn变成未登录。排查顺序检查httpCookies domain.devin.cn /是否配置注意前面那个点不能少检查两个站点是否共用应用程序池若分开且用 InProcSession 不通若用 StateServer确认 ASP.NET State Service 已启动stateConnectionString端口对默认 42424检查 Cookie 是否被浏览器拦截跨子域 Cookie 需要同主域。5.4 Unable to serialize the session state 序列化异常StateServer 模式下往 Session 存了不可序列化的对象。解决自定义类加[Serializable]特性或者改用 InProc 模式但失去多服务器共享能力。5.5 URL 重写后静态资源 404重写规则把 CSS/JS 请求也重写了。解决在规则条件里加{REQUEST_FILENAME}IsFile判断文件存在就不重写上面配置里的UserSubPage规则已经带了negatetrue。5.6 重写循环导致 500LookFor和SendTo指向同一路径会死循环。确保重写目标/User/xxx.aspx不会再被同一条规则匹配。用stopProcessingtrue让规则命中后停止后续匹配。6. 把域名解析和模型调用一起管起来多租户站点跑起来之后很多团队会顺手接入大模型能力——比如给每个租户的站点加一个 AI 客服、内容摘要或者代码助手。这时候你会发现域名、Key、模型 ID 这三样东西的管理方式和二级域名很像都需要一套统一的入口来分发和隔离。我自己的做法是把模型调用统一走 TaoToken 的 API 网关Base URL 填https://taotoken.net/api每个租户或每个环境分配独立的 Key模型 ID 按需指定。这样切换模型或者排查某个租户的调用量时不用改代码改配置就行。如果你也在做多租户 AI 的组合可以先把 Key 管理这块理清楚到 TaoToken API Keys 建好 Key再对照 接入文档 把 Base URL 和 Model ID 填进你的配置。想先验证模型通不通直接用 模型对话 发一条请求看返回。长期做编码和 Agent 的Coding Plan 更适合按量管理。回到二级域名本身最后补一个实用技巧用户注册时别只写数据库顺手校验一下用户名是否和保留前缀冲突www、api、admin、mail这些在写入前就拦掉比事后重定向到错误页体验好得多。另外泛解析虽然方便但要注意别让搜索引擎把每个子域都当独立站点收录必要时在用户区页面加robotsmeta 控制。