1. https://pub.dev/ 網(wǎng)站登錄
接入谷歌OAuth2登錄闽晦。
Google 登錄 JavaScript 客戶端參考文檔:
https://developers.google.com/identity/sign-in/web/reference#gapiauth2authorizeconfig
針對(duì)網(wǎng)絡(luò)服務(wù)器應(yīng)用使用 OAuth 2.0:https://developers.google.com/identity/protocols/oauth2/web-server?hl=zh-cn#offline
pub-dev登錄的具體流程:
- 用戶打開 pub.dev 網(wǎng)站并點(diǎn)擊登錄按鈕。
- 網(wǎng)站重定向到 Google 賬號(hào)登錄界面。
- 用戶輸入其 Google 賬號(hào)和密碼孕似。
- Google 驗(yàn)證用戶身份并向用戶顯示一個(gè)授權(quán)頁面送朱,列出網(wǎng)站將獲得的權(quán)限列表。
- 用戶選擇是否授權(quán)該網(wǎng)站訪問其 Google 賬戶的特定信息搔谴。
- 用戶點(diǎn)擊授權(quán)按鈕魁袜。
- Google 發(fā)送一個(gè)包含授權(quán)令牌的回調(diào) URL 給 pub.dev 網(wǎng)站
- pub.dev 網(wǎng)站收到授權(quán)令牌并使用它來訪問用戶的 Google 賬戶信息(例如用戶的電子郵件地址和用戶名)。
- pub.dev 網(wǎng)站將這些信息與其自己的用戶數(shù)據(jù)庫進(jìn)行匹配敦第,如果沒有與用戶的 Google 賬戶關(guān)聯(lián)的本地帳戶峰弹,則 pub.dev 將創(chuàng)建一個(gè)新的帳戶,并將其與用戶的 Google 賬戶關(guān)聯(lián)芜果。
此過程基于 OAuth 2.0 協(xié)議鞠呈。OAuth 2.0 是一種授權(quán)協(xié)議,用于在不共享用戶憑據(jù)的情況下右钾,允許第三方應(yīng)用程序訪問用戶資源蚁吝。在這種情況下,Google 充當(dāng)身份提供者舀射,pub.dev 充當(dāng)客戶端應(yīng)用程序窘茁,而用戶是資源所有者。Google 授權(quán) pub.dev 訪問用戶的資源脆烟,同時(shí)確保不會(huì)向 pub.dev 共享用戶的憑據(jù)山林。
pub.dev源碼地址:https://github.com/dart-lang/pub-dev
這里用google授權(quán)登錄,跟pub 組件 的谷歌登錄是一樣的邢羔。pub組件包源碼地址:https://github.com/dart-lang/pub
pub項(xiàng)目和pub-dev項(xiàng)目用到的客戶端ID驼抹,客戶端密鑰都是相同的
pub-dev項(xiàng)目:
/pub-dev/pkg/pub_integration/lib/src/pub_tool_client.dart
// OAuth clientId and clientSecret also hardcoded into the `pub` client.
final _pubClientId =
'818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.googleusercontent.com';
final _pubClientSecret = 'SWeqj8seoJW0w7_CpEPFLX0K';
登錄pub.dev
https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?redirect_uri=storagerelay://https/pub.dev?id=auth358956&response_type=permission id_token&scope=email profile openid&openid.realm&include_granted_scopes=true&client_id=818368855108-e8skaopm5ih5nbb82vhh66k7ft5o7dn3.apps.googleusercontent.com&ss_domain=https://pub.dev&prompt=select_account&fetch_basic_profile=true&gsiwebsdk=2&service=lso&o2v=1&flowName=GeneralOAuthFlow
pub 項(xiàng)目:
/pub/lib/src/oauth2.dart
/// The pub client's OAuth2 identifier.
const _identifier = '818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.'
'googleusercontent.com';
/// The pub client's OAuth2 secret.
///
/// This isn't actually meant to be kept a secret.
const _secret = 'SWeqj8seoJW0w7_CpEPFLX0K';
https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?access_type=offline&approval_prompt=force&response_type=code&client_id=818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.googleusercontent.com&redirect_uri=http://localhost:54969&code_challenge=MhrdCM1Z0xkd2GdNgfbxhD5h487Mug_Uj4q8AUevSJY&code_challenge_method=S256&scope=openid https://www.googleapis.com/auth/userinfo.email&service=lso&o2v=1&flowName=GeneralOAuthFlow
2. pub publish 發(fā)布組件
通過調(diào)用 pub 包中的函數(shù)實(shí)現(xiàn)的。而 flutter pub publish 命令本身只是一個(gè)包裝器张抄,它調(diào)用 pub 包中的函數(shù)來完成實(shí)際的操作砂蔽。
dart提供的 pub 包項(xiàng)目地址:
https://github.com/dart-lang/pub
發(fā)布組件的大致整體流程如下:
- 解析 pubspec.yaml 文件,獲取包名和版本號(hào)署惯,并檢查包名和版本號(hào)的合法性左驾。
- 構(gòu)建上傳包的壓縮文件,包括 lib极谊、bin诡右、example 等文件夾中的文件以及 pubspec.yaml 文件。
- 對(duì)上傳包進(jìn)行驗(yàn)證轻猖,包括檢查上傳包的大小帆吻、檢查是否包含非法字符等。
- 通過 OAuth2 或 Bearer 身份驗(yàn)證機(jī)制登錄 pub.dev 等服務(wù)器咙边。
- 上傳壓縮文件到服務(wù)器猜煮,并等待服務(wù)器響應(yīng)結(jié)果次员。
- 如果上傳成功,更新本地緩存并顯示上傳成功的信息王带;如果上傳失敗淑蔚,拋出相應(yīng)的異常并顯示上傳失敗的信息。
2.1 組件包解析
- 檢查命令行參數(shù) --server 是否已解析愕撰,如果已解析刹衫,則輸出一條警告信息,表明此選項(xiàng)已過時(shí)搞挣,應(yīng)該使用 pubspec.yaml 文件中的 publish_to 字段或設(shè)置 $PUB_HOSTED_URL 環(huán)境變量代替带迟。
- 檢查命令行參數(shù) --force 和 --dry-run 是否同時(shí)存在,如果是囱桨,則拋出一個(gè)使用異常仓犬,表示這兩個(gè)選項(xiàng)不能同時(shí)使用。
- 檢查當(dāng)前包是否為私有包蝇摸,如果是婶肩,則輸出一條錯(cuò)誤信息,表示私有包不能被發(fā)布貌夕,應(yīng)該通過更改 pubspec.yaml 文件中的 publish_to 字段來啟用律歼。
- 調(diào)用 entrypoint.acquireDependencies() 函數(shù)獲取包依賴項(xiàng)。
- 獲取當(dāng)前包的文件列表啡专,打印一條消息险毁,說明正在打包和發(fā)布當(dāng)前包,并展示包含的文件列表们童。
- 創(chuàng)建并壓縮當(dāng)前包的文件畔况,并獲取壓縮包的字節(jié)數(shù)組。
- 驗(yàn)證當(dāng)前包是否有效慧库,如果無效跷跪,則設(shè)置退出碼并返回,否則繼續(xù)執(zhí)行下一步齐板。
- 如果是 --dry-run 模式吵瞻,則打印一條消息,表示服務(wù)器可能會(huì)執(zhí)行額外的檢查甘磨,然后返回橡羞,否則繼續(xù)執(zhí)行下一步。
- 調(diào)用 _publish() 函數(shù)济舆,將壓縮包上傳到指定的服務(wù)器卿泽,并等待上傳完成。
@override
Future runProtected() async {
if (argResults.wasParsed('server')) {
await log.errorsOnlyUnlessTerminal(() {
log.message(
'''
The --server option is deprecated. Use `publish_to` in your pubspec.yaml or set
the \$PUB_HOSTED_URL environment variable.''',
);
});
}
if (force && dryRun) {
usageException('Cannot use both --force and --dry-run.');
}
if (entrypoint.root.pubspec.isPrivate) {
dataError('A private package cannot be published.\n'
'You can enable this by changing the "publish_to" field in your '
'pubspec.');
}
await entrypoint.acquireDependencies(SolveType.get, analytics: analytics);
var files = entrypoint.root.listFiles();
log.fine('Archiving and publishing ${entrypoint.root.name}.');
// Show the package contents so the user can verify they look OK.
var package = entrypoint.root;
log.message(
'Publishing ${package.name} ${package.version} to $host:\n'
'${tree.fromFiles(files, baseDir: entrypoint.root.dir, showFileSizes: true)}',
);
var packageBytesFuture =
createTarGz(files, baseDir: entrypoint.root.dir).toBytes();
// Validate the package.
var isValid = await _validate(
packageBytesFuture.then((bytes) => bytes.length),
files,
);
if (!isValid) {
overrideExitCode(exit_codes.DATA);
return;
} else if (dryRun) {
log.message('The server may enforce additional checks.');
return;
} else {
await _publish(await packageBytesFuture);
}
}
pub使用谷歌帳戶來管理包上載權(quán)限滋觉。
acquireDependencies- 》 _validate-》_publish
2.2 使用谷歌賬號(hào)登錄
請(qǐng)求授權(quán)碼:
https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force
get請(qǐng)求 結(jié)果通過callback URL回調(diào)傳回來:
pub的啟動(dòng)本地服務(wù)器:
Future<Client> _authorize() async {
var completer = Completer();
var server = await bindServer('localhost', 0);
shelf_io.serveRequests(server, (request) {
return shelf.Response.found('https://pub.dev/authorized');
});
var client = await completer.future;
return client;
}
完整代碼:
Future<Client> _authorize() async {
var grant = AuthorizationCodeGrant(
_identifier, _authorizationEndpoint, tokenEndpoint,
secret: _secret,
// Google's OAuth2 API doesn't support basic auth.
basicAuth: false,
httpClient: _retryHttpClient,
);
// Spin up a one-shot HTTP server to receive the authorization code from the
// Google OAuth2 server via redirect. This server will close itself as soon as
// the code is received.
var completer = Completer();
var server = await bindServer('localhost', 0);
shelf_io.serveRequests(server, (request) {
if (request.url.path.isNotEmpty) {
return shelf.Response.notFound('Invalid URI.');
}
log.message('Authorization received, processing...');
var queryString = request.url.query;
// Closing the server here is safe, since it will wait until the response
// is sent to actually shut down.
server.close();
completer
.complete(grant.handleAuthorizationResponse(queryToMap(queryString)));
return shelf.Response.found('https://pub.dev/authorized');
});
var authUrl = grant.getAuthorizationUrl(
Uri.parse('http://localhost:${server.port}'),
scopes: _scopes,
);
log.message(
'Pub needs your authorization to upload packages on your behalf.\n'
'In a web browser, go to $authUrl\n'
'Then click "Allow access".\n\n'
'Waiting for your authorization...');
var client = await completer.future;
log.message('Successfully authorized.\n');
return client;
}
用授權(quán)碼獲取訪問token签夭,使用授權(quán)碼POST請(qǐng)求獲取token:https://accounts.google.com/o/oauth2/token
保存訪問口令齐邦。
可以看到,如果之前沒有在pub.dev創(chuàng)建過賬號(hào)的話覆致,這個(gè)過程是有授權(quán)登錄pub.dev上創(chuàng)建賬號(hào)的過程侄旬。
2.3 上傳https://pub.dev/
組件包的meta信息上傳到pub.dev
組件文件存儲(chǔ)實(shí)際是上傳到https://storage.googleapis.com,這個(gè)上傳地址是可以由pub.dev服務(wù)端控制的煌妈。
創(chuàng)建一個(gè)包含官方 pub 服務(wù)器地址和測(cè)試用的本地服務(wù)器地址的集合。
判斷當(dāng)前服務(wù)器地址是否屬于官方 pub 服務(wù)器地址集合中的地址宣羊,如果是璧诵,則檢查本地緩存中是否有對(duì)應(yīng)服務(wù)器地址的令牌,如果沒有仇冯,則使用 OAuth2 身份驗(yàn)證客戶端(oauth2.withClient)來進(jìn)行身份驗(yàn)證之宿,并調(diào)用 _publishUsingClient() 函數(shù)上傳壓縮包;如果有苛坚,則使用 Bearer 身份驗(yàn)證客戶端(withAuthenticatedClient)來進(jìn)行身份驗(yàn)證比被,并調(diào)用 _publishUsingClient() 函數(shù)上傳壓縮包。
如果當(dāng)前服務(wù)器地址不屬于官方 pub 服務(wù)器地址集合中的地址泼舱,則使用 Bearer 身份驗(yàn)證客戶端來進(jìn)行身份驗(yàn)證等缀,并調(diào)用 _publishUsingClient() 函數(shù)上傳壓縮包。
● 如果上傳過程中出現(xiàn) PubHttpResponseException 異常娇昙,則獲取請(qǐng)求的 URL尺迂,判斷該 URL 是否與當(dāng)前服務(wù)器地址相同,如果相同冒掌,則調(diào)用 handleJsonError() 函數(shù)處理錯(cuò)誤噪裕;如果不同,則將異常拋出股毫。
Future<void> _publish(List<int> packageBytes) async {
try {
final officialPubServers = {
'https://pub.dev',
// [validateAndNormalizeHostedUrl] normalizes https://pub.dartlang.org
// to https://pub.dev, so we don't need to do allow that here.
// Pub uses oauth2 credentials only for authenticating official pub
// servers for security purposes (to not expose pub.dev access token to
// 3rd party servers).
// For testing publish command we're using mock servers hosted on
// localhost address which is not a known pub server address. So we
// explicitly have to define mock servers as official server to test
// publish command with oauth2 credentials.
if (runningFromTest &&
Platform.environment.containsKey('_PUB_TEST_DEFAULT_HOSTED_URL'))
Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'],
};
// Using OAuth2 authentication client for the official pub servers
final isOfficialServer = officialPubServers.contains(host.toString());
if (isOfficialServer && !cache.tokenStore.hasCredential(host)) {
// Using OAuth2 authentication client for the official pub servers, when
// we don't have an explicit token from [TokenStore] to use instead.
//
// This allows us to use `dart pub token add` to inject a token for use
// with the official servers.
await oauth2.withClient(cache, (client) {
return _publishUsingClient(packageBytes, client);
});
} else {
// For third party servers using bearer authentication client
await withAuthenticatedClient(cache, host, (client) {
return _publishUsingClient(packageBytes, client);
});
}
} on PubHttpResponseException catch (error) {
var url = error.response.request!.url;
if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
handleJsonError(error.response);
} else {
rethrow;
}
}
}
其中publishUsingClient過程如下:
又分成3步交互:
- https://pub.dev/api/packages/versions/new上傳準(zhǔn)備獲取上傳目標(biāo)地址
- 上傳到 https://storage.googleapis.com
- https://pub.dev/api/packages/versions/newUploadFinis檢查包權(quán)限,有效性
Future<void> _publishUsingClient(
List<int> packageBytes,
http.Client client,
) async {
Uri? cloudStorageUrl;
try {
await log.progress('Uploading', () async {
/// 1. Initiate upload
final parametersResponse =
await retryForHttp('initiating upload', () async {
final request =
http.Request('GET', host.resolve('api/packages/versions/new'));
request.attachPubApiHeaders();
request.attachMetadataHeaders();
return await client.fetch(request);
});
final parameters = parseJsonResponse(parametersResponse);
/// 2. Upload package
var url = _expectField(parameters, 'url', parametersResponse);
if (url is! String) invalidServerResponse(parametersResponse);
cloudStorageUrl = Uri.parse(url);
final uploadResponse =
await retryForHttp('uploading package', () async {
// TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
// should report that error and exit.
var request = http.MultipartRequest('POST', cloudStorageUrl!);
var fields = _expectField(parameters, 'fields', parametersResponse);
if (fields is! Map) invalidServerResponse(parametersResponse);
fields.forEach((key, value) {
if (value is! String) invalidServerResponse(parametersResponse);
request.fields[key] = value;
});
request.followRedirects = false;
request.files.add(
http.MultipartFile.fromBytes(
'file',
packageBytes,
filename: 'package.tar.gz',
),
);
return await client.fetch(request);
});
/// 3. Finalize publish
var location = uploadResponse.headers['location'];
if (location == null) throw PubHttpResponseException(uploadResponse);
final finalizeResponse =
await retryForHttp('finalizing publish', () async {
final request = http.Request('GET', Uri.parse(location));
request.attachPubApiHeaders();
request.attachMetadataHeaders();
return await client.fetch(request);
});
handleJsonSuccess(finalizeResponse);
});
} on AuthenticationException catch (error) {
var msg = '';
if (error.statusCode == 401) {
msg += '$host package repository requested authentication!\n'
'You can provide credentials using:\n'
' $topLevelProgram pub token add $host\n';
}
if (error.statusCode == 403) {
msg += 'Insufficient permissions to the resource at the $host '
'package repository.\nYou can modify credentials using:\n'
' $topLevelProgram pub token add $host\n';
}
if (error.serverMessage != null) {
msg += '\n${error.serverMessage!}\n';
}
dataError(msg + log.red('Authentication failed!'));
} on PubHttpResponseException catch (error) {
var url = error.response.request!.url;
if (url == cloudStorageUrl) {
// TODO(nweiz): the response may have XML-formatted information about
// the error. Try to parse that out once we have an easily-accessible
// XML parser.
fail(log.red('Failed to upload the package.'));
} else if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
handleJsonError(error.response);
} else {
rethrow;
}
}
}