构造一个签名的 SAML2 LogOut 请求

Posted

技术标签:

【中文标题】构造一个签名的 SAML2 LogOut 请求【英文标题】:Construct a signed SAML2 LogOut request 【发布时间】:2011-12-30 07:24:57 【问题描述】:

我的目标是实现单一注销协议。首先,我了解标准的工作原理以及如何将其适应我的场景:ADFS 2.0 作为 IdP,对我来说就像一个“黑匣子”

我现在正在做的是接下来的:

    向我的 IdP 发送 <AuthnRequest>

    IdP 要求我提供凭据,我提供并成功登录。

    <LogoutRequest>获取SessionIndex值并构造一个<LogoutRequest>

<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_135ad2fd-b275-4428-b5d6-3ac3361c3a7f" Version="2.0" Destination="https://idphost/adfs/ls/" IssueInstant="2008-06-03T12:59:57Z"><saml:Issuer>myhost</saml:Issuer><NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" NameQualifier="https://idphost/adfs/ls/">myemail@mydomain.com</NameID<samlp:SessionIndex>_0628125f-7f95-42cc-ad8e-fde86ae90bbe</samlp:SessionIndex></samlp:LogoutRequest>

    将上面的<LogoutRequest> 编码成Base64

    构造下一个字符串:SAMLRequest=base64encodedRequest&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1

    用上面的字符串生成签名

    用base64编码签名

    发送请求:https://"https://idphost/adfs/ls/?SAMLRequest=base64encodedRequest&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=base64EncodedSignature

但 IdP 正在回答我:SAML 消息签名的验证失败。

为了签名,我使用的是我的私钥(2048 字节),为了验证 IdP 正在使用我的公钥(我在注册主机时发送的公钥)

签署请求的代码如下:

// Retrieve the private key
KeyStore keyStore = KeyStore.getInstance("JKS", "SUN");
FileInputStream stream;
stream = new FileInputStream("/path/to/my/keystore.jks");
keyStore.load(stream, "storepass".toCharArray());
PrivateKey key = (PrivateKey) keyStore.getKey("keyAlias","keyPass".toCharArray());

// Create the signature
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(key);
signature.update("SAMLRequest=jVJda8IwFH2e4H8ofW%2BbVmvboGWCDApusDn2sBdJm1sNtEmXmw7x1y92KDrY2Ov5uueEzJG1TUfXaqd68wIfPaBxDm0jkQ7Mwu21pIqhQCpZC0hNRTfLxzWNfEI7rYyqVONeWf52METQRijpOsVq4W7JoSzjJJnWAEAmwLMMpmRG0jCrYJICIcR13kCjdSxcG%2BA6K9tQSGYGZG9MhzQIGrUT0uPw6VegpV%2FtA8ZrDBq0ZxB7KCQaJo2NICT1yMwjk9cwonFG4%2BTdzceju%2FmpOx3EOu8qYThgGJ3j5sE1fZE%2F2X3FynlQumXm9%2BGhHw6I4F49SCm0TDRLzjWgrXiKee5ZI2oB%2Bj%2Bj8qYX6GvFtdj1cPRryzPJ4Xh%2F2%2Fe736VvRzf2nn24wmoP%2BZbMojSM4tpL6iz2plFVeYyn4NUc0hmDjJQlfCf9cI5HZ%2Fjm4%2BRf&RelayState=null&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1".getBytes());

String signatureBase64encodedString = (new BASE64Encoder()).encodeBuffer(signature.sign());

【问题讨论】:

现在我得到了这个异常:Base-64 字符数组的长度无效。基本上我从请求中删除了 NameQualifier 属性,我正在使用 rsa-sha256 而不是以前的,一步一步... 最后一个 ADFS2 日志:ADFS2:SAML 请求未使用预期的签名算法进行签名。签名:w3.org/2001/04/xmldsig-more#rsa-sha256 预期:w3.org/2000/09/xmldsig#rsa-sha1 好的,问题出在算法上。我正在使用 SHA1withRSA 生成签名。我需要使用 **w3.org/2000/09/xmldsig#rsa-sha1"。使用 Java XML API 我能够生成一个 SignedAuthnRequest xml,但我需要应用 HTTP-Redirect (SAMLRequest=value&SigAlg=value&Signature=value)...跨度> 不应该 RSAwithSHA1 对应于 w3.org/2000/09/xmldsig#rsa-sha1 吗?我有点迷茫…… 【参考方案1】:

