分類: 未分類

在Azure AppService Docker Alpine Container上連線MSSQL遇到 網路不通的問題

在Azure AppService Docker Alpine Container上連線MSSQL遇到 網路不通的問題

最近使用AppService佈署Container服務

結果在連線資料庫的時候 一直掛掉

A network-related or instance-specific error occurred while establishing a connection to SQL Server.

Container的Linux版本是Alpine

因此安裝套件的指令是改用用apk add,不是apt-get

以後telnet套件無法直接安裝,而是被封裝進 busybox-extras 

Ref: https://github.com/gliderlabs/docker-alpine/issues/397

apk add busybox-extras

但實測Telnet下

app# busybox-extras telnet sea-dev-xxxx-sqlsrv.database.windows.net 1433

是通的,但程式還是連不過去

後來終於在這篇找到了一點蛛絲馬跡

https://github.com/dotnet/dotnet-docker/issues/2346

馬上試了一下
apk add icu-libs

果然通了,浪費了我二個小時XD

LeetCode 20大挑戰- 清單-置頂

LeetCode 20大挑戰- 清單-置頂

新增HackerRank筆記

  • New Year Chaos(Link)

Finished

  1. LRU Cache (Done) Link
    • List, dictionary, linked list
  2. Median of Two Sorted Arrays Link
    • Array merge, sorting
    • 困難的是如何達成O(log(m+n))
  3. Palindrome Number Link
    • string function
  4. Majority Element Link
  5. H-Index Link
  6. Two-Sum Link
    • HashTable
  7. Task Scheduler Link
  8. Three-Sum Link
    • Two Pointer
  9. Reverse Interger Link

Selected

  • Container With Most Water
    • Array, Two Pointer, Greedy
  • Reverse Nodes in k-Group(Hard) – Problem
    • Linked List, Recursion
  • Regular Expression Matching – Problem
    • String, Dynamic Programming, Recursion
  • Longest Substring Without Repeating Characters – Problem
    • String, hashtable , Sliding window
  • Substring with Concatenation of All Words
    • Sliding Window , string, HashTable
  • Minimum Path Sum
    • Array, DP, Matrix
  • Best Time to Buy and Sell Stock III (Best Time to Buy and Sell Stock IV)
    • Array, DP

LeetCode Median of Two Sorted Arrays

LeetCode Median of Two Sorted Arrays

看到Hard就來看看

Given two sorted arrays nums1 and nums2 of size m and n respectively, return the median of the two sorted arrays.

The overall run time complexity should be O(log (m+n)).

Example 1:

Input: nums1 = [1,3], nums2 = [2]
Output: 2.00000
Explanation: merged array = [1,2,3] and median is 2.

Example 2:

Input: nums1 = [1,2], nums2 = [3,4]
Output: 2.50000
Explanation: merged array = [1,2,3,4] and median is (2 + 3) / 2 = 2.5.

Constraints:

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -106 <= nums1[i], nums2[i] <= 106
直覺寫法
public class Solution {
    public double FindMedianSortedArrays(int[] nums1, int[] nums2) {
                var merged = nums1.Concat(nums2);

        var mergedArray = merged.OrderBy(a => a).ToArray();
        
        if (mergedArray.Length % 2 > 0)
        {
            var median = mergedArray[(mergedArray.Length - 1) / 2];
            return median;
        }
        else
        {
            var medianFirst = mergedArray[((mergedArray.Length) / 2)-1];
            var medianLast = mergedArray[((mergedArray.Length) / 2)];
            return ((double)medianFirst + (double)medianLast) / 2;
        }
    }
}

結果就這樣過了,到底為什麼是Hard?

看了某些人也是這樣寫
Complexity:
Time complexity: The time complexity is dominated by the sorting operation, which takes O(N log N) time, where N is the total number of elements in nums1 and nums2.
Space complexity: The space complexity is determined by the size of the combined list nums, which is O(N), where N is the total number of elements in nums1 and nums2.

public class Solution {
    public double FindMedianSortedArrays(int[] nums1, int[] nums2) {
         List<int> nums = nums1.ToList();
            nums.AddRange(nums2.ToList());
            nums.Sort();
            if(nums.Count % 2 == 1)
                return nums[nums.Count/2];

            return (nums[nums.Count/2] + nums[nums.Count/2 - 1])/2.0;
    }
}

