登录体系太麻烦?——试试OAuth授权!

603 阅读11分钟

在学习和工作中,不少开发者都曾面对过登录注册的开发难题:无论是前端的表单校验、提交处理与权限控制,还是后端的数据库交互、令牌生成与安全校验,都涉及大量繁琐的逻辑,才能搭建起一套完整的登录注册体系。 此时,若能有一种方案简化这些重复性工作,让开发者得以将精力聚焦在核心业务上,无疑是极大的便利。而 OAuth—— 也就是我们常说的 “开放授权”—— 正是为解决这一问题而生。接下来,我们就一起探究它是如何简化开发流程的。

本文将从概念解析、原理剖析,到以Github OAuth为例的实际项目搭建,为大家系统详解 OAuth 的使用。既能帮你理清核心概念、吃透底层原理,也能让你掌握在项目中落地的具体方法 —— 相信读完本文,你定会有自己的收获。

一、概念

1.1 初步认识

相信大家在使用APP时肯定遇到过“授权登录”的情况,不管是游戏还是办公软件,几乎都离不开它。所以在概念的理解上应该没有太大问题。用一句稍微专业一点的话来解释,OAuth 的思想就是让第三方应用不获取用户账号密码的前提下,安全地获得用户在某个服务(如微信GitHub)上的部分资源权限。

这里的第三方应用指的就是APP或者说我们自己开发的应用,它们不直接获取用户的账号密码等信息,而是去向一些比较“权威”的应用(微信、QQ、Github等)去请求用户的授权,从而获得用户的权限和资源。比如一些游戏可能会使用你的好友信息和头像等信息。

1.2 优劣分析

那么从这个流程就可以得知,OAuth到底给我们带来了哪些好处,又引出了哪些问题: 分为三个方面说,分别是用户开发者、和授权服务提供商

  1. 用户来说:首先是简便性,省去 “填写手机号、设置密码、验证邮箱” 等注册流程。其次是安全性,在登录的同时可以选择“允许”和“拒绝”一些权限,如访问联系人等,让第三方应用不乱用权限。
  2. 开发者来说:无需从零开发登录功能(看似简单,实则涉及安全加密、合规性等大量细节),直接复用微信、GitHub 等成熟平台的身份验证能力,节省开发和维护成本。其次就是可以提高用户的转化率,“微信登录”肯定比“注册账号”更能让用户“留下”。
  3. 授权服务提供商来说:通过让第三方应用接入自己的账号体系,既能增强用户对自身平台的依赖(用户更难离开),也能通过开放资源(如用户关系、内容)扩大生态影响力(比如微信通过 OAuth 让小程序生态更繁荣)

然而,伴随着如此优势而来的也有一些问题,这里拿 “单点故障” 举例:

试想:当你越依赖某个人或者某件事的时候,或许会发现自己越来越不自由了,事事都要跟随对方的想法😟

这里的OAuth也是一个道理,比如授权方的服务器如果出现问题,就会严重影响第三方应用的使用。再比如,授权方可能调整接口的规则,第三方应用也需要随之调整,增加维护成本。

所以可以看到市面上并不是所有的APP都使用OAuth授权,那么知道了优缺点,现在来想想到底什么样的应用比较适合OAuth授权吧!

1.3 适用场景

  1. 轻量型工具型应用,即核心价值不在 “用户账号管理”,自建注册登录系统会浪费资源,用 OAuth 可快速复用成熟平台的身份能力。比如在学习开发的时候搭建的一些工具型平台(编辑器等);
  2. 应用的功能必须结合外部平台的数据才能实现,比如游戏的社交性(排名、邀请好友上线等功能)就依赖于一些社交软件,这种情况也适合用OAuth;

总之如果身份信息比较敏感(如银行、支付类应用)或者对授权方的稳定性要求高(如医疗系统),则不建议用OAuth授权避免导致一些问题。

在了解了OAuth“是什么”之后,还需要知道“为什么”,那么下面就是原理的讲解部分。

二、原理

不同的授权方对于接口定义和授权流程都不尽相同,这里用Github OAuth举例。

