跳至主要内容

如何在 Azure Entra ID 中設定 SSO

· 閱讀時間約 6 分鐘

在現代應用程式中,單一登入(Single Sign-On, SSO)是一個非常重要的功能,它可以讓使用者在多個應用程式之間無縫切換,而不需要重複登入。Azure Entra ID 是 Azure 的一個身份驗證服務,可以幫助開發者實現 SSO 功能。本文將介紹如何在 Azure Entra ID 中設定 SSO,並且整合到自己開發的應用程式中。

什麼是 Azure Entra ID​

Azure Entra ID 是 Azure 的一個身份驗證服務,提供了一個安全、可擴展的身份驗證解決方案,可以幫助開發者實現 SSO 功能。Azure Entra ID 支持多種身份驗證方式,包括密碼、多因素身份驗證、社交登入等。

必要準備​

  • Azure 訂閱帳戶

建立 Enterprise 應用程式​

  1. 登入 Azure 入口網站,選擇 Microsoft Entra ID,進入 Enterprise applications,點擊 Add application。

設定 Enterprise 應用程式​

  1. 點擊 Create your own application。
  2. 輸入應用程式名稱
  3. 選擇 Integrate any other application you don't find in the gallery (Non-gallery),後點擊 Create。

加入可以登入的使用者和群組​

  1. 進入建立好的 Enterprise 應用程式,點擊 Users and groups。
  2. 點擊 Add user,選擇可以登入的使用者或群組後儲存。

設定 SSO 模式​

  1. 進入 Enterprise 應用程式,點擊 Single sign-on。
  2. 選擇 SAML。

設定 SAML 基本設定​

  1. 輸入 Identifier (Entity ID)。建議可以使用 https://{tenant-name}.onmicrosoft.com/{app-name}。
  2. 輸入 Reply URL 作為登入後的返回網址後點擊 Save。

設定 SAML 可取得的使用者屬性​

  1. 輸入 claim name
  2. 選擇 claim 的來源,這裡選 Attribute。
  3. 選擇 claim 的值

SAML Metadata​

做到這裡幾乎就完成了,畫面上會顯示 SAML Metadata,這是我們後續要用到的資訊。

  1. SP Identifier (Entity ID)
  2. IdP Identifier (Entity ID)
  3. IdP Login URL
  4. IdP Logout URL
  5. X.509 Certificate

整合到應用程式​

這裡以 Python FastAPI 示範如何整合 Azure Entra ID 的 SSO 功能。

  1. 安裝 fastapi 和 python3-saml。
pip install "fastapi[standard]" python3-saml
  1. 程式碼如下:
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse

from onelogin.saml2.auth import OneLogin_Saml2_Auth

app = FastAPI()

# SAML Settings (Replace with your own settings)
SAML_PROVIDERS = {
"microsoft": {
"sp_entity_id": "<SP entity ID>",
"idp_entity_id": "<IdP entity ID>",
"idp_login_url": "<IdP login URL>",
"idp_logout_url": "<IdP logout URL>",
"idp_cert_filepath": "<IdP certificate filepath>"
}
}

SAML_EMAIL_CLAIM = "email"
SAML_USERNAME_CLAIM = "username"

# SAML Helper Functions
def init_saml_auth_settings(request:Request, provider:str, provider_setting: Dict):
sp_entity_id = provider_setting.get("sp_entity_id", None)
idp_entity_id = provider_setting.get("idp_entity_id", None)
idp_login_url = provider_setting.get("idp_login_url", None)
idp_logout_url = provider_setting.get("idp_logout_url", None)
idp_cert_filepath = provider_setting.get("idp_cert_filepath", None)

if not sp_entity_id:
error_text = f"SAML {provider} SP entity ID not found"
log.warning(error_text)
raise HTTPException(status_code=500, detail=error_text)

if not idp_entity_id:
error_text = f"SAML {provider} IdP entity ID not found"
log.warning(error_text)
raise HTTPException(status_code=500, detail=error_text)