所以這題的難度感覺是如何 說明其時間複雜度吧XD

終於為部落格加上SSL設定(劇情急轉直下)

終於為部落格加上SSL設定(劇情急轉直下)

我的網站在http的世界中許久 ,這陣子終於想來弄SSL環境了(因為 老婆要寫網址XD) 先前我記得是卡在PH …

以上就是庫存頁檔救回的一小段字 XD

無意見發現多年前設定的mariadb,密碼一點都沒有安全性,結果就被破解了

苦腦許久,發現我在 6月初有備份image,至少還救的回2022年以前的文章 Orz

目前我先 救到這邊…

後續故事跟被入侵綁架的故事再補上….

以下是phpmyadmin被瘋逛try 密碼的logs(從docker logs來的)

資料庫還給我留下警語

.NetCore的ApiGateway +Consul的本機實驗

.NetCore的ApiGateway +Consul的本機實驗

一直以來都知道.netcore可以開始跨平台運作在linux上,因此運作在 docker的container中也是可行的

不過最近因為要研究apigateway,需要模擬infra的架構 ,因此需要在本機環境buildup起來多台主機的模式

沒想到入坑玩了一下,就發現,其實眉眉角角也不少,但來來回回操作後,其實是相對會愈來愈熟悉

首頁,我solution的構成包含了以下幾層 (apigateway還可做loadbalance,再加個nginx就可以, 不是這次的重點)

1.Redis(多台 instance作counter的cache)
2.WebApp(.net core)
3.ApiGateway(.net core + ocelot.net sdk)
4.Consul(service discovery service 作ha)

這次主要研究的機制為 3跟4之間的協作機制
基本上,有了3,預設走configuration就可以手動配置 ,但若要能做到動態ha的話,就必須加入service discovery的機制,剛好ocelot框架有支援 consul(還有其他套,但預設consul)

docker-compose

為了方便 重建,這次採用 docker-compose的yaml來維護(以下情境的container -name 都一般化了,只需要區分角色就好)

version: "3"

