跳至主要内容

如何在 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. 安裝 fastapipython3-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 克服函式綁定挑戰

· 閱讀時間約 5 分鐘

在 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.bindcallapply 方法

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.callFunction.prototype.apply 方法可以用來臨時地綁定函式的 this 值,並立即執行這個函式

callapply 的差別在於傳入參數的方式,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,你應該選擇哪個?

· 閱讀時間約 4 分鐘

在 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 中檢查物件是否有某個屬性

· 閱讀時間約 3 分鐘

在 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

· 閱讀時間約 3 分鐘

在 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 服務

· 閱讀時間約 8 分鐘

問題

雖然 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 chainValidate certificate name
  5. Authorization credentialsHeaders 新增
    • 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 nameName
  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
)

參考資料