asp.net程序通过Microsoft Azure中SAML协议实现单点登录

1. 新建应用程序
登录Azure门户,进入左侧菜单“企业应用程序--所有应用程序”,点“新建应用程序”, 继续点“创建你自己的应用程序”,如下图选择和录入名称:

填好应用的名称、想要如何处理应用程序 必须选择第三个“继承未在库中找到的任何其他应用程序(非库)”,之后点“创建”按钮;

2. 单一登录设置
继续1中步骤,进入左侧菜单“单一登录”,选择单一登录方法为“SAML”,如下图:

继续,编辑“基本SAML配置”,如下图:

其中:
** 标识符(实体ID)**,从进入左侧菜单“应用注册”,双击进入该应用,进入左侧菜单“公开API”里复制,如下图:

回执URL,就是你自己web程序中用来处理响应数据的页面,参见后面步骤中的Response.aspx页面;

3、将用户增加到该应用中,此处不赘述;

4、idp--->sp模式测试:

4.1. Azure管理台中操作如下图:

上图中,点击“测试登陆”,之后按照浏览器中显示内容,输入用户名密码,登录Azure成功后会重定向到4.2中Response.aspx页面;

4.2. 我的网站Response.aspx页面中处理如下(仅仅解析数据,不做验签):

点击查看源代码
 `  
   //只解析XML中的用户唯一ID(NameID),不验签
   protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            try
            {  
                string key = "SAMLResponse";
                if (Request.Form.GetValues(key) != null)
                {
                    string sourceSamlResponseXml = Request.Form.GetValues(key)[0];  
                    string samlResponseXml = Encoding.UTF8.GetString(Convert.FromBase64String(sourceSamlResponseXml));  
                    txtSAMLResponse.Text = samlResponseXml;

                    XmlDocument xmlDoc = new XmlDocument();
                    xmlDoc.LoadXml(samlResponseXml);
                    //命名空间
                    XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
                    xmlNamespaceManager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol");
                    xmlNamespaceManager.AddNamespace("assertion", "urn:oasis:names:tc:SAML:2.0:assertion");
                    //只读取数据,不验证数据的完整性(不验签)
                    string loginNo = xmlDoc.SelectSingleNode("/samlp:Response/assertion:Assertion/assertion:Subject/assertion:NameID", xmlNamespaceManager).InnerXml;

                    Response.Write("SP端收到的用户名为:" + loginNo); 
                    txtNameID.Text = loginNo;
                }
                else
                {
                    Response.Write("非法访问,不能直接浏览本页面!");
                }
            }
            catch (Exception ex)
            {
                Response.Write("异常:" + ex.Message);
            }
        }
    }`

5、sp--->idp 模式测试:

由我网站端发起:
5.1. 请求登陆的字符,点击查看源代码:

<samlp:AuthnRequest
  xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
  ID="{0}"
  Version="2.0" IssueInstant="2013-03-18T03:28:54.1839884Z"
  xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" > 
  <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">{1}</Issuer> 
</samlp:AuthnRequest> 

其中:
ID,Azure AD 使用此属性来填充返回的响应的 InResponseTo 属性。 ID 的开头不能是数字,因此常见的策略是在 GUID 的字符串表示形式前面加上类似于“ID”的字符串。 例如,id6c1c178c166d486687be4aaf5e482730 是有效的 ID。

Issuer,必须与 Azure AD 中云服务的一个 ServicePrincipalNames 完全匹配。 通常,此参数设置为应用程序注册期间指定的应用 ID URI。参照2中实体ID;

重定向URL,从进入左侧菜单“应用注册”后,最上面的“终结点”中获取;

5.2. 重定向时:

点击查看源代码
` protected void Button1_Click(object sender, EventArgs e)
    {
        string requestData = ReadData();
        requestData = string.Format(requestData, "id" + Guid.NewGuid().ToString("N"), "http://XXXXXXX.com/Response.aspx", "https://sts.windows.net/40cfad67-3660-44d8-9f47-XXXXXXXXXXX");

        string requestDataDo = EncodeSamlAuthnRequest(requestData);
        string requestUr = "https://login.microsoftonline.com/40cfad67-3660-44d8-9f47-XXXXXXXXX/saml2?SAMLRequest=" + requestDataDo; 

        Response.Redirect(requestUr, false);//必须增加第二个参数false,否则报:线程正在终止 的异常; 
    }

    //先压缩后转base64字符串
    public static string EncodeSamlAuthnRequest(string authnRequest)
    {
        var bytes = Encoding.UTF8.GetBytes(authnRequest);
        using (var output = new MemoryStream())
        {
            using (var zip = new DeflateStream(output, CompressionMode.Compress))
            {
                zip.Write(bytes, 0, bytes.Length);
            }
            var base64 = Convert.ToBase64String(output.ToArray());
            return HttpUtility.UrlEncode(base64);
        }
    }

    private string ReadData()
    {
        string returnData = string.Empty;
        string filepath = Server.MapPath("") + "\\AuthnRequest.txt";
        using (FileStream fs = new FileStream(filepath, FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite, FileShare.ReadWrite))
        {
            using (StreamReader sr = new StreamReader(fs, Encoding.UTF8))
            {
                returnData = sr.ReadToEnd().ToString();
            }
        }
        return returnData;
    }`
AuthnRequest.txt的内容:参照5.1中介绍;
 

5.3. 重定向后:
重定向后,浏览器跳转到Azure网站,按照提示输入用户名和密码,登陆后,会重定向到4中的回调Response.aspx页面中,此页面能解析到用户名NameID;

5.4. 注销请求:

