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

基于.NET Framework的SaaS应用如何通过纯RESTful实现无第三方库的SAML/SSO?

实现无侵入式SAML2单点登录(基于.NET Framework)

核心思路

为避免修改应用启动配置(startup.cs/web.config)且仅依赖微软官方API,可将SAML2逻辑完全封装在独立的HTTP处理单元(Generic Handler/独立页面)中,通过纯HTTP重定向/POST完成完整认证流程:发起认证请求→身份提供商(IdP)验证→回调接收并验证断言,全程不侵入现有应用核心架构。

依赖的微软官方API

仅使用.NET Framework原生组件,无需额外安装任何第三方库:

  • System.Xml/System.Xml.Linq:构建、解析SAML XML结构
  • System.Security.Cryptography.Xml:处理SAML签名与验证(满足国防级审计的安全要求)
  • System.Web:处理HTTP请求、重定向、自动提交表单

分步实现示例

1. 生成SAML认证请求并引导至IdP

创建独立的SamlAuthInitiator.ashx处理器,负责构建SAML AuthnRequest并通过自动提交表单重定向到IdP:

using System;
using System.Web;
using System.Xml.Linq;
using System.Security.Cryptography.Xml;
using System.Security.Cryptography.X509Certificates;

public class SamlAuthInitiator : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        // 从安全配置/数据库读取IdP与SP参数(避免硬编码)
        string idpSsoUrl = "https://your-idp-domain.com/saml/sso";
        string spEntityId = "https://your-saas-app.com/saml/metadata";
        string acsUrl = "https://your-saas-app.com/saml/callback";
        string requestId = "_" + Guid.NewGuid().ToString();
        string issueInstant = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");

        // 构建符合SAML2标准的AuthnRequest XML
        XNamespace saml2 = "urn:oasis:names:tc:SAML:2.0:assertion";
        XNamespace saml2p = "urn:oasis:names:tc:SAML:2.0:protocol";

        var authnRequest = new XElement(saml2p + "AuthnRequest",
            new XAttribute("ID", requestId),
            new XAttribute("Version", "2.0"),
            new XAttribute("IssueInstant", issueInstant),
            new XAttribute("Destination", idpSsoUrl),
            new XAttribute("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"),
            new XAttribute("AssertionConsumerServiceURL", acsUrl),
            new XElement(saml2 + "Issuer", spEntityId),
            new XElement(saml2p + "NameIDPolicy",
                new XAttribute("Format", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"),
                new XAttribute("AllowCreate", "true"))
        );

        // 对请求签名(若IdP要求)
        var xmlDoc = new System.Xml.XmlDocument();
        xmlDoc.LoadXml(authnRequest.ToString());
        SignXmlDocument(xmlDoc, GetSPSigningCert());

        // 编码请求并生成自动提交表单
        string encodedRequest = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(xmlDoc.OuterXml));
        string relayState = context.Request.QueryString["RelayState"] ?? "/";

        context.Response.ContentType = "text/html";
        context.Response.Write($@"
        <html>
            <body onload='document.forms[0].submit()'>
                <form method='POST' action='{HttpUtility.HtmlEncode(idpSsoUrl)}'>
                    <input type='hidden' name='SAMLRequest' value='{HttpUtility.HtmlEncode(encodedRequest)}' />
                    <input type='hidden' name='RelayState' value='{HttpUtility.HtmlEncode(relayState)}' />
                    <p>正在跳转至身份验证提供商...</p>
                </form>
            </body>
        </html>");
    }

    // 从安全存储读取SP签名证书
    private X509Certificate2 GetSPSigningCert()
    {
        return new X509Certificate2(
            context.Server.MapPath("~/App_Data/SpsSigningCert.pfx"), 
            "your-cert-password",
            X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet
        );
    }

    // 给XML文档添加数字签名
    private void SignXmlDocument(System.Xml.XmlDocument doc, X509Certificate2 cert)
    {
        var signedXml = new SignedXml(doc);
        signedXml.SigningKey = cert.PrivateKey;

        var reference = new Reference { Uri = "#" + doc.DocumentElement.GetAttribute("ID") };
        reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
        reference.AddTransform(new XmlDsigExcC14NTransform());

        signedXml.AddReference(reference);
        signedXml.KeyInfo = new KeyInfo();
        signedXml.KeyInfo.AddClause(new KeyInfoX509Data(cert));

        signedXml.ComputeSignature();
        var signatureNode = signedXml.GetXml();
        doc.DocumentElement.AppendChild(doc.ImportNode(signatureNode, true));
    }

    public bool IsReusable => false;
}

