我的MVC网站遇到了问题。我们使用Shibboleth和Kerberos根据用户的网络ID处理自动登录。我们配置Shibboleth以便它保护整个网站,减去一些匿名允许的页面以显示错误或访问被拒绝的消息。
如果您尝试导航到网站的根目录,系统会很有效。它转发给Shibboleth进行身份验证,然后使用包含用户ID的Headers转发回我的站点,然后我们根据用户和权限的数据库列表进行授权。
如果第一次导航到/ Home以外的任何页面,则会出现问题。重定向步骤如下。请注意,这是一个内部网站,我更改了域名,因此请勿尝试以下任何实际链接。他们不会工作。
更新:Kestrel Web服务器是将我们传递给HTTP的服务器,而前面的Apache负载均衡器猛扑进去并说“不允许http,请转到https”。
1)用户导航到https://ets.com/ets/Employees,并由于Shibboleth的站点范围保护而收到302。这指向我们的IDP服务器的SSOService.php?SAMLResponse = giantquerystringhere根据登录到计算机的用户ID执行自动登录。
2)Shibboleth验证然后转发到https://ets.com/Shibboleth.sso/SAML2/POST的POST事件,该事件用标题填充标题。
3)POST事件完成并按预期将用户转发到https://ets.com/ets/Employees。
4)但我们需要在应用程序本身处理登录,因此Kestrel服务器调用302并将用户转发到http://ets.com/ets/Home/Index?ReturnUrl=%2Fets%2FEmployees INSTEAD的https
5)Apache LB接受了这一点,并且由于不允许HTTP,它调用301(永久移动)并转发到https://ets.com/ets/Home/Index?ReturnUrl=%252Fets%252FEmployees(注意双编码查询字符串)
6)该URL响应302,处理返回URL,并将用户转发到https://ets.com/ets/Home/%2Fets%2FEmployees的错误网址,而不是https://ets.com/ets/Employees
有没有办法阻止它首先转发到http?我已经尝试了一些我在搜索中看到的“强制HTTPS”方法,但是大多数这些方法都需要改变才能打破Shibboleth集成,或者只是不起作用。 (强制依赖ASP身份,URL重写等)
我的问题是,为什么Kestrel / MVC转发到HTTP呢?配置错误了吗?有没有选项可以防止这种情况发生?我无法更改我的登录机制,但我可以更改处理Shibboleth响应的方式并处理应用程序端的登录。
我会尝试包含相关代码。如果有任何其他信息可以提供帮助,请告诉我。
Startup.cs
public class Startup
{
public static IConfigurationRoot Configuration { get; private set; }
public Startup(IHostingEnvironment env) {
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services) {
services.AddMvc(options => {
// Only allow authenticated users.
var defaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(defaultPolicy));
});
// Authorization policies.
services.AddAuthorization(options => {
options.AddPolicy("EditPolicy", policy => {
policy.RequireClaim("readwrite", "true");
});
options.AddPolicy("AdminPolicy", policy => {
policy.RequireClaim("admin", "true");
});
options.AddPolicy("SuperAdminPolicy", policy => {
policy.RequireClaim("superadmin", "true");
});
});
services.AddDbContext<LoanDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("ETS_Web")));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, LoanDbContext context) {
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseStatusCodePagesWithReExecute("/Home/Error/{0}");
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
} else {
app.UseExceptionHandler("/Home/Error");
}
app.UseExceptionLogger(loggerFactory); // Custom middleware to log exceptions. Rethrows the exception for Exception Handler/Page
// Forces the website to use en-US locale, no matter what the browser settings are.
// This is done to enforce date format of mm/dd/yyyy (Canada uses dd/mm/yyyy)
var supportedCultures = new[] {
new CultureInfo("en-US")
};
app.UseRequestLocalization(new RequestLocalizationOptions {
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = supportedCultures, // Formatting numbers, dates, etc.
SupportedUICultures = supportedCultures // UI strings that may be localized.
});
app.UseStaticFiles(); // Allows serving pages that don't belong to a controller.
app.UseCookieAuthentication(new CookieAuthenticationOptions() {
CookieName = $"ETS.Web.{env.EnvironmentName.ToLower()}",
AuthenticationScheme = "ETSCookieAuth",
LoginPath = new PathString("/Home/Index"),
AccessDeniedPath = new PathString("/Home/AccessDenied"),
AutomaticAuthenticate = true,
AutomaticChallenge = true,
ExpireTimeSpan = TimeSpan.FromHours(2),
SlidingExpiration = true
});
app.UseMvc(routes => {
routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(name: "loan", template: "{controller=LoanRequests}/{action=Index}/{id?}");
routes.MapRoute(name: "budget", template: "{controller=Budgets}/{action=Index}/{id?}");
routes.MapRoute(name: "company", template: "{controller=Companies}/{action=Index}/{id?}");
routes.MapRoute(name: "employee", template: "{controller=Employees}/{action=Index}/{id?}");
routes.MapRoute(name: "product", template: "{controller=Products}/{action=Index}/{productID?}");
routes.MapRoute(name: "inventory", template: "{controller=Inventory}/{action=Index}/{productID?}");
routes.MapRoute(name: "salesarea", template: "{controller=SalesAreas}/{action=Index}/{id?}");
routes.MapRoute(name: "reports", template: "{controller=Reports}/{action=Index}/{id?}");
});
}
HomeController.cs
public class HomeController : Controller
{
private const string SHIB_HEAD_IDP = "IDP_uid";
private readonly LoanDbContext _context;
public HomeController(LoanDbContext context) {
_context = context;
}
[AllowAnonymous]
public async Task<IActionResult> Index(string returnUrl = null) {
string ldapID = "";
if (HttpContext.Request.Headers.ContainsKey(SHIB_HEAD_IDP)) { // In a hosted environment, the Shibboleth IDP will automatically log the user in.
ldapID = HttpContext.Request.Headers[SHIB_HEAD_IDP].ToString();
} else if (HttpContext.Request.Host.ToString().Contains("localhost")) { // For a Development machine, there is no Shibboleth to read Headers from.
ldapID = "FallbackIdNumber";
}
if (!string.IsNullOrWhiteSpace(ldapID)) {
Employee loginUser = await _context.Employees.SingleOrDefaultAsync(m => m.LdapID == ldapID);
if (loginUser?.AccessLevel == AccessType.Disabled)
return RedirectToAction("AccessDenied"); // If the user isn't in the DB, or is Disabled, they are not authorized to access ETS.
List<Claim> claims = new List<Claim>
{
new Claim("id", loginUser.LdapID),
new Claim("name", loginUser.FullName),
new Claim("email", loginUser.Email),
new Claim("role", loginUser.AccessLevel.ToString()),
new Claim("department", loginUser.Department),
new Claim("readonly", (loginUser.AccessLevel >= AccessType.ReadOnly).ToString()),
new Claim("readwrite", (loginUser.AccessLevel >= AccessType.ReadWrite).ToString()),
new Claim("admin", (loginUser.AccessLevel >= AccessType.Admin).ToString()),
new Claim("superadmin", (loginUser.AccessLevel >= AccessType.SuperAdmin).ToString())
};
// Update the Employee's Last Login time
loginUser.LastLogin = DateTime.UtcNow;
await _context.SaveChangesAsync();
// Create a cookie to hold the user's login details
var id = new ClaimsIdentity(claims, "local", "name", "role");
await HttpContext.Authentication.SignInAsync("ETSCookieAuth", new ClaimsPrincipal(id));
if (string.IsNullOrWhiteSpace(returnUrl))
return RedirectToAction("Login");
else {
// May require multiple Decodes to change characters like "%252F" to "%2F" to "/" This code seems not to help.
if (returnUrl.Contains("%")) {
returnUrl = System.Net.WebUtility.UrlDecode(returnUrl);
if (returnUrl.Contains("%"))
returnUrl = System.Net.WebUtility.UrlDecode(returnUrl);
}
return Redirect(returnUrl);
}
}
return RedirectToAction("AccessDenied");
}
[HttpGet]
public IActionResult Login(string returnUrl = null) {
ViewData["ReturnUrl"] = System.Net.WebUtility.UrlDecode(returnUrl);
return View();
}
[AllowAnonymous]
public async Task<IActionResult> Logout() {
await HttpContext.Authentication.SignOutAsync("ETSCookieAuth");
return View();
}
[AllowAnonymous]
public IActionResult Error(int id = 0) {
string pageTitle = "An Unhandled Exception Occurred";
string userMessage = "";
string returnUrl = "/Home";
string routeWhereExceptionOccurred;
try {
if (id > 0) {
userMessage = GetErrorTextFromCode(id);
pageTitle = $"An HTTP Error {id} has occurred; {userMessage}";
}
// Get the details of the exception that occurred
var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionFeature != null) {
routeWhereExceptionOccurred = exceptionFeature.Path;
Exception ex = exceptionFeature.Error;
if (ex is ArgumentException argEx) {
userMessage = argEx.Message;
} else if (ex is InvalidOperationException ioEx) {
userMessage = "An error occurred while trying to fetch a single item from the database. Either none, or more than one were found. "
+ "This may require manual database changes to fix.";
} else if (ex is System.Data.SqlClient.SqlException sqlEx) {
userMessage = $"A SQL database error occurred. Error Number {sqlEx.Number}";
} else if (ex is NullReferenceException nullEx) {
userMessage = $"A Null Reference error occurred. Source: {nullEx.Source}.";
} else if (ex is DbUpdateConcurrencyException dbEx) {
userMessage = "An error occurred while trying to update your item in the database. This is usually due to someone else modifying the item since you loaded it. Please try again.";
} else {
userMessage = "An unhandled exception has occurred.";
}
}
} catch (Exception) {
pageTitle = "Your Error Cannot Be Displayed";
userMessage = "An unknown error occurred while trying to process your error. The original error has been logged, but please report this one as it has not been logged.";
// TODO: Log the exception
} finally {
// Set up the Error page by passing data into the ViewBag/ViewData
ViewBag.Title = pageTitle;
ViewBag.UserMessage = userMessage;
ViewBag.ReturnURL = returnUrl;
}
return View();
}
[AllowAnonymous]
public IActionResult AccessDenied() {
return View();
}
答案 0 :(得分:0)
在aspnet核心中,我设法通过以下代码解决了这个问题。
app.Use(async (ctx, next) =>
{
ctx.Request.Scheme = "https";
await next();
});
ForwardedHeadersOptions forwardOptions = new
ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto,
RequireHeaderSymmetry = false
};
forwardOptions.KnownNetworks.Clear();
forwardOptions.KnownProxies.Clear();
app.UseForwardedHeaders(forwardOptions);
设置: 我的 Web 应用程序位于 traefik 反向代理后面,它使用 letencrypt 证书处理 https 请求。对我托管在 kestrel 中的网络应用程序的调用是 https。
当用户尝试调用带有 Authorize 属性修饰的 Action 时,kestrel 过去常常重定向到身份服务器以使用 http 方案登录,这导致了问题,因为 google/facebook 在生产中不再支持 http。
我将传入请求的方案更改为 https ,这应该是安全的,因为 traefik 已经处理了来自互联网的公共请求,现在只知道 http 的 kestrel 被迫从上面的代码转换为 https 方案。同样,我的身份服务器也位于处理身份验证请求的 traefik 反向代理后面。
注意:我的应用没有仅限 http 的端点。