ARTICLE DETAIL

资讯详情

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

IdentityServer4与ASP.NET Core MVC集成:Hybrid Flow混合流程全解析

IdentityServer4与ASP.NET Core MVC集成:Hybrid Flow混合流程全解析 在企业级Web应用开发里凡是涉及登录、授权、单点登录这类需求Identity Server几乎是绕不开的一个名字。而如果你用的是ASP.NET Core MVC做前端站点又要对接Identity Server保护的后端API那么Hybrid Flow就是一条非常典型的“中庸之道”——它不像Implicit Flow那样拿不到刷新令牌也不像Authorization Code Flow那样把用户身份令牌和访问令牌的获取流程拉得太长。这篇文章我会从零开始带着你走一遍完整落地过程为什么要选Hybrid Flow、Identity Server端怎么配置、MVC客户端怎么接入、实际调试中我踩过的坑有哪些。不管你是第一次接触Identity Server还是已经跑通过Basic流程想进一步理解混合流程这篇内容都能给你一个可以直接照抄的作业。1. 项目整体设计与Hybrid Flow选型分析1.1 什么是Hybrid Flow它到底解决了什么问题先花点时间把概念理清楚。**Hybrid Flow混合流程**是OAuth 2.0和OpenID Connect协议里的一种授权流程从名字就能看出来它是两种流程的混合体前半段类似Implicit Flow在授权端点直接返回id_token让客户端第一时间拿到用户身份信息后半段又类似Authorization Code Flow授权端点同时返回一个code客户端再用这个code去令牌端点换取access_token和refresh_token。为什么需要这种“混合”设计我用一个实际场景说明。假设你维护的MVC站点需要两件事同时发生用户登录进来立刻展示“你好张三”这样的个性化界面同时页面里还要调用后端API拉取订单列表。在纯Authorization Code Flow里客户端先拿code再拿code换令牌这一步走完才能解密id_token知道用户是谁——虽然安全但在高延迟网络下用户会明显感觉到“登录后白屏了一会儿”。在纯Implicit Flow里虽然id_token和access_token直接从授权端点返回速度快了但access_token经过浏览器URL传递暴露风险大而且拿不到refresh_token令牌过期用户只能重新登录。Hybrid Flow把两者的优点做了结合授权端点一次性返回id_token codeid_token先解密用起来展示用户信息code同步拿去换access_token调API用。同时因为身份令牌和访问令牌的获取路径是分开的客户端可以更精细地控制令牌的安全存储策略。一句话总结Hybrid Flow适合既需要快速识别用户身份、又要安全访问API的MVC这类服务端渲染应用。1.2 对比三种主流流程为什么MVC场景优先选它这里我不空谈理论直接拿一张对比表来说话你看完就明白为什么MVC客户端身份验证场景里Hybrid Flow的综合评分最高。对比维度Implicit FlowAuthorization Code FlowHybrid Flowid_token获取位置授权端点直接返回URL片段令牌端点返回授权端点返回id_tokencodeaccess_token获取位置授权端点直接返回令牌端点换取授权端点拿code令牌端点换取refresh_token支持不支持支持支持令牌暴露风险高经过浏览器低低适合客户端类型SPA、原生App服务端Web应用、API服务端Web应用MVC场景适配度低中高我在实际项目里感受最深的一点是MVC应用的页面渲染和API调用往往是同时发生的如果走纯Authorization Code Flow你得先把code换好令牌再渲染页面白白多一次网络往返。而Hybrid Flow的id_token可以先到先用来渲染页面code换令牌的动作在后台并行执行用户感知到的登录速度明显更快。更重要的是MVC应用是服务端应用天然具备安全保存client_secret的能力不像SPA那样密钥容易泄露所以走需要密钥校验的Hybrid Flow完全可行这也是它比Implicit Flow更适合MVC的根本原因。1.3 项目整体架构与核心角色划分基于上面的分析整个项目落地时我把它拆成三个角色搭建前先在脑子里把它们的边界画清楚后面写代码才不混乱Identity Server授权服务器负责用户身份认证、发放id_token和access_token、维护客户端注册信息。它既是身份提供方也是令牌签发中心。MVC客户端你的Web应用用户直接访问的站点负责发起认证请求、接收令牌、管理登录态、展示页面并在需要的时候携带access_token调用下游API。受保护API资源服务器真正提供业务数据的服务比如订单服务、用户服务只信任Identity Server签发的access_token配合JWT Bearer认证方案校验令牌合法性。这三个角色之间有两个关键通信链路MVC客户端 → Identity Server完成登录和令牌获取MVC客户端 → 受保护API携带令牌获取数据。我这次实操用的是IdentityServer4基于.NET Core 3.1作为授权服务器、ASP.NET Core MVC 3.1作为客户端、一个极简的Web API作为资源服务器。当然现在Duende IdentityServer已经是主流版本IdentityServer4已停止维护但配置思路完全一致下面我会标注出新旧版本的差异点。2. Identity Server端配置从零建立授权服务器2.1 客户端配置的完整定义与参数解读先把授权服务器立起来。我新建了一个空的ASP.NET Core项目NuGet装了IdentityServer4包然后在Config.cs里定义客户端。Hybrid Flow的客户端配置是整套方案里的重头戏每个属性都对应协议里的一个环节我贴出核心配置再逐行解释public static IEnumerableClient GetClients() { return new ListClient { new Client { ClientId mvc_client, ClientName MVC Web Application, ClientSecrets { new Secret(mvc_client_secret.Sha256()) }, AllowedGrantTypes GrantTypes.Hybrid, // 授权码模式必须显式允许 AllowAccessTokensViaBrowser false, RedirectUris { https://localhost:5002/signin-oidc }, PostLogoutRedirectUris { https://localhost:5002/signout-callback-oidc }, AllowedScopes { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, api1 }, AllowOfflineAccess true } }; }逐行解读几个容易踩坑的点AllowedGrantTypes GrantTypes.Hybrid这是最关键的一行它告诉Identity Server这个客户端走的是混合流程授权端点要同时返回id_token和code。如果误配成GrantTypes.Code授权端点只会返回codeMVC端的OpenID Connect中间件会因为等不到id_token直接报错。AllowAccessTokensViaBrowser false这行很多人会忽略但作用极大。它禁止access_token通过浏览器URL返回强制客户端只能用code去令牌端点换access_token。这正是Hybrid Flow对比Implicit Flow更安全的核心之一必须显式设成false。RedirectUris登录完成后Identity Server回调MVC站点的地址。路径必须是/signin-oidc这是AddOpenIdConnect中间件默认的回调路径如果你改了中间件的CallbackPath这里要对应改否则回调时协议校验不通过。PostLogoutRedirectUris退出登录后的回调地址我实测中这里配错会导致登出流程卡在“正在退出”页面。AllowOfflineAccess true必须打开否则即使你在请求里加了offline_access作用域Identity Server也会静默忽略导致refresh_token永远拿不到。AllowedScopes注意我加了api1这个自定义API作用域同时包含了OpenID Connect规定的openid、profile、email。如果只配openid不带任何API作用域Hybrid Flow虽然能拿到id_token但令牌端点会返回一个空的访问令牌——这在实际调用API时会让你困惑半天。2.2 身份资源与API资源别漏配任何一个Scope客户端配置之外Identity Server还必须知道它手里有哪些身份资源和API资源。IdentityResource管的是id_token里携带的用户身份声明ApiResource管的是access_token能访问哪些API。我一开始漏配了email身份资源结果MVC端怎么都拿不到用户邮箱最后查了半天才发现是资源这里没配全。public static IEnumerableIdentityResource GetIdentityResources() { return new ListIdentityResource { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email() }; } public static IEnumerableApiResource GetApiResources() { return new ListApiResource { new ApiResource(api1, Demo API) { Scopes { api1 } } }; } public static IEnumerableApiScope GetApiScopes() { return new ListApiScope { new ApiScope(api1, Demo API Scope) { UserClaims { JwtClaimTypes.Role, JwtClaimTypes.Name } } }; }这里有个新旧版本的重要区别IdentityServer4里ApiResource自带Scopes属性但Duende IdentityServer以及IdentityServer4的后期更新要求你用独立的ApiScope类注册API作用域然后ApiResource里引用对应的ApiScope。如果你装的是较新版本但还在用老写法启动时会收到作用域未注册的警告令牌请求就会失败。ApiScope里的UserClaims可以控制access_token里嵌入哪些用户声明。比如我需要角色信息去做API端授权判断就在这里把Role声明加进去。不加的话即使Identity Server用户有role声明令牌里默认也不会带。2.3 启动配置与开发证书这步不做对后面全是坑Startup.cs里加载配置核心就三步public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryApiScopes(Config.GetApiScopes()) .AddInMemoryClients(Config.GetClients()) .AddTestUsers(Config.GetUsers()) .AddDeveloperSigningCredential(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseIdentityServer(); }我必须重点提醒开发证书的问题AddDeveloperSigningCredential()只适合开发环境它会在本地生成一个临时签名密钥用于给id_token和access_token做签名。我见过不止一个同事把这个方法直接搬到生产环境结果每次应用重启所有已签发的令牌全部失效用户集体掉线。生产环境一定要换成正式的证书签名比如.AddSigningCredential(new X509Certificate2(path/to/cert.pfx, password))开发环境下你还需要注意端口配置。我在实操里让Identity Server跑在https://localhost:5001MVC客户端跑在https://localhost:5002API跑在https://localhost:5003。三个端口分开才能模拟真实的多站点部署场景如果都挤在同一个端口上Cookie和重定向逻辑会互相干扰。3. MVC客户端接入OpenID Connect中间件全流程实操3.1 项目初始化与中间件配置MVC客户端这边首先一堆基础的NuGet包是必须的Microsoft.AspNetCore.Authentication.Cookies管理登录后的Cookie会话让用户登录一次后后续请求不再需要重复跟Identity Server通信。Microsoft.AspNetCore.Authentication.OpenIdConnect实现OpenID Connect协议客户端负责发起认证请求、处理回调、校验id_token。Microsoft.IdentityModel.Protocols.OpenIdConnect通常是上面两个包的依赖项如果手动装NuGet包会自动带进来。Startup.cs里的配置是核心我贴出关键部分public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddAuthentication(options { options.DefaultScheme CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect(options { options.Authority https://localhost:5001; options.ClientId mvc_client; options.ClientSecret mvc_client_secret; options.ResponseType code id_token; options.Scope.Clear(); options.Scope.Add(openid); options.Scope.Add(profile); options.Scope.Add(email); options.Scope.Add(api1); options.Scope.Add(offline_access); options.SaveTokens true; options.GetClaimsFromUserInfoEndpoint true; options.Events new OpenIdConnectEvents { OnTicketReceived context { // 在这里可以拿到认证后的ClaimsPrincipal return Task.CompletedTask; } }; }); }这中间有几处是Hybrid Flow成败的关键我展开细说。options.ResponseType code id_token这是整个Hybrid Flow的“开关”。ResponseType告诉Identity Server你现在要什么code id_token的含义是“我要一个授权码同时再附带一个身份令牌”。如果忘了设这个属性或者设成code流程就退化成Authorization Code Flow如果设成id_token又退化成Implicit Flow。只有code id_token才是标准的Hybrid模式。options.SaveTokens true如果你后续需要调用API这一行一定不要漏。它让认证中间件在成功登录后把从令牌端点拿到的access_token和refresh_token自动存进Cookie认证票据里。后面要调API时从HttpContext里把令牌取出来就能直接用。不设这个属性的话令牌用完了就丢了根本没机会传给下游API。options.GetClaimsFromUserInfoEndpoint true登录成功后MVC端默认只解析id_token里携带的声明。但id_token是精简版的很多声明不会全带过来。设了这行之后中间件会自动拿access_token去Identity Server的UserInfo端点拉取完整的用户资料补齐email、name这些常用声明。我实测不设这行时User.Identity.Name可能直接为空因为id_token里不带name声明很多新手在这里卡住。3.2 登录与登出流程的完整时序配置好中间件后认证流程是自动的但你在接页面时还是要理解底层发生了什么否则遇到问题根本没思路。我完整梳理一遍Hybrid Flow在MVC里的时序用户访问MVC站点的某个需要登录的页面MVC里通常用[Authorize]特性标记这个Controller或Action。未登录状态下Cookie认证方案会触发Challenge跳转到DefaultChallengeScheme指定的OpenID Connect方案MVC把我们重定向到Identity Server的/connect/authorize端点请求参数里带着response_typecode id_token、client_id、scope、redirect_uri、state、nonce。用户在Identity Server的登录页输入账号密码Identity Server认证通过后浏览器重定向回MVC的/signin-oidc回调地址URL里带着code和id_token以及state和session_state。OpenID Connect中间件先校验id_token的签名和nonce确认这个令牌确实是Identity Server签发的、确实是对应本次登录请求的。校验通过后中间件解析出身份声明创建ClaimsPrincipal。如果配置了代码换令牌只要ResponseType包含code中间件会自动执行中间件拿着code和client_secret去Identity Server的/connect/token端点换取access_token和refresh_token。中间件把令牌存进Cookie认证票据因为设置了SaveTokenstrue同时GetClaimsFromUserInfoEndpointtrue时还会去UserInfo端点拉取完整声明。最终中间件调用SignInAsync方法把用户身份写进Cookie后续请求就通过Cookie维持登录状态。登出流程同样有讲究。MVC端要支持“退出登录”通常的做法是[HttpPost] [ValidateAntiForgeryToken] public async TaskIActionResult Logout() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); return RedirectToAction(Index, Home); }这里有个容易误解的点SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme)只是清掉本地CookieSignOutAsync(OpenIdConnectDefaults.AuthenticationScheme)才是真正触发单点登出会重定向到Identity Server的/connect/endsession端点让Identity Server也把用户的登录态清掉。两者必须按顺序都调用缺一不可。我第一次只清本地Cookie就以为登出完成了结果用户再点登录Identity Server直接秒回令牌完全没要密码——因为服务端会话根本没结束。3.3 从MVC调用受保护API的细节MVC站点登录成功后页面通常要展示来自下游API的数据。我这里用一个简单场景演示首页上显示一段从api1拉取的受保护文本。public class HomeController : Controller { private readonly IHttpContextAccessor _httpContextAccessor; public HomeController(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor httpContextAccessor; } public async TaskIActionResult Index() { var accessToken await _httpContextAccessor.HttpContext .GetTokenAsync(access_token); if (string.IsNullOrEmpty(accessToken)) { return Content(未获取到访问令牌); } using var httpClient new HttpClient(); httpClient.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, accessToken); var response await httpClient.GetAsync(https://localhost:5003/api/demo); if (response.IsSuccessStatusCode) { var content await response.Content.ReadAsStringAsync(); ViewBag.ApiData content; } else { ViewBag.ApiError response.StatusCode; } return View(); } }GetTokenAsync(access_token)这个扩展方法来自Microsoft.AspNetCore.Authentication命名空间它的实现就是从Cookie认证票据里把之前SaveTokenstrue存进去的令牌取出来。我强调一下这段代码里新建HttpClient只是为了演示生产环境务必使用IHttpClientFactory来管理HttpClient生命周期不然会踩经典的socket耗尽坑。4. 常见问题与排查技巧实录4.1 授权码换令牌失败的典型原因我在实际对接中遇到过好几次“登录页面跳转正常但回调后报错”的情况。最常见的错误是unauthorized_client或invalid_client排查思路按优先级排列客户端ID和密钥是否完全匹配。注意Identity Server端配置密钥用的是Sha256()哈希但MVC端options.ClientSecret填的是明文两者对得上才行。检查一下你复制密钥时是不是带了多余的空格。AllowedGrantTypes是否包含Hybrid。如果配的是Code服务端接收到code id_token的请求会直接拒绝。回调地址是否与RedirectUris完全一致。这里会做精确匹配校验http和https不同、多一个斜杠都会失败。我建议直接把MVC端的CallbackPath显式配出来比如options.CallbackPath /signin-oidc然后Identity Server端用相同的完整URL。response_typecode id_token要求请求里必须带nonceOpenID Connect中间件会自动生成但如果你用自定义方式构建认证请求遗漏nonce也会报错。4.2 id_token签名验证失败的排查经验错误信息类似IDX10501: Signature validation failed. Unable to match key这个问题我遇到得最频繁尤其是第一次搭环境时。根本原因是MVC端无法从Identity Server获取到有效的签名公钥。OpenID Connect中间件启动时要访问{Authority}/.well-known/openid-configuration拉取发现文档里面包含了JWK公钥信息用来验证id_token签名。如果你的MVC客户端部署在内网Identity Server又部署在别的机器上:5001端口没对外开放发现文档拉不下来签名验证必然失败。排查步骤手动访问https://localhost:5001/.well-known/openid-configuration确认能正常返回JSON。检查MVC端options.Authority配置的地址是否真是Identity Server可访问的地址。注意客户端和服务器看到的“localhost”不是同一个地址——如果Identity Server在Docker容器里跑容器外的MVC不能直接用容器内的localhost访问它。开发阶段建议先临时设置options.TokenValidationParameters.ValidateIssuer false仅日志排查用如果能通过验证说明是发行人地址问题而不是签名算法不匹配的问题。4.3 拿不到用户声明或令牌为空的处理思路这一类问题表现多样根因却往往集中在几个点上User.Identity.Name为空这几乎都是因为GetClaimsFromUserInfoEndpoint没开或UserInfo端点配置有问题。开启后再看User.Claims会发现name声明就补齐了。access_token为空检查SaveTokenstrue是否设置以及Scope里是否包含至少一个API作用域。只带openid不带API作用域时Identity Server返回的访问令牌内容是空的。拿不到refresh_token检查客户端配置是否允许AllowOfflineAccess同时MVC端Scope里是否有offline_access。两个条件缺一个令牌响应里都不会有refresh_token。API返回401优先看API用的JWT Bearer认证配置里Audience是否对应API资源名称。Identity Server签发的access_token里有一个aud声明API端校验时要求aud与你配置的Audience一致。我用api1做资源名那API端要显式配options.Audience api1。4.4 过程记录与避坑清单下面这些坑是我多次实操后真正沉淀下来的按重要性排序整理成速查表检查项预期结果踩坑后果ResponseTypecode id_token配错则流程退化成其他类型令牌缺失SaveTokenstrue后续无法用access_token调APIAllowAccessTokensViaBrowserfalse安全降级令牌暴露风险上升GetClaimsFromUserInfoEndpointtrue按需用户声明不全AllowOfflineAccesstrue无refresh_token过期只能重新登录RedirectUris与回调路径完全一致授权回调报错API端Audience匹配资源名401 Unauthorized除了表格里的这些我再补充一个比较容易忽视的点多个MVC客户端同时接入同一个Identity Server时每个客户端的RedirectUris一定要独立配置不要把两个站点的回调地址同时塞进同一个客户端配置里。否则会出现A站点通过B站点的回调地址拿到了令牌但票据里的声明跟A站点对不上最终表现为诡异的“登录成功但跳转后立刻登出”。另外如果你的部署环境里Identity Server是HTTPS而MVC客户端是HTTP本地调试常见Cookie的Secure属性可能导致本地调试时登录状态一直保持不住。本地调试建议临时把Cookie的SecurePolicy设为SameAsRequest或者统一用HTTPS这个细节能省下半小时。5. 一点实操体会最后再说点个人经验。Hybrid Flow这套方案我在多个实际项目里用过第一次配置的时候磕磕绊绊花了整整一天大部分时间都耗在RedirectUris不匹配和SaveTokens漏配这类低级问题上。后来我养成一个习惯在动手写代码之前先在纸上把三个角色的地址、端口、作用域、回调路径全部列出来做成一张表配置的时候照着抄。这个习惯让我后面几次对接几乎是一遍过。如果你做的项目还在技术选型阶段我建议先想清楚自己到底要不要refresh_token。如果MVC站点只是登录用、不调API那用更简单的Authorization Code Flow就够了不必上Hybrid Flow增加配置复杂度。但只要涉及“登录后立即调API拿数据”Hybrid Flow在当前技术栈下依然是最稳的选择。最后分享一个小技巧调试时在MVC端的OnTicketReceived事件里打个断点把context.Principal.Claims和context.Properties.GetTokens()看一眼一半以上的认证问题当场就能定位比翻日志快多了。
返回列表