services:
  nginx:
    build: ./nginx
    container_name: core-app.local
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - core-app-gateway1
    # network_mode: "host"
    networks:
      - backend

  consul1:
    image: consul
    container_name: node1
    command: agent -server -bootstrap-expect=2 -node=node1 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1
    # network_mode: "host"
    networks:
      - backend

  consul2:
    image: consul
    container_name: node2
    command: agent -server -retry-join=node1 -node=node2 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1
    depends_on:
      - consul1
    # network_mode: "host"
    networks:
      - backend

  consul3:
    image: consul
    hostname: discover-center
    container_name: node3
    command: agent -retry-join=node1 -node=node3 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1 -ui
    ports:
      - 8500:8500
    depends_on:
      - consul1
      - consul2
    #network_mode: "host"
    networks:
      - backend

  redis:
    container_name: myredis
    image: redis:6.0.6
    restart: always
    ports:
      - 6379:6379
    privileged: true
    command: redis-server /etc/redis/redis.conf --appendonly yes
    volumes:
      - $PWD/data:/data
      - $PWD/conf/redis.conf:/etc/redis/redis.conf
    networks:
      - backend

  core-app-discover-a1:
    build: ./dotnetapp/dotnetapp-discover
    hostname: webapp1
    environment:
      - HOSTNAME=http://0.0.0.0:40001
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://core-app-discover-a1
      - CLIENT_PORT=40001
      - SERVICE_NAME=core-app-discover-a
      - RedisConnection=redis:6379,password=12345678,allowAdmin=true
    depends_on:
      - redis
      - consul3
    ports:
      - "40001"
    networks:
      - backend

  core-app-discover-a2:
    build: ./dotnetapp/dotnetapp-discover
    hostname: webapp2
    environment:
      - HOSTNAME=http://0.0.0.0:40001
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://core-app-discover-a2
      - CLIENT_PORT=40001
      - SERVICE_NAME=core-app-discover-a
      - RedisConnection=redis:6379,password=12345678,allowAdmin=true
    depends_on:
      - redis
      - consul3
    ports:
      - "40001"
    networks:
      - backend

  core-app-discover-a3:
    build: ./dotnetapp/dotnetapp-discover
    hostname: webapp3
    environment:
      - HOSTNAME=http://0.0.0.0:40001
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://core-app-discover-a3
      - CLIENT_PORT=40001
      - SERVICE_NAME=core-app-discover-a
      - RedisConnection=redis:6379,password=12345678,allowAdmin=true
    depends_on:
      - redis
      - consul3
    ports:
      - "40001"
    networks:
      - backend

  core-app-discover-b:
    build: ./dotnetapp/dotnetapp-discover
    hostname: webapp-b
    environment:
      - HOSTNAME=http://0.0.0.0:50001
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://core-app-discover-b
      - CLIENT_PORT=50001
      - SERVICE_NAME=core-app-discover-b
      - RedisConnection=192.168.1.115:6379,password=xxxxxxx
    depends_on:
      - redis
      - consul3
    ports:
      - "50001:50001"
    networks:
      - backend

  core-app-discover-c:
    build: ./dotnetapp/dotnetapp-discover
    hostname: webapp-c
    environment:
      - HOSTNAME=http://0.0.0.0:50002
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://core-app-discover-c
      - CLIENT_PORT=50002
      - SERVICE_NAME=core-app-discover-c
      - RedisConnection=192.168.1.115:6379,password=xxxxxxx
    depends_on:
      - redis
      - consul3
    ports:
      - "50002:50002"
    networks:
      - backend

  core-app-gateway1:
    build: ./dotnetapp/dotnetapp-gateway
    hostname: gateway1
    environment:
      - HOSTNAME=http://0.0.0.0:20001
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://192.168.1.115
      - CLIENT_PORT=20001
      - SERVICE_NAME=core-app-gateway1
    depends_on:
      - consul3
    ports:
      - 20001:20001
    networks:
      - backend

networks:
  backend:
    driver: bridge

以上yaml就會建立一個network讓 container之前可以用 hostname來通訊。

這次研究的過程,會發現一件事情就是若要綁service discovery,那麼每個末端的webapp都需要跟register center做註冊,並提供一個health接口。但是若要做到auto scale,那依賴docker-compose所做的scale就會綁定動態的host_name(有序號),因此在此不建議 用scale直接長n台容器,而是先用多個端點來建置(如我的a1、a2、a3三台)
這樣就可以指定 hostname跟port給 webapp,而不會被scaling的機制而無法抓到動態的port跟名稱

.netcore application的Dockerfile

以下是webapp的dockerfile (gateway的打包方式差不多,就不貼了)

FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
WORKDIR /src
COPY ["dotnetapp-discover.csproj", "dotnetapp-discover/"]
RUN dotnet restore "dotnetapp-discover/dotnetapp-discover.csproj"
COPY . ./dotnetapp-discover
WORKDIR "/src/dotnetapp-discover"
RUN dotnet build "dotnetapp-discover.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "dotnetapp-discover.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "dotnetapp-discover.dll"]

為了計數器功能與多台sharing cache,因此這次 還 模擬了一下連redis
而redis的connection string 可以透過 environment variable 注入進來(docker-compose可以另外用.env的檔案來管理不同環境,有興趣可以看 官方文件),而在.netcore的c#程式中,要抓到docker的參數的話可以使用GetEnvironmentVariable來取得設定值

    public static IServiceCollection AddCacheService(this IServiceCollection services)
    {
        var redisConnection = Environment.GetEnvironmentVariable("RedisConnection");
#if DEBUG
        redisConnection = "192.168.1.115:6379,password=xxxxxx,allowAdmin=true";
#endif
        
        services.AddSingleton<IConnectionMultiplexer>(
            ConnectionMultiplexer.Connect(redisConnection)
        );
        return services;
    }

註∶為了反覆測試,才加上allow admin,不然這個開關是很危險的,可以直接flush掉你的所有快取(清空)

Redis記錄的Log型態

註冊Service Discovery

