创新编码

不说话,装高手。

Maintain silence and pretend to be an experta

nuxt3 + nestjs 实现 github 第三方授权登录

2024-10-09 16:43:28记录

国庆在家这段时抽空研究了下如何实现 github 第三方授权登录,并且给自己的小破站加上了这个功能,写这篇文章的目的主要是记录下实现流程以及结果。

其实关于如何创建 github OAuth 应用授权 OAuth 应用官方文档已经写的很清楚,这里我将对整个流程进行一遍梳理。

创建 OAuth 应用文档
授权 OAuth 应用文档

流程说明

UML diagrams.png

创建 OAuth 应用

登录 github 并点击右上角头像进入 Settings,找到 Developer Settings 点击进入并选择 OAuth Apps,点击 New OAuth App 创建新应用
WX20241009-105329.png
WX20241009-110849.png

进入新增界面,添加必填项,例如应用名称、首页地址和授权回调地址,我这边创建了两个应用,分别是正式环境和开发环境使用

WX20241009-111110.png

点击进入应用,完善信息,并创建一个新的客户端密钥复制并保存起来,注意密钥只会出现一次,所以要保存起来

WechatIMG80.jpg

update 完所有信息后就可以进行下一步了

发起授权请求

通过 clientId重定向地址来获取用户的 github 标识
WX20241009-144715.png

折叠代码 复制代码
const authWitheGithub = () => {
  const authorizeUrl = "https://github.com/login/oauth/authorize";
  const clientID =
    process.env.NODE_ENV === "development"
      ? "xxxxxxxxxxxxxxxxxxxxx"
      : process.env.NODE_ENV === "production"
      ? "xxxxxxxxxxxxxxxxxxxxx"
      : undefined;
  const redirectUrl = process.env.NODE_ENV === "development"
      ? "http://localhost:3001/auth"
      : process.env.NODE_ENV === "production"
      ? "https://dawdlePig.cn/auth"
      : undefined;
  location.href = `${authorizeUrl}?client_id=${clientID}&redirect_uri=${redirectUrl}`;
};

通过 code 交换 token

用户允许授权,github 会根据你应用配置的重定向地址跳转回来并携带 code

https://dawdlePig.cn/auth?code=xxxxxxxxxxxxxxxxxxx

后面我们需要拿这个 code 去申请 access_token,然后再去获取用户信息,注意申请 access_token 需要在后端处理,因为客户端密钥不能泄露,所以拿到 code 后要传给后端,由后端来处理后续步骤

使用 code 申请 access_token

WX20241009-162156.png
通过 client_idclient_secret 去申请 token

折叠代码 复制代码
const client_id = 'xxxxxxxxxxxxxxxxx';
const client_secret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const { data } = await firstValueFrom(
  this.httpService.post(
     `https://github.com/login/oauth/access_token?client_id=${client_id}&client_secret=${client_secret}&code=${code}`,
      {
        headers: {
          Accept: 'application/json',
        },
        timeout: 999999999,
      },
  ),
);

得到的返回结果是这样一种形式

折叠代码 复制代码
access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer

使用 URLSearchParams 将参数提取出来

折叠代码 复制代码
const urlParams = new URLSearchParams(data);
const access_token = urlParams.get('access_token');
const token_type = urlParams.get('token_type');
const error = urlParams.get('error');

// 判断错误,有错误直接 throw
if (error) {
   throw new HttpException(error, HttpStatus.OK);
}

没问题进行下一步

使用 access_tokentoken_type 获取用户信息

折叠代码 复制代码
private async getUserByGithubToken(token: string, tokenType: string) {
  const { data } = await firstValueFrom(
    this.httpService.get(`https://api.github.com/user`, {
      headers: {
        Accept: 'application/json',
        Authorization: `${tokenType} ${token}`,
      },
      timeout: 999999999,
    }),
  );

  return data;
}

请求成功后得到如下数据:

折叠代码 复制代码
{
    "login" : "ljnMeow",
    "id" : xxxxxxx,
    "node_id" : "xxxxxxxxxxxxxxx",
    "avatar_url" : "https://avatars.githubusercontent.com/u/87659948?v=4&size=64",
    "gravatar_id" : "",
    "url" : "https://api.github.com/users/ljnMeow",
    "html_url" : "https://github.com/ljnMeow",
    "followers_url" : "https://api.github.com/users/ljnMeow/followers",
    "following_url" : "https://api.github.com/users/ljnMeow/following{/other_user}",
    "gists_url" : "https://api.github.com/users/ljnMeow/gists{/gist_id}",
    "starred_url" : "https://api.github.com/users/ljnMeow/starred{/owner}{/repo}",
    "subscriptions_url" : "https://api.github.com/users/ljnMeow/subscriptions",
    "organizations_url" : "https://api.github.com/users/ljnMeow/orgs",
    "repos_url" : "https://api.github.com/users/ljnMeow/repos",
    "events_url" : "https://api.github.com/users/ljnMeow/events{/privacy}",
    "received_events_url" : "https://api.github.com/users/ljnMeow/received_events",
    "type" : "User",
    "site_admin" : false,
    "name" : null,
    "company" : null,
    "location" : "中国",
    "email" : null,
    "hireable" : null,
    "bio" : "",
    "twitter_username" : null,
    "public_repos" : 198,
    "public_gists" : 0,
    "followers" : 17,
    "following" : 5,
    "created_at" : "2018-06-20T12:50:51Z",
    "updated_at" : "2022-08-03T08:18:05Z"
}

得到用户信息后使用 id 去数据库查,因为 github 用户 id 是唯一的,login是可以修改的,所以我们要使用 id 做唯一查询

用户数据储存

使用 id 去数据库检查是否存在该数据

折叠代码 复制代码
async checkBlogRegisterInfo(
  blogUserAddDTO: BlogUserAddDTO,
): Promise<boolean | BlogUser> {
  const { siteId } = blogUserAddDTO;
  const hasBlogUser = await this.blogUserRepository
    .createQueryBuilder('blog_user')
    .where('blog_user.siteId = :siteId', { siteId })
    .andWhere({ isDelete: false })
    .getOne();

  if (hasBlogUser) {
    return hasBlogUser;
  } else {
    return false;
  }
}

如果不存在则插入新数据,存在就直接返回数据

折叠代码 复制代码
async blogUserResister(blogUserAddDTO: BlogUserAddDTO) {
  const { name, username, avatar, site, siteId } = blogUserAddDTO;
  const blogUser = await this.checkBlogRegisterInfo(blogUserAddDTO);

  if (!blogUser) {
    const newBlogUser = new BlogUser();
    newBlogUser.name = name;
    newBlogUser.username = username;
    newBlogUser.avatar = avatar;
    newBlogUser.site = site;
    newBlogUser.siteId = siteId;

    const result = await this.blogUserRepository.save(newBlogUser);

    return result;
  }

  return blogUser;
}

到这一步就算完成了整个 github OAuth 的对接,接下来怎么处理数据就看自己的需求了

功能预告

现在已经完成授权登录与网站用户注册功能,下一步开始留言板功能设计和开发