2. 处理IdP回调并验证SAML断言

创建SamlCallback.ashx处理器,负责接收IdP的POST响应,验证签名并提取用户信息:

using System;
using System.Web;
using System.Xml.Linq;
using System.Security.Cryptography.Xml;
using System.Security.Cryptography.X509Certificates;

public class SamlCallback : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        // 获取IdP发送的SAML响应
        string samlResponse = context.Request.Form["SAMLResponse"];
        string relayState = context.Request.Form["RelayState"] ?? "/";

        if (string.IsNullOrEmpty(samlResponse))
        {
            context.Response.Redirect("/Login?error=invalid_saml_response");
            return;
        }

        // 解码SAML响应XML
        byte[] decodedBytes = Convert.FromBase64String(samlResponse);
        string xmlContent = System.Text.Encoding.UTF8.GetString(decodedBytes);
        var xmlDoc = new System.Xml.XmlDocument();
        xmlDoc.LoadXml(xmlContent);

        // 验证IdP签名
        if (!VerifyXmlSignature(xmlDoc, GetIdpPublicKey()))
        {
            context.Response.Redirect("/Login?error=invalid_saml_signature");
            return;
        }

        // 解析SAML断言,提取用户标识与属性
        XNamespace saml2 = "urn:oasis:names:tc:SAML:2.0:assertion";
        var xDoc = XDocument.Parse(xmlContent);
        var assertion = xDoc.Descendants(saml2 + "Assertion").FirstOrDefault();
        
        if (assertion == null)
        {
            context.Response.Redirect("/Login?error=no_saml_assertion");
            return;
        }

        string userId = assertion.Descendants(saml2 + "NameID").FirstOrDefault()?.Value;
        string userEmail = assertion.Descendants(saml2 + "Attribute")
            .Where(a => a.Attribute("Name")?.Value == "email")
            .Select(a => a.Descendants(saml2 + "AttributeValue").FirstOrDefault()?.Value)
            .FirstOrDefault();

        // 复用现有登录逻辑创建用户会话
        if (!string.IsNullOrEmpty(userId))
        {
            YourAppAuthHelper.SignInUser(userId, userEmail);
            context.Response.Redirect(relayState);
        }
        else
        {
            context.Response.Redirect("/Login?error=user_not_found");
        }
    }

    // 读取IdP公钥证书(从安全存储/IdP元数据获取)
    private X509Certificate2 GetIdpPublicKey()
    {
        byte[] certBytes = System.IO.File.ReadAllBytes(context.Server.MapPath("~/App_Data/IdpPublicKey.cer"));
        return new X509Certificate2(certBytes);
    }

    // 验证XML签名有效性
    private bool VerifyXmlSignature(System.Xml.XmlDocument doc, X509Certificate2 cert)
    {
        var signedXml = new SignedXml(doc);
        var signatureNodes = doc.GetElementsByTagName("Signature");
        
        if (signatureNodes.Count == 0) return false;
        
        signedXml.LoadXml((XmlElement)signatureNodes[0]);
        return signedXml.CheckSignature(cert, true);
    }

    public bool IsReusable => false;
}

关键合规与兼容性注意事项

  • 审计要求:所有证书、IdP配置需存储在加密的配置系统或安全数据库中,禁止硬编码;完整记录所有SAML交互日志(请求ID、时间、用户标识等)。
  • 元数据支持:若IdP需要SP元数据,可单独创建SamlMetadata.ashx处理器,动态生成符合SAML2标准的元数据XML,无需修改全局配置。
  • IdP适配:根据目标IdP的要求调整AuthnRequest参数(如NameID格式、签名算法),部分IdP可能需要对SAML请求进行DEFLATE压缩(使用System.IO.Compression.DeflateStream实现)。
  • 无侵入性:所有SAML逻辑均封装在独立处理器中,不修改现有startup.cs/web.config,OAuth2与直接登录逻辑完全不受影响。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 05:24:20