JWT 的定义和组成部分就不多说了,网上有很多,我的理解就是一个有规范内容的 token。这里记录一下我项目中使用到的 JWT 的经历。
nodejs 部分:
在 nodejs 中我使用的是 jsonwebtoken 这个第三方依赖,它对 JWT 的解密,加密,校验都已经封装好了。
- 首先是登录验证时的签发 token 部分:
// ...
// 生成 token
let token = jwt.sign({
userId: data.rows[0].userId,
roleId: data.rows[0].roleId
}, tokenConfig.secret, {
expiresIn: 60 * 60 * 24 * 3// 授权时效 3 天
})
showRes.showData(res, {
token: token,
userInfo: {
userId: data.rows[0].userId,
userName: data.rows[0].userName,
roleId: data.rows[0].roleId
}
}, '请接收 token')
// ...
(tokenConfig.secret 是一个配置文件存储了密钥)jwt 可以在 token 中放入用户的 id 和用户的权限,jwt.sigh 签发加密,可以单向加密,也可以非对称加密。我这里是默认的,token 准备好后发送给前端。
- nodejs 我使用的是 express 框架,需要挂载一个中间件用来校验 token(因为之后的前端发送 ajax 请求后来都会在 http 的头部带上 token 以供后端校验):
let express = require('express')
let app = express.Router()
let jwt = require('jsonwebtoken');
var tokenConfig = require('../common/token-config')
// 校验 token 中间件
app.use(function (req, res, next) {
// 登录接口不检测
if (req.originalUrl != '/api/v1/validate') {
// 拿取 token 数据
let token = req.body.token || req.query.token || req.headers['authorization'];
if (token) {
// 解码 token (验证 secret 和检查有效期(exp))
jwt.verify(token, tokenConfig.secret, function (err, decoded) {
if (err) {
return res.status(401).send({ code: 0, msg: '无效的token' });
} else {
// 继续下一步路由
return next();
}
});
} else {
// 没有拿到token 返回错误
return res.status(401).send({
code: 0,
msg: '没有找到token'
});
}
} else {
return next();
}
});
验证通过的话就放行,不通过的话就返回 http 状态给前端,我这里用的是 401。
前端部分:
前端使用的是 vue-cli 搭出来的目录结构,使用了 axios。
- 前端首先要把后端返回过来的 token 存放好,我选择存放在 localStorage 中,login.vue:
// ...
localStorage.setItem("NR_JWT_TOKEN", res.data.data.token)
// 在 vuex 中存入用户信息
this.$store.commit({
type: 'setUserInfo',
data: res.data.data.userInfo
})
this.$message.success('验证成功~');
// ...
这里我有一步将用户基本信息存入到 vuex 的步骤,目的是让各处的组件都能方便去获取到,用户的基本信息是后端登录验证签发 token 的时候一起发过来的。
- 在 vuex 中:
import { getUserInfoByToken } from '@/api/api'
const state = {
userInfo: sessionStorage.getItem('NR-USER-INFO') && sessionStorage.getItem('NR-USER-INFO') != 'undefined' ? JSON.parse(sessionStorage.getItem('NR-USER-INFO')) : null
}
const mutations = {
setUserInfo(state, playload) {
state.userInfo = playload.data
sessionStorage.setItem('NR-USER-INFO', JSON.stringify(playload.data))
}
}
const getters = {
}
const actions = {
getUserInfoByToken: context => {
getUserInfoByToken({}).then((res) => {
if (res.data.code === 1) {
context.commit({
type: 'setUserInfo',
data: res.data.data
})
return new Promise((resolve, reject) => {
resolve()
})
} else {
// 返回一个 Promise 告知获取用户信息失败需要回到登录页
return new Promise((resolve, reject) => {
reject()
})
}
})
}
}
export default {
state,
getters,
actions,
mutations
}
这里除了将信息保存在 vuex 里面,还存了一份在 sessionStorage 里面,因为,vuex 虽然用起来方便,但是页面一刷新就没有了,sessionStorage 是页面刷新都存在,但是页面一关就没有了(关页面不怕,因为 token 已经存在 localStorage 里面了,到时候再拿了去查信息就行,我这里的 actions 里面的 getUserInfoByToken 方法就是做这个用的)。在 state.userInfo 属性里和 sessionStorage 绑上,页面一刷新,vuex 中信息丢失,立即会去 sessionStorage 里拿。
- 专门新建了一个 api.js 用来管理 axios 的请求以及 axios 拦截器的设置:
import axios from 'axios'
const Qs = require('qs')
// 为了错误码 401 时路由跳转到登录页面
import router from '../router'
import { MessageBox } from 'element-ui';
console.log(process.env.NODE_ENV)
const base = '/api/v1'
// axios request 拦截器
axios.interceptors.request.use(
config => {
if (localStorage.getItem('NR_JWT_TOKEN')) {
config.headers.Authorization = localStorage.getItem('NR_JWT_TOKEN')
}
return config
},
error => {
return Promise.reject(error);
}
)
// axios response 拦截器
axios.interceptors.response.use(
response => {
return response;
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
MessageBox.alert('您的验证信息已失效,请重新登录~', '提示', {
confirmButtonText: '确定',
callback: action => {
// 清除 token 信息
localStorage.removeItem("NR_JWT_TOKEN");
sessionStorage.removeItem("NR-USER-INFO");
router.replace({
path: 'login',
query: { redirect: router.currentRoute.fullPath }
});
}
});
break;
}
}
}
)
// get,post,put,delete按照这个套路写
// export const GetSpecialty = params => { return axios.get(`${base}/GetSpecialty`, { params: params }) }
// export const Register = params => { return axios.post(`${base}/Register`, Qs.stringify(params)) }
// export const AddMenu = params => { return axios.put(`${base}/menu`, Qs.stringify(params)) }
// export const DeleteMenu = params => { return axios.delete(`${base}/menu`, { params: params }) }
// 测试 api
export const TestApi = params => { return axios.get(`${base}/test`, { params: params }) }
// 用户表
export const userValidate = params => { return axios.post(`${base}/validate`, Qs.stringify(params)) }
export const getUserInfoByToken = params => { return axios.post(`${base}/getUserInfoByToken`, Qs.stringify(params)) }
axios 这里写了两个拦截器,一个是 http 请求的时候,一个是响应的时候,请求的时候在 header 里面附带着。接收响应的时候如果是 401 说明失效了,清空 localStorage,sessionStorage 并转至登录页面。
- 下面是 vue-router 路由里面的配置:
// 登录,权限验证
router.beforeEach((to, from, next) => {
if (localStorage.getItem('NR_JWT_TOKEN')) {
/* vuex 中用户信息不存在,说明页面关过,目前无需登录,但需要携带 token 查询一下用户信息
(利用 token 中的 userId,顺便验证 token 是否过期)*/
if (!store.state.user.userInfo) {
// 调用 vuex 中的 getUserInfoByToken action
store.dispatch('getUserInfoByToken').catch(() => {
ElementUI.MessageBox.alert('您的验证信息已失效,请重新登录~', '提示', {
confirmButtonText: '确定',
callback: action => {
// 清除 token 信息
localStorage.removeItem("NR_JWT_TOKEN");
sessionStorage.removeItem("NR-USER-INFO");
router.replace({
path: 'login',
query: { redirect: router.currentRoute.fullPath }
});
}
});
return
})
}
if (to.path === '/login') {
// token 存在前往 login 页不用再登录转到首页
next('/')
} else {
next()
}
} else {
sessionStorage.removeItem("NR-USER-INFO");
if (to.path === '/login') {
// token 不存在前往 login 页放行
next()
} else {
next({ path: '/login' })
}
}
})
这里的逻辑是首先判断 LS 中 token 在不在,不在直接再次登录。如果存在,再看 VUEX 中信息有没有,如果没有的话,说明页面关过,目前无需登录,但需要携带 token 查询一下用户信息 (利用 token 中的 userId,顺便验证 token 是否过期,用的是 vuex 中 actions 里面的 getUserInfoByToken 方法)。
大概的流程和需要用的地方就是这些,请大家多多指教。