本文最后更新于 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 Forbidden
或 404 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 创建的时候访问的是如下接口:
在请求头中加上 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) { 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); final headerF = resp.headers.value('Access-Control-Expose-Headers' ); print (headerF); 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); 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, }); factory GithubAuthEntity.fromJson(Map <String , dynamic > json) => _$GithubAuthEntityFromJson(json); Map <String , dynamic > toJson() => _$GithubAuthEntityToJson(this ); }