gitalk就是我现在用的评论工具,它基于GitHub Issue。集成倒是简单,对于git pages的博客来讲,算是很方便的评论系统了,只要有GitHub账号就可以留评论。不过使用之前还担心会对GitHub仓库带来隐患,下面是整个调查过程。
我不会介绍如何配置gitalk。我先介绍 OAuth 2 和 GitHub OAuth APPs,然后做一个demo来具体化 GitHub OAuth 认证流程,最后结合gitalk源码来看gitalk的实现。
Gitalk 配置要求
按照说明https://github.com/gitalk/gitalk新建一个 OAuth App,然后配置几个参数即可。
1 | clientID: 'GitHub Application Client ID', |
摘取关键参数做下简单介绍:
clientID: 唯一标识一个OAuth App,比如小赵和小钱都使用gitalk作为评论工具,那么gitalk怎么知道别人是在评论小赵的博客还是小钱的博客。
clientSecret:OAuth2.0协议的一环,用户允许授权后gitalk需要拿clientID+clientSecret+grant_code向GitHub索要该用户的access_token.
id: 每篇文章的唯一标识。每个评论就是一个Issue,gitalk提交Issue时会带着这个id作为tag参数,这样每篇文章在打开时根据这个tag获取Issue列表,作为博客下面的评论展示出来。
clientID区分哪个博客,id区分哪篇文章。
什么是OAuth APP呢?
什么是 GitHub OAuth Apps
GitHub有三类不同的token:
- GitHub Apps
- OAuth Apps
- Personal access tokens
gitalk 使用的是OAuth这种,那我简单的把我的理解说一下,参考官网https://developer.github.com/apps/about-apps/。
- GitHub Apps
它可以作为一个APP来使用, 比如我有10个repo,我建一个GitHub App来管理其中的3个repo,权限都是读写,我把小明也加入到这个App来,那么小明就有了对这3个repo的读写权限,即读写权限是属于APP,而不是属于我或小明。 - OAuth Apps
实现了OAuth2.0认证协议,主要针对第三方平台想要通过GitHub API获取用户私密信息时,用户可不必向第三方平台提供GitHub账号密码即可完成认证。通过OAuth Apps授权的token代表的是该授权用户,而不是App,即App本身不具有任何权限。在权限限制上也与GitHub App不同,OAuth App只能读。值得一提的是GitHub使用的是Authorization code grant模式,也是最推荐的模式。以gitalk为例在该模式下的认证流程为:sequenceDiagram participant u as 游客 participant g as gitalk participant h as GitHub Note left of u: 还没登录GitHub u->>g: 想留条评论 g->>h: 跳到GitHub登录页 Note right of h: user决定给不给授权 h->>h: 授权给gitalk吗? h->>g: access_token g->>h: 获取user信息 h->>g: 返回user信息 u->>g: 写评论 g->>h: 提一个Issue
这里的标签gitalk就是新创建的OAuth App。 - Personal access token
与用账号密码登录的token类似,不同点在于可以设置权限范围,并且可随时revoke。
什么是 OAth2.0 协议
OAuth2 有4个角色:
- 资源拥有者 (user)
- 第三方客户端 (client)
- 资源服务器 (resource server)
- 认证服务器 (authentication server)
Authentication code grant 模式流程如下:
sequenceDiagram participant u as user participant c as client participant as as authentication server participant rs as resource server u->>c: login c->>+as: authorize as->>-c: grant_code c->>+as: require access token as->>-c: access_token c->>rs: fetch resource
- user在第三方平台A登录,登录方式是使用另一个平台B的身份信息。
- A携带clientID跳转到B的授权页,请user授权。(这个clientID就是A在B登记的OAuth身份标识,这也表明使用OAuth协议前必须先在平台注册登记)
- user授权A可以从B获取身份信息,grant_code作为凭据。
- A的【后台】向B发起access_token申请,携带参数clientID+clientSecret+grant_code。
- B验证无误后返回代表该用户的access_token。
- A可以使用access_token通过B的API调取user信息了。
可以看出认证授权有两个阶段,这也是OAuth协议被认为安全的关键。第一阶段只能拿到一个临时许可令牌grant_code,可能十几分钟的过期。第二阶段拿access_token。
- 第一阶段
clientID可以保存在前端,比如web页面,跳转认证时直接从前端取,不需要请求后台。grant_code只作为一个用户许可,并不能确保是否是可信任平台发起的请求,所以要验证平台是否可信,通过clientSecret。 - 第二阶段
clientSecret只能保存在后台,而且获取access_token的请求只能由后台服务器发起和接收。这个过程跟前端无关,所以被认为是最安全的。
access_token一般会被保存起来,过期时间会久一些,可能几个月。
既然如此,是否可以说凡是使用OAuth2.0的认证就一定是最安全的呢?并不是,这要看是否使用的规范。gitalk就是一个不规范使用的例子,它把clientSecret也暴露在前端。接下来写一个demo来展示一下规范使用和不规范使用的区别。
GitHub OAuth2 认证
新建一个OAuth Apps,Authorization callback URL
填:
1 | http://localhost:5000 |
规范示例
创建 node project
mkdir OAuth2Demo
cd OAuth2Demo
npm init -y
安装依赖
npm install express
npm install axios
新建 server.js
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
40const express = require('express')
const app = express()
// Import the axios library, to make HTTP requests
const axios = require('axios')
// This is the client ID and client secret that you obtained
// while registering the application
const clientID = '6b2b6a2d87e8389e341a'
const clientSecret = '1e313e588e2ffc5b74146f6a5822e6afcce17b20'
// Declare the redirect route
app.get('/carl-zk/blog', (req, res) => {
// The req.query object has the query params that were sent to this route.
const requestToken = req.query.code
axios({
method: 'post',
url: `https://github.com/login/oauth/access_token?client_id=${clientID}&client_secret=${clientSecret}&code=${requestToken}`,
// Set the content type header, so that we get the response in JSON
headers: {
accept: 'application/json'
}
}).then((response) => {
const accessToken = response.data.access_token
console.log(response.data)
// redirect the user to the home page, along with the access token
res.redirect(`/home.html?access_token=${accessToken}`)
})
})
app.use(express.static(__dirname + '/public'))
app.listen(5000,()=>{
console.log("Server listening on port : 5000")
})运行
node server.js
后台部署完成,配置前端页面
当前目录下新开一个终端窗口mkdir public
cd public
新建两个html文件
index.html
1 |
|
home.html
1 |
|
目录结构为:
OAuth2Demo/
…server.js
…public/
……index.html
……home.html
说明:
- clientSecret保存在【后台】
- 用户从
index.html
页面GitHub登录,跳转到授权页,授权后跳转到http://localhost:5000/carl-zk/blog
- API接口
/carl-zk/blog
接收到请求后提取grant_code,调用GitHub获取access_token,然后跳转到前端页面/home.html
并携带access_token,这个过程是【后台】完成的 /home.html
页面的script代码表示,页面打开时提取access_token,然后从GitHub获取user
- 测试认证
http://localhost:5000
为了方便查看记录,可以先设置下Chrome的Network和Console,即使网页发生跳转也不清除任何记录:
依次出现的请求有:
- 跳转到GitHub授权页HTTP Status 302,说明当请求完成后会发生跳转,即http://localhost:5000/carl-zk/blog
1
https://github.com/login/oauth/authorize?client_id=6b2b6a2d87e8389e341a&redirect_uri=http://localhost:5000/carl-zk/blog
- 授权成功,返回到指定URL,且携带grant codeHTTP Status 302,说明还要跳转,跳转地址在header的Location中,即/home.html?access_token=dc6d8739c3b3aa221c7991b0eda175d038637e65
1
http://localhost:5000/carl-zk/blog?code=5612e51bf4d1bd4c289d
- 跳转到/home.htmlHttp Status 200, 说明请求完成。
1
http://localhost:5000/home.html?access_token=dc6d8739c3b3aa221c7991b0eda175d038637e65
- 通过GitHub API获取用户信息access_token 在header的Authorization中。
1
https://api.github.com/user
从1到3是一个完整的认证授权流程,获取grant code是从前端index.html
发起的,获取access token是从后台发起的。
不规范示例
server.js
1 | const express = require('express') |
index.html
1 |
|
home.html
1 |
|
改动:
- clientSecret保存在前端
- 授权后跳转到前端页面
/home.html
- 从
home.html
的script中发起access_token请求
请求依次为:
- 跳转到授权页,授权后跳转到 home.html 页面HTTP Status 302
1
https://github.com/login/oauth/authorize?client_id=6b2b6a2d87e8389e341a&redirect_uri=http://localhost:5000/home.html
- home.html 页面,携带grant codeHTTP Status 200,再无跳转
1
http://localhost:5000/home.html?code=17ac33b2893f2d038dbb
- 申请access token申请失败,请求无法完成,查看Console信息如下:
1
https://github.com/login/oauth/access_token
原因是这个接口禁止跨域请求,如果用Postman去请求就可以获取。解决方法是找个代理服务器转发,代理服务器再把access_token返回给我们。
使用https://github.com/Rob--W/cors-anywhere作为代理服务器,启动在3000端口:
1 | git clone git@github.com:Rob--W/cors-anywhere.git |
home.html 改动:
1 | const proxy = "http://localhost:3000/" |
再次请求:
3. 申请access_token
1 | http://localhost:3000/https://github.com/login/oauth/access_token |
HTTP Status 200,获取成功
4. 获取user
1 | https://api.github.com/user |
使用代理服务器解决了github.com/login/oauth/access_token
不能跨域请求问题,代理服务器本质上属于后端服务器,所以不存在跨域问题。
从1到3都是前端完成,无后台server。缺点是暴露了clientSecret,不符合OAuth2.0规范。但是在GitHub上这样做是安全的,因为access_token只有读权限,不能对user的repo造成任何损害。
结合 Gitalk 源码
gitalk没有后台server,clientSecret也是保存在前端,很明显它使用的是不规范的那种方式。下面根据源码找下它的代理服务器地址。
gitalk的代码在https://github.com/gitalk/gitalk/blob/master/src/gitalk.jsx
使用的代理为https://github.com/gitalk/gitalk/blob/ab55125516b418e2bd9bb6ace44c6df4750a175a/src/gitalk.jsx#L63
1 | proxy: 'https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token', |
一个公开的免费代理,声称是不会保存任何转发信息。
下面简单介绍几个方法:
- access_token
https://github.com/gitalk/gitalk/blob/ab55125516b418e2bd9bb6ace44c6df4750a175a/src/gitalk.jsx#L153-L159access_token获取之后被保存起来,不关闭浏览器是不需要重复授权的。1
2
3
4
5
6
7get accessToken () {
return this._accessToke || window.localStorage.getItem(GT_ACCESS_TOKEN)
}
set accessToken (token) {
window.localStorage.setItem(GT_ACCESS_TOKEN, token)
this._accessToken = token
} - comments
https://github.com/gitalk/gitalk/blob/ab55125516b418e2bd9bb6ace44c6df4750a175a/src/gitalk.jsx#L267-L272注意labels后面连接的id,这个id就是文章的唯一标识,一般设置为URL。1
2
3
4
5
6createIssue () {
const { owner, repo, title, body, id, labels, url } = this.options
return axiosGithub.post(`/repos/${owner}/${repo}/issues`, {
title,
labels: labels.concat(id),
body: body || `${url} \n\n ${
GitHub的label长度限制在63字符之内,超过就不能创建Issue。为此可以对URL进行md5哈希,md5后生成128-bit摘要,用16进制表示,长度恒等于32.
Issues获取也是根据labels,不同文章的id不同,获取的列表自然也就不同,把Issues作为评论展示出来,这样一个简单的评论系统就完成了。
结论
虽然gitalk使用了第三方代理服务器来获取access_token属于违反OAuth2.0规范,但是作为一个评论系统来说安全性可以不必担心,因为GitHub已经对access_token做了只读限制。希望这篇文章对你理解gitalk有所帮助,也欢迎留评论。
附:demo code
初始化Comment脚本 python