上一篇博客的验证方式是session。最近抽空改成jwt,顺便了解了一下jwt。jwt的介绍可以看阮一峰的文章。jwt实现流程上图是最简单的jwt流程。token过期或失效后,会跳转回登录页面。不过我的理想状态应该是一周之内不用登录,每次登录后过期时间延长一周。在不修改数据库的前提下,我想到了以下两种方法:设置token的过期时间为7天,每次访问接口,后台根据当前时间重新生成token,然后返回每个接口上新生成的令牌。前端收到后将token保存在cookie中,下次请求时带上新的token。登录后会生成一个过期时间为3小时的token和一个过期时间为7天的refreshToken。前端请求时,在消息头中携带token信息。如果token过期,前端携带refreshToken访问刷新token接口。如果refreshToken没有过期,后端返回新的token和refreshToken,否则前端跳转到登录页面。前端再根据新返回的token重新访问刚才的界面。方案一的缺点很明显,就是每次调用接口都需要生成token,增加了不必要的开销。而且每个后端接口都要携带token信息,后端变化很大=。=方案2好像没有明显缺点,选择方案2进行练习。上图是refreshtoken的认证流程。可以看出,刷新token和refreshtoken比最简单的过程多了一步,这也是refreshtoken的关键。实现项目中约定的代码后端节点,401表示token超时,402表示登录时refreshToken无效,初始化token和refreshToken//jwt.jsconstjwt=require('jsonwebtoken')const{SECRET_KEY,JWT_EXPIRES,REFRESH_JWT_EXPIRES}=require('../config/config')//从配置文件导入SECRET_KEY,token过期时间,refreshToken过期时间const{cacheUser}=require('../cache/user')//使用闭包缓存用户name/***生成token和initToken*@param{string}username*/functioninitToken(username){return{token:jwt.sign({username:username},SECRET_KEY,{expiresIn:JWT_EXPIRES}),refresh_token:jwt.sign({username},SECRET_KEY,{expiresIn:REFRESH_JWT_EXPIRES})}}/***validationtoken/refreshToken*@param{string}token格式为`Beare${token}`*/functionvalidateToken(token,type){try{token=token.replace(/^Beare/,'')const{username}=jwt.verify(token,SECRET_KEY)if(type!=='refreshToken'){//如果它是一个令牌并且token有效,缓存tokencacheUser.setUserName(username)}}catch(e){thrownewError(e.message)}}module.exports={initToken,getTokenUser,validateToken}然后调用中间件中的validateToken方法//检查是否登录中间件方法checkLogin(req,res,next){const{headers:{authorization}}=reqif(!authorization){res.status(402).json({code:'ERROR',data:'Nologininformationdetected'})returnfalse}try{validateToken(authorization)}catch(e){if(e.message==='jwtexpired'){//令牌超时res.status(401).json({code:'ERROR',data:'logintimeout'})returnfalse}else{res.status(402).json({code:'ERROR',data:'Nologininformationdetected'})returnfalse}}在一些需要登录验证的接口(比如接口对于获取用户消息),会先调用中间件验证是否登录中间件会先验证一次token。这里我写了一个带闭包的用户缓存。中间件验证成功后,将用户存储在闭包中,然后在接口中获取用户信息时,可以调用闭包的getUserName方法,而不用解析token,这样可以避免中间件带来的麻烦文档验证令牌成功,但接口中的验证令牌已过期。/***写一个闭包来缓存用户名*/constcacheUser=(()=>{letusername=''return{setUserName:(user)=>{if(username===user){returnfalse}username=user},getUserName:()=>{returnusername},clearUserName:()=>{username=''}}})()module.exports={cacheUser}登录成功后获取token和refreshToken返回前端.客户端jsjs需要重新封装axios函数importaxiosfrom'axios'importqsfrom'qs'……//根据refreshToken刷新token和refreshTokenconstfetchRefreshToken=()=>{consttoken=Cookies.get('refreshToken')returnaxios({url:'/api/signin/refreshToken',headers:{Authorization:`Beare${token}`},method:'get'})}exportconstpost=(url,formData,headers={})=>{consttoken=Cookies.get('token')headers=Object.assign({},headers,{Authorization:`Beare${token}`})returnaxios({url,headers,方法:'post',数据:qs.stringify(formData)}).then(res=>{returnres}).catch(e=>{if(e.response.status===401){//token超时,访问刷新token接口返回fetchRefreshToken().then(res=>{const{data}=res.dataconst{token,refresh_token:refreshToken}=dataCookies.set('token',token)Cookies.set('refreshToken',刷新令牌n)returnpost(url,formData,headers)//再次调用该接口})}})}...修改axios拦截函数//如果axios拦截器没有登录,则跳转到登录页面axios.interceptors.response.use(res=>{if(res.data.code==='OK'){returnres}else{//提示错误信息message.error(res.data.data,10)returnPromise.reject(res)}},error=>{if(error.response){switch(error.response.status){case402://登录超时,跳转到登录页面,自己实现//store.dispatch(actionCreators.logoutSuccess())break//跳转到登录页面default:break}}returnPromise.reject(error)})优化点至此已经实现了token自动刷新的功能,但是这个方法还是有缺陷的。比如同时调用多个接口时,如果token过期,refreshtoken接口会被调用多次,因为请求是异步的。这个最优方案应该是最先刷新token的接口,其余先等待,待token刷新后,其余接口开始调用。sf上有一篇文章更详细地解释了这种方法。
