1

我目前正在开发一个问答游戏,作为 Moodle 中的一个外部工具。我遵循 IMS LTI 规范并根据需要使用 OAuth 进行身份验证。

我最近设法验证了来自 Moodle 的启动 POST 请求,现在我正在尝试使用 LTI 基本结果服务从我的工具将成绩发送回 Moodle。

这是我遇到问题的地方:我构建 POX 消息,签署请求并发送它,但由于某种原因,它们每隔几次尝试就成功一次(30% 的时间,或多或少)。对于其余部分,Moodle 会以失败的 POX 消息进行响应,其中包含“消息签名无效”作为其描述。

显然,我想相信我的请求会在我需要的时候成功。

出于说明目的,我在下面向您展示了一个成功请求和失败请求的示例,以及每个请求的基本字符串(OAuth 用于签名的字符串):

对于成功的请求:

  1. 请求的基本字符串(换行符仅用于显示目的):

POST&http%3A%2F%2F127.0.0.1%2Fmoodle%2Fmod%2Flti%2Fservice.php &oauth_body_hash%3DhPssgohenJEvtKta2so7Y27p3kU%253D%26oauth_callback%3Dabout%253Ablank %26oauth_consumer_key%3Dkey%26oauth_nonce%3D63cc0764c4cc4701abe28fa5fd406378 %26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1525940779%26oauth_version%3D1.0

  1. 回复正文:

    <?xml version="1.0" encoding="UTF-8"?>
    <imsx_POXEnvelopeResponse xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
        <imsx_POXHeader>
            <imsx_POXResponseHeaderInfo>
                <imsx_version>V1.0</imsx_version>
                <imsx_messageIdentifier>1602471533</imsx_messageIdentifier>
                <imsx_statusInfo>
                    <imsx_codeMajor>success</imsx_codeMajor>
                    <imsx_severity>status</imsx_severity>
                    <imsx_description>Result read</imsx_description>
                    <imsx_messageRefIdentifier>1339905165</imsx_messageRefIdentifier>
                    <imsx_operationRefIdentifier>readResultRequest</imsx_operationRefIdentifier>
                </imsx_statusInfo>
            </imsx_POXResponseHeaderInfo>
        </imsx_POXHeader>
        <imsx_POXBody>
            <readResultResponse>
                <result>
                    <resultScore>
                        <language>en</language>
                        <textString>0.5</textString>
                    </resultScore>
                </result>
            </readResultResponse>
        </imsx_POXBody>
    </imsx_POXEnvelopeResponse>
    

对于失败的请求:

  1. 请求的基本字符串(换行符仅用于显示目的):

POST&http%3A%2F%2F127.0.0.1%2Fmoodle%2Fmod%2Flti%2Fservice.php &oauth_body_hash%3DYLigJE%252B8wr7rCwOITqdc1IP3zFs%253D%26oauth_callback%3Dabout%253Ablank %26oauth_consumer_key%3Dkey%26oauth_nonce%3D6de4380ce2ab4d9a90e3fe1723dc5141 %26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1525940816%26oauth_version%3D1.0

  1. 回复正文:

    <?xml version="1.0" encoding="UTF-8"?>
    <imsx_POXEnvelopeResponse xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
        <imsx_POXHeader>
            <imsx_POXResponseHeaderInfo>
                <imsx_version>V1.0</imsx_version>
                <imsx_messageIdentifier>1688463600</imsx_messageIdentifier>
                <imsx_statusInfo>
                    <imsx_codeMajor>failure</imsx_codeMajor>
                    <imsx_severity>status</verity>
                    <imsx_description>Message signature not valid</imsx_description>
                    <imsx_messageRefIdentifier/>
                    <imsx_operationRefIdentifier>unknownRequest</imsx_operationRefIdentifier>
                </imsx_statusInfo>
            </imsx_POXResponseHeaderInfo>
        </imsx_POXHeader>
        <imsx_POXBody>
            <unknownResponse/>
        </imsx_POXBody>
    </imsx_POXEnvelopeResponse>
    

如您所见,两个基本字符串非常相似,我只能找到应该不同的部分(例如时间戳或随机数)的差异,所以我无法弄清楚为什么某些请求被拒绝而其他一些被接受。

