Contents

LINE Message API Webhooks Signature Validation

Contents

LINE Message API Webhooks Signature Validation

一開始需求源自於紀錄LINE bot訊息,因為只是記錄訊息而已,並沒有其餘應用,所以沒有採用社群維護的LINE SDK以求精簡,結果在簽名驗證的時候卡住了…最後還是參考了社群的Source Code才解決,所以這邊簡單紀錄一下過程。

驗證簽名最主要是為了確保紀錄的訊息,是透過LINE Platform發送的,LINE Platform在發送到Webhooks指定網址的時候,會在header中包含一組簽名X-Line-Signature,我們要將Channel Secret 和 Request Body用HMAC-SHA256 進行雜湊後,與X-Line-Signature比對是否相符,Channel Secret 在啟動Webhooks就可以看到。

環境

  • .NET Core 3.1
  • 啟用LINE Message API服務

實作

  1. 啟動Webhooks功能

    LINE Developers Console 或是 LINE Official Account Manager 都可以啟動

    /static/LINE_Message_API_Webhooks_Signature_Validation_90905c03646e402796f655b8b97d83db/2020-10-21_16-20-49.png /static/LINE_Message_API_Webhooks_Signature_Validation_90905c03646e402796f655b8b97d83db/2020-10-21_16-04-52.png
  2. 使用授權篩選條件,驗證X-Line-Signature是否為LINE Platform發送

    1
    2
    3
    4
    5
    6
    7
    
    public class LineVerifySignatureFilter : IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            throw new NotImplementedException();
        }
    }
    
  3. 以下程式碼參考pierre3/LineMessagingApi WebhookRequestMessageHelper,要注意.NET Core 3.1 需使用EnableBuffering來多次讀取body

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    
    public class LineVerifySignatureFilter : IAuthorizationFilter
    {
        private readonly string _channelSecret;
    
        public LineVerifySignatureFilter(IConfiguration configuration)
        {
            _channelSecret = configuration["ChannelSecret"];
        }
    
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var request = context.HttpContext.Request;
            // .NET Core 3.1 需使用EnableBuffering來多次讀取body
            request.EnableBuffering();
    
            string requestBody = new StreamReader(request.Body).ReadToEndAsync().GetAwaiter().GetResult();
            request.Body.Position = 0;
            var xLineSignature = request.Headers["X-Line-Signature"].FirstOrDefault();
            if (string.IsNullOrEmpty(xLineSignature) || !VerifySignature(xLineSignature, requestBody))
            {
                throw new Exception("Signature validation faild.");
            }
        }
    
        private bool VerifySignature(string xLineSignature, string requestBody)
        {
            try
            {
                var key = Encoding.UTF8.GetBytes(_channelSecret);
                var body = Encoding.UTF8.GetBytes(requestBody);
    
                using HMACSHA256 hmac = new HMACSHA256(key);
                var hash = hmac.ComputeHash(body, 0, body.Length);
                var xLineBytes = Convert.FromBase64String(xLineSignature);
                return SlowEquals(xLineBytes, hash);
            }
            catch
            {
                return false;
            }
        }
    
        private static bool SlowEquals(byte[] a, byte[] b)
        {
            uint diff = (uint)a.Length ^ (uint)b.Length;
            for (int i = 0; i < a.Length && i < b.Length; i++)
                diff |= (uint)(a[i] ^ b[i]);
            return diff == 0;
        }
    }
    
  4. 使用TypeFilterAttribute簡化Filter

    1
    2
    3
    4
    5
    6
    
    public class LineVerifySignatureAttribute : TypeFilterAttribute
    {
        public LineVerifySignatureAttribute() : base(typeof(LineVerifySignatureFilter))
        {
        }
    }
    
  5. 簡單寫個logs檔看結果,透過Line發送訊息,如果通過驗證,就會有值

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    [ApiController]
    [Route("api/{controller}/{action}/{id?}")]
    public class LineLogsController : ControllerBase
    {
        [HttpPost]
        [LineVerifySignature]
        public IActionResult Index(Rootobject request)
        {
            var requestToJson = JsonConvert.SerializeObject(request);
            using (StreamWriter w = System.IO.File.AppendText(@"C:\test.txt"))
            {
                w.Write(requestToJson);
            }
            return Ok();
        }
    }
    
    /static/LINE_Message_API_Webhooks_Signature_Validation_90905c03646e402796f655b8b97d83db/2020-10-21_16-57-56.png

參考

Line Signature validation 其他範例

https://developers.line.biz/zh-hant/reference/messaging-api/#signature-validation

Line Message API 社群

https://github.com/pierre3/LineMessagingApi

ASP.NET Core 中的篩選條件

https://docs.microsoft.com/zh-tw/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.1