Github API 杂记及 Flutter 实现 Basic 认证的简易代码

本文最后更新于 2021年4月4日 晚上

本文主要是看 Github API 文档所做的一些摘要内容, 比较杂乱. 另外就是提供了 Flutter 中使用 dio 来实现认证的代码.

Github API 详见文档

文档地址在这里.

HTTPS 请求, 且基地址是 https://api.github.com, 请求头推荐带上 Accept: application/vnd.github.v3+json.

所有的数据的首发都是使用 JSON 进行.

有两种请求类型, 一种是列表(Summary representations), 获取的是数据的摘要列表, 一种是单一数据(Detailed representations
), 获取的是数据的完整内容.

比如列表 GET 这个端点 /orgs/octokit/repos, 比如数据详情, GET 这个端点 /repos/octokit/octokit.rb.

有些端点需要登录(即认证)后拿到令牌才能访问, 另外如果不登录的话会有访问限制(每小时最多 60 次, 登录的话则每小时最多 5000 次). 故需要看看如下认证方式.

有三种认证方式:

  • 使用 Basic Auth. 方式就是在头中提供用户名和密码的 base64 加工形式的 Basic auth 头.

  • 使用 OAuth2 token 头: 即在请求头中携带 Authorization: token OAUTH-TOKEN.

  • 使用 OAuth2 token 参数: 即在每次请求参数中携带 https://api.github.com/?access_token=OAUTH-TOKEN.

并且 OAuth2 token 可以使用代码获取, 故适用于应用程序.

没有认证的话, 会收到 403 Forbidden404 Not Found 错误(少数端点).

使用无效的身份信息认证的话, 会收到 401 Unauthorized 响应.

如果短时间内多次触发 401, 则会限制一段时间后才能再次登录.

1
2
3
4
5
6
curl -i https://api.github.com -u valid_username:valid_password
HTTP/1.1 403 Forbidden
{
"message": "Maximum number of login attempts exceeded. Please try again later.",
"documentation_url": "https://developer.github.com/v3"
}

请求的参数位置都是可选的, 对于 GET 请求, 路径参数也可以通过 queryString 传递. 对于 POST, PUT, PATCH, DELETE, 如果没有通过 URL 传递的参数, 则需要通过请求体 JSON 传递, 且要加上 Content-Type: application/json 头.

可以访问 https://api.github.com 来获取所有的端点分类列表.

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
{
"current_user_url": "https://api.github.com/user", // 需要登录
"current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}",
"authorizations_url": "https://api.github.com/authorizations",
"code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}",
"commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}",
"emails_url": "https://api.github.com/user/emails",
"emojis_url": "https://api.github.com/emojis",
"events_url": "https://api.github.com/events",
"feeds_url": "https://api.github.com/feeds",
"followers_url": "https://api.github.com/user/followers",
"following_url": "https://api.github.com/user/following{/target}",
"gists_url": "https://api.github.com/gists{/gist_id}",
"hub_url": "https://api.github.com/hub",
"issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}",
"issues_url": "https://api.github.com/issues",
"keys_url": "https://api.github.com/user/keys",
"notifications_url": "https://api.github.com/notifications",
"organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}",
"organization_url": "https://api.github.com/orgs/{org}",
"public_gists_url": "https://api.github.com/gists/public",
"rate_limit_url": "https://api.github.com/rate_limit",
"repository_url": "https://api.github.com/repos/{owner}/{repo}",
"repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}",
"current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}",
"starred_url": "https://api.github.com/user/starred{/owner}{/repo}",
"starred_gists_url": "https://api.github.com/gists/starred",
"team_url": "https://api.github.com/teams",
"user_url": "https://api.github.com/users/{user}",
"user_organizations_url": "https://api.github.com/user/orgs",
"user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}",
"user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"
}

任意的响应都可能是重定向的(301, 302, 307), 在响应头中会包含重定向到的地址, 故需要允许重定向并跟着走, 而非产生错误!(即不要只假定 200..<300 才是正常).

对于返回列表的端点, 统一都使用 per_page 指定每页条数, 使用 page 指定页码, 页码从 1 开始.

响应头中会包含 Link 头来表示当前页的前后跳转, 这样不用在本地记录页数了!

1
Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next", <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

请求的时候客户端必须传 User-Agent 头, 否则会 403.

使用 Time-Zone 头可以让响应中生成基于请求头中时区的时间戳: Time-Zone: Europe/Amsterdam.

所有的时区名称可以在这里找到.

如果没有指定时区, 则使用的是 UTC 时间戳. Asia/Shanghai

Github 认证

官方文档中写的 OAuth2 tokens 可以通过编程方式获取.

tokens can be created using the OAuth Authorizations API using Basic Authentication.

首先在 Github 提供的开发者页面中建立一个 OAuth App, 有了 App 后就可以获取到客户端 ID 和客户端 Secret 了, 类似如下所示:

1
2
3
4
5
Client ID
bb60c55dd9f3axxa6xxe

Client Secret
3012xx2045xxb064b9daaxx341415cd0b9516a44

通过这个 APP, 就可以创建多个和这个 App 关联的 OAuth token 了, 另外在这个页面中可以手动创建 token, 但这种方式创建的 token 是和 user 关联的, 而非和 app 关联的.

另外如果有两步验证, 响应则是 401 且响应头中会包含 X-GitHub-OTP: required; :2fa-type, 具体的处理在这个链接.

