简体   繁体   English

比较JSON对象的哈希

[英]Compare the Hash of a JSON object

I am attempting to get a hash comparer to work so I can validate an incoming request. 我正在尝试让哈希比较器工作,以便可以验证传入的请求。

Flow: 流:
Sender creates json object -> sender creates hash of json object with a key that they and I know -> sender sends json object and header with hash in it -> I recieve request -> I hash the json object with the common key -> I compare my hash to the one in the header to validate user sending it Sender creates json object -> sender creates hash of json object with a key that they and I know Sender creates json object sender creates hash of json object with a key that they and I know -> sender sends json object and header with hash in it -> I recieve request -> I hash the json object with the common key -> I compare my hash to the one in the header to validate user sending it

I am struggling to create a hash from my json object. 我正在努力从json对象创建哈希。

This is the example code in Ruby (from the sender) where request_payload is JSON object. 这是Ruby中(来自发送方)的示例代码,其中request_payload是JSON对象。

hmac=OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'),YOUR_COMPANY_SIGNING_KEY,request_payload)  
signature = Base64.strict_encode64(hmac)

I want to do this in C# . 我想在C#中做到这一点。

I am using the data from the Call Rail API (see right side) and attempting to hash it into string and then encode it. 我正在使用来自Call Rail API的数据(请参见右侧),并尝试将其哈希为字符串,然后对其进行编码。

[HttpPost]
public async Task<ActionResult> PostAsync(dynamic request)
{

    string signature = GetHash(request.ToString(), "072e77e426f92738a72fe23c4d1953b4"); // this key is from the example in Call Rail

    string encodedSignature = Base64Encode(signature);

    return Ok();
}

public static String GetHash(dynamic text, String key)
{
    ASCIIEncoding encoding = new ASCIIEncoding();

    Byte[] textBytes = encoding.GetBytes(text);
    Byte[] keyBytes = encoding.GetBytes(key);

    Byte[] hashBytes;

    using (HMACSHA1 hash = new HMACSHA1(keyBytes))
        hashBytes = hash.ComputeHash(textBytes);

    return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}

public static string Base64Encode(string plainText)
{
    var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
    return System.Convert.ToBase64String(plainTextBytes);
}

I think where I am struggling is how I can take my incoming JSON 我认为我在努力的地方是如何接收传入的JSON

{"answered":false,"business_phone_number":"","call_type":"voicemail","company_id":155920786,"company_name":"Boost Marketing","company_time_zone":"America/Los_Angeles","created_at":"2018-02-19T13:41:00.252-05:00","customer_city":"Rochester","customer_country":"US","customer_name":"Kaylah Mills","customer_phone_number":"+12148654559","customer_state":"PA","device_type":"","direction":"inbound","duration":"13","first_call":false,"formatted_call_type":"Voicemail","formatted_customer_location":"Rochester, PA","formatted_business_phone_number":"","formatted_customer_name":"Kaylah Mills","prior_calls":16,"formatted_customer_name_or_phone_number":"Kaylah Mills","formatted_customer_phone_number":"214-865-4559","formatted_duration":"13s","formatted_tracking_phone_number":"404-555-8514","formatted_tracking_source":"Google Paid","formatted_value":"--","good_lead_call_id":715587840,"good_lead_call_time":"2016-06-17T10:23:33.363-04:00","id":766970532,"lead_status":"previously_marked_good_lead","note":"","recording":"https://app.callrail.com/calls/766970532/recording/redirect?access_key=aaaaccccddddeeee","recording_duration":8,"source_name":"Google AdWords","start_time":"2018-02-19T13:41:00.236-05:00","tags":[],"total_calls":17,"tracking_phone_number":"+14045558514","transcription":"","value":"","voicemail":true,"tracker_id":354024023,"keywords":"","medium":"","referring_url":"","landing_page_url":"","last_requested_url":"","referrer_domain":"","conversational_transcript":"","utm_source":"google","utm_medium":"cpc","utm_term":"","utm_content":"","utm_campaign":"Google AdWords","utma":"","utmb":"","utmc":"","utmv":"","utmz":"","ga":"","gclid":"","integration_data":[{"integration":"Webhooks","data":null}],"keywords_spotted":"","recording_player":"https://app.callrail.com/calls/766970532/recording?access_key=aaaabbbbccccdddd","speaker_percent":"","call_highlights":[],"callercity":"Rochester","callercountry":"US","callername":"Kaylah Mills","callernum":"+12148654559","callerstate":"PA","callsource":"google_paid","campaign":"","custom":"","datetime":"2018-02-19 18:41:00","destinationnum":"","ip":"","kissmetrics_id":"","landingpage":"","referrer":"","referrermedium":"","score":1,"tag":"","trackingnum":"+14045558514","timestamp":"2018-02-19T13:41:00.236-05:00"}

