wumi_blog icon indicating copy to clipboard operation
wumi_blog copied to clipboard

项目中碰到的JSON Web Token & 代理请求 等

Open 5Mi opened this issue 7 years ago • 1 comments

项目中碰到了个token失效的问题,明明有不停发心跳的.之前对token这个也是一知半解(虽然现在仍是),自己一点点看了项目中的代码才稍微清晰了一些.这次简单记录下

JSON Web Token

What's JSON Web Token? JSON Web Token (JWT, pronounced jot) is a relatively new token format used in space-constrained environments such as HTTP Authorization headers. JWT is architected as a method for transferring security claims based between parties.

server side 以koa为例

var koa = require('koa');
var jwt = require('koa-jwt');
 
var app = new Koa();
 
// Middleware below this line is only reached if JWT token is valid 
// use jwt 下面的中间件 只能在token验证后才能到达
// unless the URL starts with '/public' 
app.use(jwt({ secret: 'shared-secret' }).unless({ path: [/^\/public/] }));
 
// Unprotected middleware 
app.use(function(ctx, next){
  if (ctx.url.match(/^\/public/)) {
    ctx.body = 'unprotected\n';
  } else {
    return next();
  }
});
 
// Protected middleware 
app.use(function(ctx){
  if (ctx.url.match(/^\/api/)) {
    ctx.body = 'protected\n';
  }
});
 
app.listen(3000);

项目伪代码

...

app.use(koaJwt({
    secret: config.secret,
    key: 'user',
  }).unless({
    method: 'GET',
    path: [
      /^\/login/,
    ],
  }));
//请求先经过token验证  
app.use(router.middleware());

...

在login完成登录返回用户信息时可将生成的token存入Storage中.

Now we have the JWT saved on sessionStorage. If the token is set, we are going to set the Authorization header for every outgoing request done using $http. As value part of that header we are going to use Bearer <token>.

客户端

// 发送请求 请求头带有Authorization
$.ajax({
    type: "POST",
    headers: {
        'Content-Type': 'application/json;charset=utf-8',
        Authorization: `Bearer ${store.get('token')}`,
    },
    contentType: "application/json;charset=utf-8",
    url: "/someUrl/",
    data: JSON.stringify({
      datas
    }),
    success: function(){},
    error: function (){},
});

token rolling

实际中会需要用户持续保持登录要与服务器长连接.会通过客户端每隔一段时间向服务器发送一次'心跳请求', 服务端接受到请求后会查看token过期时间.快接近token过期时间的时候,再设定新的过期时间重新生成token,将新token返回给客户端.客户端收到(监听到)新token后,用新token替换请求头Authorization中的token,达到token续期的目的;

  • 服务端伪代码
...

app.use(koaJwt({
    secret: config.secret,
    key: 'user',
  }).unless({
    method: 'GET',
    path: [
      /^\/login/,
    ],
  }));
// 增加一个tokenrolling的中间件
app.use(jwt({
    key: 'user',
    rolling: config.tokenRolling,
}));
//请求先经过token验证  
app.use(router.middleware());

...
//jwt.js
const moment = require('moment');
const jwtTool = require('../common/jwtTool');
const log4js = require('log4js');

const log = log4js.getLogger('jwt');

function jwt({ key, rolling, }) {
  const midware = async (ctx, next) => {
    try {
      const userInfo = ctx.state[key];
      if (userInfo) {
        //从token生成时间到rolling时间的范围内无需重生成token (请求时超过rolling时间 时才重生成token)
        if (moment.unix(userInfo.iat).add(...rolling).valueOf() < moment().valueOf()) {
          const newToken = jwtTool.sign(userInfo, global.config.secret, { exp: userInfo.tokenInfo.expParam, });
          //响应头设置token-rolling;
          ctx.set('token-rolling', newToken);
          ctx.cookies.set('token', newToken, { httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 365 * 20, expires: new Date('2050-01-01'), });
        }
      }
    } catch (ex) {
      log.error(`jwt midware 发生错误,错误信息:${ex}`);
    }
    await next();
  }
  return midware;
}

