在本文中采用koa2框架编写接口的方式实现一遍jwt token登录认证的流程,主要涉及的知识点有如何生成登录校验码,koa2如何配置session以及前后端对token的处理.
session配置
在koa2项目中安装一个依赖包 koa-session 能够方便的操作session.配置如下:
const session = require('koa-session');
const key = 'koa:sess';
exports.sessionKey = key;
exports.configSession = (app) => {
// 使用session
app.keys = ['secret'];
const CONFIG = {
key, // cookie key (默认koa:sess)
maxAge: 60000, // cookie的过期时间,毫秒,默认为1分钟
overwrite: true, // 是否覆盖 (默认default true)
httpOnly: false, // cookie是否只有服务器端可以访问,默认为true
signed: true, // 签名默认true
rolling: false, // 在每次请求时强行设置cookie,这将重置cookie过期时间(默认:false)
renew: false, // (boolean) 会话即将到期时,续订会话
};
app.use(session(CONFIG, app));
};
再将此配置文件引入到项目入口文件中运行.接下来使用session就变的异常简单.比如给session加入一条用户数据,只需调用 ctx.session.user_info = {name:"张三"}即可.获取session中该用户的值直接使用ctx.session.user_info.
生成登录校验码
为了生成类似上方中的图片检验码,首先需要安装一个第三方依赖包 svg-captcha.编写一个工具方法如下:
const vCode = require('svg-captcha');
exports.genCaptcha = () => {
const captcha = vCode.create({ fontSize: 50, width: 100, height: 40 }); //{text:"",data:""}
return captcha;
};
调用genCaptcha方法会返回形似{text:"",data:""}结构的数据.data是生成图片的二进制数据,而text是对应的值.
现在结合前面的介绍的session相关知识编写一个生成登录校验码的接口,代码如下:
/**
* 获取登录校验码
*/
router.get('/getCaptcha', async (ctx) => {
const { text, data } = genCaptcha();
ctx.session.captcha = text.toLowerCase(); //转化成小写字母
ctx.set('Content-Type', 'image/svg+xml');
ctx.body = String(data);
});
打开浏览器访问接口地址 http://localhost:3000/getCaptcha,就能出现下方图片了.
后端登录
用户向后端发送三个字段,分别是用户名,密码和校验码.首先从session中获取校验码判断用户发送过来的值是否正确,只有当校验码相等时才进行下一步.
/**
* 登录
*/
router.post('/login', async (ctx) => {
const { user_name, password, captcha } = ctx.request.body;
if (ctx.session.captcha != captcha.toLowerCase()) {
ctx.body = {
error_no: 50,
message: '验证码不正确',
};
return false;
}
ctx.body = await login({ user_name, password }, ctx);
});
userIsExist函数是持久层的一个方法,通过给它传递user_name和password参数让其去数据库中查询,判断该用户传递过来的用户名和密码是否正确.
如果发现用户名和密码都输入正确,我们就判定该用户为登录成功的状态.用户信息userInfo此时需要存储在session当中,为了方便用户下一次调用其他接口时可以快速获取用户数据.但在实际开发中,用户信息的数据建议存放在redis里,如果用户调用其他接口时,就从redis里获取身份信息.
用户信息的id需要单独取出来做一层加密生成一个随机字符串token,生成token的方式通过调用generateToken函数实现.将得到的token赋予用户信息上返回给前端.那么前端就能获取并保存token信息了.
exports.login = async ({ user_name, password }, ctx) => {
//验证用户名密码是否正确
const userInfo = await userIsExist({ user_name, password: md5(password) });
if (userInfo == null) {
return {
error_no: 51,
message: '用户名或密码错误',
};
}
ctx.session[(`user_id_${userInfo.user_id}`, userInfo)];
const token = generateToken(userInfo.user_id, 1);
userInfo.token = token;
return {
error_no: 0,
message: userInfo,
};
};
生成token
generateToken函数的代码如下.通过调用jwt.sign函数,传入需要加密的数据data就可以生成字符串token并返回.其中scope是自定义参数,可以传递任意数值服务于业务上的需要.
const jwt = require('jsonwebtoken');
const { security } = require('../config');
exports.generateToken = (data, scope) => {
const { secretKey, expiresIn } = security;
const token = jwt.sign(
{
...data,
scope,
},
secretKey,
{
expiresIn,
}
);
return token;
};
security的值如下,secretKey是自定义的加密或者解密的盐值.expiresIn是过期时间.
module.exports = {
security: {secretKey:"xxxxxxx",expiresIn: 60 * 60 * 24 *30}
}
token验证
现在前端已经登录成功了,它想访问其他接口比如修改用户信息,后端就要先对请求进行拦截判断是否真的登录了.如何判断该请求是否登录了呢?
在前面介绍登录的流程时,后端最终将token返回给了前端.前端会携带token再次发起请求,后端获取token之后进行解密.如果发现解密成功得到了user_id,那就可以判定该请求是合法的.通过user_id就可以在session中寻找到用户信息.
编写一个校验用户是否登录的中间件(代码如下)
- basicAuth认证:basicAuth函数执行完后获取的userToken不存在说明前端没有采用basicAuth相应的规范包装token.
- jwt认证:经过第一轮basicAuth认证后,将userToken.name和盐值传入jwt校验函数中进行认证.如果最终能解密出decode的值就判断该请求合法并放行.
const basicAuth = require('basic-auth');
const jwt = require('jsonwebtoken');
const { security } = require('../config');
class Auth {
constructor() {}
get m() {
return async (ctx, next) => {
const userToken = basicAuth(ctx.req);
//这个userToken的name属性是前端自定义的,userToken.name就是token值
if (!userToken || !userToken.name) {
ctx.body = {
error_no: 54,
message: 'token无效',
};
return false;
}
const { secretKey } = security; //加密或者解密的盐值
try {
var decode = jwt.verify(userToken.name, secretKey);
} catch (error) {
if (error.name == 'TokenExpiredError') {
ctx.body = {
error_no: 54,
message: 'token已过期',
};
}
return false;
}
//decode只是user_id
ctx.auth = ctx.session[`user_id_${decode}`]; //获取存储在session中的用户数据
await next();
};
}
}
module.exports = Auth;
在更新用户信息的接口中可以引入上方定义的中间件.如果用户通过了token认证,那么在ctx.auth中就能获取到用户数据.
/**
* 更新用户信息
*/
router.post('/update_user', new Auth().m, async (ctx) => {
/**
* 测试一下用户是否登录,如果登录了就能得到用户数据
*/
const { user_name, user_id } = ctx.auth;
ctx.body = {
error_no: 0,
message: {
user_id,
user_name,
},
};
});
前端包装处理token
前端调用登录接口后会接收到后端返回的token字符串(如下图).token字符串一般需要存储在localstorage和数据框架(vuex或者redux)当中.另外前端还需要对token进行包装处理装载到http的请求头里,这样才能畅通不足的访问后端其他需要登录认证的接口.
下面以axios为例,介绍如何封装处理token.
后端返回的token值加上一个冒号会组成一个新的字符串,对这个新字符串做base64编码赋予变量baseCode.`Basic ${baseCode}`组成的字符串就是最终需要发送给后端的值.token值还需要使用属性名Authorization装载到http请求头上.后端分别通过basicAuth认证和jwt认证才能判断该请求是否合法.
import axios from 'axios';
import { service_ip } from './tool';
import { Base64 } from 'js-base64';
const _axios = axios.create({
baseURL: service_ip, //请求的公共地址
timeout: 5000, // 请求超时时间
});
/**
* 将token装载在http请求头上
/
_axios.interceptors.request.use( (params) => {
// 在发送请求之前做些什么
params.data = { data: params.data };
params.headers['Authorization'] = getEncode();
return params;
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error);
}
);
/**
* 获取处理后的token值
/
function getEncode() {
const token = getToken();//后端返回的token值
const baseCode = Base64.encode(token + ':');
return `Basic ${baseCode}`;
}