egg.js使用oAuth2.0鉴权,egg-oauth2-server用法

egg-oauth2-server官方文档:https://github.com/Azard/egg-oauth2-server

egg-oauth2-server官方文档提供的实现例子:https://github.com/Azard/egg-oauth2-server/blob/master/test/fixtures/apps/oauth2-server-test/app/extend/oauth.js

egg-oauth2-server官方文档的实例讲解(不是最新,只做参考):https://cnodejs.org/topic/592b2aedba8670562a40f60b

egg-oauth2-server是干什么用的?简单的讲,就是当你在向服务器发送请求时,让服务端能知道你是谁,是不是合法请求,这样服务器才能放心的把数据返回给你。以往的后端开发人员可能会通过自定义token的方式和前台人员约定,在需要验证的请求头上加上token信息,其实egg-oauth2-server也是这个原理,只不过token信息不需要后端开发人员自定义了,egg-oauth2-server会自动生成。

官网介绍了egg-oauth2-server的两种模式:1.password mode 2.authorization_code mode,本文只介绍password模式(因为本人研究了3天,只把这个模式研究明白了,惭愧。。。。)

下面来看实现步骤:

1.安装egg-oauth2-server模块:

npm i egg-oauth2-server --save (用cnpm速度能快一些,不,我收回我说的话,用cnpm会非常快);

2.在app->extend目录下新建oauth.js文件

3.在config->config.default.js文件中,加入

config.oauth2Server = {
    grants: [ 'password' ],
  };

4.在config->plugin.js文件中加入

oAuth2Server: {
    enable: true,
    package: 'egg-oauth2-server',
  }

5.经过以上配置之后,接下来就可以来写具体的功能实现代码了,首先我们需要了解一下password模式鉴权的步骤:

在上面建立的oauth.js文件中,简易代码如下:

'use strict';

// need implement some follow functions
module.exports = app => {  
  class Model {
    constructor(ctx) {}
    async getClient(clientId, clientSecret) {}
    async getUser(username, password) {}
    async saveToken(token, client, user) {}
    async getAccessToken(bearerToken) {}
  }  
  return Model;
};

在此文件中,需要实现4个方法(仅password模式需要实现这4个方法),具体步骤是:

当前端用户需要获取token时,系统会自动的按步骤执行getClient --> getUser --> saveToken这几个方法,而当服务端需要验证token时,系统会自动执行getAccessToken这一个方法。

那么触发客户端获取token的步骤和服务端验证token的方式是什么呢?在代码中的router.js文件中,代码如下:

module.exports = app => {
  const { router, controller } = app;
  router.get('/api/currentUser', controller.home.currentUser);
  router.get('/api/getUser', app.oAuth2Server.authenticate(),controller.admin.getUserList);
  app.all('/api/user/token', app.oAuth2Server.token());

};

后端的路由接口,在不需要鉴权的情况下,写法如router.get('/api/currentUser', controller.home.currentUser),即前端发送请求api/currentUser时,后端通过controller中的代码执行返回结果。当加入鉴权时,那么前端需要先获取token信息,app.all('/api/user/token', app.oAuth2Server.token());这行代码就是前端通过api/user/token接口获取token的路由,接口可自定义,但app.oAuth2Server.token()是固定写法,每当调用api/user/token这个接口时,系统都会自动执行oauth.js中getClient --> getUser --> saveToken这三个方法。具体实现内容见下文。

router.get('/api/getUser', app.oAuth2Server.authenticate(),controller.admin.getUserList);这个路由的含义时,当前端调用api/getUser'这个接口时,服务端会将请求header中的token与之前返回给前端的token信息进行比较,如果一致则校验通过。 app.oAuth2Server.authenticate()是固定写法。

6.接下来看一下前端是如何发送获取token的请求的。

在我的项目中,我把获取token的请求放在了登录按钮的点击方法中

export async function fakeAccountLogin(params: LoginParamsType) {
  return request('/api/user/token', {
    method: 'POST',
    data: qs.stringify({username:params.userName,password:params.passWord, grant_type: "password"}),
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization:
      "Basic bXlfYXBwOm15X3NlY3JldA=="
    },
  });
}

当点击登录按钮时,我会获取到登录框中的用户名和密码,即username和password,然后将这两个字段作为请求的参数,除了这两个参数外,还需要发送一个grant_type参数,因为本例使用的是password鉴权模式,所以参数为password;

传参的时候,我使用了qs.stringify进行转换,qs我引自query-string模块,这个前端开发的同学应该比较了解,这里不再赘述。为什么要用这个转换呢?因为在header中的Content-Type,我们必须要传application/x-www-form-urlencoded这个值,而这种传参的方式,不用qs转换的话,后台无法识别。(至于为什么不能识别,暂时没搞清楚);

获取token的这个请求的header比较特殊,需要在这里单独设置,“Content-Type”的值必须是“application/x-www-form-urlencoded”,而Authorization中的值来自与后端开发人员的约定,固定写法为:“Basic 约定的值的64位转换值”。这个是什么意思呢?