module.exports = jwt;
//上文中的jwtTool.js 
const jwt = require('jsonwebtoken');
const moment = require('moment');
const log4js = require('log4js');
const log = log4js.getLogger('jwtTool');
function sign(data, secret, { exp = 1, } = {}) {
  const iat = moment().unix();
  //token过期时间
  let expTime = moment.unix(iat).add(global.config.tokenExp, 'minutes').unix();
  try {
    const intExp = parseInt(exp, 10);
    if (intExp === 30) expTime = moment.unix(iat).add(global.config.tokenLongExp, 'minutes').unix();
  } catch (ex) {
    log.error(`解析登录信息exp出错 exp:${exp}`);
  }
  const useInfo = Object.assign({}, data, {
    //生成时间
    iat,
    //过期时间
    exp: expTime,
    tokenInfo: {
      expParam: exp,
    },
  });
  //通过jsonwebtoken的sign生成token,用于传递给客户端 在请求中会被服务端koaJwt中间件检查;
  const token = jwt.sign(useInfo, global.config.secret);
  return token;
}

module.exports = { sign, };
  • 客户端 每次发送请求 检查响应头中是否有 之前服务端设置的 token-rolling(const = response.headers.get('token-rolling');) 有就将token-rolling的值存入Storage中,下次请求就用新的token啦
export async function request(url, options) {
  const response = await fetch(url, options);
  checkStatus(response);
  checkToken(response);
  const contentType = response.headers.get('Content-type');

  if (contentType.startsWith('application/json')) {
    const data = await response.json();
    return data;
  }
  if (contentType.startsWith('text/html')) {
    const data = await response.text();
    return data;
  }
  throw new Error('unknown content type');
}
function checkToken(response) {
  const token = response.headers.get('token-rolling');
  //onTokenRolling中将token存入Storge中
  if (token && token.length > 0) onTokenRolling(token);
}
function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }
  if (response.status === 401) {
    //鉴权 token过期
    if (unauthorizedListener) unauthorizedListener();
  }
  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}

参考:

5Mi avatar Sep 20 '17 12:09 5Mi

记录项目中服务代理

前端用node服务中引入proxyMiddleware

const proxyMiddleware = require('http-proxy-middleware');

请求代理设置

const proxyOptions = {
  // target host 会将请求代理到此
  target: 'http://target.something.some:8080',
  // needed for virtual hosted sites
  changeOrigin: true,               
  // proxy websockets
  ws: true,
  pathRewrite:function (path, req) {

  }
}
...
// 在需要代理的路径中使用中间件
app.use('/needProxyPath', proxyMiddleware(proxyOptions));

这时再在项目中访问/needProxyPath会将请求代理到http://target.something.some:8080 项目中是将所有请求/api转向代理

如果项目中后台也跑在本地可以结合hosts修改与nginx设置代理 如 hosts文件中增加

# 访问target.something.some时指向本地 (本地运行nginx)
127.0.0.1         target.something.some

在nginx.conf中增加

#target.something.some:8080 指向http://127.0.0.1:8087/someweb/
server {
        listen       8080;
        server_name  target.something.some;
    
        location / {
            # 代理到本地跑着的后台服务
            proxy_pass   http://127.0.0.1:8087/someweb/;
        }
    }

其实只要在proxyMiddleware 的配置中 直接代理到 本地后台服务就行,但这种就不要再更改前端代码啦

这样下来开发时前端请求/needProxyPath都会代理到本地后台服务http://127.0.0.1:8087/someweb/

上面大概流程就是 前端开发请求/needProxyPath时 会被前端node服务的proxyMiddleware 代理到http://target.something.some:8080 然后由于修改了hosts文件http://target.something.some:8080会被指向本地127.0.0.1 ,本地又跑着nginx,server_name target.something.some,之后会被nginx指向proxy_pass http://127.0.0.1:8087/someweb/;

5Mi avatar Jan 04 '18 07:01 5Mi