ApiGateway跟WebApp我都有跟Consul註冊Service Discovery
因此這次主要的寫法也是follow consul client sdk的套件,Consul的註冊管理上,同一個Name的會被Grouping起來,ID要給不一樣的,若ServiceName都不一樣,就算是一樣的程式,還是會被視為不同節點,這樣會無法作HA的判斷

static void RegisterConsul(string serviceName, string serverHostName , string serverPort, string clientHostName , string clientPort, IHostApplicationLifetime appLifeTime)
{
    using var consulClient = new Consul.ConsulClient(p => 
        { p.Address = new Uri($"{serverHostName}:{serverPort}"); });
    var registration = new AgentServiceRegistration()
    {
        Check = new AgentServiceCheck()
        {
            Interval = TimeSpan.FromSeconds(10),
            HTTP = $"{clientHostName}{(clientPort == string.Empty ? "" : $":{clientPort}")}/health",
            Timeout = TimeSpan.FromSeconds(5),
            Method = "GET",
            Name = "check",
        },
        ID = Guid.NewGuid().ToString(),
        Name = serviceName,
        Address = Dns.GetHostName(),
        Port = int.Parse(clientPort),
        
    };

    try
    {
        consulClient.Agent.ServiceRegister(registration).Wait();
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
    }

    //web服務停止時,向Consul解註冊
    consulClient.Agent.ServiceDeregister(registration.ID).Wait();
}

以上function可以在program.cs的startup時期先進行註冊,而這個function所有的參數一樣可以來自於 docker-compose
範例∶

自動生成HealthCheck端點

在webapp的startup可以使用以下語法就長出health接口

builder.Services.AddHealthChecks();

app.MapHealthChecks("/health",  new HealthCheckOptions()
{
    ResultStatusCodes =
    {
        [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Healthy] = StatusCodes.Status200OK,
        [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Degraded] = StatusCodes.Status200OK,
        [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    }
});

但還是要視服務加上MapHealthChecks的ResultStatus的設定,以consul來說,就是回200就好了,若沒有加上這個設定,其實health的回應並不是200,所以會被視為失敗

正常情況打 health的接口

Ocelot的設定檔

最後就是ocelot的json設定,傳統的Ocelot自已就有Load-Balance機制,因此若有多台Downstream的服務,本來就可以用以下設定去管理服務

不過這樣做的缺點就是若隨時要增減 容器/主機時,都要再去重新配置設定檔。造成主機可能的reset與維運上的難度,因此透過Service Discovery的好處就浮上了臺面!以下的配置,只需要注意,不用定義DownStream的Host,只要給一個ServiceName,這個ServiceName要跟WebApp在Startup的時候註冊的一樣。並在最後GloablConfiguration有註冊Service DiscoveryProvider的設定,就可以打通ApiGateway跟Service Discovery之間的便利機制。

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/test",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/newapi/get",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "ServiceName": "core-app-discover-a",
      "LoadBalancerOptions": {
        "Type": "LeastConnection"
      }
    },
    {
      "DownstreamPathTemplate": "/test/list",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/newapi/list",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "ServiceName": "core-app-discover-a",
      "LoadBalancerOptions": {
        "Type": "LeastConnection"
      }
    },
    {
      "DownstreamPathTemplate": "/test",
      "DownstreamScheme": "http",
      "DownstreamHttpMethod": "DELETE",
      "UpstreamPathTemplate": "/newapi/reset",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "ServiceName": "core-app-discover-a"
    }, 
    {
      "Key": "api1",
      "DownstreamPathTemplate": "/test/list",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/newapi/list1",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "ServiceName": "core-app-discover-a"
    },
    {
      "Key": "api2",
      "DownstreamPathTemplate": "/test/list",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/newapi/list2",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "ServiceName": "core-app-discover-a"
    }
  ],
  "Aggregates": [
    {
      "RouteKeys": [
        "api1",
        "api2"
      ],
      "UpstreamPathTemplate": "/newapi/listall"
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://0.0.0.0:20001",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "192.168.1.115",
      "Port": 8500,
      "Type": "PollConsul",
      "PollingInterval": 100
    },
    "QoSOptions": {
      "ExceptionsAllowedBeforeBreaking": 0,
      "DurationOfBreak": 0,
      "TimeoutValue": 0
    },
    "LoadBalancerOptions": {
      "Type": "LeastConnection",
      "Key": null,
      "Expiry": 0
    },
    "DownstreamScheme": "http",
    "HttpHandlerOptions": {
      "AllowAutoRedirect": false,
      "UseCookieContainer": false,
      "UseTracing": false
    }
  }
}

