You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于ITfoxtec SAML 2.0与ASP.NET Core MVC的IDP登录跳转实现问询

问题:基于ITfoxtec Identity SAML 2.0与ASP.NET Core MVC构建IDP时的登录页面跳转处理

我正在用ITfoxtec Identity SAML 2.0和ASP.NET Core MVC搭建基于Cookie认证的SAML SSO身份提供商(IDP)。需求是当服务提供商(SP)发送SAML认证请求时,IDP自动跳转到登录页面,验证用户凭据后再把用户重定向回SP并返回SAML响应。想请教怎么在GUI中处理用户登录逻辑,实现跳转至登录页面的功能?

以下是我当前的SAML登录方法代码:

public async Task<IActionResult> Login()
{
    bool isSamlRequest = HttpContext.Request.Query.ContainsKey("SAMLRequest")
        || HttpContext.Request.Headers.ContainsKey("SAMLRequest");

    if(!isSamlRequest)
        return RedirectToAction(actionName:"login",controllerName: "Account");
    
    var requestBinding = new Saml2RedirectBinding();
    var relyingParty = await ValidateRelyingParty(ReadRelyingPartyFromLoginRequest(requestBinding));

    var saml2AuthnRequest = new Saml2AuthnRequest(GetRpSaml2Configuration(relyingParty));
    try
    {
        requestBinding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnRequest);

        // ****  Handle user login e.g. in GUI ****
        // Test user with session index and claims
        var sessionIndex = Guid.NewGuid().ToString();
        var claims = CreateTestUserClaims(saml2AuthnRequest.Subject?.NameID?.ID);

        return LoginResponse(saml2AuthnRequest.Id, Saml2StatusCodes.Success, requestBinding.RelayState, relyingParty, sessionIndex, claims);
    }
    catch (Exception exc)
    {
        return LoginResponse(saml2AuthnRequest.Id, Saml2StatusCodes.Responder, requestBinding.RelayState, relyingParty);
    }
}

解决方案

核心思路:收到SP的SAML请求后,先保存请求上下文到会话,跳转至登录页面;用户登录验证通过后,从会话中取出上下文生成SAML响应返回给SP。

1. 修改SAML登录方法,保存请求上下文并跳转登录页

替换原代码中直接生成测试用户的逻辑,改为保存请求信息后跳转登录页面:

public async Task<IActionResult> Login()
{
    bool isSamlRequest = HttpContext.Request.Query.ContainsKey("SAMLRequest")
        || HttpContext.Request.Headers.ContainsKey("SAMLRequest");

    if (!isSamlRequest)
        return RedirectToAction("Login", "Account");
    
    var requestBinding = new Saml2RedirectBinding();
    var relyingParty = await ValidateRelyingParty(ReadRelyingPartyFromLoginRequest(requestBinding));

    var saml2AuthnRequest = new Saml2AuthnRequest(GetRpSaml2Configuration(relyingParty));
    try
    {
        requestBinding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnRequest);

        // 将SAML请求关键信息存入会话,用于登录完成后生成响应
        HttpContext.Session.SetString("SamlAuthnRequestId", saml2AuthnRequest.Id.ToString());
        HttpContext.Session.SetString("RelayState", requestBinding.RelayState);
        // 假设你的RelyingParty有唯一标识Id,保存到会话以便后续获取
        HttpContext.Session.SetString("RelyingPartyId", relyingParty.Id);

        // 跳转到Account控制器的登录页面
        return RedirectToAction("Login", "Account");
    }
    catch (Exception exc)
    {
        return LoginResponse(saml2AuthnRequest.Id, Saml2StatusCodes.Responder, requestBinding.RelayState, relyingParty);
    }
}

2. 实现Account控制器的登录逻辑

在AccountController中处理登录页面展示和表单提交:

// 展示登录页面
public IActionResult Login(string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    return View();
}

// 处理登录表单提交
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    // 替换为你的实际用户凭据验证逻辑(比如用Identity框架或自定义验证)
    var user = await _userManager.FindByNameAsync(model.UserName);
    if (user == null || !await _userManager.CheckPasswordAsync(user, model.Password))
    {
        ModelState.AddModelError(string.Empty, "无效的登录凭据");
        return View(model);
    }

    // 生成用户Claims
    var claims = await _userManager.GetClaimsAsync(user);
    var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    var authProperties = new AuthenticationProperties { IsPersistent = model.RememberMe };

    // 完成Cookie认证
    await HttpContext.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme,
        new ClaimsPrincipal(claimsIdentity),
        authProperties);

    // 检查会话中是否有SAML请求上下文
    var samlAuthnRequestId = HttpContext.Session.GetString("SamlAuthnRequestId");
    var relayState = HttpContext.Session.GetString("RelayState");
    var relyingPartyId = HttpContext.Session.GetString("RelyingPartyId");

    if (!string.IsNullOrEmpty(samlAuthnRequestId) && !string.IsNullOrEmpty(relyingPartyId))
    {
        // 清理会话中的临时数据
        HttpContext.Session.Remove("SamlAuthnRequestId");
        HttpContext.Session.Remove("RelayState");
        HttpContext.Session.Remove("RelyingPartyId");

        // 根据Id获取对应的RelyingParty配置
        var relyingParty = await GetRelyingPartyById(relyingPartyId);
        var sessionIndex = Guid.NewGuid().ToString();

        // 返回SAML响应到SP
        return LoginResponse(new Saml2Id(samlAuthnRequestId), Saml2StatusCodes.Success, relayState, relyingParty, sessionIndex, claims);
    }

    // 无SAML请求时,跳转到默认页面
    return RedirectToLocal(returnUrl);
}

// 辅助方法:安全跳转本地页面
private IActionResult RedirectToLocal(string returnUrl)
{
    if (Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }
    return RedirectToAction("Index", "Home");
}

3. 确保项目启用会话

在Program.cs中添加会话配置(需放在认证中间件之前):

// 注册会话服务
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30); // 设置会话超时时间
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

// ...

// 启用会话中间件
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();

内容的提问来源于stack exchange,提问作者Hjal Abderrazak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 21:00:34