先上图,看看大概的流程。

whiteboard_exported_image.png (前端登录后页面可以自行定义,这里只是展示了一个示例)

可以在图中看到三个角色,分别是第三方应用的前端后端以及github授权方

据图总结出来的以用户为主的流程如下:

  1. 用户点击 "使用 GitHub 登录" 按钮
  2. 用户跳转到 GitHub 的授权页面,点击“同意授权账号信息”按钮
  3. GitHub验证用户身份并准备用户信息
  4. 用户授权成功后,GitHub 向后端地址发送授权码
  5. 后端使用授权码向 GitHub 请求访问令牌并使用令牌获取用户信息
  6. 后端创建 / 更新用户,重定向到对应登陆后的页面
  7. 前端进行后续身份验证和请求,流程完毕

这个简要的流程很好理解,但是如果上手开发,会发现还是有不少疑惑,那么对于开发者来说,我们还有哪些需要注意的地方呢?下面以问题为导向,给大家解释一下详细的流程。

  • Q1:用户点击登录按钮之后,跳转的URL是什么?我们如何确定?

  • A1:这其实是前置的一个准备操作,我们想要使用github的OAuth服务,那自然也要去github网站做对应的申请。下面给出步骤:

    github个人主页——Settings——Developer Settings——OAuth Apps

    根据应用实际情况填写正确的前后端地址,得到两个关键参数,Client IDClient Secrets,都是用于验证第三方应用的身份信息。界面如下图所示:

    在得到ClientId之后,前端就可以由此拼接出github约定的URL完成用户授权,具体拼接规则后面“实现”章节会详细给出。

image.png

  • Q2:后端和github服务器的交互为什么需要两次(授权码换取令牌,令牌换取用户信息)?

  • A2:首先,明确授权码是临时凭证而不是核心凭证,有效期通常只有几分钟,需要后端通过clientSecret+授权码才能得到核心凭证令牌,这样是为了防止前端或者其他方窃取授权码从而窃取用户信息,而第二次的交互就是常见的通过令牌请求用户信息;

    由此我们不难发现,client_secret千万千万不能泄漏,才能杜绝 “伪造的第三方应用” 通过窃取授权码获取令牌的可能(因为伪造应用没有 client_secret)。

    另一方面,这也是GitHub服务器单一职责的体现,token令牌由授权服务器(GitHub 的 /login/oauth 端点)发放,而用户信息由“资源服务器”(GitHub 的 API 端点)发放,符合设计模式。

通过上面两个问题的解释相信大家在原理方面已经有了一定的认识,接下来就一起来在项目中通过GithubOAuth来简化登录操作吧!

三、实现

项目背景:本次示例项目主体是仿飞书协同编辑器(目前在起步阶段),属于工具型轻量型项目,所以对于登录功能我们选择GithubOAuth授权。

关于技术栈:前端nextjs后端nestjs

GitHub地址:DocVault(求求star嘿嘿😊)

3.1前端实现

通过前面的步骤可以看到,前端需要准备登录页(跳转前),逻辑也比较简单,按钮绑定跳转事件即可。话不多说直接上代码,前面提到的授权页url拼接规则也一并在代码中给出。

//核心逻辑:跳转函数
const handleSignIn = () => {
    const clientId =
      process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID;
    //后端地址
    const redirectUrl =
      process.env.NEXT_PUBLIC_AUTH_CALLBACK_URL;

    //拼接规则:基本url+client_id+redirect_url+scope
    //scope即需要的权限范围,这里选择最基础的user权限也就是用户信息
    const githubAuthUrl =
      `https://github.com/login/oauth/authorize?` +
      `client_id=${clientId}&` +
      `redirect_uri=${redirectUrl}&&` +
      `scope=user&`;
    //跳转到授权页
    window.location.href = githubAuthUrl;
  };

3.2后端实现

后端需要在对应的回调接口实现对于授权码的处理逻辑(请求token进而请求用户信息),最后将用户信息存入数据库。以下是代码实现,注释详细标注了每个步骤。