上述的ApiGateway一樣用DockerFile把專案包成Image

整合測試

docker-compose up -d –force-recreate

會看到一次起來這麼多服務

http://localhost:20001/newapi/get(透過api gateway打到downstream的service)

這邊多請求幾次會發現machineName一直在webapp1~3跳來跑去,採用load-balance的機制,ocelot會採用你設定的策略進行請求分派

ApiGateway有Aggregation的功能,就是可以把多個Api請求Combine在一起,讓請求變成1次,減少頻寬的消耗,這邊我也做了配置,來試看看吧

出於Ocelot.json的設定檔段落:

透過api gateway的組合讓 多個 Api可以一次 回傳

最後,打了很多次請求後,來總覽一下所有的請求摘要(程式是自已另外撈Redis的Request記錄來統計)

http://localhost:20001/newapi/list(透過api gateway打到down stream的service)

嘗試Docker Stop某一台 Discover-a1的服務,Consul DashBoard馬上就看的到紅點了!

這個時候再去打 之前load-balance的端點http://localhost:20001/newapi/get,會發現machineName會避開stop掉的服務只剩下 webapp1跟webapp3在輪循,因此做到了後端服務品質的保護

小插曲

ID不小心用成了Guid.New,所以會導致重新啟動就又拿到新的ID,造成了多一個Instance。但舊的也活了,因為他們的HealthCheck是一樣的

其實ApiGateway的議題,從Infra的角度上,可以玩的東西還有很多。

像是他也支援RateLimiting, QualityOfService(Polly) , 服務聚合(Aggregation)等機制,都可以從他們的官方文件看到設定方式,算是配置上不算太複雜。

不過 Ocelot在.net這邊發展一陣子後,近一年好像停更了!取而代之的好像是其他語言寫的更強的Apigateway: Envoy。

Envoy資源
https://www.envoyproxy.io/docs/envoy/latest/intro/what_is_envoy
https://www.netfos.com.tw/Projects/netfos/pages/solution/plan/20210111_plan.html

若只是要輕量級的,或是想參考其架構設計的,倒是可以翻github出來看看~

Github∶ https://github.com/ThreeMammals/Ocelot

這次Lab程式碼

若要拿去玩的,請把Ip, Password自行調整一下

一些文章的參考

https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html?highlight=ServiceDiscoveryProvider

https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html?highlight=ServiceDiscoveryProvider#

https://www.uj5u.com/net/232249.html

.NET Core WebApi 加入CORS

.NET Core WebApi 加入CORS

有時候我們希望測試Client端呼叫Server的Api,但若domain不相同或是沒有合理的開放的話,Browser會直接進行安全性的封鎖

<html>
    <head>
    </head>
    <body>
        <script language="javascript">
            window.onload = function(e){
                // Make a request for a user with a given ID
                axios.get('https://localhost:5001/api/test/history')
                .then(function (response) {
                    // handle success
                    console.log(response.status);
                })
                .catch(function (error) {
                    console.log(error.response.status);
                    // handle error
                    console.log(error);
                })
                .then(function () {
                    // always executed
                });
            }
        </script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.2/axios.min.js" integrity="sha512-odNmoc1XJy5x1TMVMdC7EMs3IVdItLPlCeL5vSUPN2llYKMJ2eByTTAIiiuqLg+GdNr9hF6z81p27DArRFKT7A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    </body>
</html>

若我們真的去打了一個404不存在的網頁,但實際上Server有接到請求,所以回的了404,但是js端卻會因為CORS的Policy導致報錯,這個錯誤的例外,會導致js拿到ERR_NETWORK而且也無其他訊息或完整的異常資訊

