Files
Atomx/Atomx.Admin/Atomx.Admin/Extensions/AuthorizationExtension.cs
2025-12-04 19:07:04 +08:00

151 lines
7.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Atomx.Admin.Utils;
using Atomx.Common.Constants;
using Atomx.Common.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace Atomx.Admin.Extensions
{
/// <summary>
/// AuthorizationExtension 重构说明:
/// - 为混合部署Blazor Server + WASM与跨站点场景优化 Challenge 行为:
/// * 对浏览器导航/页面请求执行重定向到登录页(保持友好体验)
/// * 对 API / XHR / Fetch / SignalR 请求返回 401 JSON避免 HTML 重定向导致前端错误)
/// - 保持 JwtBearer 对 SignalR query string 的读取
/// - Cookie 的 SecurePolicy 根据环境设置SameSite 使用 Lax可在跨站点场景改为 None 并开启 AllowCredentials
/// </summary>
public static class AuthorizationExtension
{
public static void AddAuthorize(this IServiceCollection services, IConfiguration Configuration, IWebHostEnvironment environment)
{
var jwtSetting = Configuration.GetSection("Authentication:JwtBearer").Get<JwtSetting>();
if (jwtSetting == null)
{
throw new Exception("缺少配置信息 Authentication:JwtBearer");
}
services.AddSingleton(jwtSetting);
services.AddAuthentication(options =>
{
// 为混合 Server + API 场景选择:默认用于认证/读取身份的方案为 CookieServer 端 Circuit 使用)
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// 当需要挑战Challenge时使用 JwtBearer 的行为(其 OnChallenge 可做重定向处理)
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
// JwtBearer 保持用于 API token 校验
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.RequireHttpsMetadata = !environment.IsDevelopment();
options.SaveToken = true;
options.ClaimsIssuer = jwtSetting.Issuer;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = jwtSetting.Audience,
ValidIssuer = jwtSetting.Issuer,
ClockSkew = TimeSpan.FromMinutes(Convert.ToDouble(jwtSetting.ClockSkew)),
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.SecurityKey))
};
// 允许从 query string 中获取 access_token用于 SignalR / WebSocket
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"].FirstOrDefault();
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/hubs") || path.StartsWithSegments("/hub") || path.StartsWithSegments("/api")))
{
context.Token = accessToken;
}
return Task.CompletedTask;
},
OnChallenge = async context =>
{
// 当 JwtChallenge 触发时,根据请求类型决定行为:
// - 浏览器导航 / Accept:text/html页面请求 => 重定向到登录页
// - API / XHR / JSON 请求 => 返回 401 JSON
try
{
var request = context.Request;
var accept = request.Headers["Accept"].FirstOrDefault() ?? string.Empty;
var isApiRequest = request.Path.StartsWithSegments("/api") || request.Path.StartsWithSegments("/hubs") || request.Headers["X-Requested-With"] == "XMLHttpRequest";
var expectsHtml = accept.Contains("text/html", StringComparison.OrdinalIgnoreCase);
context.HandleResponse();
if (!isApiRequest && expectsHtml)
{
var absoluteUri = $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}";
context.Response.Redirect($"/account/login?returnUrl={Uri.EscapeDataString(absoluteUri)}");
}
else
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json; charset=utf-8";
var payload = new { success = false, message = "Unauthorized" };
await context.Response.WriteAsJsonAsync(payload);
}
}
catch
{
// 兜底:返回 401
context.HandleResponse();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
},
OnAuthenticationFailed = context =>
{
if (context.Exception?.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Append("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
})
// 明确配置 Cookie用于 Server 交互渲染Circuit 使用 Cookie 进行认证)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
// LoginPath/AccessDeniedPath 可根据需要指定路由
options.LoginPath = "/account/login";
options.AccessDeniedPath = "/account/login";
// Cookie 安全策略HttpOnly + SameSite = Lax避免 Strict 在重定向/导航时不发送)
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
// 根据环境调整 SecurePolicy生产环境强制 HTTPS
options.Cookie.SecurePolicy = environment.IsDevelopment()
? CookieSecurePolicy.SameAsRequest
: CookieSecurePolicy.Always;
options.Cookie.Path = "/";
// 当使用 Blazor Server 时Cookie 名称可以指定(可选)
// options.Cookie.Name = "AtomxAuth";
// 控制到期与滑动过期
options.ExpireTimeSpan = TimeSpan.FromDays(7);
options.SlidingExpiration = true;
});
// 注册基于权限的策略
services.AddAuthorization(options =>
{
var allPermissions = Permissions.GetAllPermissions();
foreach (var permission in allPermissions)
{
options.AddPolicy(permission, policy => policy.Requirements.Add(new PermissionRequirement(permission)));
}
});
}
}
}