if not idp_login_url:
error_text = f"SAML {provider} IdP login URL not found"
log.warning(error_text)
raise HTTPException(status_code=500, detail=error_text)

if not idp_logout_url:
error_text = f"SAML {provider} IdP logout URL not found"
log.warning(error_text)
raise HTTPException(status_code=500, detail=error_text)

if not idp_cert_filepath:
error_text = f"SAML {provider} IdP certificate filename not found"
log.warning(error_text)
raise HTTPException(status_code=500, detail=error_text)

if not os.path.exists(idp_cert_filepath):
log.warning(f"SAML certificate not found: {idp_cert_filepath}")
raise HTTPException(status_code=500, detail="SAML certificate not found.")


# Load the SAML certificate, and remove the header and footer lines and newlines to get the raw certificate string
with open(idp_cert_filepath, 'rb') as f:
saml_cert = f.read().decode('utf-8').replace('-----BEGIN CERTIFICATE-----', '').replace('-----END CERTIFICATE-----','').replace('\n','').strip()

pototcal = "https" if request.url.scheme == "https" else "http"
host = request.headers.get("host", "localhost")

origin_host = f"{pototcal}://{host}"
sp_acs_url = f"{origin_host}/saml/{provider}/acs"
sp_sls_url = f"{origin_host}/saml/{provider}/logout"

