PostgreSQL 流復(fù)制認(rèn)證機(jī)制詳解

物理復(fù)制(流復(fù)制 Streaming Replication )作為 PostgreSQL 高可用架構(gòu)的核心技術(shù),其安全性直接關(guān)系到數(shù)據(jù)庫(kù)集群的可靠性;本文選擇物理復(fù)制中備庫(kù)向主庫(kù)請(qǐng)求建立流復(fù)制連接的認(rèn)證過(guò)程,即 walreceiver 進(jìn)程連接主庫(kù)時(shí)的認(rèn)證機(jī)制,并結(jié)合源碼解析其實(shí)現(xiàn)原理
01 數(shù)據(jù)庫(kù)物理復(fù)制

如上圖所示,PostgreSQL 的主備物理復(fù)制即流復(fù)制(Streaming Replication)機(jī)制確保主庫(kù)(Primary)生成的預(yù)寫日志(WAL)能實(shí)時(shí)傳輸?shù)絺鋷?kù)(Standby)并正確應(yīng)用,從而實(shí)現(xiàn)數(shù)據(jù)的同步,其實(shí)現(xiàn)依賴于三個(gè)關(guān)鍵進(jìn)程:
- walsender(主庫(kù)):推送 WAL 數(shù)據(jù)到備庫(kù)
- walreceiver(備庫(kù)):接收并存儲(chǔ) WAL 數(shù)據(jù)
- startup(備庫(kù)):應(yīng)用 WAL 數(shù)據(jù)到數(shù)據(jù)庫(kù)文件
在流復(fù)制過(guò)程中,預(yù)寫日志(Write Ahead Log)即圖中的 XLOG 的生命周期如下:
- 主庫(kù)生成 WAL:主庫(kù)執(zhí)行事務(wù)時(shí),將變更寫入 WAL 緩沖區(qū),最終持久化到 WAL 文件
- walsender 發(fā)送 WAL:
walsender進(jìn)程從 WAL 文件或緩沖區(qū)讀取數(shù)據(jù),通過(guò)復(fù)制協(xié)議發(fā)送給備庫(kù)的walreceiver - walreceiver 接收并存儲(chǔ) WAL:備庫(kù)的
walreceiver將接收到的 WAL 數(shù)據(jù)寫入本地pg_wal目錄,并通知startup進(jìn)程 - startup 應(yīng)用 WAL:
startup進(jìn)程讀取本地 WAL 文件,按順序?qū)⒆兏鼞?yīng)用到備庫(kù)的數(shù)據(jù)文件中,完成數(shù)據(jù)同步
02 連接主庫(kù)認(rèn)證
當(dāng)備庫(kù)以恢復(fù)模式(Recovery Mode)啟動(dòng)時(shí)(例如存在 standby.signal 或 recovery.conf 文件),PostgreSQL 主進(jìn)程postmaster會(huì)直接啟動(dòng) startup 進(jìn)程。在 startup 進(jìn)程初始化過(guò)程中對(duì) primary_conninfo 中的參數(shù)信息解析后填充到共享內(nèi)存中的 WalRcvData 數(shù)據(jù)結(jié)構(gòu)中,然后備庫(kù)在啟動(dòng) walreceiver 進(jìn)程時(shí)根據(jù)配置嘗試連接到主庫(kù)。連接成功,該備庫(kù)的 walreceiver 進(jìn)程,與主庫(kù)的 walsender 建立復(fù)制流
所以,備庫(kù)想要和主庫(kù)建立復(fù)制流,需要進(jìn)行連接認(rèn)證
2.1 根據(jù)配置文件獲取密碼
通過(guò)配置文件中的 primary_conninfo 參數(shù) password 明文配置連接密碼是最常用的方式,正確配置對(duì)應(yīng)字段之后,walreceiver 進(jìn)程則根據(jù)該信息進(jìn)行連接認(rèn)證, primary_conninfo 參數(shù)配置樣例如下,
primary_conninfo = 'host=192.168.1.100 port=5432 user=replicator password=yourpassword application_name=standby1 sslmode=require sslcompression=0 keepalives=on connect_timeout=10'
primary_conninfo 中常見(jiàn)的配置項(xiàng)及其說(shuō)明如下:
| 參數(shù) | 說(shuō)明 | 示例值 |
|---|---|---|
host | 主庫(kù)的 IP 地址或主機(jī)名 | host=192.168.1.100 |
port | 主庫(kù)的監(jiān)聽(tīng)端口(默認(rèn) 5432) | port=5432 |
user | 主庫(kù)上具有 REPLICATION 權(quán)限的用戶名(用于復(fù)制的專用用戶) | user=replicator |
password | 復(fù)制用戶的密碼 | password=yourpassword |
dbname | 主庫(kù)的數(shù)據(jù)庫(kù)名(通常固定為 replication 或主庫(kù)的某個(gè)數(shù)據(jù)庫(kù)) | dbname=postgres |
application_name | 備庫(kù)的標(biāo)識(shí)名稱,主庫(kù)的 pg_stat_replication 視圖會(huì)顯示此名稱 | application_name=standby1 |
channel_binding | 是否啟用通道綁定(Channel Binding),增強(qiáng) SSL/TLS 安全性(可選) | channel_binding=prefer |
replication | 固定值 true 或 database,用于聲明連接為復(fù)制流(通常設(shè)置為 true) | replication=true |
connect_timeout | 連接主庫(kù)的超時(shí)時(shí)間(單位:秒) | connect_timeout=10 |
keepalives | 是否啟用 TCP ?;顧C(jī)制(默認(rèn) on) | keepalives=on |
keepalives_idle | TCP ?;畎目臻e時(shí)間(單位:秒) | keepalives_idle=60 |
keepalives_interval | TCP ?;畎闹卦囬g隔(單位:秒) | keepalives_interval=5 |
keepalives_count | TCP 保活包的最大重試次數(shù) | keepalives_count=3 |
sslmode | SSL 連接模式 | sslmode=require |
sslcompression | 是否啟用 SSL 壓縮(默認(rèn) 0,即禁用) | sslcompression=0 |
sslkey | 客戶端 SSL 私鑰文件路徑 | sslkey=/etc/ssl/client.key |
sslcert | 客戶端 SSL 證書文件路徑 | sslcert=/etc/ssl/client.crt |
sslrootcert | 根證書文件路徑(用于驗(yàn)證主庫(kù)證書) | sslrootcert=/etc/ssl/ca.crt |
如果需要避免在 primary_conninfo 中明文存儲(chǔ)密碼,可以通過(guò)接下來(lái)的兩種方式進(jìn)行認(rèn)證:在備庫(kù)啟動(dòng)時(shí)通過(guò)環(huán)境變量提供密碼或通過(guò).pgpass 密碼文件提供密碼
2.2 通過(guò)環(huán)境變量注入密碼
PostgreSQL 的 libpq 庫(kù)通過(guò)一系列環(huán)境變量為連接參數(shù)提供默認(rèn)值,在代碼中沒(méi)有顯式指定對(duì)應(yīng)參數(shù)時(shí),這些變量會(huì)在調(diào)用 PQconnectdb、PQsetdbLogin 或 PQsetdb 時(shí)生效;這些環(huán)境變量同樣可以適用于 walreceiver 進(jìn)程向主庫(kù)申請(qǐng)建立連接的認(rèn)證過(guò)程
以下是 libpq 支持的常用環(huán)境變量,更多的環(huán)境變量適用說(shuō)明可以參考官方文檔
https://www.postgresql.org/docs/current/libpq-envars.html
| 環(huán)境變量 | 作用 | 示例值 |
|---|---|---|
PGHOST | 數(shù)據(jù)庫(kù)服務(wù)器主機(jī)名或 IP | localhost |
PGHOSTADDR | 數(shù)據(jù)庫(kù)服務(wù)器的 IP 地址(跳過(guò) DNS) | 192.168.1.100 |
PGPORT | 數(shù)據(jù)庫(kù)端口號(hào) | 5432 |
PGDATABASE | 要連接的數(shù)據(jù)庫(kù)名 | mydb |
PGUSER | 數(shù)據(jù)庫(kù)用戶名 | postgres |
PGPASSWORD | 數(shù)據(jù)庫(kù)密碼 | yourpassword |
PGPASSFILE | 密碼文件路徑 | ~/.pgpass |
PGOPTIONS | 連接選項(xiàng)(如 -c search_path=...) | -c statement_timeout=1000 |
PGSSLMODE | SSL 模式(disable/require 等) | require |
PGREQUIRESSL | 強(qiáng)制 SSL 連接(優(yōu)先用 PGSSLMODE) | 1 |
PGURI | 完整的連接 URI(覆蓋其他參數(shù)) | postgresql://user:pass@host/db |
通過(guò)環(huán)境變量注入密碼,需要確保 walreceiver 進(jìn)程啟動(dòng)時(shí)的環(huán)境變量中已經(jīng)配置了 PGPASSWORD,即在備庫(kù)啟動(dòng)之前需要先使用如下命令設(shè)置 PGPASSWORD 環(huán)境變量,當(dāng)然也可以直接通過(guò)編輯 .bashrc 等文件進(jìn)行配置
export PGPASSWORD="yourpassword"
這樣就可以在 primary_conninfo 沒(méi)有配置 password 字段的情況下進(jìn)行驗(yàn)證,但需要保證該密鑰與流復(fù)制用戶正確匹配才能認(rèn)證成功
但在實(shí)際使用中 PGPASSWORD明文密碼可能被進(jìn)程監(jiān)控工具捕獲,同樣存在安全風(fēng)險(xiǎn),推薦使用 .pgpass 密碼文件
2.3 通過(guò)密碼文件獲取密碼
PostgreSQL 中通過(guò)密碼文件 .pgpass 存儲(chǔ)數(shù)據(jù)庫(kù)密碼是一種較為安全的方式,避免在代碼、命令行或環(huán)境變量中暴露明文密碼。當(dāng)客戶端工具連接數(shù)據(jù)庫(kù)時(shí),若未通過(guò)其他方式指定密碼,會(huì)自動(dòng)從 .pgpass 文件中匹配條目獲取密碼;該密碼文件的默認(rèn)路徑是 ~/.pgpass ,文件格式如下
hostname:port:database:username:password
| 字段 | 說(shuō)明 |
|---|---|
| hostname | 主機(jī)名或 IP,* 表示匹配任意主機(jī)(包括本地套接字) |
| port | 端口號(hào),* 表示匹配任意端口 |
| database | 數(shù)據(jù)庫(kù)名,* 表示匹配任意數(shù)據(jù)庫(kù) |
| username | 用戶名,* 表示匹配任意用戶 |
| password | 明文密碼 |
需要注意的是,密碼文件必須限制訪問(wèn)權(quán)限,僅允許文件所有者讀寫,否則 PostgreSQL 會(huì)忽略該文件
chmod 600 ~/.pgpass
除了默認(rèn)的文件路徑 ~/.pgpass ,也可以通過(guò)環(huán)境變量 PGPASSFILE 或者直接設(shè)置連接參數(shù) passfile 來(lái)指定自定義密碼文件路徑
export PGPASSFILE=/path/to/custom_passfile
walreceiver 進(jìn)程通過(guò) libpq 進(jìn)行認(rèn)證時(shí),如果未顯示指定密碼,則會(huì)嘗試在備庫(kù)的密碼文件中查找匹配的密碼,但作為流復(fù)制用戶在 .pgpass 文件中該記錄的數(shù)據(jù)庫(kù)名稱需要配置成 replication
hostname:port:replication:username:password
03 walreceiver 認(rèn)證源碼解析
前文提到 startup 進(jìn)程在主進(jìn)程postmaster發(fā)現(xiàn)作為備庫(kù)啟動(dòng)即以恢復(fù)模式(Recovery Mode)啟動(dòng)時(shí)直接啟動(dòng);而 walreceiver 進(jìn)程則是由 startup 進(jìn)程在進(jìn)行一系列條件判斷后,通知 postmaster 來(lái)啟動(dòng),該過(guò)程執(zhí)行順序如下:
- 觸發(fā)條件:當(dāng)備庫(kù)負(fù)責(zé) WAL 恢復(fù)的
startup進(jìn)程發(fā)現(xiàn)本地 WAL 日志不完整需要從主庫(kù)流式傳輸時(shí),會(huì)通過(guò)信號(hào)通知postmaster啟動(dòng)walreceiver進(jìn)程 - 信號(hào)傳遞:
startup調(diào)用SendPostmasterSignal(PMSIGNAL_START_WALRECEIVER),向postmaster發(fā)送啟動(dòng)walreceiver的請(qǐng)求 postmaster響應(yīng):postmaster收到信號(hào)后,在其主循環(huán)中調(diào)用LaunchMissingBackgroundProcesses(),發(fā)現(xiàn)需要啟動(dòng)walreceiver,隨即創(chuàng)建子進(jìn)程
進(jìn)程啟動(dòng):postmaster 通過(guò) fork() 創(chuàng)建子進(jìn)程,子進(jìn)程執(zhí)行 WalReceiverMain(),成為 walreceiver 進(jìn)程,連接到主庫(kù)拉取 WAL 數(shù)據(jù)
StartupProcessMain() // 備庫(kù)啟動(dòng) startup 進(jìn)程的主函數(shù)
->StartupXLOG() // 負(fù)責(zé) WAL 恢復(fù)的核心邏輯
->InitWalRecovery() // 初始化 WAL 恢復(fù)環(huán)境
->XLogReaderAllocate() // 分配 WAL 讀取器
->XLogPageRead() // 讀取 WAL 頁(yè)
->WaitForWALToBecomeAvailable() // 檢查 WAL 是否可用
->RequestXLogStreaming() // 判斷需要流復(fù)制,觸發(fā)啟動(dòng) walreceiver
->SendPostmasterSignal(PMSIGNAL_START_WALRECEIVER) // 通知 postmaster
// (postmaster 進(jìn)程側(cè)操作)
->process_pm_pmsignal() // 處理信號(hào) PMSIGNAL_START_WALRECEIVER
->LaunchMissingBackgroundProcesses() // 檢查并啟動(dòng)缺失的后臺(tái)進(jìn)程
->StartChildProcess(B_WAL_RECEIVER) // 啟動(dòng) walreceiver 進(jìn)程
->postmaster_child_launch() // 創(chuàng)建子進(jìn)程
->WalReceiverMain() // walreceiver 主函數(shù)walreceiver 進(jìn)程啟動(dòng)之后,根據(jù) WalRcvData 中已經(jīng)初始化好的連接信息 conninfo 嘗試和主庫(kù)建立連接,連接過(guò)程使用 libpq 和核心函數(shù) PQconnectStartParams 建立連接,認(rèn)證密碼獲取方式有:
- 通過(guò)配置參數(shù):在根據(jù)
primary_conninfo初始化好的WalRcvData中包含password信息 - 通過(guò)環(huán)境變量:在調(diào)用
conninfo_add_defaults獲取默認(rèn)值時(shí),會(huì)使用getenv函數(shù)遍歷PQconninfoOptions數(shù)組中的所有環(huán)境變量并獲取對(duì)應(yīng)的值,其中就包括PGPASSWORD用于給pgpass賦值 - 通過(guò)密碼文件:在調(diào)用
pqConnectOptions2函數(shù)時(shí)如果發(fā)現(xiàn)當(dāng)前的conn->pgpass仍然為空,則根據(jù)默認(rèn)的密碼文件~/.pgpass或用戶自定義的密碼文件路徑 PGPASSFILE 并調(diào)用passwordFromFile函數(shù)獲取所有 host 對(duì)應(yīng)的密碼
WalReceiverMain() // walreceiver 進(jìn)程主入口
->walrcv_connect() // 觸發(fā)連接主庫(kù)的邏輯
->libpqrcv_connect() // 調(diào)用 libpq 庫(kù)的封裝接口
->libpqsrv_connect_params() // 增加一些額外的參數(shù)選項(xiàng) options
->PQconnectStartParams() // 初始化非阻塞連接
->conninfo_array_parse() // 解析連接參數(shù)數(shù)組
->conninfo_add_defaults() // 補(bǔ)充默認(rèn)連接參數(shù)(從 service file 或者環(huán)境變量中獲取默認(rèn)值)
->pqConnectOptions2() // 處理認(rèn)證相關(guān)選項(xiàng)(如密碼文件)
->passwordFromFile() // 從 .pgpass 文件讀取密碼
->pqConnectDBStart() // 啟動(dòng)異步連接過(guò)程
->PQconnectPoll() // 處理連接狀態(tài)機(jī)(包括認(rèn)證協(xié)商)認(rèn)證過(guò)程中使用密碼時(shí),優(yōu)先使用從密碼文件中獲取的密碼conn->connhost[conn->whichhost].password,該邏輯由 PQpass 函數(shù)實(shí)現(xiàn)
char *
PQpass(const PGconn *conn)
{
char *password = NULL;
if (!conn)
return NULL;
if (conn->connhost != NULL)
password = conn->connhost[conn->whichhost].password;
if (password == NULL)
password = conn->pgpass;
/* Historically we've returned "" not NULL for no password specified */
if (password == NULL)
password = "";
return password;
}04 libpq 的連接控制函數(shù)
在介紹 walreceiver 連接認(rèn)證時(shí),提到使用PQconnectStartParams 去建立于主庫(kù)節(jié)點(diǎn)的連接,這個(gè)函數(shù)通過(guò)參數(shù)數(shù)組接收連接信息,這種直接傳遞鍵值對(duì)可以自動(dòng)處理特殊字符,是新版本引入的啟動(dòng)異步連接函數(shù)
PQconnectStartParams 函數(shù)定義如下,接受兩個(gè)數(shù)組:keywords 包含參數(shù)關(guān)鍵字,values 包含參數(shù)值,并通過(guò) expand_dbname 指定是否允許擴(kuò)展參數(shù)
PGconn *PQconnectStartParams(const char *const *keywords, const char *const *values, int expand_dbname)
PQconnectStart 函數(shù)是另一種支持連接字符串的連接控制函數(shù),定義如下,數(shù)據(jù)庫(kù)連接信息是用從 conninfo 連接字符串里取得的參數(shù)中解析出來(lái)的
PGconn *PQconnectStart(const char *conninfo)
PQconnectPoll 函數(shù)則是PQconnectStartParams和PQconnectStart最終進(jìn)行連接建立時(shí)調(diào)用的函數(shù),該函數(shù)輪詢異步連接狀態(tài),推動(dòng)連接過(guò)程直至完成或失敗
PostgresPollingStatusType PQconnectPoll(PGconn *conn)
PQconnectPoll 函數(shù)返回狀態(tài) PostgresPollingStatusType 定義如下
typedef enum
{
PGRES_POLLING_FAILED = 0, // 連接成功
PGRES_POLLING_READING, // 需等待套接字可讀
PGRES_POLLING_WRITING, // 需等待套接字可寫
PGRES_POLLING_OK, // 連接成功
PGRES_POLLING_ACTIVE /* unused; keep for backwards compatibility */
} PostgresPollingStatusType;上述三個(gè)函數(shù)PQconnectStart, PQconnectStartParams, PQconnectPoll 都是用于打開一個(gè)與數(shù)據(jù)庫(kù)服務(wù)器之間的非阻塞的連接,即應(yīng)用程序在執(zhí)行這些函數(shù)的時(shí)候不會(huì)因遠(yuǎn)端的 I/O 而被阻塞
基于這三個(gè)函數(shù),libpq 提供了三種連接控制接口:PQconnectdb, PQconnectdbParams, PQsetdbLogin
PQconnectdb, PQconnectdbParams 分別對(duì)應(yīng)對(duì)PQconnectStart, PQconnectStartParams 函數(shù)的封裝,函數(shù)調(diào)用參數(shù)一致,如下所示
PGconn *
PQconnectdbParams(const char *const *keywords,
const char *const *values,
int expand_dbname)
PGconn *
PQconnectdb(const char *conninfo)PQsetdbLogin函數(shù)則是 libpq 早期的遺留函數(shù),仍保留對(duì)舊版本的兼容,接受不太靈活的分立的參數(shù)形式:host, port, options, dbname, user, password,其定義如下
PGconn *
PQsetdbLogin(const char *pghost, const char *pgport, const char *pgoptions,
const char *pgtty, const char *dbName, const char *login,
const char *pwd)這三種接口區(qū)別在于參數(shù)傳遞方式:
PQconnectdbParams函數(shù)建立連接的示例如下,通過(guò)關(guān)鍵字和值的數(shù)組傳遞連接參數(shù),這種方式在動(dòng)態(tài)生成參數(shù)時(shí)更安全,無(wú)需轉(zhuǎn)義能避免字符串拼接錯(cuò)誤,而且支持參數(shù)擴(kuò)展
const char *keywords[] = {"host", "port", "dbname", NULL};
const char *values[] = {"localhost", "5432", "mydb", NULL};
PGconn *conn = PQconnectdbParams(keywords, values, 0); PQconnectdb函數(shù)建立連接的示例如下,通過(guò)連接字符串傳遞連接參數(shù),這種方式在處理密碼等字符串時(shí)需要手動(dòng)進(jìn)行轉(zhuǎn)義,也支持?jǐn)U展參數(shù)
PGconn *conn = PQconnectdb("host=127.0.0.1 port=5432 dbname=mydb");PQsetdbLogin函數(shù)建立連接的示例如下,通過(guò)固定參數(shù)傳遞有限的連接參數(shù),這種方式缺乏靈活性,新代碼不建議使用該接口,該接口僅用于舊版本的兼容
PGconn *conn = PQsetdbLogin("localhost", "5432", "", "mydb", "postgres", "yourpassword");參考資料
https://www.kancloud.cn/taobaomysql/monthly/81110
https://zhuanlan.zhihu.com/p/530628881
PostgreSQL: Documentation: 17: 19.6. Replication
PostgreSQL: Documentation: 17: 32.15. Environment Variables
PostgreSQL: Documentation: 17: 32.16. The Password File
https://www.postgresql.org/docs/current/libpq-connect.html//LIBPQ-PQCONNECTDB
到此這篇關(guān)于PostgreSQL 流復(fù)制認(rèn)證機(jī)制的文章就介紹到這了,更多相關(guān)PostgreSQL 流復(fù)制認(rèn)證機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- PostgreSQL?流復(fù)制配置環(huán)境搭建過(guò)程
- PostgreSQL12同步流復(fù)制搭建及主備切換方式
- PostgreSQL13基于流復(fù)制搭建后備服務(wù)器的方法
- postgresql流復(fù)制原理以及流復(fù)制和邏輯復(fù)制的區(qū)別說(shuō)明
- PostgreSQL 流復(fù)制異步轉(zhuǎn)同步的操作
- PostgreSQL流復(fù)制參數(shù)max_wal_senders的用法說(shuō)明
- Postgresql主從異步流復(fù)制方案的深入探究
- PostgreSQL流復(fù)制(主從復(fù)制)詳細(xì)教程
相關(guān)文章
PostgreSQL教程(五):函數(shù)和操作符詳解(1)
這篇文章主要介紹了PostgreSQL教程(五):函數(shù)和操作符詳解(1),本文講解了邏輯操作符、比較操作符、數(shù)學(xué)函數(shù)和操作符、三角函數(shù)列表、字符串函數(shù)和操作符等內(nèi)容,需要的朋友可以參考下2015-05-05
postgresql 將逗號(hào)分隔的字符串轉(zhuǎn)為多行的實(shí)例
這篇文章主要介紹了postgresql 將逗號(hào)分隔的字符串轉(zhuǎn)為多行的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02
postgresql兼容MySQL on update current_timestamp
這篇文章主要介紹了postgresql兼容MySQL on update current_timestamp問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
PostgreSQL實(shí)現(xiàn)跨數(shù)據(jù)庫(kù)授權(quán)查詢的詳細(xì)步驟
在PostgreSQL中,由于一個(gè)數(shù)據(jù)庫(kù)實(shí)例下的不同數(shù)據(jù)庫(kù)在邏輯上是隔離的,你不能像在同一個(gè)數(shù)據(jù)庫(kù)內(nèi)跨模式那樣直接查詢,因此,你需要分兩步走:先授權(quán),后查詢,所以本文給大家介紹了PostgreSQL實(shí)現(xiàn)跨數(shù)據(jù)庫(kù)授權(quán)查詢的詳細(xì)步驟,需要的朋友可以參考下2025-10-10
postgresql 賦權(quán)語(yǔ)句 grant的正確使用說(shuō)明
這篇文章主要介紹了postgresql 賦權(quán)語(yǔ)句 grant的正確使用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-01-01