关于这可能发生的原因的任何想法?关于如何找出的任何建议?

预先感谢您的帮助。

编辑:我在这里添加我在签名过程中使用的代码

private IEnumerator SendGradeRequest(XmlDocument xml)
{
    string url = parametrosIniciales["lis_outcome_service_url"];

    byte[] entityBody = Encoding.UTF8.GetBytes(xml.OuterXml); 
    string bodyHash = oAuth.GetBodyHash(entityBody);
    Dictionary<string, string> oAuthParameters = oAuth.PrepareOAuthParameters(oAuth.GetSessionOAuthParameters(parametrosIniciales));
    oAuthParameters.Add("oauth_body_hash", bodyHash);

    Dictionary<string, string> headers = new Dictionary<string, string>();
    headers.Add("Content-Type", "application/xml");
    headers.Add("Authorization", oAuth.GetAuthorizationHeader(oAuthParameters));

    string signature = oAuth.GetRequestSignature(url, headers, entityBody); 
    oAuthParameters.Add("oauth_signature", signature);
    headers["Authorization"] = oAuth.GetAuthorizationHeader(oAuthParameters);

    WWW web = new WWW(url, entityBody, headers);

    // ... Code to manage the response
}

public string GetRequestSignature(string url, Dictionary<string, string> headers, byte[] body)
{
    string baseString = GetBaseString(url, headers, body);
    string key = GetKeyParts(JsonToDict(GameManager.Instance.parametrosIniciales));
    return ComputeHMACSHA1(key, baseString);
}

private string GetBaseString(string url, Dictionary<string, string> headers, object body = null)
{
    string[] parts = {  UrlEncodeRFC3986 (GetNormalizedHttpMethod(body)),
                        UrlEncodeRFC3986 (GetNormalizedHttpURL(url)),
                        UrlEncodeRFC3986 (GetSignableParameters(url, headers, body)) };
    return String.Join("&", parts);
}

private string GetKeyParts(Dictionary<string, string> oAuthParameters)
{
    string consumer, token, consumerSecret, tokenSecret;

    if (oAuthParameters.TryGetValue("oauth_consumer_key", out consumer))
        consumerSecret = UrlEncodeRFC3986(secrets[consumer]);
    else
        throw new Exception("Not consumer key found. If not using any encription, this method should not be called");

    if (oAuthParameters.TryGetValue("oauth_token", out token))
        tokenSecret = UrlEncodeRFC3986(secrets[token]);
    else
        tokenSecret = "";

    return consumerSecret + "&" + tokenSecret;
}

private string GetSignableParameters(string url, Dictionary<string, string> headers, object body = null)
{
    List<String> parametros = new List<string>();

    // 1. Parameters in the OAuth HTTP Authorization header excluding the realm parameter
    string oAuthHeader;
    if (headers.TryGetValue("Authorization", out oAuthHeader))
    {
        oAuthHeader = oAuthHeader.Substring(oAuthHeader.IndexOf(" ") + 1).Replace("\"", "");
        string[] pairs = oAuthHeader.Split(',');

        for (int i = 0; i < pairs.Length; i++)
        {
            string pair = pairs[i];
            string[] temp = pair.Split(new char[] { '=' }, 2);
            temp = UrlEncodeRFC3986(temp);
            pairs[i] = String.Join("=", temp);
        }
        parametros.InsertRange(parametros.Count, pairs);
    }

    // 2. HTTP GET parameters added to the URLs in the query part (as defined by [RFC3986] section 3)
    if (url.Contains("?"))
        parametros.InsertRange(parametros.Count, url.Substring(url.IndexOf("?")).Split('&'));

    // 3. Parameters in the HTTP POST request body (with a content-type of application/x-www-form-urlencoded)
    if (body != null && headers["Content-Type"] == "application/x-www-form-urlencoded" && body is Dictionary<string, string>)
    {
        Dictionary<string, string> cuerpo = RemoveSpareParameters((Dictionary<string, string>)body);
        parametros.InsertRange(parametros.Count, DictToPairsList(cuerpo, true));
    }

    // 4. Sort all parameters by lexicographical value
    parametros.Sort();

    // 5. Concatenate all parameters with '&'
    return String.Join("&", parametros.ToArray());
}
4

0 回答 0