//核心函数1,两次请求得到用户信息
async handleGitHubCallback(code: string) {
    //通过code请求accesstoken,nest官方封装了axios为HttpService,使用请求很方便
    const { access_token: accessToken, error } = await this.getGitHubAccessToken(code);
    if (error || !accessToken) {
      throw new Error(`GitHub 令牌获取失败: ${error}`);
    }
    //通过accessToken换取用户信息
    const githubUser = await this.getGitHubUserInfo(accessToken);
    //通过数据库更新/新建用户信息
    const localUser = await this.userService.findOrCreate({
      name: githubUser?.name || '',
      githubUserId: githubUser.id.toString(),
      id: githubUser.id,
      avatar: githubUser?.avatar_url || '',
    });

    return { user: localUser };
  }
//核心函数2:与数据库的交互
async findOrCreate(dto: FindOrCreateUserDto): Promise<FindOrCreateUserInterface|null> {
    //prisma的upsert函数逻辑,先根据唯一性参数查找对应用户
    //如果找到了就根据传入信息更新,没找到则根据传入信息新建
    const user = await this.prisma.user.upsert({
      // 1. 查找条件:根据 githubUserId 匹配
      where: {
        githubUserId: dto.githubUserId, 
      },
      // 2. 若存在:更新字段
      update: {
        name: dto?.name || '', 
        avatar: dto?.avatar || '', 
      },
      // 3. 若不存在:创建新用户
      create: {
        githubUserId: dto.githubUserId,
        name: dto?.name || '',
        avatar: dto?.avatar || '',
      },
    });
    return user;
  }

至此授权工作就已经圆满完成了,后续前端对应请求后端的用户信息就可以进行使用和操作。

3.3优化和完善

当然,作为一个成熟的项目开发,最基本的功能远远不能满足业务的需要,所以提出一些优化方案或者说后续思考(和GitHubOAuth也就是本文的主题不太相关所以不展开叙述),并从中挑选出有代表性的进行讲解,提升项目的健壮性。

  • 登录成功后后端jwt的生成与发放(推荐采用httpOnly Cookie防止安全问题)
  • 前端的访问控制,不登陆无法访问的路由需要进行拦截
  • 登出/注销操作的实现

由此可见,GitHubOAuth作为登录只能省去注册和校验的部分步骤,后续的维护还是和传统登录体系一样比较复杂。不过总而言之,它对我们的团队开发来说,还是能节约相当大一部分时间的!

四、总结

一个好的learner不仅要在短时间内学会知识并加以应用,更要学会总结(具体的知识变为抽象的思想)方可每次有进步。这也是我每篇文章都在给大家传递的想法。以下是我这两周调研并开发项目登录体系的一些小总结:

  1. 原理和实践的关系:有的人认为在实践之前必须要将原理吃透,在做事时才能胸有成竹。在接触开发前我也是这个想法,但是我们作为开发者,一方面有时候没有充足的时间;另一方面技术的更迭导致我们从低到高的学习成本不断加重。

    所以个人最佳实践应该是:从应用入手,在应用的同时保持对原理的好奇和探究(通俗点说就是方便提问)。这样在完成目标的同时收获了知识。 比如我在为项目调研github登录时,就是先知道步骤开始实施,过程中提出一些问题并解决,比如辩证的发现其局限性,以及我们的项目为什么合适用这种方式。

  2. 关于知识本身,OAuth 的本质是一套开放授权的标准化协议(Protocol) ,更准确地说,是一套定义了 “第三方应用如何安全获取用户在某服务上的资源权限” 的规则体系。核心目标是解决 “跨平台授权” 的安全与标准化问题。为了让文章更易懂,我弱化了这部分知识的讲解,有兴趣的同学可以自行学习。

最后相信大家通过文章「概念→原理→实现→总结」的递进逻辑,从 “是什么(OAuth 定义 / 优劣 / 适用场景)” 到 “为什么(GitHub OAuth 原理)”,再到 “怎么做(前后端具体代码)”,最后升华到 “学习方法”,都会有自己的感受和收获。