前后端分离OAuth授权登录实现方式

2,041 阅读4分钟

大家好,今天我们来讲讲OAuth的实现方式。OAuth有授权码模式和密码模式,通常情况下我们都是使用授权码模式。应用首先会点击一个授权链接,然后携带应用ID和重定向地址,第三方认证服务器在接收到对应请求后,如果验证成功,会继续跳转到上一步传过来的重定向地址,并携带一个code,应用可以根据code向第三方认证服务器获取访问token,到了这一步授权就完成了。后面可以根据token向第三方授权服务器获取对应的授权信息。

我们以码云作为OAuth的演示,码云OAuth的验证流程如下图:

为了演示,我们需要先在码云上申请一个应用。

1、在 修改资料 -> 第三方应用,创建要接入码云的应用。

2、填写应用相关信息,勾选应用所需要的权限。其中: 回调地址是用户授权后,码云回调到应用,并且回传授权码的地址。

3、创建成功后,会生成 Cliend IDClient Secret。他们将会在上述OAuth2 认证基本流程用到。

应用创建成功后,就可以进入编码了。我们以egg.js作为我们后端的实现。

1、安装

$ mkdir oauth && cd oauth
$ npm init egg --type=ts
$ npm i
$ npm run dev

2、全局配置gitee参数

修改oauth/config/config.default.ts,在其中添加一个gitee的配置,并扩展到bizConfig。

const giteeOauthConfig = {
  client_id: '510c2a54e0c942bc13b5ca8e88d4048f46af9a5535f',
  client_secret: '2c64bcefcd37f10ff99f5cc84bc9e6739',
  redirect_uri: 'http://localhost:7001/api/users/passport/gitee/callback',
  authURL: 'https://gitee.com/oauth/token?grant_type=authorization_code',
  giteeUserAPI: 'https://gitee.com/api/v5/user',
};

const bizConfig = {
  giteeOauthConfig,
};

3、编写controller方法

在oauth/app/controller目录下添加一个user.ts,我们在其中实现两个方法oauth,oauthByGitee,分别处理授权链接的跳转与code换取token的操作。

import { Controller } from 'egg';

export default class UserController extends Controller {
  public async oauth() {
    
  }
  public async oauthByGitee() {
   
  }
}

同时我们在oauth/app/router.ts中添加对应对控制器路由

import { Application } from 'egg';

export default (app: Application) => {
  const { controller, router } = app;

  router.get('/', controller.home.index);

  // 用户模块
  router.get('/api/users/passport/gitee', controller.user.oauth);
  router.get('/api/users/passport/gitee/callback', controller.user.oauthByGitee);
};

4、oauth方法实现

oauth方法中主要处理应用授权链接的跳转,跳转时我们需要传递应用ID与回调地址。

public async oauth() {
  const { ctx, app } = this;
  const { client_id, redirect_uri } = app.config.giteeOauthConfig;
  const url = `https://gitee.com/oauth/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=code`;
  ctx.redirect(url);
}

我们从app.config.giteeOauthConfig获取对应的配置,然后拼接成授权链接,并使用egg提供的ctx.redirect方法进行重定向跳转。当前端访问授权接口时,服务端重定向到第三方授权服务器页面

用户点击同意授权,第三方授权服务器会根据传过去的redirect_uri参数进行重定向跳转,并携带一个code。我们在前面配置了redirect_uri,根据路由配置重定向后就进入到了oauthByGitee方法中。我们添加oauthByGitee的实现:

public async oauthByGitee() {
  const { ctx } = this;
  const { code } = ctx.query;
  try {
    const result = await ctx.service.user.loginByGitee(code);
    ctx.body = result;
  } catch (error) {
    console.log(error);
  }
}

oauthByGitee中通过code,获取了访问token,服务端根据获取到的token向码云获取了对应的用户信息,对应的service代码如下:

import { Service } from 'egg';

/**
 * Test Service
 */
export default class User extends Service {


  /**
     * 通过用户授权码从码云获取对应用户信息
     * @param code 用户授权码
     */
  public async getAccessToken(code:string) {
    const { ctx, app } = this;
    const { client_id, redirect_uri, client_secret, authURL } = app.config.giteeOauthConfig;
    const access_token = await ctx.curl(authURL, {
      method: 'POST',
      contentType: 'json',
      dataType: 'json',
      data: {
        code,
        client_id,
        redirect_uri,
        client_secret,
      },
    });
    return access_token;
  }

  public async getGiteeUserData(access_token: string) {
    const { ctx, app } = this;
    const { giteeUserAPI } = app.config.giteeOauthConfig;
    const { data } = await ctx.curl(`${giteeUserAPI}?access_token=${access_token}`, {
      dataType: 'json',
    });
    return data;
  }

  public async loginByGitee(code: string) {
    // 获取access_token
    const access_token = await this.getAccessToken(code);
    // 获取用户信息
    const user = await this.getGiteeUserData(access_token.data.access_token);
    return user;
  }
}

在loginByGitee,我们获取了access_token,并根据access_token获取了用户信息。此时oauth授权登录我们就完成了一半,也就是只完成了授权,登录还没实现。我们从第三方拿到了用户信息,此时我们就可以根据这些信息来完成登录的操作。

5、前后端分离授权方式

目前市面上的应用基本是前后端分离的,通常我们前后端是分开部署的,此时就会有一个跨源通信的问题。前面我们已经根据第三方提供的信息生成了自己的用户token,那么在前后端分离的情况下我们怎么将token传给前端呢?其实浏览器已经给我们提供了方法,我们可以通过postMessage来实现跨源通信。

前端通过window.open打开授权链接,用户同意授权后,跳转到回调页面。后端拿到token后自己渲染一个页面提示授权成功,渲染的页面逻辑:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>授权成功</title>
  </head>
  <body>
   <h1>授权 成功</h1>
   <h2>两秒后关闭</h2>
  </body>
  <script>
    window.onload=function(){
        setTimeout(()=>{
            window.opener.postMessage('{{token}}','http://127.0.0.1:5173/')
            window.close()
        },2000)
    }
  </script>
</html>

这时候前端通过addEventListener就可以监听到对应事件,并接收对应参数了,这样就授权成功了。