# 第三方注册登录

现在很多网站、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 服务

  1. 登陆 GitHub 官网并注册账号登陆后

  1. 创建一个应用并填写信息

  1. 获得 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 应用,并通过回调方法完成注册登录,以及在两个不同端口不同域名的页面之间怎么传递信息。

阅读全文