SAML_SETTINGS = {
"strict": True,
"debug": True,
"sp": {
"entityId": sp_entity_id,
"assertionConsumerService": {
"url": sp_acs_url,
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
"singleLogoutService": {
"url": sp_sls_url,
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
},
"idp": {
"entityId": idp_entity_id,
"singleSignOnService": {
"url": idp_login_url,
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"singleLogoutService": {
"url": idp_logout_url,
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509cert": saml_cert
}
}

return SAML_SETTINGS


def init_saml_auth(request_data: Dict, request: Request, provider:str, provider_setting: Dict):
"""
Initialize the SAML authentication object
"""
settings = init_saml_auth_settings(request, provider, provider_setting)
auth = OneLogin_Saml2_Auth(request_data, settings)
return auth

def get_saml_provider_setting(provider: str):
"""
Get the SAML provider setting
"""
if len(SAML_PROVIDERS.items()) == 0:
raise HTTPException(404)

provider_setting = SAML_PROVIDERS.get(provider, None)
if not provider_setting:
raise HTTPException(status_code=500, detail="Provider setting not found")
return provider_setting


# SAML Routes

@app.get("/saml/{provider}/login")
async def saml_login(provider: str, request: Request):
provider_setting = get_saml_provider_setting(provider)
req = {
'https': 'on' if request.url.scheme == 'https' else 'off',
'http_host': request.headers.get('host', ''),
'script_name': request.url.path,
'get_data': dict(request.query_params),
'post_data': await request.form()
}

auth = init_saml_auth(req, request, provider, provider_setting)
sso_url = auth.login()
return RedirectResponse(url=sso_url)

@app.post("/saml/{provider}/acs")
async def acs(provider: str, request: Request, response: Response):
provider_setting = get_saml_provider_setting(provider)
req = {
'https': 'on' if request.url.scheme == 'https' else 'off',
'http_host': request.headers.get('host', ''),
'script_name': request.url.path,
'get_data': dict(request.query_params),
'post_data': await request.form()
}

auth = init_saml_auth(req, request, provider, provider_setting)
auth.process_response()
errors = auth.get_errors()
error_reason = auth.get_last_error_reason()

if errors:
log.error(f"Errors: {errors}, Reason: {error_reason}")
raise HTTPException(status_code=401, detail=f"SAML Authentication Failed: {error_reason}")

if not auth.is_authenticated():
raise HTTPException(status_code=401, detail="Not authenticated")

attributes = auth.get_attributes()
name_id = auth.get_nameid()

email = attributes.get(SAML_EMAIL_CLAIM, [None])[0]
username = attributes.get(SAML_USERNAME_CLAIM, ["User"])[0]

if not email:
log.warning(f"SAML login callback failed, email is missing: {attributes}")d in attributes")

return {"email": email, "username": username, "name_id": name_id}

@app.get("/saml/{provider}/logout")
async def logout(provider: str, request: Request):
provider_setting = get_saml_provider_setting(provider)
req = {
'https': 'on' if request.url.scheme == 'https' else 'off',
'http_host': request.headers.get('host', ''),
'script_name': request.url.path,
'get_data': dict(request.query_params),
'post_data': await request.form()
}

auth = init_saml_auth(req, request, provider, provider_setting)
return RedirectResponse(url=auth.logout())

@app.get("/saml/{provider}/metadata")
async def metadata(provider: str, request: Request):
provider_setting = get_saml_provider_setting(provider)
saml_setting = init_saml_auth_settings(request, provider, provider_setting)
settings = OneLogin_Saml2_Settings(settings=saml_setting)
metadata = settings.get_sp_metadata()
return Response(content=metadata, media_type="text/xml")

參考資料​

深入解析 JavaScript this - 用 apply、call 和 bind 克服函式綁定挑戰

· 閱讀時間約 3 分鐘

在 JavaScript 中,this 的值取決於函式的呼叫上下文,而不是函式的定義位置。這使得 this 的行為有時候很難預測和控制。

首先我們先來複習一下 JavaScript 中 this 的行為:

this​

在 JavaScript 中,this 是一個很特別的關鍵字,它的值會根據它所在的環境而改變。簡單來說,this 指的是呼叫函式時的上下文(context),也就是「誰」在呼叫這個函式。

傳統函式中的 this​

在傳統函式裡,this 的指向取決於函式是**「怎麼」被呼叫的**

const person = {
name: "Alice",
greet: function () {
console.log(`Hi, I'm ${this.name}`);
},
};

// 這個 greet 函式是透過 person 物件來呼叫的。
// 這意味著,JavaScript 會把 this 綁定到 person,讓 this.name 指向 person.name。
person.greet(); // Hi, I'm Alice

const greetFunction = person.greet;
// 這裡的 this 就指向 window 物件,因為 greetFunction 是直接被呼叫的。
// 這時候 this.name 就會是 undefined 或 window.name 的值。
greetFunction(); // Hi, I'm

箭頭函式中的 this​

箭頭函式的 this 行為可以理解為**「綁定」到箭頭函式定義時的詞法環境 (Lexical Environment)**,而不是函式被呼叫時的上下文。

const person = {
name: "Alice",
greet: () => {
console.log(`Hi, I'm ${this.name}`);
},
greet1: function () {
// 這裡的箭頭函式會繼承外層的 this,這個 this 是指向 person
const innerGreet = () => {
console.log(`Hi, I'm ${this.name}`); // 這裡的 this 指向 person
};
innerGreet();
},
};

// 這裡的 this 指向的是全域對象,而不是 person 物件
// person 物件並不是 greet 函式的外層上下文;this 的繼承來源是 greet 函式的定義上下文
person.greet(); // Hi, I'm

person.greet1(); // Hi, I'm Alice

this 的難題主要來自於它的綁定方式,這使得 this 的行為有時候很難預測和控制。 要解決 this 的問題,我們可以使用 Function.prototype.bind、call 和 apply 方法

Function.prototype.bind​

Function.prototype.bind 方法可以用來永久地綁定函式的 this 值,並回傳一個新的函式。

const person = {
name: "Alice",
greet: function () {
console.log(`Hello, ${this.name}`);
},
};

const greet = person.greet;
greet(); // 輸出 'Hello, undefined',因為 this 指向了全域物件

const boundGreet = person.greet.bind(person);
boundGreet(); // 輸出 'Hello, Alice',因為 this 被綁定到 person

Function.prototype.call 和 Function.prototype.apply​

Function.prototype.call 和 Function.prototype.apply 方法可以用來臨時地綁定函式的 this 值,並立即執行這個函式。

call 和 apply 的差別在於傳入參數的方式,call 是逐個傳入,而 apply 是以陣列的方式傳入。

const person = {
name: "Alice",
};

function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}

greet.call(person, "Hello"); // 輸出 'Hello, Alice'
greet.apply(person, ["Hi"]); // 輸出 'Hi, Alice'

總結​

  1. bind():用於創建一個新的函式,這個函式的 this 被固定為指定的物件。適用於需要保證 this 的綁定時機的情境,例如回調函式。
  2. call() 和 apply():用於立即呼叫函式並指定 this 的值。call() 用逗號分隔的參數,apply() 用數組分隔的參數。
  3. 箭頭函式:箭頭函式的 this 是從函式定義時的詞法環境繼承的,適合在函式內部使用時保證 this 的一致性。

這些方法可以幫助你更好地控制和理解 this 的行為,使你的 JavaScript 程式碼更易於維護和預測。

JavaScript ++ vs. += 1,你應該選擇哪個?

· 閱讀時間約 3 分鐘

在 JavaScript 中,我們常常需要對變數進行增值操作,最常見的兩種方式就是使用 ++ 運算符和 += 1。 這兩者看起來都能達到相同的目的:將變數的值增加 1,但實際上它們之間有一些細微的差異。這篇文章將帶你了解這兩者之間的不同之處,幫助你在編寫程式時做出更明智的選擇。

++ 運算符​

++ 運算符是一種一元運算符,用於將變數的值增加 1。它有兩種形式:前置遞增和後置遞增。

前置遞增:++variable,先將變數的值增加 1,然後回才遞增後的值。

function makeCounter(initialValue = 0) {
let count = initialValue;
return () => {
return count++; // 先回傳 count 的值,然後再將其增加 1
};
}

const counter = makeCounter();
counter(); // 0
counter(); // 1
counter(); // 2

後置遞增:variable++,先回傳變數的值,然後再將其增加 1。

function makeCounter(initialValue = 0) {
let count = initialValue;
return () => {
return ++count; // 先將 count 的值增加 1,然後再回傳增加後的值
};
}

const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3

+= 運算符​

+= 運算符是一種賦值運算符,用於將變數的值增加指定的數量。它的形式是 variable += value,表示將變數的值增加 value。

function makeCounter(initialValue = 0) {
let count = initialValue;
return () => {
return (count += 1); // 將 count 的值增加 1
};
}

const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3

總結​

特性x++++xx += 1
操作順序先回傳變數的舊值,再進行自增先進行自增,再回傳變數的新值直接加 1 並回傳結果
回傳結果回傳原本的變數值回傳變數加 1 後的結果回傳變數加 1 後的結果
可讀性可能會讓人有點困惑行為明確,容易理解簡單易懂,適合大多數情況
適用場景需要先用原本的變數值的情況需要馬上使用加 1 後的結果進行簡單的增值操作

總的來說,++ 運算符和 += 1 運算符都是用來對變數進行增值操作的方法,但它們之間有一些細微的差異。在選擇使用哪一種方法時,你可以根據自己的需求和程式的可讀性來做出選擇。

如何在 JavaScript 中檢查物件是否有某個屬性

· 閱讀時間約 2 分鐘

在 JavaScript 中,我們經常需要檢查物件是否有某個屬性,以便進行相應的操作。有幾種方法可以實現這個功能,下面介紹其中幾種常用的方法:

TL;DR​

方法是否能檢查原型鏈上的屬性說明
in 運算子✅檢查物件自身及其原型鏈上的屬性,若存在則回傳 true,否則回傳 false。
Reflect.has()✅檢查物件自身及其原型鏈上的屬性,行為類似於 in 運算子。
Object.prototype.hasOwnProperty()❌僅檢查物件自身的屬性,若存在則回傳 true,否則回傳 false。
Object.hasOwn()❌類似於 hasOwnProperty,但語法更簡潔,僅檢查物件自身的屬性。

in 運算子​

in 運算子是 JavaScript 中用來檢查物件是否有指定屬性的方法之一。這個運算子會檢查物件自身及其原型鏈上是否有指定的屬性,如果有則回傳 true,否則回傳 false。

const parent = {
type: "Person",
};

const city = Symbol("city");

const child = Object.create(parent);
child.name = "Kevin";
child.age = 30;
child[city] = "Taipei";

console.log("name" in child); // true
console.log("type" in child); // true
console.log(city in child); // true

Reflect.has() 方法​

Reflect.has() 方法是 JavaScript 中用來檢查物件是否有指定屬性的方法之一。這個方法和 in 運算子類似,會檢查物件自身及其原型鏈上是否有指定的屬性。

const parent = {
type: "Person",
};

const city = Symbol("city");

const child = Object.create(parent);
child.name = "Kevin";
child.age = 30;
child[city] = "Taipei";

console.log(Reflect.has(child, "name")); // true
console.log(Reflect.has(child, "type")); // true
console.log(Reflect.has(child, city)); // true

Object.prototype.hasOwnProperty() 和 Object.hasOwn()​

Object.prototype.hasOwnProperty() 是 JavaScript 中用來檢查物件是否有指定屬性的方法之一。這個方法會檢查物件自身是否有指定的屬性,如果有則回傳 true,否則回傳 false。需要注意的是,Object.prototype.hasOwnProperty() 方法不會檢查原型鏈上的屬性。 如果你想要更簡潔的方式來檢查物件是否有指定屬性,可以使用 Object.hasOwn()

const parent = {
type: "Person",
};

const city = Symbol("city");

const child = Object.create(parent);
child.name = "Kevin";
child.age = 30;
child[city] = "Taipei";

console.log(Object.prototype.hasOwnProperty.call(child, "name")); // true
console.log(Object.prototype.hasOwnProperty.call(child, "type")); // false
console.log(Object.prototype.hasOwnProperty.call(child, city)); // true

console.log(Object.hasOwn(child, "name")); // true
console.log(Object.hasOwn(child, "type")); // false
console.log(Object.hasOwn(child, city)); // true

如何在 JavaScript 中獲取物件的 keys

· 閱讀時間約 2 分鐘

在 JavaScript 中,我們經常需要取得物件的 keys 來進行迴圈操作或其他資料處理。不同的方法會帶來不同的結果,根據需求的不同,我們可以選擇適合的方法來取得物件的 keys。以下介紹幾種常用的方法:

TL;DR​

方法是否能輸出 Symbol 屬性是否能輸出原型鏈上的屬性說明
Object.keys(obj)❌❌回傳物件自身的可枚舉字符串屬性的鍵的陣列,不包含 Symbol 屬性和原型鏈上的屬性
for...in❌✅迭代物件自身及其原型鏈上的所有可枚舉屬性,不包含 Symbol 屬性
Reflect.ownKeys(obj)✅❌回傳物件所有屬性鍵的陣列,包括可枚舉和不可枚舉屬性以及 Symbol 屬性,不包含原型鏈上的屬性

Object.keys()​

Object.keys() 是 JavaScript 中最常用來取得物件 keys 的方法之一。這個方法會回傳一個陣列,裡面包含了該物件所有的可枚舉屬性的 key。需要注意的是,它只會回傳物件自身的屬性,不會包含繼承自原型鏈的屬性,也不會包含 Symbol 類型的屬性。

const parent = {
type: "Person",
};
const city = Symbol("city");

const child = Object.create(parent);
child.name = "Kevin";
child.age = 30;
child[city] = "Taipei";

const keys = Object.keys(child);

console.log(keys); // [ 'name', 'age']

for...in 迴圈​

for...in 迴圈是一種用來迭代物件屬性的方法,不僅會迭代物件自身的屬性,還會包含那些繼承自原型鏈的屬性。不過,和 Object.keys() 一樣,它也不會列舉 Symbol 類型的屬性。

const parent = {
type: "Person",
};
const city = Symbol("city");

const child = Object.create(parent);
child.name = "Kevin";
child.age = 30;
child[city] = "Taipei";

for (const key in obj) {
console.log(key); // name, age, type
}

Reflect.ownKeys()​

Reflect.ownKeys() 是一個功能更強大的方法,它會回傳一個包含物件所有屬性的陣列,這其中包括了可枚舉和不可枚舉屬性,也包括 Symbol 屬性。

const parent = {
type: "Person",
};
const city = Symbol("city");

const child = Object.create(parent);
child.name = "Kevin";
child.age = 30;
child[city] = "Taipei";

const keys = Reflect.ownKeys(child);

console.log(keys); // [ 'name', 'age', Symbol(city) ]

透過 Azure API Management 打造可靠的 Azure OpenAI 服務

· 閱讀時間約 6 分鐘

問題​

雖然 Azure OpenAI 提供了很強大的服務,但它的一些使用限制,比如每分鐘的 Token 數量(TPM)和每分鐘的請求次數(RPM),對於一些需要大量使用 OpenAI 服務的應用程式來說,可能會造成瓶頸。

這些限制對一般使用者來說可能不太明顯,但對於大型系統卻是不能忽視的問題。

幸好,透過 Azure API Management 可以有效解決這些限制,提升應用程式的效能和穩定性。

  • 提升使用者體驗 (UX):透過控制請求速度,避免因超出限制而導致的服務中斷,提供更穩定且一致的服務。
  • 提高應用程式彈性:在遇到限制時,自動調整請求策略,確保應用程式可以持續運作。
  • 強化錯誤處理:透過 API 管理提供的錯誤處理機制,您可以有效地處理各種錯誤狀態碼,並採取相應的措施。
  • 優化模型選擇:您可以根據不同的需求選擇適當的 OpenAI 模型,並透過 API 管理進行有效管理。
  • 靈活配置 API 策略:您可以根據實際情況調整 API 策略,例如設定不同的請求速率限制、控制權限等。
  • 加強監控和記錄:透過 API 管理的監控功能,您可以追蹤 API 呼叫的流量、性能和錯誤狀況,並進行相關分析。

架構說明​

透過 APIM 來管理 Azure OpenAI 服務,當使用者發送請求時,APIM 會根據設定的策略來控制請求速率,並根據服務的狀態來調整請求策略,確保服務的穩定性和可靠性。

當主要 OpenAI 服務發生錯誤時,APIM 會自動切換到次要服務,以確保服務的持續運作。

圖片來源

必要準備​

準備 Azure OpenAI API 規格文件​

首先我們需要先從官方 Github 中找到 Azure OpenAI API 的 OpenAPI 規格文件,並且下載到本機。 下載完成後,去掉 servers 這個欄位

{
"openapi": "3.0.0",
"info": {
"title": "Azure OpenAI Service API",
"description": "Azure OpenAI APIs for completions and search",
"version": "2024-02-01"
},
"security": [
{
"bearer": ["api.read"]
},
{
"apiKey": []
}
],
...
}

註冊多個 Azure OpenAI 服務到 Azure API Management Backends​

  1. 進入 Azure API Management 服務,從側邊欄點擊 Backends。
  2. 點擊 Add
  3. 填入
    • Name: Backend 名稱
    • Type: Custom URL
    • Runtime URL: https://{aoaiHostname}.openai.azure.com/openai
  4. 點擊 Advanced,勾選 Validate certificate chain 和 Validate certificate name
  5. 在 Authorization credentials 的 Headers 新增
    • Key: api-key
    • Value: Azure OpenAI 服務的 API 金鑰,可以使用 APIM 的 Named value 來存取
  6. 點擊 Create

上傳規格文件到 Azure API Management 建立 API​

  1. 進入 Azure API Management 服務,從側邊欄點擊 API。
  2. 點擊 OpenAPI Specification。
  3. 點擊 Add a new OpenAPI specification,然後選擇 File,選擇剛剛下載的 OpenAPI 規格文件。
  4. 填入 Display name 和 Name。
  5. 在 API URL suffix 填入 openai。
  6. 點擊 Create.

建立 API Policy​

  1. API 建立完成後,點擊剛剛建好的 API。
  2. 點擊 All operation。
  3. 點擊 Policies </> 按鈕。
  4. 複製以下範例,並貼上到編輯器中後儲存。
<policies>
<inbound>
<base />
<!-- default values if wasn't specified by the caller -->
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<!-- 節流方案 https://learn.microsoft.com/zh-tw/azure/api-management/api-management-sample-flexible-throttling -->

<!-- 1. 限速着重於短時間內(例如每分鐘)的請求頻率,旨在防止短時間內過度使用資源。 -->
<!-- 2. 配額則關注於長時間內(例如每天、每月)的總請求量,確保長期內資源的合理分配與使用。 -->
<!-- By Ip 限制請求頻率 https://learn.microsoft.com/en-us/azure/api-management/rate-limit-by-key-policy -->
<!-- 範例中的 calls 屬性設定為 10,表示每個 IP 在 60 秒內最多可以發出 10 個請求。 -->
<rate-limit-by-key calls="10" renewal-period="60" counter-key="@(context.Request.IpAddress)" />

<!-- By Ip 限制使用量配額 https://learn.microsoft.com/zh-tw/azure/api-management/quota-by-key-policy -->
<!-- 範例中的 calls 屬性設定為 100,表示每個 IP 在 3600 秒內最多可以發出 100 個請求。 -->
<quota-by-key calls="100" renewal-period="3600" counter-key="@(context.Request.IpAddress)" increment-condition="@(context.Response.StatusCode >= 200 && context.Response.StatusCode < 400)" first-period-start="2024-01-01T00:00:00Z" />

<!-- By Ip 限制 TPM https://learn.microsoft.com/zh-tw/azure/api-management/azure-openai-token-limit-policy -->
<!-- 範例中的 tokens-per-minute 屬性設定為 5000,表示每個 IP 在每分鐘內最多可以發出 5000 token。 -->
<azure-openai-token-limit counter-key="@(context.Request.IpAddress)" tokens-per-minute="5000" estimate-prompt-tokens="false" remaining-tokens-header-name="remaining-tokens" tokens-consumed-header-name="consumed-tokens" />


<!-- 預設 backend service 名稱為 primary -->
<set-backend-service backend-id="primary" />
</inbound>
<backend>
<retry condition="@(context.Response.StatusCode == 429 || context.Response.StatusCode >= 500)" count="5" interval="1" delta="1" max-interval="8" first-fast-retry="false">
<!-- Failover logic below - uncomment to retry on secondary backend -->
<choose>
<!-- 若發生 aoai service 發生錯誤時,會呼叫另一台名稱為 secondary 的 backend service -->
<when condition="@(context.Response.StatusCode == 429 || context.Response.StatusCode >= 500)">
<set-backend-service backend-id="secondary" />
</when>
</choose>
<forward-request buffer-request-body="true" />
</retry>
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>

將 API 加入 Product​

  1. 點擊 Products。
  2. 點擊 Add。
  3. 填寫必填欄位。
  4. 加入剛剛建立的 API。
  5. 點擊 Create.

測試 API​

  1. 點擊側邊欄的 Subscription。
  2. 找到剛剛建立的 Product,並且複製 Key。
  3. 可以使用 Postman 或者其他工具來測試 API,下面範例使用 Python OpenAI SDK 來測試。
from openai import AzureOpenAI

apim_endpoint = "APIM 的 Gateway URL"
apim_product_name = "APIM 有註冊 OpenAI API 的 Product Name"
apim_product_subscription_key = "APIM 有註冊 OpenAI API 的 Product Key"
aoai_api_version = "OpenAI 的 API Version"

# 透過 Azure OpenAI SDK 來呼叫 APIM API
# 從 Azure OpenAI SDK 帶入 Product Key
# APIM 會從 Backend 服務選擇適當的服務,並帶入 API Key,處理請求
aoai = AzureOpenAI(
azure_endpoint=apim_endpoint,
api_key=apim_product_subscription_key,
api_version=aoai_api_version,
default_headers={"Ocp-Apim-Subscription-Key": f"{apim_product_subscription_key};product={apim_product_name}"},
)

messages = [{
"role": "user",
"content": "What are you?"
}]
model = "gpt-35-turbo"

response = aoai.chat.completions.create(
messages=messages,
model=model
)

參考資料​