.Net Core Startup.cs中,找到以下段落
//public void ConfigureServices(IServiceCollection services)內
services.AddCors(options => { options. AddPolicy(name: "MyAllowSpecificOrigins", policy => { policy. AllowAnyOrigin() . AllowAnyHeader(); }); });
//public void Configure(IApplicationBuilder app, IWebHostEnvironment env)內

app.UseRouting();
            
app.UseCors("MyAllowSpecificOrigins");

app.UseSentryTracing(); // 要啟用 Sentry 上的 Performance Monitoring 需加這行

app.UseAuthentication();

備註:UseCors的呼叫必須放在UseRouting之後 ,但在UseAuthorization之前,若有具名的policy,就也要具名UseCors加進來,若有具名但沒有UseCors(“xxxxxx”)的話,仍視為無效

所以成功加進去後,axios套件也是可以抓到

MSDN Reference∶ https://docs.microsoft.com/zh-tw/aspnet/core/security/cors?view=aspnetcore-6.0

SQL Azure 垂直Partitioned DB Query T-SQL範例

SQL Azure 垂直Partitioned DB Query T-SQL範例

SQL Azure的連線字串雖然允許你在SQL工具上切換資料庫,但是其實要下Cross Database的T-SQL是沒有辦法允許的
因此我們需要利用Elastic database query + External Data Source來協助我們

以下假設我們有主資料庫 (A),與延伸資料庫(B)

在B的資料庫中
我們先建立以下的CREDENTIAL資訊

CREATE MASTER KEY ENCRYPTION BY PASSWORD = '{A資料庫的密碼}';
CREATE DATABASE SCOPED CREDENTIAL ds_pool WITH IDENTITY = '{A資料庫的帳號}',SECRET = '{A資料庫的密碼}';
--以下語法提供方便重新建立/測試
--drop MASTER KEY --drop DATABASE SCOPED CREDENTIAL ds_pool

ds_pool不需要用字串包起來,你可以命名成你可理解的名稱,下面我們會再用到
執行成功的話,接著執行 以下建立Remote DataSource的物件

CREATE EXTERNAL DATA SOURCE REMOTE_DS WITH ( 
   TYPE=RDBMS, 
   LOCATION='{A資料庫的Localtion}stage.database.windows.net', 
   DATABASE_NAME='{A資料庫的名稱}', 
   CREDENTIAL= ds_pool );

--以下語法提供方便重新建立/測試
--Drop EXTERNAL DATA SOURCE REMOTE_DS

這邊我們要給定一個REMOTEDS到DataSouce,並指定他的CREDENTIAL與資料庫名稱、類型與 Location,從MSDN上可以看到Location似乎可以支援檔案格式,可以填寫blob的位置,這個有機會再研究

CREATE EXTERNAL TABLE [dbo].[{A資料庫裡面的Table Or View都可以}] 
 ( [A資料庫的某一個table的欄位1] [欄位型別1], 
   [A資料庫的某一個table的欄位2] [欄位型別2], 
   .....略
 ) WITH ( DATA_SOURCE = REMOTE_DS )

--drop EXTERNAL TABLE [dbo].[{A資料庫裡面的Table Or View都可以}]
select * from [dbo].[{A資料庫裡面的Table Or View都可以}]

這邊我們必須在B資料庫端,定義 A資料庫希望 remote存取的物件的schema,可以完整也可以只撈取部分~~端看需求,在B資料庫端,也可以給定一個新的別名,語法可以參考以下。

CREATE EXTERNAL TABLE [dbo].[remote_xxxxxx]
( [A資料庫的某一個table的欄位1] [欄位型別1], 
   [A資料庫的某一個table的欄位2] [欄位型別2], 
   .....略
 ) WITH ( 
    DATA_SOURCE = REMOTE_DS
    ,SCHEMA_NAME = 'A資料庫裡面的Table Or View的schema,通常是dbo'
    ,OBJECT_NAME = '{A資料庫裡面的Table Or View}'
)

實測後,確實看起來是可以運作查詢,透過在B資料庫的連接中,直接查詢A

這與Elastic Pool似乎不一樣,而目前我們使用 到的費用也是包含在SQL Database中因此不另外計費