点击查看源代码
` /// <summary>
    /// 注销,Azure
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    protected void btnCancel_Click(object sender, EventArgs e)
    {
        if (string.IsNullOrEmpty(txtNameID.Text.Trim()))
        {
            Response.Write("NameID不能为空!");
            return;
        }
        string requestData = ReadData();

        DateTime dtUtc
            = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second, DateTimeKind.Utc);
        string issueInstant = dtUtc.ToString("o");//这是具有 UTC 值和往返格式(“o”)的日期时间字符串。 Azure AD 需要这种类型的日期时间值,但不评估或使用该值。

        requestData = string.Format(requestData, "id" + Guid.NewGuid().ToString("N"), issueInstant, "https://sts.windows.net/40cfad67-3660-44d8-9f47-XXXXXXXXX", txtNameID.Text.Trim());

        string requestDataDo = EncodeSamlAuthnRequest(requestData);
        string cancelRequestUrl = "https://login.microsoftonline.com/40cfad67-3660-44d8-9f47-XXXXXXXXX/saml2?SAMLRequest=" + requestDataDo;
 
        Response.Redirect(cancelRequestUrl, false);//必须增加第二个参数false,否则报:线程正在终止 的异常; 
    }

    public static string EncodeSamlAuthnRequest(string authnRequest)
    {
        var bytes = Encoding.UTF8.GetBytes(authnRequest);
        using (var output = new MemoryStream())
        {
            using (var zip = new DeflateStream(output, CompressionMode.Compress))
            {
                zip.Write(bytes, 0, bytes.Length);
            }
            var base64 = Convert.ToBase64String(output.ToArray());
            return HttpUtility.UrlEncode(base64);
        }
    }

    private string ReadData()
    {
        string returnData = string.Empty;
        string filepath = Server.MapPath("") + "\\LogoutRequest.txt";
        using (FileStream fs = new FileStream(filepath, FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite, FileShare.ReadWrite))
        {
            using (StreamReader sr = new StreamReader(fs, Encoding.UTF8))
            {
                returnData = sr.ReadToEnd().ToString();
            }
        }
        return returnData;
    }`

LogoutRequest.txt文件内容:

<samlp:LogoutRequest xmlns="urn:oasis:names:tc:SAML:2.0:metadata" ID="{0}" Version="2.0" IssueInstant="{1}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
  <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">{2}</Issuer>
  <NameID xmlns="urn:oasis:names:tc:SAML:2.0:assertion">{3}</NameID>
</samlp:LogoutRequest>

5.5. 注销请求后的响应:

点击查看源代码
` protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            try
            {
                string logoutBackRequestUrl = Request.Url.ToString();//对方请求我们的URL地址(带参数) 也就是我们自己的URL(对方加了参数)
                string logoutBackSamlResponse = Request.QueryString["SAMLResponse"];
                if (!string.IsNullOrEmpty(logoutBackSamlResponse))
                {
                    string logoutBackSignature = Request.QueryString["Signature"];
                    string logoutBackSigAlg = Request.QueryString["SigAlg"];
 
                    string decCodeLogoutBackSamlResponse = InflateData(logoutBackSamlResponse);

                    txtSAMLResponse.Text = logoutBackSamlResponse;
                    txtSignature.Text = logoutBackSignature;
                    txtSigAlg.Text = logoutBackSigAlg;
                    txtDecCodeSAMLResponse.Text = decCodeLogoutBackSamlResponse;

                    ////////////////////////最终的XML格式参照本目录中文件“LogoutResponse.txt”;
                }
                else
                {
                    Response.Write("非法访问,URL中参数SAMLResponse值不能为空!");
                }

            }
            catch (Exception ex)
            {
                Response.Write("异常:" + ex.Message);
            }
        }
    }
    /// <summary>
    /// 解析:先通过base64转byte[],然后解压缩后得到byte[],然后转成普通 字符串;
    /// </summary>
    /// <param name="compressedData"></param>
    /// <returns></returns>
    public string InflateData(string logoutBackSamlResponse)
    {
        byte[] compressedData = Convert.FromBase64String(logoutBackSamlResponse);
 
        if (compressedData == null) return null;
 
        int deflen = compressedData.Length * 2;
        byte[] buffer = null;
        string deSamlResponseXml = "";

        using (MemoryStream stream = new MemoryStream(compressedData))
        {
            using (DeflateStream inflatestream = new DeflateStream(stream, CompressionMode.Decompress))
            {
                using (MemoryStream uncompressedstream = new MemoryStream())
                {
                    using (BinaryWriter writer = new BinaryWriter(uncompressedstream))
                    {
                        int offset = 0;
                        while (true)
                        {
                            byte[] tempbuffer = new byte[deflen];

                            int bytesread = inflatestream.Read(tempbuffer, offset, deflen);

                            writer.Write(tempbuffer, 0, bytesread);

                            if (bytesread < deflen || bytesread == 0) break;
                        }    

                        uncompressedstream.Seek(0, SeekOrigin.Begin);
                        buffer = uncompressedstream.ToArray();

                        deSamlResponseXml = Encoding.UTF8.GetString(buffer);
 
                    }
                }
            }
        }

        return deSamlResponseXml;
    }`
注销请求后的响应的最终的XML格式 “LogoutResponse.txt”的内容:
`<samlp:LogoutResponse ID="_3f6da426-39ca-45c1-8c2c-363746bbe4b0" Version="2.0" IssueInstant="2023-04-24T05:02:19.752Z" Destination="https://xxxxxx.com/Logout.aspx" InResponseTo="idf237c5991545481eb6fce7825a4171b0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
    <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">https://sts.windows.net/40cfad67-3660-44d8-9f47-XXXXXXXXXXX/</Issuer>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status>
</samlp:LogoutResponse>`
posted @ 2023-04-20 16:37  黑星2003  阅读(846)  评论(1编辑  收藏  举报