三层服务架构下OAuth2授权码流程Session异常问题咨询
授权码流程跨服务会话丢失问题
架构概述
- 第一部分:基于C# Web API的自定义授权服务器,遵循授权码流程返回令牌,负责用户信息处理。
- 第二部分:基于TypeScript的React前端,包含授权服务器管理面板和登录表单页面。
- 第三部分:ASP.NET Core MVC应用(单实例),兼具前后端功能。
问题复现流程
- 访问
https://third.com/verysecretpage时,授权过滤器检查Session中是否存在令牌,若不存在则重定向到React登录页https://second:3000/auth?clientId=b4db6042-ae11-47ca-a1e7-c737fb0ee7f1&state=987654321,过滤器代码如下:
public void OnAuthorization(AuthorizationFilterContext context) { if (IsAnonymousAction(context)) return; string? accessToken = context.HttpContext.Session.GetString("accessToken"); if (string.IsNullOrEmpty(accessToken)) context.Result = new RedirectResult($"https://second:3000/auth?clientId=b4db6042-ae11-47ca-a1e7-c737fb0ee7f1&state=987654321"); } private static bool IsAnonymousAction(AuthorizationFilterContext filterContext) { return filterContext.ActionDescriptor.EndpointMetadata.Any(em => em.GetType() == typeof(AllowAnonymousAttribute)); }
- 在React登录页提交用户名密码后,向授权服务器发起请求,代码如下:
async function handleSubmit(event: React.FormEvent<HTMLFormElement>): Promise<void> { event.preventDefault(); const formData = new FormData(event.currentTarget); const username = formData.get("username")!.toString(); const password = formData.get("password")!.toString(); const fetchURL = "https://first.com/auth-code"; const data = { "responseType": "code", // 此处传递查询参数中的clientId和state "clientId": clientId, "state": state }; await fetch(fetchURL, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', 'Authorization': 'Basic ' + btoa(`${username}:${password}`), }, redirect: "follow" }).then( response => { window.location.href = "https://third.com/verysecretpage" }); }
- 授权服务器返回302重定向到
https://third.com/receive-tokens,该端点成功获取并保存令牌到Session,代码如下:
[Route("/receive-tokens")] [AllowAnonymous] [EnableCors("Interface")] public async Task<IActionResult> ReceiveTokens(string code, string? status = null) { var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Post, "https://first.com/token"); var content = new StringContent("{" + $"\n \"grantType\": \"authorization_code\",\n \"code\": \"{code}\",\n \"clientId\": \"b4db6042-ae11-47ca-a1e7-c737fb0ee7f1\"\n" + "}", null, "application/json"); request.Content = content; var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); // 授权服务器通过HttpOnly Cookie发送RefreshToken,此处获取 var refreshToken = CookieGetter.GetRefreshTokenCookie(response); string responseContents = await response.Content.ReadAsStringAsync(); var accessTokenResponse = JsonSerializer.Deserialize<AccessTokenResponse>(responseContents, jsonSerializerOptions)!; var tokensResponse = new TokensResponse { AccessTokenResponse = accessTokenResponse, RefreshToken = refreshToken }; auth.SaveTokenValues(tokensResponse); return Ok(); } // 来自Models中的Auth类 public void SaveTokenValues(TokensResponse tokensResponse) { _contextAccessor.HttpContext!.Session.SetString("accessToken", tokensResponse.AccessTokenResponse.AccessToken); _contextAccessor.HttpContext.Session.SetString("expiresAt", tokensResponse.AccessTokenResponse.ExpiresAt.ToString()); _contextAccessor.HttpContext.Session.SetString("refreshToken", tokensResponse.RefreshToken); } // 上述用到的部分类 public class TokensResponse { public required AccessTokenResponse AccessTokenResponse { get; set; } public required string RefreshToken { get; set; } } public class AccessTokenResponse { public required string TokenType { get; set; } public required string AccessToken { get; set; } public required DateTime ExpiresAt { get; set; } }
Session配置:
builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(10); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; });
- React收到200响应后重定向到
https://third.com/verysecretpage,但此时Session中无令牌,导致循环重定向。
疑问点
- ASP.NET Core的Session是否仅在当前请求上下文有效,跨域名/服务后无法延续?
- 是否应该改用ASP.NET Core内置授权机制?
- 有没有跨服务/域名保存会话的方法?
- 能否让React不等待
receive-tokens的响应,直接重定向到MVC页面完成令牌保存? - 是否必须将登录页放在ASP.NET Core端,不能单独用React客户端?
内容的提问来源于stack exchange,提问作者TyuJas
相关产品推荐
相关产品推荐