Currently, the elastic database query feature is included into the cost of your Azure SQL Database.

註1∶但是開一台elastic pool的主機,至少2000nt/月起跳…用途與解決問題的目的性應該不太一樣吧(沈思…)

註2∶若你的store procedure中有使用到External DataSource,語法中有使用With (nolock)的話,會出現 – “Table hints are not supported on queries that reference external tables”.的錯誤訊息

Reference

https://docs.microsoft.com/en-us/azure/azure-sql/database/elastic-query-getting-started-vertical

https://social.technet.microsoft.com/wiki/contents/articles/37170.azure-sql-database-using-external-data-sources.aspx

Line Login Jwt 驗證簽名 自行實作 c#範例

Line Login Jwt 驗證簽名 自行實作 c#範例

跟Line整合後,經過OAuth第一步驟拿到的grant_code可以再請求 Line Api拿到Open Id協定的id_token

這個是一個標準,從id_token可以解出平台授權的使用者資訊,可能包含廠商的id, 用戶的識別碼

但是若人人都可以拿這個token重新replay使用的話,那就有更大的風險

因此我們需要驗證其合法性,而這個驗證通常都要在server進行,因為我們不可能把密鑰丟到client的browser去嘛

所以還是好好的保留在Server吧。

以下Line有python3的範例∶ https://developers.line.biz/en/docs/line-login/verify-id-token/#write-original-code

今天我試著改寫一下成c#的版本,這個版本主要是驗證簽名是否合法,至於想再驗證payload上的資訊,是否到期(exp、aud)、正確性,可以再自行取出後比對。

註∶若是自行實作的jwt,可以依不用的加簽實作驗證handler,此處為line的演算法sha256為例

using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;

var idToken =
    "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FjY2Vzcy5saW5lLm1lIiwic3ViIjoiVTVkZDllZWNjYmUzMTB..略..c3cKg";

var secret = "{請換成你的line channel secret}";

var result =TryValidateToken(idToken);

Console.WriteLine($"token is valid(only check signature): {result}");
Console.ReadKey();


string base64url_decode(string target)
{
    return Base64UrlEncoder.Decode(target);
}

bool check_signature(string key, string target, byte[] signature)
{
    var hmacsha256 = new HMACSHA256();
    hmacsha256.Key = Encoding.UTF8.GetBytes(key);
    var cal_signature = hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(target));
    var signatureHashed = signature;

    return CompareArraysExhaustively(cal_signature, signatureHashed);
}

bool CompareArraysExhaustively(byte[] first, byte[] second)
{
    if (first.Length != second.Length)
    {
        return false;
    }

    bool ret = true;
    for (int i = 0; i < first.Length; i++)
    {
        ret = ret & (first[i] == second[i]);
    }

    return ret;
}


bool TryValidateToken(string token)
{
    if (string.IsNullOrWhiteSpace(token))
    {
        return false;
    }

    var handler = new JwtSecurityTokenHandler();

    try
    {
        var jwt = handler.ReadJwtToken(token); //JWT驗證物件
        if (jwt == null)
        {
            return false;
        }

        var idTokens = token.Split('.');
        var header = idTokens[0];
        var payload = idTokens[1];
        var signature = idTokens[2];

        var header_decoded = base64url_decode(header);
        var payload_decoded = base64url_decode(payload);
        var signature_decoded = Base64UrlEncoder.DecodeBytes(signature);

        var valid_signature = check_signature(secret,
            header + '.' + payload,
            signature_decoded);

        return valid_signature;
        
        //傳統驗證方法 ..
        // ClaimsPrincipal principal = null;
        // var secretBytes = Convert.FromBase64String(secret);
        //
        // var validationParameters = new TokenValidationParameters
        // {
        //     RequireExpirationTime = true,
        //     ValidateIssuer = false,
        //     ValidateAudience = false,
        //     IssuerSigningKey = new SymmetricSecurityKey(secretBytes),
        //
        //     //LifetimeValidator = LifetimeValidator
        //
        //     ClockSkew = TimeSpan.Zero
        // };
        //
        // SecurityToken securityToken;
        // principal = handler.ValidateToken(token, validationParameters, out securityToken);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"msg:{ex.Message}");
        return false;
    }
}
Line Login 的Web Js程式碼實作

