# 第三方注册登录
现在很多网站、APP都支持第三方登录。第三方登录不得不说的一个优势:那就是本地注册和第三方注册的选择。虽然这是一个不能称之为问题的问题,做为一个新用户,你有两个选择:1、做一个很受欢迎的用户,注册帐号,完善信息,over。2、选用第三方登录/注册,简单方便。二者供你选择,随心挑选。而我们作为一名技术人员,第三方注册登录就选择了 github。

# OAuth 2.0
说到第三方登录,不得不提的一个知识点就是 oauth 2.0。
OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
这个协议在认证和授权的时候涉及到:
- 服务提供方,例如 GitHub,GitHub上储存了用户的登录名,Email,昵称,头像等信息 用户
- 客户端,例如我的博客就是一个客户端,需要服务方向我提供用户的一些基本信息
OAuth 协议的认证和授权的过程如下:
- 用户打开我的网站后,我想要通过GitHub获取该用户的基本信息
- 在转跳到GitHub的授权页面后,用户同意我获取他的基本信息
- 后端获得GitHub提供的授权码,使用该授权码向GitHub申请一个令牌
- GitHub对后端提供的授权码进行验证,验证无误后,发放一个令牌给后端
- 后端使用令牌,向GitHub获取用户信息
- GitHub 确认令牌无误,返回给后端基本的用户信息
# 使用GitHub 提供的 OAuth 服务
- 登陆 GitHub 官网并注册账号登陆后


- 创建一个应用并填写信息


- 获得 Client ID 和 Chient Secret

# 配置 GitHub 参数
接下来将这些参数放到配置文件 (opens new window)中:
config.github = {
// 固定的
login_url: 'https://github.com/login/oauth/authorize',
// github Client ID
client_id: 'xxxxxxxxxxxxxx',
// github Client Secret
client_secret: 'xxxxxxxxxxxxx',
// 此参数表示只获取用户信息
scope: [ 'user' ],
};
这样我们不仅可以方便的通过
this.ctx.app.config.github拿到所需的数据,还有利于多环境的区分,不同环境使用不同的配置。
# 第三方登陆URL
获取 GitHub 授权的 url 并不需要操作我们的数据库,所以在这个流程中,并没有什么逻辑。
# 授权 URL
在这个方法 (opens new window)中我们只需要拼接我们之前定义好的参数即可。
# 返回结果
这就是一个简单的查询 (opens new window),我们直接返回结构即可。
export default {
Query: {
async githubURL(_root: any, {}, { connector }) {
return await connector.utils.githubURL();
},
},
};
# 定义查询
在这里我们指定返回的必须是个 string 类型,string! 必须返回
type Query {
githubURL: String!
}
看一下我们的运行结果:

# 登陆授权
由于我们这是一个 API 项目,并不具备前端页面。不过这可难不倒我们,让我们打开浏览器的 console 工具栏,执行一次跳转。

我们看到弹出一个页面,并让我们进行授权。那大家心里可能犯迷糊了,授权后跳转到哪里呢?又是怎么知道授权成功或者失败的?不知道大家还记得我们在创建一个应用填写的回调地址吗?
# 创建路由
Router 主要用来描述请求 URL 和具体承担执行动作的 Controller 的对应关系, 框架约定了 app/router.js 文件用于统一所有路由规则。
# 如何定义 Router
https://github.com/push-over/egg-example/blob/70dc0ac730c452f2344568a75cf5ce1f6394811d/app/router.ts
app/router.ts 里面定义 URL 路由规则
import { Application } from 'egg';
export default (app: Application) => {
const { controller, router } = app;
router.get('/', controller.home.index);
// 回调地址
router.get('/github/callback', controller.user.githubLogin);
};
# 实现 Controller
https://github.com/push-over/egg-example/blob/70dc0ac730c452f2344568a75cf5ce1f6394811d/app/controller/user.ts
app/controller 目录下面 实现 Controller
import { Controller } from 'egg';
export default class UserController extends Controller {
async githubLogin() {
const { ctx } = this;
ctx.body = {
name: 'github 登陆',
};
}
}
这样就完成了一个最简单的 Router 定义,当用户执行 GET /github/callback,user.ts 这个里面的 githubLogin方法就会执行。
# 实现回调方法
接下来我们来实现这个方法
验证 code
当回调到我们的项目时,会带给我们一个授权码,我们拿到这个授权码 ,使用该授权码向GitHub申请一个令牌
const { ctx, app } = this;
const { client_id, client_secret } = app.config.github;
const code = ctx.query.code;
const tokenResult = await ctx.curl('https://github.com/login/oauth/access_token', {
method: 'POST',
contentType: 'json',
data: {
client_id,
client_secret,
code,
},
dataType: 'json',
timeout: 8000,
});
// 如果有错误信息或者状态码不等于200 那我们就不让他继续走下去了
if (tokenResult.data.error || tokenResult.status !== 200) {
return await this.ctx.render('transit.html', { uuid: tokenResult.data.error });
}
获取用户信息
当我们获得令牌后,就可以拿着这个令牌去或许用户的信息了。
const { access_token } = tokenResult.data;
const userResult = await ctx.curl(`https://api.github.com/user?access_token=${access_token}`, {
dataType: 'json',
timeout: 8000,
});
// 如果有错误信息或者状态码不等于200 那我们就不让他继续走下去了
if (userResult.data.error || userResult.status !== 200) {
return await this.ctx.render('transit.html', { uuid: userResult.data.error });
}
返回 token
获取到用户信息后,会有一个第三方的唯一标识,我们需要拿到这个标识后判断当前用户有没有注册过我们的应用,如果有的话直接返回 token,没有的话我们需要先把用户信息插入到数据库之后再返回 token 给前端。
const { login, node_id } = userResult.data;
// login: 名称 node_id: 第三方唯一标识
if (!login || !node_id) {
return await this.ctx.render('transit.html', { uuid: '权限验证失败, 请重试' });
} else {
const uuid = ctx.helper.uuidv1();
// 查询数据库有没有这个用户信息
const oauth = await ctx.service.oauth.findById(node_id);
// 如果不存在要先添加到数据库
if (!oauth) {
userResult.data.oauth_type = 'GtiHub';
const user = await ctx.service.user.githubRegister(userResult.data);
const userInfo = JSON.stringify(user);
await ctx.service.redis.set(uuid, userInfo, 3600 * 24);
} else await ctx.service.redis.set(uuid, oauth, 3600 * 24);
// 返回token
await this.ctx.render('transit.html', { uuid });
}
不知道大家有没有注意 await this.ctx.render('transit.html', { uuid }); 这段代码。 在讲这段代码之前,大家先思考一下我们怎么把登陆消息传递给前端呢?
transit.html (opens new window) 是我在第三方登录的中一个 loading 页面,在这个页面中向前端传递信息,具体是怎么操作的,让我们来看代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
登陆中...
<script>
window.onload = function () {
// 这里的 link 就等于是你前端应用接收信息的页面
const link = "https://www.baidu.com/";
window.opener.postMessage("<%= uuid %>", link);
window.close();
}
</script>
</body>
</html>
# 窗口间通信 postMessage
window.open方法会返回一个窗口对象,使用这个对象可以向子窗口发送消息,而子窗口可以通过window.opener向父窗口发送消息
由于postMessage是通过网络协议,所以不能以直接在浏览器打开html的方式进行调试。而是应该放在服务器上,走网络协议。

# 小结
在这一小节中我们学习了如何创建 oauth 应用,并通过回调方法完成注册登录,以及在两个不同端口不同域名的页面之间怎么传递信息。