使用 JSON 的 WebAPI2.0 OWIN 令牌请求
WebAPI2.0 OWIN Token request using JSON
我在 visual studio 中创建了一个新的 WebAPI 解决方案,并且正在研究代码以尝试了解发生了什么。
我有一个测试 API 全部完成,运行 一个授权控制器和另一个实现所有实际功能的控制器。
控制器 (API) 都通过接收 JSON 并回复 JSON 来工作,除了 /Token request.This 必须是:
Content-Type: application/x-www-form-urlencoded
否则我只会返回一个错误。
创建此端点的代码部分似乎是这样的:
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = false
};
像这样调用它会导致 200 成功响应,带有 Bearer 令牌:
$("#token_button").click(function ()
{
var username = $("#token_email").val();
var password = $("#token_password").val();
postData("Token", "grant_type=password&username=" + username + "&password=" + password, "application/x-www-form-urlencoded", function (data)
{
user = data;
$("#feedback_display").html(user.access_token);
}, function ()
{
user = null;
});
});
像这样调用它会导致 400 响应:
$("#token_button").click(function ()
{
var username = $("#token_email").val();
var password = $("#token_password").val();
var data = {
"grant_type": "password",
"username": username,
"password": password
}
postData("Token", JSON.stringify(data), "application/json", function (data)
{
user = data;
$("#feedback_display").html(user.access_token);
}, function ()
{
user = null;
});
});
响应正文是:
{"error":"unsupported_grant_type"}
这里唯一的区别是用于传输请求的编码。
在我看到的每个地方,所有示例都使用表单编码来请求此令牌。
在 /api/Account/ExternalLogin 下的代码上放置断点,永远不会命中。
这只接受表单编码有什么原因吗?如果不能,我该如何更改控制器以接受 JSON?
或者我刚刚做了什么蠢事?
不需要[=10th=]直接传递数据。
将 application/x-www-form-urlencoded
用作 Content-Type
的原因很简单:OAuth2 specification (RFC 6749) 需要此内容类型来进行令牌请求。
任何其他 content-type 都会破坏 OAuth2 兼容的客户端兼容性。我建议您不要更改此标准行为。
备注
请注意:
postData("Token", data, "application/json", function (data)
{
//...
}
有效只是因为您根本没有发送JSON!即使您将 application/json
添加为 Content-Type
header,您的请求 body 也会序列化为形式 key-value 对(jQuery 默认 object AJAX 调用中的序列化)。
OAuthAuthorizationServerMiddleware
(更准确地说是内部使用的 OAuthAuthorizationServerHandler
)来自 Microsoft.Owin.Security.OAuth
的默认实现只是忽略 Content-Type
header 并尝试读取无论如何请求 body 作为表格。
OAuth2 需要 application/x-www-form-urlencoded
令牌请求的内容类型。
不过,我想到了这个解决方法:
// GET api/Account/GetToken
[HttpPost]
[AllowAnonymous]
[Route("GetToken")]
public async Task<IHttpActionResult> GetToken(TokenRequest request)
{
var client = new HttpClient()
{
BaseAddress = new Uri(Request.RequestUri.GetLeftPart(UriPartial.Authority))
};
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("username", request.Username),
new KeyValuePair<string, string>("password", request.Password)
});
var result = await client.PostAsync("/token", content);
string resultContent = await result.Content.ReadAsStringAsync();
resultContent = resultContent.Replace(".issued", "issued").Replace(".expires", "expires");
TokenResponse tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(resultContent);
return Ok(tokenResponse);
}
型号:
public class TokenRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
public class TokenResponse
{
public string access_token { get; set; }
public string token_type { get; set; }
public int expires_in { get; set; }
public string userName { get; set; }
public DateTime issued { get; set; }
public DateTime expires { get; set; }
public string error { get; set; }
public string error_description { get; set; }
}
它可以改进,但效果很好。
客户的技术专家要求我们的 /token 端点可以在正文中使用 "application/x-www-form-urlencoded" 和 "application/json" 格式。
所以我不得不实施它,尽管它违反了规范。
如果路径为“/api/token”且 content-type 为 "application/json",则创建一个将 JSON 正文转换为 Url-encoded 正文的 Owin 中间件。不要忘记在 Startup.cs.
中注册它
public sealed class JsonBodyToUrlEncodedBodyMiddleware : OwinMiddleware
{
public JsonBodyToUrlEncodedBodyMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
if (string.Equals(context.Request.ContentType, "application/json")
&& string.Equals(context.Request.Method, "POST", StringComparison.InvariantCultureIgnoreCase)
&& context.Request.Path == new PathString("/avi/token/"))
{
try
{
await ReplaceJsonBodyWithUrlEncodedBody(context);
await Next.Invoke(context);
}
catch (Exception)
{
context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
context.Response.Write("Invalid JSON format.");
}
}
else
{
await Next.Invoke(context);
}
}
private async Task ReplaceJsonBodyWithUrlEncodedBody(IOwinContext context)
{
var requestParams = await GetFormCollectionFromJsonBody(context);
var urlEncodedParams = string.Join("&", requestParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
var decryptedContent = new StringContent(urlEncodedParams, Encoding.UTF8, "application/x-www-form-urlencoded");
var requestStream = await decryptedContent.ReadAsStreamAsync();
context.Request.Body = requestStream;
}
private static async Task<Dictionary<string, string>> GetFormCollectionFromJsonBody(IOwinContext context)
{
context.Request.Body.Position = 0;
var jsonString = await new StreamReader(context.Request.Body).ReadToEndAsync();
var requestParams = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonString);
return requestParams;
}
}
我在 visual studio 中创建了一个新的 WebAPI 解决方案,并且正在研究代码以尝试了解发生了什么。
我有一个测试 API 全部完成,运行 一个授权控制器和另一个实现所有实际功能的控制器。
控制器 (API) 都通过接收 JSON 并回复 JSON 来工作,除了 /Token request.This 必须是:
Content-Type: application/x-www-form-urlencoded
否则我只会返回一个错误。
创建此端点的代码部分似乎是这样的:
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = false
};
像这样调用它会导致 200 成功响应,带有 Bearer 令牌:
$("#token_button").click(function ()
{
var username = $("#token_email").val();
var password = $("#token_password").val();
postData("Token", "grant_type=password&username=" + username + "&password=" + password, "application/x-www-form-urlencoded", function (data)
{
user = data;
$("#feedback_display").html(user.access_token);
}, function ()
{
user = null;
});
});
像这样调用它会导致 400 响应:
$("#token_button").click(function ()
{
var username = $("#token_email").val();
var password = $("#token_password").val();
var data = {
"grant_type": "password",
"username": username,
"password": password
}
postData("Token", JSON.stringify(data), "application/json", function (data)
{
user = data;
$("#feedback_display").html(user.access_token);
}, function ()
{
user = null;
});
});
响应正文是:
{"error":"unsupported_grant_type"}
这里唯一的区别是用于传输请求的编码。 在我看到的每个地方,所有示例都使用表单编码来请求此令牌。
在 /api/Account/ExternalLogin 下的代码上放置断点,永远不会命中。
这只接受表单编码有什么原因吗?如果不能,我该如何更改控制器以接受 JSON?
或者我刚刚做了什么蠢事?
不需要[=10th=]直接传递数据。
将 application/x-www-form-urlencoded
用作 Content-Type
的原因很简单:OAuth2 specification (RFC 6749) 需要此内容类型来进行令牌请求。
任何其他 content-type 都会破坏 OAuth2 兼容的客户端兼容性。我建议您不要更改此标准行为。
备注
请注意:
postData("Token", data, "application/json", function (data)
{
//...
}
有效只是因为您根本没有发送JSON!即使您将 application/json
添加为 Content-Type
header,您的请求 body 也会序列化为形式 key-value 对(jQuery 默认 object AJAX 调用中的序列化)。
OAuthAuthorizationServerMiddleware
(更准确地说是内部使用的 OAuthAuthorizationServerHandler
)来自 Microsoft.Owin.Security.OAuth
的默认实现只是忽略 Content-Type
header 并尝试读取无论如何请求 body 作为表格。
OAuth2 需要 application/x-www-form-urlencoded
令牌请求的内容类型。
不过,我想到了这个解决方法:
// GET api/Account/GetToken
[HttpPost]
[AllowAnonymous]
[Route("GetToken")]
public async Task<IHttpActionResult> GetToken(TokenRequest request)
{
var client = new HttpClient()
{
BaseAddress = new Uri(Request.RequestUri.GetLeftPart(UriPartial.Authority))
};
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("username", request.Username),
new KeyValuePair<string, string>("password", request.Password)
});
var result = await client.PostAsync("/token", content);
string resultContent = await result.Content.ReadAsStringAsync();
resultContent = resultContent.Replace(".issued", "issued").Replace(".expires", "expires");
TokenResponse tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(resultContent);
return Ok(tokenResponse);
}
型号:
public class TokenRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
public class TokenResponse
{
public string access_token { get; set; }
public string token_type { get; set; }
public int expires_in { get; set; }
public string userName { get; set; }
public DateTime issued { get; set; }
public DateTime expires { get; set; }
public string error { get; set; }
public string error_description { get; set; }
}
它可以改进,但效果很好。
客户的技术专家要求我们的 /token 端点可以在正文中使用 "application/x-www-form-urlencoded" 和 "application/json" 格式。 所以我不得不实施它,尽管它违反了规范。
如果路径为“/api/token”且 content-type 为 "application/json",则创建一个将 JSON 正文转换为 Url-encoded 正文的 Owin 中间件。不要忘记在 Startup.cs.
中注册它public sealed class JsonBodyToUrlEncodedBodyMiddleware : OwinMiddleware
{
public JsonBodyToUrlEncodedBodyMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
if (string.Equals(context.Request.ContentType, "application/json")
&& string.Equals(context.Request.Method, "POST", StringComparison.InvariantCultureIgnoreCase)
&& context.Request.Path == new PathString("/avi/token/"))
{
try
{
await ReplaceJsonBodyWithUrlEncodedBody(context);
await Next.Invoke(context);
}
catch (Exception)
{
context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
context.Response.Write("Invalid JSON format.");
}
}
else
{
await Next.Invoke(context);
}
}
private async Task ReplaceJsonBodyWithUrlEncodedBody(IOwinContext context)
{
var requestParams = await GetFormCollectionFromJsonBody(context);
var urlEncodedParams = string.Join("&", requestParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
var decryptedContent = new StringContent(urlEncodedParams, Encoding.UTF8, "application/x-www-form-urlencoded");
var requestStream = await decryptedContent.ReadAsStreamAsync();
context.Request.Body = requestStream;
}
private static async Task<Dictionary<string, string>> GetFormCollectionFromJsonBody(IOwinContext context)
{
context.Request.Body.Position = 0;
var jsonString = await new StreamReader(context.Request.Body).ReadToEndAsync();
var requestParams = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonString);
return requestParams;
}
}