bXlfYXBwOm15X3NlY3JldA==这个值其实是经过base64位转换后的值,不知道如何转换的同学可以直接百度base64在线转换。这个值是这样的一个组成形式:clientId:clientSecret,这两个值是我们在oauth.js文件中定义的,我定义的是my_app:my_secret,注意,在进行base64转换的时候,是整体转换的,即将my_app:my_secret整体转换为bXlfYXBwOm15X3NlY3JldA==

7.当前端发送完获取token的请求后,服务端将会按步骤执行那三个方法,具体实现代码如下:

'use strict';
module.exports = app => {
  class Model {
    constructor(ctx) {
      this.ctx = ctx;
    }
    async getClient(clientId, clientSecret) {
      if (
        clientId !== 'my_app' &&
        clientSecret !== 'my_secret'
      ) {
        return;
      }
      return { clientId, clientSecret, grants: [ 'password' ] };
    }
    async getUser(username1, password) {
      const user = await app.mysql.get('user', { username: username1 });
      if (!user) {
        return;
      }
      if (user.password === password) {
        return { id: user.uid };
      }
      return;
    }
    async saveToken(token, client, user) {
      console.log(token);
      console.log(user);
      const _token = Object.assign({}, token, { user }, { client });
      const updateToken = await app.mysql.update('user',
        {
          accessToken: token.accessToken,
          expires: token.accessTokenExpiresAt,
          clientId: client.clientId,
        }, {
          where: {
            uid: user.id,
          },
        });
      if (updateToken) {
        return _token;
      }
    }
    async getAccessToken(bearerToken) {
      console.log(bearerToken);
      const data = await app.mysql.get('user', { accessToken: bearerToken });
      if (data) {
        const user = { username: data.username, id: data.uid };
        let token = {};
        token.user = user;
        token.client = {
          clientId: 'my_app',
          clientSecret: 'my_secret',
          grants: [ 'password' ],
        };
        token.accessTokenExpiresAt = new Date(data.expires);
        token.refreshTokenExpiresAt = new Date(data.expires);
        return token;
      }
      return false;
    }
  }
  return Model;
};

在执行getClient方法时,有两个参数clientId, clientSecret,这两个参数是系统自动获取到的前端获取token请求中的header中的Authorization的值(Basic之后的值),并将base64位的值转换成我们能看懂值(哈哈,这么说有些外行,但比较好理解),即my_app和my_secret。将获取的值和之前设定好的值进行比对,如果一致,则返回return { clientId, clientSecret, grants: [ 'password' ] };然后系统自动进入第二个步骤,执行getUser。

getUser也有两个参数,username和password,这两个参数来自于用户调用获取token接口时传的username和password,即登录系统的用户名和密码。在getUser方法中,我通过查询数据库来比较前端传过来的密码是否和数据库中的一致,如果一致,则返回return { id: user.uid };(uid是我数据库中用户表的主键。)然后自动进入第三个步骤:saveToken。

saveToken有三个参数(token, client, user),其中第一个参数token的值里面就包含了系统要返回给前端的token值,也就是说,当进行到此步骤的时候,token的值就已经产生了,返回给前台的值的样式如下:


{"access_token":"059a38f92e64dcc39589032fa4a96afbd9248627","token_type":"Bearer","expires_in":3599,"refresh_token":"4780c55d39bfa6a5facef48485ad162fd604fabb"}

我们前端用到的就是access_token。

第二个参数client,就是在第一个步骤中的返回值,第三个参数user,是第二个步骤中的返回值。我在saveToken方法中写了一个保存token到数据库中的逻辑,用于和用户发送请求中的token进行比对。

8.当前端用户获取token后,可以将token保存在cookie中或者本地存储中,然后在向后端发送其它请求时,在header中加入token信息即可。我在使用umi-request封装的request方法中,使用拦截器来设置每个请求的token

request.interceptors.request.use((url, options) => {
  if(!url.includes('api/user/token')){
    return (
      {
        options: {
          ...options,headers: {
            Authorization: 'Bearer ' + Cookies.get('SESSION_TOKEN'),
          },
        },
      }
    );
  }
  
});

代码含义是,除了获取token的接口api/user/token外,其余的接口都要加入Authorization: 'Bearer ' + Cookies.get('SESSION_TOKEN')。(Bearer是固定写法,后面要有空格,Cookies.get('SESSION_TOKEN')是获取cookie中保存的token值)

9.最后一步,当后端服务接收到含有token的请求时,会自动调用getAccessToken方法,该方法有一个参数,即请求中的token值,是系统自动读取的。然后将这个token值与数据库中的token值进行对比即可。以上就是password模式鉴权的整个流程,如果各位同学在调试过程中遇到问题,欢迎留言,或关注公众号“web前端梦工厂”留言,我会及时为各位解答。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值