Latest Posts

2024年7月26日 星期五

如何在Android中儲存機要敏感資訊 文/恆逸資深講師 何孟翰

文/恆逸資深講師 何孟翰

在手持式的應用程式開發如Android、iOS中,如何安全的溝通伺服器和應用程式一直是個重要的課題,特別是在一個dev/ops持續建置與交付的環境中,為了安全起見,不要將api的key或者是secret存在git的repo是一種較為建議的常規。
當然在應用程式的實作中,有各種客製的方式能夠實作,但是Google其實替android/gradle提供了一種好方式能夠系統性的解決這個問題,Google採用的方式如下:

1.在專案的目錄下準備一個不會被版本控制的檔案,如同預設的local.properties,使用key/value的形式存放

2.程式在建置時,gradle會生成BuildConfig,就可以動態的存取到機敏資訊


這樣在dev/ops時的CICD時,可以動態的修改該檔案,建置出具有存取機敏資訊的artifact,但是source code並不會外泄這些機敏資訊,如此在一般的client/server通訊的程式。特別是目前這些GenAI的環境,這些生成式的框架都在伺服器,例如要存取OpenAI的ChatGPT或者是Gemini Pro甚至是一些企業內部的檢索增強生成RAG(Retrieval-Augmented Generation)系統都可以用這樣的方式處理。

讓我們用一個簡單的Android應用程式說明這樣的情境,假設有一個apiKey是abcde12345,我們可以用如下的方式存進一個不被版控的檔案並且在gradle建置時再取出。

首先將Android Studio新增到最新版,例如本文撰稿時是2023.3.1 Patch1(JellyFish),並且新增一個Empty View Activity。


專案名稱設定成SecretProperty,語言可以是Java或Kotlin,此處我們選取Java,最低API此處設定成API 25。


按下Finish即可,稍待片刻等待Gradle建置確認Hello World能夠生成即可。


套用Google提供的Secrets Gradle Plugin
  1. 找到project level的gradle,在此檔案中加入buildScript並且指定secret-gradle-plugin下的secrets-gradle-plugin,目前的版本是2.0.1
  2. 在App level中的plugins加入secrets-gradle-plugin的id如下:
  3. 要存取secret value必須要使用到一個BuildScript的類別,為了讓gradle能夠生成,可以在android中增加buildFeatures,設定buildConfig=true來啟動BuildConfig的使用
  4. 為了檢視結果,我們可以試著將Hello World!的字樣換成apiKey,因此在主程式的src/main/re/layout/activity_main.xml中設定id,並且將字型大小調大一點,方便程式存取驗證
  5. 在MainActivity.java中存取到該TextView並且透過BuildConfig來取得機敏資訊
  6. 在專案目錄下找到local.properties檔,如果沒有就生成一份新的
  7. 加入apiKey如下
  8. 此時可以重新建置,在模擬器下看到的結果應該如下
  9. 如果發現程式無法正確的解析到BuildConfig檔,則需要先點擊File、Invalid Caches,並且要重新啟動之後,將專案清空並且重新建置,也就是點擊Build/Clean Project再選取Build Rebuild Project即可
  10. 雖然應該在系統預設的範本中已經有設定了,但是還是可以檢查一下,在專案的.gitignore檔案中如果local.properties沒有加入請把它加入


🔎學習推薦

2024年7月9日 星期二

使用Ollama與Open WebUI在本地端部署Llama3大語言模型

文/恆逸資深講師 申建忠

Ollama是一個使用Go語言開發的本地端大語言模型框架,可以在本地端執行多種開放原始碼的大語言模型,如:Llama 3、Mistral、Gemma等。其中Meta Llama 3以15T語料進行訓練,模型效果號稱無限接近ChatGPT4。

本文介紹如何在本地端安裝Ollama與Open WebUI,並使用Ollama運行Meta Llama 3:8b模型。

