使用JSON的WebAPI2.0 OWIN令牌请求

使用JSON的WebAPI2.0 OWIN令牌请求,json,asp.net-web-api,asp.net-web-api2,owin,forms,Json,Asp.net Web Api,Asp.net Web Api2,Owin,Forms,我已经在VisualStudio中创建了一个新的WebAPI解决方案,并且正在使用这些代码来尝试和理解正在发生的事情 我有一个测试API,它与一个授权控制器和另一个实现所有实际功能的控制器一起运行 除了/Token请求之外,控制器(API)都通过接收JSON并用JSON应答来工作。这必须是: Content-Type: application/x-www-form-urlencoded 否则我只会得到一个错误 创建此端点的代码部分如下所示: OAuthOptions = new OAuthAu

我已经在VisualStudio中创建了一个新的WebAPI解决方案,并且正在使用这些代码来尝试和理解正在发生的事情

我有一个测试API,它与一个授权控制器和另一个实现所有实际功能的控制器一起运行

除了/Token请求之外,控制器(API)都通过接收JSON并用JSON应答来工作。这必须是:

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个成功响应,其中包含一个承载令牌:

$("#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


或者,我只是做了一些愚蠢的事情吗?

不需要
JSON.stringify(data)
直接传递数据。

使用
application/x-www-form-urlencoded
作为
内容类型
的原因很简单:令牌请求需要这种内容类型

任何其他内容类型都将破坏与OAuth2兼容的客户端兼容性。我建议你不要改变这种标准行为

注意
请注意:

postData("Token", data, "application/json", function (data)
{
    //...
}
只因为您根本不发送JSON,所以工作正常!即使您将
application/json
添加为
Content-Type
头,您的请求主体也会序列化为表单键值对(AJAX调用中的jQuery默认对象序列化)


OAuthAuthorizationServerMiddleware的默认实现(更确切地说是内部使用的)从
Microsoft.Owin.Security.OAuth
中,只需忽略
内容类型
标题,并尝试将请求正文作为表单读取。

OAuth2需要
应用程序/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);
    }

它可以改进,但效果很好。

一位客户的技术专家要求我们的/token端点可以在正文中同时使用“application/x-www-form-urlencoded”和“application/json”格式。 所以我不得不实现它,尽管它违反了规范

如果路径为“/api/token”,内容类型为“application/JSON”,则创建一个Owin中间件,将JSON正文转换为Url编码的正文。别忘了在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;
        }
    }
公共密封类JSONBodyTourenCodedBodymiddleware:OwinMiddleware
{
公共JsonBodyToUrlEncodedBodyMiddleware(OWINNEXT)
:base(下一个)
{
}
公共重写异步任务调用(IOwinContext上下文)
{
if(string.Equals(context.Request.ContentType,“application/json”)
&&等于(context.Request.Method,“POST”,StringComparison.InvariantCultureIgnoreCase)
&&context.Request.Path==新路径字符串(“/avi/token/”)
{
尝试
{
等待替换OnBodyWithUrlEncodedBody(上下文);
等待下一步。调用(上下文);
}
捕获(例外)
{
context.Response.StatusCode=(int)HttpStatusCode.BadRequest;
Write(“无效的JSON格式”);
}
}
其他的
{
等待下一步。调用(上下文);
}
}
专用异步任务ReplaceJsonBodyWithUrlEncodedBody(IOwinContext上下文)
{
var requestParams=await GetFormCollectionFromJsonBody(上下文);
var urlEncodedParams=string.Join(“&”,requestParams.Select(kvp=>$”{kvp.Key}={kvp.Value}”);
var decryptedContent=newstringcontent(urlEncodedParams,Encoding.UTF8,“application/x-www-form-urlencoded”);
var requestStream=await decryptedContent.ReadAsStreamAsync();
context.Request.Body=requestStream;
}
私有静态异步任务GetFormCollectionFromJsonBody(IOwinContext上下文)
{
context.Request.Body.Position=0;
var jsonString=wait new StreamReader(context.Request.Body).ReadToEndAsync();
var requestParams=JsonConvert.DeserializeObject(jsonString);
返回请求参数;
}
}

无需
JSON.stringify(数据)
感谢@gauravbhavsar现在可以工作了。如果你把它作为一个答案贴出来,我会认为它是正确的。我想这就提出了一个问题,为什么我需要JSON.stringify发布到其他端点的数据,而不是那个端点?我刚刚测试过,我确实需要。我会仔细研究一下,看看我是否能解决这个问题。谢谢你的全面回答。我已经验证了在不使用JSON.stringify()时POST数据不是JSON。我会按照你的建议去做,让它保持原样,这一个调用必须是form,其余的调用必须是JSON。谢谢你的链接。这对我帮助很大。有时阅读协议定义更有意义:)@Morvael Hi,你能解释一下你是如何验证POST数据不是JSON的吗?@psj01我通常使用任何浏览器的“网络”选项卡,我使用开发人员工具,并深入到请求有效负载,如果内容是“名称”=“值”对或JSON,应该很明显。除此之外,像Fiddler()这样的工具将允许您从基于非浏览器(或浏览器)的应用程序捕获流量。再次感谢您的帮助,但是我现在明白了为什么这样做,我觉得我不能将此标记为正确答案。
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;
        }
    }