如何在ASP.NET Core 2.0 API+Angular 6中实现多子域名共用代码库及用户验证
解决方案:多子域名共用代码库的身份验证与子域名传递
首先,你的核心问题有两个:把解析到的子域名可靠传递到API层,以及实现用户只能在注册的子域名登录/访问。咱们一步步来解决:
一、替换自定义IRouter为中间件,全局获取子域名
你当前用SubdomainRoute实现子域名解析,但这种方式会干扰默认路由配置(比如你的SPA fallback路由可能失效)。更可靠的方式是用中间件,它能在所有请求处理前执行,不管是MVC页面还是API接口,都能拿到子域名:
1. 创建Subdomain中间件
public class SubdomainMiddleware { private readonly RequestDelegate _next; // 定义需要忽略的子域名列表 private readonly List<string> _ignoredSubdomains = new() { "www", "drivebuyrei", "mail" }; public SubdomainMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { var host = context.Request.Host.Host; if (!string.IsNullOrEmpty(host)) { var index = host.IndexOf('.'); if (index > 0) { var subdomain = host.Substring(0, index); // 忽略指定子域名,视为主站请求 if (!_ignoredSubdomains.Contains(subdomain, StringComparer.OrdinalIgnoreCase)) { // 把域名存入HttpContext.Items,全局可访问 context.Items["Subdomain"] = subdomain; // 同时添加请求头,方便Angular前端获取 context.Request.Headers.TryAdd("X-Subdomain", subdomain); } } } // 继续执行后续中间件 await _next(context); } }
2. 注册中间件
在Startup.cs的Configure方法中,把这个中间件放在UseMvc之前(确保路由处理前就拿到子域名):
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ...其他中间件(比如UseStaticFiles、UseAuthentication) // 注册子域名中间件 app.UseMiddleware<SubdomainMiddleware>(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( "Sitemap", "sitemap.xml", new { controller = "Home", action = "SitemapXml" }); routes.MapSpaFallbackRoute( name: "spa-fallback", defaults: new { controller = "Home", action = "Index" }); }); }
二、在API层获取并使用子域名
现在所有请求都能拿到子域名了,咱们在API控制器中使用它:
1. 创建API基类(可选,但更优雅)
[ApiController] [Route("api/[controller]")] public class BaseApiController : ControllerBase { // 封装获取子域名的方法 protected string GetCurrentSubdomain() { return HttpContext.Items["Subdomain"] as string ?? string.Empty; } }
2. 注册/登录时关联子域名
假设你的用户表已经添加了Subdomain字段(用来存储用户注册时的子域名),在注册和登录逻辑中使用:
public class UserController : BaseApiController { private readonly UserManager<ApplicationUser> _userManager; private readonly SignInManager<ApplicationUser> _signInManager; public UserController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager) { _userManager = userManager; _signInManager = signInManager; } [HttpPost("register")] public async Task<IActionResult> Register(RegisterViewModel model) { var subdomain = GetCurrentSubdomain(); if (string.IsNullOrEmpty(subdomain)) { return BadRequest("请从合法子域名注册"); } var user = new ApplicationUser { UserName = model.Username, Email = model.Email, Subdomain = subdomain // 保存子域名到用户表 }; var result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: false); return Ok(); } return BadRequest(result.Errors); } [HttpPost("login")] public async Task<IActionResult> Login(LoginViewModel model) { var subdomain = GetCurrentSubdomain(); // 先根据用户名/邮箱找用户,再验证子域名 var user = await _userManager.FindByNameAsync(model.Username) ?? await _userManager.FindByEmailAsync(model.Email); if (user == null || user.Subdomain != subdomain) { return Unauthorized("未授权:只能在注册的子域名登录"); } var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, lockoutOnFailure: false); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: model.RememberMe); return Ok(); } return Unauthorized("用户名或密码错误"); } }
三、全局验证用户子域名权限
除了登录时验证,咱们还需要确保已登录用户只能访问自己注册的子域名,可以用ASP.NET Core的授权策略实现:
1. 创建授权需求和处理器
// 定义授权需求(空类,用来标记策略) public class SubdomainAuthorizationRequirement : IAuthorizationRequirement { } // 实现授权处理器 public class SubdomainAuthorizationHandler : AuthorizationHandler<SubdomainAuthorizationRequirement> { private readonly IHttpContextAccessor _httpContextAccessor; private readonly UserManager<ApplicationUser> _userManager; public SubdomainAuthorizationHandler(IHttpContextAccessor httpContextAccessor, UserManager<ApplicationUser> userManager) { _httpContextAccessor = httpContextAccessor; _userManager = userManager; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, SubdomainAuthorizationRequirement requirement) { var httpContext = _httpContextAccessor.HttpContext; var subdomain = httpContext.Items["Subdomain"] as string; // 如果是忽略的子域名(比如www),直接允许访问 if (string.IsNullOrEmpty(subdomain)) { context.Succeed(requirement); return; } // 获取当前登录用户ID var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userId)) { context.Fail(); return; } // 验证用户的子域名是否匹配当前请求的子域名 var user = await _userManager.FindByIdAsync(userId); if (user != null && user.Subdomain == subdomain) { context.Succeed(requirement); } else { context.Fail(); } } }
2. 注册授权服务
在Startup.cs的ConfigureServices中添加:
public void ConfigureServices(IServiceCollection services) { // ...其他服务配置 // 注册HttpContextAccessor(用于在授权处理器中获取请求上下文) services.AddHttpContextAccessor(); // 添加授权策略 services.AddAuthorization(options => { options.AddPolicy("SubdomainPolicy", policy => policy.Requirements.Add(new SubdomainAuthorizationRequirement())); }); // 注册授权处理器 services.AddScoped<IAuthorizationHandler, SubdomainAuthorizationHandler>(); }
3. 应用授权策略
在需要验证的API控制器或动作上添加[Authorize(Policy = "SubdomainPolicy")]:
[ApiController] [Route("api/[controller]")] [Authorize(Policy = "SubdomainPolicy")] public class ProfileController : BaseApiController { // 该控制器下的所有动作都会验证子域名权限 [HttpGet("me")] public async Task<IActionResult> GetCurrentUser() { var user = await _userManager.GetUserAsync(User); return Ok(user); } }
四、Angular前端获取子域名
前端可以直接解析当前域名拿到子域名,用来显示或请求时验证:
// 创建一个子域名服务 import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class SubdomainService { getCurrentSubdomain(): string { const hostname = window.location.hostname; const parts = hostname.split('.'); // 处理多级域名(比如abc.example.com) if (parts.length >= 3) { const subdomain = parts[0]; const ignoredSubdomains = ['www', 'drivebuyrei', 'mail']; if (!ignoredSubdomains.includes(subdomain)) { return subdomain; } } return ''; } }
在组件中使用:
import { Component } from '@angular/core'; import { SubdomainService } from './subdomain.service'; @Component({ selector: 'app-register', templateUrl: './register.component.html' }) export class RegisterComponent { currentSubdomain: string; constructor(private subdomainService: SubdomainService) { this.currentSubdomain = subdomainService.getCurrentSubdomain(); } }
关键注意事项
- 数据库设计:必须在用户表中添加
Subdomain字段(建议为字符串类型,可加索引优化查询)。 - Cookie共享:确保所有子域名共享身份验证Cookie,需要在Startup.cs的
ConfigureServices中配置:services.ConfigureApplicationCookie(options => { options.Cookie.Domain = ".example.com"; // 注意前面的点,允许所有子域名共享Cookie options.Cookie.Name = ".YourApp.Auth"; // ...其他Cookie配置 }); - HTTPS配置:生产环境确保所有子域名都配置了HTTPS,避免Cookie安全问题。
内容的提问来源于stack exchange,提问作者Dhruv Patel
相关产品推荐
相关产品推荐