而使用 Basic Auth 创建的时候访问的是如下接口:

1
POST /authorizations

在请求头中加上 Authorization: Basic Base64字符串, 其中 Base64 字符串是 用户名:密码 的 Base64 编码形式.

请求参数的列表在这里, 需要以 JSON 方式写到请求体中, 其中可用的 scope 在这个链接, 在页面的左上角搜索 scopes 就可以找到这个页面.

Flutter 中实现认证的过程

Dart 中的 Base64 编解码功能在 dart:convert 中的 base64 对象上.

1
2
3
4
5
final userName = '[email protected]';
final passwd = 'xxxxxx';
final preStr = '$userName:$passwd';
final bytes = utf8.encode(preStr);
final base64Str = base64.encode(bytes);

生成了正确的 base64 字符串后, 就可以开始请求了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
方法: POST

基地址: https://api.github.com

端点: /authorizations

请求头:
Accept: application/vnd.github.v3+json
Content-Type: application/json
Authorization: Basic base64字符串

请求体JSON:

{
"scopes": [
"public_repo"
],
"client_id": "bb60c55dd9f3axxa6xxe",
"client_secret": "3012xx2045xxb064b9daaxx341415cd0b9516a44",
"note": "随便写个标记"
}

下面就是进行请求了, 这里可以选择纯 http 库, 也可以使用 dio. 先看 http 库, 熟悉了再看 dio.

遇到的问题1: 实体转字典, 这里暂时手动解决, 即实体中提供一个转字典方法. 然后通过 dart:convert 的 json.encode 就可以转 json 字符串了.

关于 flutter 无法用 Charles 捕捉的解决方案是在 httpClient 的建立时设置代理, 估计 dio 也有提供类似的功能, 详见这个链接. 其中提供有两种方式, 一种是 dart:io 里面的 HttpClient, 一种是 dio, 需要好好看看 HttpClient 的使用方式, 然后是 dio, 以及 http 库中的封装, 估计就是在 HttpClient 上面进行封装的.

如下是完整代码:

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
76
77
78
79
80
81
82

void simpleReq() {
final baseURL = 'https://api.github.com';
final options = BaseOptions(
connectTimeout: 5000,
receiveTimeout: 5000,
baseUrl: baseURL,
headers: _getHeaders(),
);
final client = Dio(options);
final adapter = client.httpClientAdapter as DefaultHttpClientAdapter;
adapter.onHttpClientCreate = (client) {
// 设置该客户端的代理为指定的 ip:端口
client.findProxy = (uri) {
return 'PROXY 192.168.2.201:9999';
};
// 安卓机上面的话自定义证书是不被信任的, 故还需要设置下面这一步:
client.badCertificateCallback = (cert, host, port) => true;
};
client.post('/authorizations', data: _getBodyJSONString()).then((resp) {
print(resp);
// 通过如下的方式获取自定义的响应 header 域的内容.
final headerF = resp.headers.value('Access-Control-Expose-Headers');
print(headerF);
// dio 的响应体在没有特别指定的情况下, 默认是经过了 json 解析那一步的(和下面一行类似).
// final jsonDict = json.decode(respJSONString);
final authRespEntity = GithubAuthEntity.fromJson(resp.data);
print(authRespEntity.token);
}).catchError((err, stack) {
print(err);
print(stack);
});
}

Map<String, String> _getHeaders() {
final userName = '用户名';
final passwd = '密码';
final preStr = '$userName:$passwd';
final bytes = utf8.encode(preStr);
// 要进行 Basic Auth 的话, 需要对用户名和密码进行 base64 编码计算.
final base64Str = base64.encode(bytes);
final headers = {
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
'Authorization': 'Basic $base64Str',
};
return headers;
}

String _getBodyJSONString() {
final dict = BasicAuthParam().toDict();
print(dict);
final str = json.encode(dict);
print(str);
return str;
}

class BasicAuthParam {
final List<String> scopes;
final String note;
final String noteURL;
final String clientID;
final String clientSecret;

BasicAuthParam({
this.scopes = const ['user', 'repo', 'gist', 'notifications'],
this.note = 'nothing to note',
this.noteURL = 'https://rayy.top',
this.clientID = 'bb60c55dd9f3axxa6xxe',
this.clientSecret = '3012xx2045xxb064b9daaxx341415cd0b9516a44',
});

Map<String, dynamic> toDict() {
return {
'scopes': scopes,
'note': note,
'note_url': noteURL,
'client_id': clientID,
'client_secret': clientSecret,
};
}
}

其中响应中携带的实体定义如下:

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
import 'package:json_annotation/json_annotation.dart';

// 注意这句是生成代码所在的文件
part 'github_auth_entity.g.dart';

@JsonSerializable()
class GithubAuthEntity {
final int id;
final String url;
final List<String> scopes;
final String token;

GithubAuthEntity({
this.id,
this.url,
this.scopes,
this.token,
});

// JSON 转实体
factory GithubAuthEntity.fromJson(Map<String, dynamic> json) =>
_$GithubAuthEntityFromJson(json);

// 实体转 JSON 字典
Map<String, dynamic> toJson() => _$GithubAuthEntityToJson(this);
}

Github API 杂记及 Flutter 实现 Basic 认证的简易代码
https://blog.rayy.top/2019/02/09/2019-47-GitHubAPI/
作者
貘鸣
发布于
2019年2月9日
许可协议