LTI 1.3 activityProgress更新401未授权问题排查(C#实现)
初始401错误的核心原因
你在发送activityProgress更新时,错误用了Launch流程返回的id_token作为Bearer令牌。LTI规范里,Launch的id_token仅用于工具自身的身份验证,不能用来调用平台的服务接口(比如分数/进度更新)。调用这类服务得走OAuth 2.0 Client Credentials流程:用工具私钥生成JWT断言,向平台申请专门的access_token。
获取AccessToken时500错误的排查点
从你补充的代码看,几个关键错误导致了内部服务器错误:
- JWT Payload字段大小写错误
Payload里用了大写的Nbf,但JWT标准要求是小写的nbf,平台解析时会因识别不了字段报错。 - 时间字段格式错误
iat、exp、nbf必须是Unix时间戳整数,你转成字符串了,这会让JWT断言格式无效,直接用EpochTime.GetIntDate()返回的整数就行。 - 多余请求头干扰
你给HttpClient加了typ和alg请求头,这俩是JWT自身的Header字段,不用作为HTTP请求头发送,多余的头会打乱平台的请求解析。 - Audience字段配置错误
aud字段应该是平台的Issuer URL(也就是Launch时id_token里的iss值),不是AccessTokenUrl。LTI的Client Credentials流程中,JWT断言的受众是平台的issuer,而非令牌端点。 - 参数名错误
申请令牌的表单参数里,JWT断言对应的参数名是client_assertion,不是你写的assertion。
修正后的GetAccessTokenAsync代码示例
public async Task<TokenResponse> GetAccessTokenAsync(string scope, LtiConfiguration platform) { if (string.IsNullOrEmpty(scope)) throw new ArgumentNullException(nameof(scope)); if (platform is null) throw new ArgumentNullException(nameof(platform)); var clientId = _encryptionService.Decrypt(platform.ClientId); var issuer = _encryptionService.Decrypt(platform.Issuer); var tokenEndpoint = _encryptionService.Decrypt(platform.AccessTokenUrl); var payload = new JwtPayload { { "iss", clientId }, { "sub", clientId }, { "aud", issuer }, // 修正为平台的issuer { "iat", EpochTime.GetIntDate(DateTime.UtcNow) }, // 直接用整数时间戳 { "exp", EpochTime.GetIntDate(DateTime.UtcNow.AddMinutes(5)) }, { "nbf", EpochTime.GetIntDate(DateTime.UtcNow.AddSeconds(-5)) }, // 改为小写nbf { "jti", Guid.NewGuid().ToString() } // 用Guid生成jti更可靠 }; var handler = new JwtSecurityTokenHandler(); var credentials = PemHelper.SigningCredentialsFromPemString(_encryptionService.Decrypt(platform.PrivateKey)); var jwt = handler.WriteToken(new JwtSecurityToken(new JwtHeader(credentials), payload)); using var httpClient = _clientFactory.CreateClient(); var content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("client_id", clientId), new KeyValuePair<string, string>("grant_type", "client_credentials"), new KeyValuePair<string, string>("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), new KeyValuePair<string, string>("scope", scope), new KeyValuePair<string, string>("client_assertion", jwt) // 修正参数名 }); var response = await httpClient.PostAsync(tokenEndpoint, content); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize<TokenResponse>(responseContent); } else { // 捕获错误内容方便排查 var errorContent = await response.Content.ReadAsStringAsync(); throw new Exception($"获取AccessToken失败: {response.StatusCode} - {errorContent}"); } }
调用进度更新接口的修正
在UpdateLtiStatusAsync里,先获取合法的access_token再发起请求:
public async Task UpdateLtiStatusAsync(LtiStatusUpdateInputModel inputModel, LtiConfiguration platform) { // 拼接所需权限范围,确保包含AGS服务的分数更新权限 var scope = string.Join(" ", inputModel.EndpointClaim.Scope ?? new List<string>()); var tokenResponse = await GetAccessTokenAsync(scope, platform); using var client = _clientFactory.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Post, inputModel.EndpointClaim.LineItemScoreEndpoint); request.Headers.Add("Authorization", $"Bearer {tokenResponse.AccessToken}"); var payload = new { timestamp = inputModel.Timestamp.ToString("s"), userId = inputModel.UserId, activityProgress = inputModel.ActivityProgress.ToString(), gradingProgress = inputModel.GradingProgress.ToString() }; var jsonPayload = JsonSerializer.Serialize(payload); var content = new StringContent(jsonPayload, Encoding.UTF8, "application/vnd.ims.lis.v1.score+json"); request.Content = content; var response = await client.SendAsync(request); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); throw new Exception($"更新LTI进度失败: {response.StatusCode} - {errorContent}"); } }
额外注意事项
- 确保工具私钥和平台配置的公钥匹配,平台需要提前知晓工具的公钥(通过JWKS端点或直接配置)。
- 检查
scope是否正确,必须包含平台要求的权限(比如https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.score)。 - 调试时一定要捕获并打印平台返回的错误内容,这是排查问题最直接的方式。
内容的提问来源于stack exchange,提问作者Pauldb
相关产品推荐
相关产品推荐

