jsonwebtoken + eggjs 实现token校验(包含指定过期时间)
框架说明
前端 vuejs
后端 eggjs
数据库 MySQL
jsonwebtoken是一个常见的校验身份的方法,若想仔细了解详见 click here
整体流程
- 前端页面中输入 用户名+ 密码,发送请求到 /user/login ;
- 后端收到请求去数据库中核验信息是否匹配,若匹配返回登录成功的响应并颁发一个 token 返回给前端;
- 前端收到后端登录成功的响应后获取数据中的token,存到本地cookie中(这里可以随意 localStorage什么的也可以);
- 之后发送所有请求之前都会在header中加上token信息给后端核验(在axios请求拦截器中设置);
- 后端收到其他请求(非 login )后核验token是否合法,返回响应结果。
配置
后端
这里使用egg相关的库 egg-jwt 来搭建
先按照readme里说的在后端配置文件里写好:
// {app_root}/config/plugin.js
exports.jwt = {
enable: true,
package: "egg-jwt"
};
// {app_root}/config/config.default.js
exports.jwt = {
secret: "try0929",
expiresIn: 60 * 60, // 过期时间
};
在上面这样写好之后,Application 对象(app)几乎可以在编写应用时的任何一个地方获取到
- controller、service里:this.app
- 也可以通过上下文取到,this.ctx.app
- 另外,几乎所有被框架 loader 加载的文件(Controller,Service,Schedule 等),都可以 export 一个函数,这个函数会被 Loader 调用,并使用 app 作为参数:
module.exports = app => {
class RenderController extends app.Controller {
}
return RenderController;
};
具体实现
前端
前端登录 发送请求(调用写在vuex里的可以发送异步请求的函数login)
成功后拿到后端传来的token,先存到vuex中,再用setToken存到本地cookie里。
// view/login.vue 页面
this.$store.dispatch('login', this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || '/'})
})
// store/index.js
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
// 这里才开始发送请求
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
存到vuex和cookie里之后,前端就能简单的拿到了,这样以后再在axios的请求拦截器中设置将本地的token放到header中随着请求发送出去:
// 请求拦截器(在请求发出之前拦截 通常用来添加一些参数)
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['Authorization'] = `Bearer ${getToken()}`
}
return config
},
error => {
console.log(error)
return Promise.reject(error)
}
)
请求拦截器通常是用来在发送请求之前对请求本体添加一些信息用的,与之对应的也有相应拦截器,就是得到响应之后需要对响应做出一些判断和格式化。
这里我们可以根据后端传来的响应体中的信息(通常是状态码 status code)判断出是否是token有问题,这里规定:
- 响应code为0,代表正常返回响应;
- 响应code为-2,代表token有问题,调用store(vuex)中的resetToken这个action将本地的(包括vuex和cookie)无效token清除。
// utils/request.js
// 响应拦截器(通常用来对返回的数据做一些处理)
service.interceptors.response.use(
response => {
const res = response.data
if (+res.code !== 0) {
ElMessage({
message: res.msg || 'Error',
type: 'error',
duration: 5 * 1000
})
if (res.code === -2) {
ElMessageBox.confirm('您的登录状态已经失效,请重新登录', '登录失效', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.msg))
} else {
return res
}
},
error => {
console.log(error)
ElMessage({
message: error.message || error.msg,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
// store/index.js
resetToken({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
},
后端
// 在router.js中用user这个controller的login方法来处理对/login的请求
router.post('/user/login', controller.user.login);
login函数里主要流程为:
- 校验用户信息,即去数据库中(eggjs中的对数据库的操作一般都在service里)查询用户名和密码是否匹配;
- 若成功,则颁发一个合法的token给前端(包含在响应体中返回出去)。
这里的jwt和里面配置的参数,就都可以通过 application 这个对象拿到:
async login(ctx) {
const { username, password } = ctx.request.body;
await ctx.service.user.login(username, password).then(user => {
if (!user || user.length === 0) {
new Result('登录失败').fail(ctx.response);
} else {
// 查询到有匹配的用户 则颁发一个token给前端
const { secret, expiresIn } = this.app.config.jwt;
const token = this.app.jwt.sign(
{ username },
secret,
{ expiresIn }
);
new Result({ token }, '登录成功').success(ctx.response);
}
});
}
后续在其他请求处理函数里都先拿到请求里header的token进行校验,返回结果就可以了,这里校验的 decode 函数为:
function decode(req, app) {
let token = req.header.authorization || '';
if (token.indexOf('Bearer') >= 0) {
token = token.replace('Bearer ', '');
}
return app.jwt.verify(token, app.config.jwt.secret);
}
调用 jwt.verify 函数去验证:
- 若token合法则会返回里面附带的数据,一般有用户名什么的;
- 若不合法,则会抛出一个错误,这个错误会被后端框架自己配置的错误处理捕捉到,可以自己进行处理。
异常处理
这里也添加一个error中间件,这个中间件将捕捉到所有被异常抛出且没有被处理的错误:
// middleware/error.js
module.exports = () => {
return async function handleError(ctx, next) {
try {
await next();
} catch (err) {
if (err.name) {
const { status = 401, message } = err;
if (err.name === 'TokenExpiredError') {
new Result(null, 'token验证失败', {
error: status,
errorMsg: message,
}).expired(ctx.response);
} else if (err.name === 'JsonWebTokenError') {
new Result(null, 'token不合法', {
error: status,
errorMsg: message,
}).fail(ctx.response);
}
} else {
const msg = (err && err.message) || '系统错误';
const statusCode = (err.output && err.output.statusCode) || 500;
const errorMsg = (err.output && err.output && err.output.payload && err.output.payload.error) || err.message;
ctx.response.statusCode = statusCode;
new Result(null, msg, {
error: statusCode,
errorMsg,
}).fail(ctx.response);
}
}
};
};
看上面 jwt 包里的定义,若jwt.verify 验证出来不合法,则会抛出error,error有三种类型,详见 click here
我们把要处理的错误找出来并返回给前端响应的错误信息即可。