我终于找到了正确的食谱:

    生成 SAMLRequest 值 在 Base64 中编码 SAMLRequest 值 对 SAMLRequest 值进行 URL 编码 对 SigAlg 值进行 URL 编码: http://www.w3.org/2000/09/xmldsig#rsa-sha1 将算法签名 (SHA1withRSA) 与 SAMLRequest=value&SigAlg=value 对生成的签名进行 URL 编码

我们可以使用 SAML 2.0 调试器执行第 2 步和第 3 步 (https://rnd.feide.no/simplesaml/module.php/saml2debug/debug.php)。而对于 URL 编码使用经典的 w3schools (http://www.w3schools.com/tags/ref_urlencode.asp)

警告!确保您的依赖方的算法在 ADFS2 中设置为 SHA1!

最好的问候,

路易斯

ps:现在我必须编写一点代码......

pps:你可以在这里找到代码:https://github.com/cerndb/wls-cern-sso/tree/master/saml2slo

【讨论】:

感谢您提供有关哈希算法的提示!这让我着迷。 这对我有用,但只有当我停止 URLEncoding 它时 NAM = Novell 访问管理器? 我们不应该在对 SAML 请求进行 base64 编码之前应用默认压缩吗? 你好@mavis。您可以在此处查看源代码:github.com/cerndb/wls-cern-sso/blob/master/saml2slo/src/ch/cern/… 我已经有一段时间没有看这门课了,但我确实认为它正在满足您的要求。如果你试一试,请告诉我。希望对您有所帮助!【参考方案2】:

a bug in the ADFS implementation 给出的错误消息是向后的。当它说:

SAML 请求未使用预期的签名算法进行签名。 SAML 请求使用签名算法 http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 进行签名。预期的签名算法是http://www.w3.org/2000/09/xmldsig#rsa-sha1

这实际上意味着您使用的是 SHA1,而它期待的是 SHA256。

【讨论】:

澄清一下,这意味着您的应用正在使用 sha1 请求注销,并且您的 AD FS 配置为 SHA256。【参考方案3】:

由于我们需要采取许多步骤才能最终在 Domino 9.0.1 上成功实施 SLO,因此我决定编写代码,允许使用任何(未来的)IdP 配置在我们的 Domino 服务器上运行。我实施了以下策略:

尽可能多地使用传入 SAML 注销请求中的可用信息 在 idpcat.nsf 中标识 IdP 配置,以查找有关要发送到 IdP 服务提供商(SAML 服务器)的 IdP SLO 响应的相应信息 在 idpcat.nsf 的相应 IdP 配置中定义 SAML 注销响应,以便在 SAML 配置更改时动态适应新要求。

因此,代码将传入 SAML 注销请求的所有字段读取到参数映射中,并对查询字符串进行解码和扩充,以将请求的 XML 参数提取到参数映射中。由于 domino 服务器上的不同网站可以为不同的 IdP 服务提供商配置以允许 SSO 连接,因此我使用相应的“主机名”来识别 IdP 配置,并在同一个 Parameter Map 中读取其所有字段。为了定义适用的 XML 响应,我决定将所有需要的定义写入 IdP 配置的 Comment 中,这允许调整单个 IdP 配置以对不同的 IdP 提供者使用相同的代码,即使使用不同的 SAML 版本。 idpcat.nsf 中 IdP 配置的 Comment 字段中的定义如下所示:

SLO 响应:/idp/SLO.saml2;

SLO 响应 XML:“" ""HTTP_HSP_LISTENERURI"" "" "" "" "";

XML 值:#xmlns:urn=protocol -> 断言mlns:urn=protocol -> 状态:成功;

响应参数:RelayState&SigAlg&Signature;

签名类型:SHA256withRSA;

密钥库类型:PKCS12;

密钥库文件:D:\saml_cert.pfx;

密钥库密码:**********;

证书:xxxxxxxxxx

此定义中的键与值用“:”分隔,值的结尾用“;”指定(不是新行)这允许根据 IdP 服务提供商的要求在用于 SSO 连接的相应 IdP 配置中设置 SAML 响应的完整参数化。 定义如下:

• SLO 响应:这是必须将 SLO 响应发送到相应 IdP 服务器上的相对地址。

• SLO 响应 XML:这是定义以 XML 格式结构化的 SLO 响应的文本字符串(使用“”而不带“)。在参数映射中找到的标识参数的字符串被交换为它们各自的值。到确保正确识别相似的参数 Cookie 参数有一个前导“$”,请求查询的 XML 参数有一个前导“#”。另外提供了 2 个公式,其中“@UUID”将计算具有正确的随机 UUID XML 响应的 ID 参数的格式和“@ACTUAL_TIME”将为 XML 响应的 IssueInstant 参数以 Instant 格式计算正确的时间戳

• XML 值:此文本字符串标识附加参数,其中基本上使用已知参数,但需要交换部分参数值以匹配所需文本。参数由字符串“XML_Paramater”标识,后跟字符串中的位置,在 SLO 响应 XML 文本中用“&”分隔每个值。 XML 值的文本结构如下:参数标识后跟“=”,要替换的文本后跟“->”以及新文本。

• 响应参数:响应参数用“&”分隔,并将按定义添加到 SLO 响应中。如果需要签名,则此字符串中需要参数 SigAlg 和 Signature,并应放在末尾。

• 签名类型:如果需要签名,则在此处指定用于计算签名的算法类型。

• KeyStore 类型:这是用于证书的 KeyStore 的类型。

• KeyStore 文件:这是保存 KeyStore 的文件,包括 Lotus Notes 服务器上的驱动器和路径。我们在测试服务器上使用了 D:\saml_cert.pfx。

• KeyStore 密码:这是打开 KeyStore 文件和其中存储的证书所需的密码。

• 证书:这是在 KeyStore 文件中标识证书的证书的别名。如果将证书存储在新的 KeyStore 文件中以将多个证书组合在一个位置,则别名总是更改为新值,必须在此处进行调整。

我实现的代码是 domcfg.nsf 中名称为“Logout”的 Java 代理,但它基本上可以在 SSO 用户可用的任何数据库中实现,它作为服务器运行以允许保护 IdP 配置在具有最高安全性的 idpcat.nsf 中。在 IdP 服务提供商上,您必须分别将相应网站的 Domino 服务器的 SLO 请求配置为“https://WEBSITE/domcfg.nsf/Logout?Open&”,然后是 SAML 请求。如果 IdP 服务提供商要求签名,您必须存储一个带有证书的 KeyStore 文件,其中包括签名所需的 PrivateKey。可以使用 MMC 管理单元功能管理 KeyStore 文件(请参阅https://msdn.microsoft.com/en-us/library/ms788967(v=vs.110).aspx)。导出功能可以将多个证书合并到一个文件中,但您必须确保通过导出向导中的相应设置将私钥导出到文件中。

这是“注销”代理的代码,它将用户从 domino 服务器注销并将 SAML 注销响应发送到 IdP 服务提供商:

import lotus.domino.*;
import java.io.*;
import java.util.*;
import java.text.*;
import com.ibm.xml.crypto.util.Base64;
import java.util.zip.*;
import java.net.URLEncoder;
import java.security.*;

public class JavaAgent extends AgentBase 
    public void NotesMain() 
        try 
            Session ASession = getSession();
            AgentContext AContext = ASession.getAgentContext();
            DateTime date = ASession.createDateTime("Today 06:00");
            int timezone = date.getTimeZone();

            Database DB = AContext.getCurrentDatabase();
            String DBName = DB.getFileName();
            DBName = DBName.replace("\\", "/").replace(" ", "+");

            //Load PrintWriter to printout values for checking (only to debug)
            //PrintWriter pwdebug = getAgentOutput();
            //pwdebug.flush();

            //Load Data from Logout Request
            Document Doc = AContext.getDocumentContext();
            Vector<?> items = Doc.getItems();
            Map<String, String> Params = new LinkedHashMap<String, String>();
            for (int j=0; j<items.size(); j++) 
                Item item = (Item)items.elementAt(j);
                if (!item.getValueString().isEmpty()) Params.put(item.getName(), item.getValueString());
            
            String ServerName = Params.get("HTTP_HSP_HTTPS_HOST");
            int pos = ServerName.indexOf(":");
            ServerName = pos > 0 ? ServerName.substring(0, ServerName.indexOf(":")) : ServerName;
            Params.put("ServerName", ServerName);
            Doc.recycle();
            DB.recycle();

            //Load Cookie Variables
            Params = map(Params, Params.get("HTTP_COOKIE"), "$", "; ", "=", false, false);
            //Load Query Variables
            Params = map(Params, Params.get("QUERY_STRING_DECODED"), "", "&", "=", false, false);
            //Decode and Infalte SAML Request
            String RequestUnziped = decode_inflate(Params.get("SAMLRequest"), true);
            //pwdebug.println("Request unziped: " + RequestUnziped);
            //System.out.println("Request unziped: " + RequestUnziped);
            String RequestXMLParams = RequestUnziped.substring(19, RequestUnziped.indexOf("\">"));
            //Load XML Parameters from Request
            Params = map(Params, RequestXMLParams, "#", "\" ", "=\"", false, false);
            //for (Map.Entry<String, String> entry : Params.entrySet()) pwdebug.println(entry.getKey() + " value: " + entry.getValue());
            //for (Map.Entry<String, String> entry : Params.entrySet()) System.out.println(entry.getKey() + " value: " + entry.getValue());
            String Issuer = RequestUnziped.substring(RequestUnziped.indexOf(":Issuer"), RequestUnziped.indexOf("Issuer>"));
            Issuer = Issuer.substring(Issuer.indexOf(">") + 1, Issuer.indexOf("<"));
            Params.put("SLO_Issuer", Issuer);

            //Load Parameters for the Response
            DbDirectory Dir = ASession.getDbDirectory(null);
            Database idpcat = Dir.openDatabase("idpcat.nsf");
            View idpView = idpcat.getView("($IdPConfigs)");
            Document idpDoc = idpView.getDocumentByKey(ServerName, false);
            items = idpDoc.getItems();
            for (int j=0; j<items.size(); j++) 
                Item item = (Item)items.elementAt(j);
                if (!item.getValueString().isEmpty()) Params.put(item.getName(), item.getValueString());
            
            Params = map(Params, idpDoc.getItemValueString("Comments"), "", ";", ": ", false, false);
            Params.put("SLO_Response", Issuer + Params.get("SLO Response"));
            Params.put("@UUID", "_" + UUID.randomUUID().toString());
            Params.put("@ACTUAL_TIME", actualTime(Params.get("#IssueInstant"), Params.get("#NotOnOrAfter"), timezone));
            //for (Map.Entry<String, String> entry : Params.entrySet()) pwdebug.println(entry.getKey() + " value: " + entry.getValue());
            //for (Map.Entry<String, String> entry : Params.entrySet()) System.out.println(entry.getKey() + " value: " + entry.getValue());
            idpDoc.recycle();
            idpView.recycle();
            idpcat.recycle();
            Dir.recycle();

            //Setup XML Response as defined
            String ResponseString = Params.get("SLO Response XML");
            for (Iterator<String> itRq = Params.keySet().iterator(); itRq.hasNext();) 
                String Key = (String) itRq.next();
                ResponseString = ResponseString.replace(Key, Params.get(Key));
            
            //pwdebug.println("Response String replaced: " + ResponseString);
            //System.out.println("Response String replaced: " + ResponseString);
            //Load Values to be exchanged in the defined Response
            Map<String, String> RsXMLValues = map(new LinkedHashMap<String, String>(), Params.get("XML Values"), "", "&", "=", true, false);
            //for (Map.Entry<String, String> entry : RsXMLValues.entrySet()) pwdebug.println(entry.getKey() + " value: " + entry.getValue());
            //for (Map.Entry<String, String> entry : RsXMLValues.entrySet()) System.out.println(entry.getKey() + " value: " + entry.getValue());
            //Exchange defined Strings with Values from the Request
            int itc = 0;
            for (Iterator<String> itRXV = RsXMLValues.keySet().iterator(); itRXV.hasNext();) 
                itc = itc + 1;
                String Key = (String) itRXV.next();
                int lock = Key.indexOf(" -> ");
                String KeyRq = lock > 0 ? Key.substring(0, lock) : Key;
                int lockRq = KeyRq.indexOf(" ");
                KeyRq = lockRq > 0 ? KeyRq.substring(0, lockRq) : KeyRq;
                String Parameter = Params.get(KeyRq);
                String Value = RsXMLValues.get(Key);
                if (!Value.isEmpty()) 
                    int locv = Value.indexOf(" -> ");
                    String ValueS = locv > 0 ? Value.substring(0, locv) : Value;
                    String ValueR = locv > 0 && Value.length() > locv + 4 ? Value.substring(locv + 4) : ValueS;
                    Parameter = Parameter.replace(ValueS, ValueR);
                
                ResponseString = ResponseString.replace(("XML_Parameter" + itc), Parameter);
            
            //pwdebug.println("Final XML Response String: " + ResponseString);
            //System.out.println("Final XML Response String: " + ResponseString);
            //Deflate and Encode the XML Response
            String ResponseZiped = deflate_encode(ResponseString, Deflater.DEFAULT_COMPRESSION, true);
            //pwdebug.println("Response Ziped: " + ResponseZiped);
            //System.out.println("Response Ziped: " + ResponseZiped);
            //Setup Response URLQuery as defined
            String ResponseEncoded = "SAMLResponse=" + URLEncoder.encode(ResponseZiped, "UTF-8");
            //pwdebug.println("Response to Sign: " + ResponseEncoded);
            //System.out.println("Response to Sign: " + ResponseEncoded);
            //Load Parameters to be added to the Response
            Map<String, String> ResponseParams = map(new LinkedHashMap<String, String>(), Params.get("Response Parameters"), "", "&", "=", false, true);
            //for (Map.Entry<String, String> entry : ResponseParams.entrySet()) pwdebug.println(entry.getKey() + " value: " + entry.getValue());
            //for (Map.Entry<String, String> entry : ResponseParams.entrySet()) System.out.println(entry.getKey() + " value: " + entry.getValue());
            //Add defined Parameters with Values from the Request
            for (Iterator<String> itRP = ResponseParams.keySet().iterator(); itRP.hasNext();) 
                String Key = (String) itRP.next();
                if (Key.contains("Signature")) 
                    //pwdebug.println("Response to Sign: " + ResponseEncoded);
                    //System.out.println("Response to Sign: " + ResponseEncoded);
                    Signature signature = Signature.getInstance(Params.get("Signature Type"));
                    //pwdebug.println("Signature: Initiated");
                    //System.out.println("Signature: Initiated");
                    KeyStore keyStore = KeyStore.getInstance(Params.get("KeyStore Type"));
                    //pwdebug.println("Key Store: Initiated");
                    //System.out.println("Key Store: Initiated");
                    keyStore.load(new FileInputStream(Params.get("KeyStore File")), Params.get("KeyStore Password").toCharArray());
                    //pwdebug.println("Key Store: Loaded");
                    //System.out.println("Key Store: Loaded");
                    PrivateKey key = (PrivateKey) keyStore.getKey (Params.get("Certificate"), Params.get("KeyStore Password").toCharArray());
                    //pwdebug.println("Key Store: Private Key Loaded");
                    //System.out.println("Key Store: Private Key Loaded");
                    signature.initSign(key);
                    //pwdebug.println("Signature: Private Key Initiated");
                    //System.out.println("Signature: Private Key Initiated");
                    signature.update(ResponseEncoded.getBytes("UTF-8"));
                    //pwdebug.println("Signature: Signed");
                    //System.out.println("Signature: Signed");
                    String ResponseSignature = URLEncoder.encode(Base64.encode(signature.sign()), "UTF-8"); 
                    //pwdebug.println("Signature: Signed");
                    //System.out.println("Signature: Signed");
                    ResponseEncoded = ResponseEncoded.concat("&").concat(Key).concat("=").concat(ResponseSignature);
                
                else ResponseEncoded = ResponseEncoded.concat("&").concat(Key).concat("=").concat(URLEncoder.encode(Params.get(Key), "UTF-8"));
            
            String ResponseURL = Params.get("SLO_Response").concat("?").concat(ResponseEncoded);
            //pwdebug.println("Final Response URL: " + ResponseURL);
            //pwdebug.close();
            //System.out.println("Final Response URL: " + ResponseURL);

            //Send Logout to Server and redirect to Response to defined Destination
            PrintWriter pwsaml = getAgentOutput();
            pwsaml.flush();
            pwsaml.println("[" + Params.get("HTTP_HSP_LISTENERURI") + "/" + DBName + "?logout&redirectto=" + URLEncoder.encode(ResponseURL, "UTF-8") + "]");
            pwsaml.close();

            //Recycle Agent and Session
            AContext.recycle();
            ASession.recycle();

         catch(Exception e) 
            PrintWriter pwerror = getAgentOutput();
            pwerror.flush();
            pwerror.println(e);
            System.out.println(e);
            pwerror.close();
         
    

    //Load Maps from Strings to identify Paramteres and Values
    private static Map<String, String> map(Map<String, String> map, String input, String keys, String spliting, String pairing, Boolean keycount, Boolean empty) 
        Map<String, String> output = map.isEmpty() ? new LinkedHashMap<String, String>() : map;
        String[] Pairs = input.split(spliting);
        int kc = 0;
        for (String Pair : Pairs) 
            kc = kc + 1;
            int pos = Pair.indexOf(pairing);
            String Key = pos > 0 ? Pair.substring(0, pos) : Pair;
            if (keycount) Key = Key + " " + kc;
            String Value = pos > 0 && Pair.length() > (pos + pairing.length()) ? Pair.substring(pos + pairing.length()) : "";
            if (!output.containsKey(Key) && (empty || !Value.trim().isEmpty())) output.put((keys + Key).trim(), Value.trim());
        
        return output;
    

    //Decode and Inflate to XML
    private static String decode_inflate(String input, Boolean infflag) throws IOException, DataFormatException 
        byte[] inputDecoded = Base64.decode(input.getBytes("UTF-8"));
        Inflater inflater = new Inflater(infflag);
        inflater.setInput(inputDecoded);
        byte[] outputBytes = new byte[1024];
        int infLength = inflater.inflate(outputBytes);
        inflater.end();
        String output = new String(outputBytes, 0, infLength, "UTF-8");
        return output;
    

    //Deflate and Encode XML
    private static String deflate_encode(String input, int level , Boolean infflag) throws IOException 
        byte[] inputBytes = input.getBytes("UTF-8");
        Deflater deflater = new Deflater(level, infflag);
        deflater.setInput(inputBytes);
        deflater.finish();
        byte[] outputBytes = new byte[1024];
        int defLength = deflater.deflate(outputBytes);
        deflater.end();
        byte[] outputDeflated = new byte[defLength];
        System.arraycopy(outputBytes, 0, outputDeflated, 0, defLength);
        String output = Base64.encode(outputDeflated);
        return output;
    

    //Define Date and Time Formats
    private static SimpleDateFormat DateFormat = new SimpleDateFormat("yyyy-MM-dd");
    private static SimpleDateFormat TimeFormat = new SimpleDateFormat("HH:mm:ss.SSS");

    //Formated Actual Time
    private static String actualTime(String minTime, String maxTime, int localZone) throws ParseException 
        Date actualtime = new Date();
        long acttime = actualtime.getTime();
        long mintime = resetTime(minTime, localZone);
        long maxtime = resetTime(maxTime, localZone);
        acttime = (acttime > mintime) && (acttime < maxtime) ? acttime: mintime + 1000;
        return formatTime(acttime);
    

    //Reset timemillis from String as defined
    private static long resetTime(String givenTime, int localZone) throws ParseException 
        Date date = DateFormat.parse(givenTime.substring(0, givenTime.indexOf("T")));
        long days = date.getTime();
        Date time = TimeFormat.parse(givenTime.substring(givenTime.indexOf("T") + 1, givenTime.indexOf("Z")));
        long hours = time.getTime();
        long zonecorr = localZone * 3600000;
        return days + hours - zonecorr;
    

    //Format timemillis into a String as required
    private static String formatTime(long totalmilliSeconds) 
        long date = 86400000 * (totalmilliSeconds / 86400000);
        long time = totalmilliSeconds % 86400000;
        String dateString = DateFormat.format(date).concat("T");
        String timeString = TimeFormat.format(time).concat("Z");
        return dateString.concat(timeString);
    

    public static String noCRLF(String input)  
        String lf = "%0D";
        String cr = "%0A";
        String find = lf;
        int pos = input.indexOf(find);
        StringBuffer output = new StringBuffer();
        while (pos != -1) 
            output.append(input.substring(0, pos));
            input = input.substring(pos + 3, input.length());
            if (find.equals(lf)) find = cr;
            else find = lf;
            pos = input.indexOf(find);
        
        if (output.toString().equals("")) return input;
        else return output.toString();
    

您可能已经认识到,如果定义不正确并且不会导致成功注销,则可以使用几行注释行来调试代理。您可以通过删除开始这些行的“//”轻松更改这些行,并打印出您希望在屏幕上看到的参数或将它们发送到日志。

为了在 domino 服务器上启动 SLO,我使用相同的概念编写了另一个 Java 代理。该代理称为 startSLO,与“注销”代理位于同一数据库中。通过创建打开相对 URL“/domcfg.nsf/startSLO?Open”的按钮,可以在任何应用程序中轻松实现此代理的使用。 “startSLO”代理的代码如下:

import lotus.domino.*;
import java.io.*;

public class JavaAgent extends AgentBase 
    public void NotesMain() 
        try 
            Session ASession = getSession();
            AgentContext AContext = ASession.getAgentContext();

            Database DB = AContext.getCurrentDatabase();
            String DBName = DB.getFileName();
            DBName = DBName.replace("\\", "/").replace(" ", "+");

            //Load Data from Logout Request
            Document Doc = AContext.getDocumentContext();
            String ServerName = Doc.getItemValueString("HTTP_HSP_HTTPS_HOST");
            int pos = ServerName.indexOf(":");
            ServerName = pos > 0 ? ServerName.substring(0, ServerName.indexOf(":")) : ServerName;
            String Query = Doc.getItemValueString("Query_String");
            pos = Query.indexOf("?Open&");
            Query = pos > 0 ? "?" + Query.substring(Query.indexOf("?Open") + 6) : "";
            Doc.recycle();
            DB.recycle();

            //Load Parameters for the Response
            DbDirectory Dir = ASession.getDbDirectory(null);
            Database idpcat = Dir.openDatabase("idpcat.nsf");
            View idpView = idpcat.getView("($IdPConfigs)");
            Document idpDoc = idpView.getDocumentByKey(ServerName, false);
            String SAMLSLO = idpDoc.getItemValueString("SAMLSloUrl");
            idpDoc.recycle();
            idpView.recycle();
            idpcat.recycle();
            Dir.recycle();

            //Send Logout to Server and redirect to Response to defined Destination
            PrintWriter pwsaml = getAgentOutput();
            pwsaml.flush();
            pwsaml.println("[" + SAMLSLO + Query + "]");
            pwsaml.close();

            //Recycle Agent and Session
            AContext.recycle();
            ASession.recycle();

         catch(Exception e) 
            PrintWriter pwerror = getAgentOutput();
            pwerror.flush();
            pwerror.println(e);
            System.out.println(e);
            pwerror.close();
         
    

【讨论】:

以上是关于构造一个签名的 SAML2 LogOut 请求的主要内容,如果未能解决你的问题,请参考以下文章

SAML2 元数据 - 多重签名证书

调用 Saml2PostBinding.Unbind() 时签名无效

OpenAM ITfoxtec Saml2 无效签名响应?

org.opensaml.saml2.metadata.provider.FilterException:元数据条目的签名信任建立失败

使用 SAML 2.0 的自签名证书

签名使用签名中包含的密钥正确验证,但该密钥不受信任