And then be able to Hash it into something useful. 然后能够将其哈希为有用的东西。

With the test signing key I am given, I should get back UZAHbUdfm3GqL7qzilGozGzWV64= . 使用给出的测试签名密钥,我应该找回UZAHbUdfm3GqL7qzilGozGzWV64= I know this from the APIDocs . 我从APIDocs知道这一点

I am currently sending up the JSON string above via postman but I notice that extra ' { } ' are added on when I treat it as datatype dynamic or object . 我目前正在通过邮递员发送上面的JSON字符串,但是当我将其视为dynamic数据类型或object类型时,我注意到会额外添加“ { } ”。

Any insight would be greatly appreciated! 任何见解将不胜感激!

I think the problem you are facing is that the .NET Core WebAPI is helpfully parsing the body into JSON (into a JObject ) for you. 我认为您面临的问题是.NET Core WebAPI正在帮助您将主体解析为JSON(转换为JObject )。

As @dbc identified, really what you need is the raw string body to use to generate the HMAC Signature, which you can verify before parsing the body into JSON yourself. 正如@dbc所标识的,实际上,您真正需要的是用于生成HMAC签名的原始字符串主体,您可以在将主体自行解析为JSON之前进行验证。

I tested This answer and was able to receive the body as a plain string: 我测试了此答案 ,并能够以纯字符串形式接收正文:

using System;
using System.Linq;
using System.Threading.Tasks;
using System.IO;
using Microsoft.AspNetCore.Mvc.Formatters;

namespace netcoretest {
    public class RawJsonBodyInputFormatter : InputFormatter
    {
        public RawJsonBodyInputFormatter()
        {
            this.SupportedMediaTypes.Add("application/json");
        }

        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            var request = context.HttpContext.Request;
            using (var reader = new StreamReader(request.Body))
            {
                var content = await reader.ReadToEndAsync();
                return await InputFormatterResult.SuccessAsync(content);
            }
        }

        protected override bool CanReadType(Type type)
        {
            return type == typeof(string);
        }
    }
}

in Startup.cs: Startup.cs中:

// in ConfigureServices()
services.AddMvc(options => {
    options.InputFormatters.Insert(0, new RawJsonBodyInputFormatter());
});

In your controller: 在您的控制器中:

[HttpPost]
public async Task<ActionResult> PostTest([FromBody]string request)
{
  // here request is now the request body as a plain string;
  // you can now compute the signature on it and then later parse it to JSON.


}

However, testing your current code to generate the Base64-encoded signature, I am not getting the correct signature: 但是,测试您当前的代码以生成Base64编码的签名,我没有得到正确的签名:

  1. you are converting the output of the HMAC to hexadecimal string, and then taking the bytes of that string and putting those into your Base64 encoding. 您将HMAC的输出转换为十六进制字符串,然后获取该字符串的字节并将其放入Base64编码中。 The sample ruby you linked returns plain bytes, not a hexadecimal string from HMAC.digest : 您链接的示例红宝石返回纯字节,而不是HMAC.digest的十六进制字符串:
[5] pry(main)> hmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), YOUR_COMPANY_SIGNING_KEY, s)
=> "Q\x90\amG_\x9Bq\xAA/\xBA\xB3\x8AQ\xA8\xCCl\xD6W\xAE"

So at least that portion needs to be corrected in your implementation as well. 因此,至少在实现中也需要纠正这一部分。

Update 更新

I was able to get the correct signature with the following code: 我可以使用以下代码获得正确的签名:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System;
using System.Security.Cryptography;
using System.Text;

namespace netcoretest.Controllers
{
    [Route("test")]
    [ApiController]
    public class TestController : ControllerBase
    {
        public TestController()
        {
        }

        // POST: /test
        [HttpPost]
        public async Task<ActionResult> PostTest([FromBody]string request)
        {
            ASCIIEncoding encoding = new ASCIIEncoding();
            Byte[] key = encoding.GetBytes("072e77e426f92738a72fe23c4d1953b4");
            HMACSHA1 hmac = new HMACSHA1(key);
            Byte[] bytes = hmac.ComputeHash(encoding.GetBytes(request));
            Console.WriteLine(ByteArrayToString(bytes));
            String result = System.Convert.ToBase64String(bytes);
            Console.WriteLine(result);

            return Ok();
        }