首先至Ollama官網(https://ollama.com)下載對應版本的ollama執行檔,下載完成後直接點擊安裝即可。本文以Apple Macbook pro 2018為例。

安裝完成後,請點擊ollama執行檔啟動ollama設定。
依下面圖示,由左向右便可設定完成。

2024年7月5日 星期五

2024年7月2日 星期二

Anycast的用途說明【冤枉呀!人不是我殺的】

 

邱顯智 Ozzy Chiou

  • 恆逸教育訓練中心-資深講師
  • 技術分類:網路管理與通訊應用

 


剛接觸IPv6的朋友應該知道IPv6封包的傳遞方式有三種:Unicast(單播)、Multicast(群播)與Anycast(任一播),取消了傳統IPv4的Broadcast(廣播),於是很多朋友就直覺得誤以為Anycast是用來取代Broadcast的新技術,這個誤會可大了。

其實Anycast並不是IPv6的新技術,IPv4早就已經在用了,只是沒有大聲嚷嚷而己,最具代表性的範例就是Google的8.8.8.8以及8.8.4.4這兩台公用DNS伺服器,這是經過Google正式官宣使用Anycast技術的案例。

Google在世界各地建置了許多資料中心,在這些資料中心裡也建置了許多IP地址都是8.8.8.8或8.8.4.4的DNS伺服器,因此,如果您使用8.8.8.8或是8.8.4.4當做您的DNS伺服器的時候,BGP路由就會根據您所在的位置,將您的名稱解析查詢導向到離您最近的Google公用DNS伺服器。 所以Anycast的特性如下:

  • 1-to-Nearest:傳送到最接近的主機,最接近的主機則是交由路由協定(例如BGP)選擇的最佳路徑來決定。
  • 與Unicast共用同一段IP地址空間,如上述8.8.8.8以及8.8.4.4這兩個地址傳統是屬於IPv4的Class A地址,但是也可以用於Anycast之用,在Cisco路由器上,您只要在配置IPv6地址命令加上Anycast參數,該地址即可搖身一變改為Anycast的用途。
  • 允許多台主機配置同一個IP地址:一般的Unicast地址不可以有兩台主機配置同一個IP地址,否則會造成IP地址衝突的狀況,Anycast地址則無此限制。
  • 適合用於需要Load-sharing的服務:如果您的伺服器在公眾雲上面,您可以透過指定伺服器所在的Region,將用戶造訪的流量導向到最鄰近的伺服器,達到地理位置上的流量分散。甚至,這樣的配置也可以用來稀釋分散式阻斷服務(Distributed Denial-of-Service)攻擊的流量,減緩遭受DDoS攻擊的影響。

您可在下列課程中了解更多技巧喔!

2024年6月24日 星期一

手動設定可用性群組 - Azure VM 上的 SQL Server(上)

文/恆逸資深講師 楊先民
本文章是在說明如何為 Azure VM 上的 SQL Server,在單一子網路內建立 Always On 可用性群組。
整個內容會建立一個在兩部 SQL Server 執行個體上都有一份資料庫複本的可用性群組。
你也可以使用 Azure 入口網站PowerShell 或 Azure CLI,或使用 Azure 快速入門範本以自動完成這些步驟。
以下是完成這個練習的必需需求:

2024年6月13日 星期四

企業最熱話題:ESG永續發展,最推薦的綠領人才課程:ISO 14064溫室氣體主導查證師/確證師訓練課程




氣候變遷議題備受全球關注,企業被要求更透明地揭露溫室氣體排放資訊。許多國家政府已意識到必須採取行動因應環境挑戰,紛紛推動碳定價等政策,未來企業將面臨支付碳排放費用的壓力。有效進行溫室氣體盤查並落實碳管理,將成為企業永續經營不可或缺的一環。

2024年6月12日 星期三

在Blazor使用Fluent UI元件-5

作 者:恆逸資深講師 許薰尹

在本站《在Blazor使用Fluent UI元件 - 4》一文介紹如何在現有的Blazor專案中,手動加入Fluent UI元件 ,並使用「FluentDialog」元件設計確認刪除的對話盒。在這一篇文章中,我們將介紹延續這系列文章的情境,介紹該如何自訂對話盒,以便讓其它元件能夠重複使用這個對話盒。

設計MyDialog


首先讓我們使用元件來自訂對話盒,元件的程式碼可放在專案根目錄下的「Shared」資料夾。從「Solution Explorer」視窗 - 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Folder」選項,將新建立的資料夾命名為「Shared」。

在「Shared」資料夾中加入Razor元件,從「Solution Explorer」視窗 -「Shared」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Razro Component」項目,將元件名稱命為「MyDialog.razor」,然後在檔案中加入以下程式碼:


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
@using Microsoft.FluentUI.AspNetCore.Components
@implements IDialogContentComponent<FluentUIDataGrid.Models.Customer>
 
@* Header *@
<FluentDialogHeader ShowDismiss = "true">
  <FluentStack VerticalAlignment = "VerticalAlignment.Center">
    <FluentIcon Value = "@(new Icons.Regular.Size24.WindowApps())" />
    <FluentLabel Typo = "Typography.PaneHeader">
        @Dialog.Instance.Parameters.Title
    </FluentLabel>
  </FluentStack>
</FluentDialogHeader>
 
@* Footer *@
<FluentDialogFooter>
  <FluentButton Appearance = "Appearance.Accent" OnClick = "@DeleteAsync"> Delete </FluentButton>
  <FluentButton Appearance = "Appearance.Neutral" OnClick = "@CancelAsync"> Cancel </FluentButton>
</FluentDialogFooter>
 
@* Body *@
<FluentDialogBody>
  <FluentTextField @bind-Value = "@Content.CustomerId"> CustomerId: </FluentTextField>
  <FluentTextField @bind-Value = "@Content.CompanyName"> CompanyName: </FluentTextField>
  <FluentTextField @bind-Value = "@Content.ContactName"> ContactName: </FluentTextField>
</FluentDialogBody>
 
@code {
  [Parameter]
  public FluentUIDataGrid.Models.Customer Content { get; set; } = default!;
 
  [CascadingParameter]
  public FluentDialog Dialog { get; set; } = default!;
 
  private async Task DeleteAsync() {
    await Dialog.CloseAsync( Content );
  }
 
  private async Task CancelAsync() {
    await Dialog.CancelAsync( );
  }
}




分別說明如下,以下這兩行程式碼是在引入了必要的命名空間,並且實作了「IDialogContentComponent」介面,該介面的泛型參數是「FluentUIDataGrid.Models.Customer」:

1
2
@using Microsoft.FluentUI.AspNetCore.Components
@implements IDialogContentComponent<FluentUIDataGrid.Models.Customer>



以下程式碼建立了自訂對話盒的標題部分。「FluentDialogHeader」是對話盒的標題,「ShowDismiss」屬性設為「true」表示可以關閉對話盒。「FluentStack」是一個容器類型的元件,裡面包含了一個圖示(FluentIcon)和一個標籤(FluentLabel),這個標籤顯示的是對話盒的標題:

1
2
3
4
5
6
7
8
9
@* Header *@
<FluentDialogHeader ShowDismiss = "true">
  <FluentStack VerticalAlignment = "VerticalAlignment.Center">
    <FluentIcon Value = "@(new Icons.Regular.Size24.WindowApps())" />
    <FluentLabel Typo = "Typography.PaneHeader">
        @Dialog.Instance.Parameters.Title
    </FluentLabel>
  </FluentStack>
</FluentDialogHeader>



以下程式碼建立對話盒的尾部部分。「FluentDialogFooter」是對話盒的底部,裡面包含了兩個按鈕,一個是刪除按鈕,點擊後會執行「DeleteAsync」方法,另一個是取消按鈕,點擊後會執行「CancelAsync」方法。

1
2
3
4
5
@* Footer *@
<FluentDialogFooter>
  <FluentButton Appearance = "Appearance.Accent" OnClick = "@DeleteAsync"> Delete </FluentButton>
  <FluentButton Appearance = "Appearance.Neutral" OnClick = "@CancelAsync"> Cancel </FluentButton>
</FluentDialogFooter>


以下程式碼建立對話盒的主體部分。「FluentDialogBody」是對話盒的主體,裡面包含了三個文字欄位,分別繫結到「Content」物件的「CustomerId」、「CompanyName」和「ContactName」屬性。

 

 

1
2
3
4
5
6
@* Body *@
<FluentDialogBody>
  <FluentTextField @bind-Value = "@Content.CustomerId"> Id: </FluentTextField>
  <FluentTextField @bind-Value = "@Content.CompanyName"> CompanyName: </FluentTextField>
  <FluentTextField @bind-Value = "@Content.ContactName"> ContactName: </FluentTextField>
</FluentDialogBody>

 

 


以下程式碼定義一些屬性和方法。「Content」屬性是一個參數,它的類型是「FluentUIDataGrid.Models.Customer」。「Dialog」性是一個「CascadingParameter」參數,它的類型是「FluentDialog」。「DeleteAsync」方法是在刪除按鈕被按下時執行,它會關閉對話盒並傳遞「Content」物件。「CancelAsync」方法是在取消按鈕被按下時執行,它會取消對話盒。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@code {
  [Parameter]
  public FluentUIDataGrid.Models.Customer Content { get; set; } = default!;
 
  [CascadingParameter]
  public FluentDialog Dialog { get; set; } = default!;
 
  private async Task DeleteAsync() {
    await Dialog.CloseAsync( Content );
  }
 
  private async Task CancelAsync() {
    await Dialog.CancelAsync( );
  }
}

 

 


下一步在「MyDataGridComponent.razor」元件中使用「MyDialog」對話盒。參考以下程式碼,當按下「FluentDataGrid 」任一筆資料右方的刪除按鈕時,便叫用「DialogService.ShowDialogAsync()」方法顯示自訂對話盒:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@using FluentUIDataGrid.Models
@using Microsoft.EntityFrameworkCore
@inject NorthwindContext DbContext
@inject IDialogService DialogService
<h3>MyDataGridComponent</h3>
<FluentDataGrid  TGridItem = "Customer" Items = "Customers" GridTemplateColumns = "1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr">
  <PropertyColumn Property = "@(c => c.CustomerId)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.CompanyName)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.ContactName)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.ContactTitle)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.Address)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.City)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.Country)" Sortable = "true" />
  <TemplateColumn Title = "Actions">
    <FluentButton IconStart = "@(new Icons.Regular.Size20.Delete())" OnClick = "@( () => DeleteClicked(context) )">
      Delete
    </FluentButton>
  </TemplateColumn>
</FluentDataGrid>
 
 
@code {
 
  public async Task DeleteClicked( Customer customer ) {
    var dialog = await DialogService.ShowDialogAsync<FluentUIDataGrid.Shared.MyDialog>(
      customer, new DialogParameters {
          Height = "240px",
          Title = $"Delete Customer : { customer.CustomerId }",
          PreventDismissOnOverlayClick = true,
          PreventScroll = true
        } );
 
    var result = await dialog.Result;
    if ( !result.Cancelled && result.Data != null ) {
      var c = await DbContext.Customers.FirstOrDefaultAsync( c => c.CustomerId == customer.CustomerId );
      DbContext.Customers.Remove( c );
      if ( c is not null ) {
        await DbContext.SaveChangesAsync( );
      }
    }
  }
 
 
  public IQueryable<Customer> Customers { get; set; } = null!;
  protected override async Task OnInitializedAsync() {
 
    await Task.Run( () => {
      Customers = DbContext.Customers.AsQueryable( );
    } );
  }
}
 
<style>
  .dialog-box {
    background-color: #f2f2f2;
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 10px;
    margin-bottom: 10px;
  }
 
  .dialog-title {
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 10px;
  }
 
  .dialog-buttons {
    margin-top: 10px;
  }
 
    .dialog-buttons button {
      margin-right: 10px;
    }
</style>

 

 

 

分別說明如下,以下這段程式碼,叫用「DialogService.ShowDialogAsync()」方法顯示對話盒,傳入了一個「customer」物件和一個「DialogParameters」物件來設定對話盒的參數,「Height」設定高度、「Title」設定標題、「PreventDismissOnOverlayClick」是否防止點選背景來關閉對話盒,「PreventDismissOnOverlayClick」設定為「true」時不可以點選背景來關閉對話盒;「PreventDismissOnOverlayClick」設為「false」則可以點選背景來關閉對話盒、「PreventScroll」用來設定是否防止滾動。

 

 

1
2
3
4
5
6
7
var dialog = await DialogService.ShowDialogAsync<FluentUIDataGrid.Shared.MyDialog>(
      customer, new DialogParameters {
          Height = "240px",
          Title = $"Delete Customer : { customer.CustomerId }",
          PreventDismissOnOverlayClick = true,
          PreventScroll = true
        } );

 


以下這行程式碼是在等待對話盒關閉並獲取結果,如果對話盒沒有被取消且返回的資料不為空,則進行以下操作:首先,從資料庫中找尋與當前「customer」的「CustomerId」相符的「Customer」物件。然後,從「DbContext.Customers」移除該筆資料。最後,如果「CustomerId」相符的資料存在於資料庫,則刪除資料庫的資料:

 

 

1
2
3
4
5
6
7
8
var result = await dialog.Result;
if ( !result.Cancelled && result.Data != null ) {
  var c = await DbContext.Customers.FirstOrDefaultAsync( c => c.CustomerId == customer.CustomerId );
  DbContext.Customers.Remove( c );
  if ( c is not null ) {
    await DbContext.SaveChangesAsync( );
  }
}

 

這個範例的執行結果請參考下圖所示:


圖 1:自訂對話盒。