Line Login 的Web Js程式碼實作

以下程式的參數請結合官方文件取用

https://developers.line.biz/en/docs/line-login/integrate-line-login/#login-flow

先在畫面置入LineLogin的按鈕

        <div class="btn btn-primary btn-sm box-shadow-primary text-white border-0 mr-3" @click.prevent="handleLineLoginButtonClick">
          Line Login
        </div>
handleLineLoginButtonClick() {
      let URL = 'https://access.line.me/oauth2/v2.1/authorize?';
      URL += 'response_type=code';
      
      //channel id
      URL += '&client_id=$your channel id$'; 

      // 與Line Developer Channel設定的一樣就好,驗證完就會經過瀏覽器轉址回來
      URL += '&redirect_uri=http://localhost:3000/';

      // 狀態碼是會從client呼叫端帶到驗證端,再從驗證端帶回
      // 因此應該可以作為一個亂數卡控確保是自   已丟出去的交易碼
      URL += '&state=abcde';   
     
      //要求存取的範圍
      URL += '&scope=openid%20profile';
      window.location.href = URL;
    },

以上足以從你的網頁導頁到line login的頁面(這個版本與2.0所提供的weblogin,還是有差別,因為我試著串接 2.0的版本,導頁過去後,沒有掃qr code登入的選項)

導頁過去line驗證通過後,會透過redirect_url轉址回來,並帶有Authorization Code
這個Authorization Code並不能直接拿來存取Profile的資料
但我們先設定某個route要能接QueryString的function如下:

beforeMount() { 
  this.checkLineLoginCallBack(); 
},
methods:{
//...略
checkLineLoginCallBack() {
      const urlParams = new URLSearchParams(window.location.search);
      if (urlParams.has('state') && urlParams.has('code')) {
        const state = urlParams.get('state');
        const code = urlParams.get('code');
        if (state === 'abcde') { 
          const URL = 'https://api.line.me/oauth2/v2.1/token?';
          const getTokenUrl = `${URL}`;
          const getTokenBody = queryString.stringify({
            grant_type: 'authorization_code',
            code,
            redirect_uri: 'http://localhost:3000/',
            client_id: '$your channel id$',
            client_secret: '$your secret$',
          });
          axios.post(getTokenUrl, getTokenBody).then((e1) => {
            const token = e1.data.access_token;
            const idToken = e1.data.id_token;
            console.log(e1);
            const getProfileApiUrl = 'https://api.line.me/v2/profile';
            axios({
              method: 'GET',
              url: getProfileApiUrl,
              responseType: 'json', // responseType 也可以寫在 header 裡面
              headers: {
                Authorization: `Bearer ${token}`, // Bearer 跟 token 中間有一個空格
              },
            }).then((e2) => {
              alert(`line user id: ${e2.data.userId}, display name:${e2.data.displayName}`);
              console.log(JSON.stringify(e2));

              const getVerifyApiUrl = 'https://api.line.me/oauth2/v2.1/verify';
              const getVerifyBody = queryString.stringify({
                client_id: '1656034758',
                id_token: idToken,
              });
              axios.post(getVerifyApiUrl, getVerifyBody).then((e3) => {
                alert(`line email: ${e3.data.email}`);
                console.log(JSON.stringify(e3));
              }).catch((error) => {
                console.log(`錯誤: ${error}`);
              });
            }).catch((error) => {
              console.log(`錯誤: ${error}`);
            });
          })
            .catch((error) => {
              console.log(error);
              alert(error);
            });
        }
      }
    }
};

上述的內容代表,Line CallBack以後,透過authorization code再一次跟line取得access Token
這個時候的access token就可以拿來放在Bearer的header去跟line的api請求資訊
注意∶authorization code與access token都有存取時間,因此timeout後就要重新請求一份
在測試的時候通常也會有錯誤訊息明確的告訴你格式錯誤、參數錯誤,建議使用postman先確認錯誤訊息或參數的問題

取得email的api
https://developers.line.biz/zh-hant/docs/line-login/integrate-line-login/#verify-id-token