我目前正在开发一个问答游戏,作为 Moodle 中的一个外部工具。我遵循 IMS LTI 规范并根据需要使用 OAuth 进行身份验证。
我最近设法验证了来自 Moodle 的启动 POST 请求,现在我正在尝试使用 LTI 基本结果服务从我的工具将成绩发送回 Moodle。
这是我遇到问题的地方:我构建 POX 消息,签署请求并发送它,但由于某种原因,它们每隔几次尝试就成功一次(30% 的时间,或多或少)。对于其余部分,Moodle 会以失败的 POX 消息进行响应,其中包含“消息签名无效”作为其描述。
显然,我想相信我的请求会在我需要的时候成功。
出于说明目的,我在下面向您展示了一个成功请求和失败请求的示例,以及每个请求的基本字符串(OAuth 用于签名的字符串):
对于成功的请求:
- 请求的基本字符串(换行符仅用于显示目的):
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
回复正文:
<?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>
对于失败的请求:
- 请求的基本字符串(换行符仅用于显示目的):
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
回复正文:
<?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());
}