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前端梦工厂”留言,我会及时为各位解答。