        public static string ByteArrayToString(byte[] ba)
        {
            return BitConverter.ToString(ba).Replace("-","");
        }
    }
}

I tested posting to this endpoint using the following: 我使用以下方法测试了向该端点的发布:

url --request POST https://localhost:5001/test --insecure --header 'Content-Type: application/json' --data-binary @test.json

where test.json is the sample JSON blob from the API Documentation. 其中test.json是API文档中的示例JSON blob。

If using this same code you cannot get the signature to match, double check that your test.json does not have any trailing newlines or whitespace. 如果使用相同的代码,则无法获得匹配的签名,请仔细检查您的test.json是否没有尾随换行符或空格。

Hope this helps! 希望这可以帮助!

Though @john_Ledbetter 's answer works great, I decided an action filter fit me better. 尽管@john_Ledbetter的答案很有效,但我还是决定使用一个动作过滤器更适合我。 So I used his answer as a base and modified it for me. 因此,我以他的答案为基础并为我修改了答案。 I don't believe this solution would need the InputFormatter 我不认为此解决方案将需要InputFormatter

ValidateCallRailRequestFiler.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace My_API.ActionFilters
{
    public class ValidateCallRailRequestFilter: ActionFilterAttribute
    {
        //private readonly ILogger<ValidateCallRailRequestFilter> _logger;
        //public ValidateCallRailRequestFilter(ILogger<ValidateCallRailRequestFilter> logger)
        //{
        //    _logger = logger;
        //}

        public override void OnActionExecuting(ActionExecutingContext actionContext)
        {
            //executing before action is called

            // this should only return one object since that is all an API allows. Also, it should send something else it will be a bad request
            var param = actionContext.ActionArguments.SingleOrDefault();
            if (param.Value == null)
            {
                //_logger.LogError("Object sent was null. Caught in ValidateCallRailRequestFilter class.");
                actionContext.Result = new BadRequestObjectResult("Object sent is null");
                return;
            }

            var context = actionContext.HttpContext;
            if (!IsValidRequest(context.Request))
            {
                actionContext.Result = new ForbidResult();
                return;
            }

            base.OnActionExecuting(actionContext);

        }

        private static bool IsValidRequest(HttpRequest request)
        {
            string json = GetRawBodyString(request.HttpContext);
            string token = "072e77e426f92738a72fe23c4d1953b4"; // this is the token that the API (Call Rail) would provide
            string signature = request.Headers["Signature"];

            // validation for comparing encoding to bytes and hashing to be the same
            //https://rextester.com/EBR67249

            ASCIIEncoding encoding = new ASCIIEncoding();
            byte[] key = encoding.GetBytes(token);
            HMACSHA1 hmac = new HMACSHA1(key);
            byte[] bytes = hmac.ComputeHash(encoding.GetBytes(json));

            string result = System.Convert.ToBase64String(bytes);            

            return signature.Equals(result, StringComparison.OrdinalIgnoreCase);
        }

        public static string GetRawBodyString(HttpContext httpContext)
        {
            var body = "";
            if (httpContext.Request.ContentLength == null || !(httpContext.Request.ContentLength > 0) ||
                !httpContext.Request.Body.CanSeek) return body;
            httpContext.Request.EnableRewind();
            httpContext.Request.Body.Seek(0, SeekOrigin.Begin);
            using (var reader = new StreamReader(httpContext.Request.Body, System.Text.Encoding.UTF8, true, 1024, true))
            {
                body = reader.ReadToEnd();
            }
            httpContext.Request.Body.Position = 0;
            return body;
        }
    }
}

This includes a body reader that reads in the JSON and a way to deny incoming requests that don't match the signature as 403 forbidden. 这包括读取JSON的主体读取器以及拒绝与403签名不匹配的传入请求的方法。

Then within my controller: 然后在我的控制器内:

[ValidateCallRailRequestFilter]
[HttpPost]
public async Task<ActionResult> PostAsync(dynamic request)
{
    ...
    return Ok();
}

with a using My_Api.ActionFilters; using My_Api.ActionFilters;

And then I hit it with postman. 然后我和邮递员打了。 请注意,它不是“美化” 注意这里的标题

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM