阅读 607

登录校验-session、token

更多文档

前言

小一个月没有写博客了,因为最近一直在忙项目和低代码平台的研究,低代码的服务端是用node去实现的,碰到的第一个问题就是登录校验的问题,这里做一下分享

session

先简单了解一下sessionhttp请求是无状态的,通常不做任何配置的情况下,客户端和服务端处于互相不认识的情况下去交流,就好比网聊只知道对方的网名客户端服务端,但是坏人很多,网聊是很危险的,我会暴露一些基本信息给你,但是想了解更多的前提是你得让我知道你是谁,session的作用就是客户端获取服务端信任的方式

接下来,以express为例简单介绍一下session验证流程

设置session

// 借助express-session中间件设置session
const session=require("express-session");

app.use(session({
    secret: "secret", // 秘钥 
    cookie: { maxAge: 8 * 1000 }, // cookie,默认{ path: '/', httpOnly: true, secure: false, maxAge: null }
    resave: false, // 强制保存,即使session没有被修改也要重新保存
    saveUninitialized: false // 强制初始化
}))

复制代码

登录存储信息

// 初次登录存储用户信息至session
router.get('/login', (req, res, next) => {
  req.session.user = 'user'
  res.json({
    code: 200,
    message: '登录成功'
  })
})
复制代码

登录后接口校验

// 为需要登录后调用的接口添加校验
router.get('/list', (req, res, next) => {
  const { session: { user } } = req
  res.json({
    code: user ? 200 : 403,
    message: user ? 'success' : '请重新登录'
  })
})
// 通常会用中间件方式统一处理
app.use((req, res, next) => {
  const { url, session: { user } } = req;
  if(url.indexOf('/login') > -1) {
    next()
  } else {
    if(user) {
      next()
    } else {
      res.json({
        code: 403,
        message: '请重新登录'
      })
    }
  }
})
复制代码

更新session

// 为了体验优化,通常对应积极用户会主动更新session过期时间,结合上边中间件,如下处理
app.use((req, res, next) => {
  const { url, session: { user } } = req;
  if(url.indexOf('/login') > -1) {
    next()
  } else {
    if(user) {
      req.session._garbage = Date();
      req.session.touch();
      next()
    } else {
      res.json({
        code: 403,
        message: '请重新登录'
      })
    }
  }
})

复制代码

删除session

// 退出登录,清除session
router.get('/loginOut', (req, res, next) => {
  req.session.destroy();
  res.json({
    code: 200,
    message: '退出成功'
  })
})
复制代码

大致流程如此,通常session配合数据库做一些持久化的方案,这里不做深究,简单总结一下:

  • session的更新较为灵活,有许多较为成熟的框架
  • 前后端都是需要存储信息的,当用户量大时,服务端开销比较大
  • cookie + session有跨域问题,服务端需设置Access-Control-Allow-Origin,同时前端需设置withCredentials
  • session信息存储在当前服务器,分布式需做session共享机制(只是知道)
  • cookie信息有CSRF安全问题

token

session不同的是token并不需要服务端存储,更像是宵禁时的口令,口令对了就可以通过,否则不允许通过,还是以express为例介绍一下大致流程

登录生成token

// 可以按一定的规则加密生成token
// 这里使用jsonwebtoken生成token,jsonwebtoken的使用不在这里详细介绍
// 将token返回前端
const sign = (jtJson = {}, options = {}) => {
  const token = jwt.sign({ iss: 'lesscode', ...jtJson}, "secret", { expiresIn: 60 * 60 * 24, ...options})
  return token
}

router.get('/login', (req, res, next) => {
  const { body: { user, pass } } = req;
  const token = sign({ user })
  res.json({
    code: 200,
    data: { token, user },
    message: '登录成功'
  })
})
复制代码

前端拿到token

// 前端从登录接口获取token并存入localStorage
// 后续接口请求携带token
const request = async (
  url: string,
  options: optionsTypes
): Promise<dataTypes> => {
  const userInfo: string | null = localStorage.getItem("userInfo");
  const headers: headersTypes = {};
  if (userInfo) {
    // token存在则加入请求头
    const { token } = JSON.parse(userInfo) ?? {};
    headers.token = token;
  }
  const res: dataTypes = await selfFetch(baseUrl + url, {
    ...options,
    headers,
  });
  const { code, message } = res ?? {};

  if (code === 403) {
    store.commit("set_loginVisible", true);
    store.commit("set_userInfo", {});
    localStorage.clear();
  } else if (code !== 200) {
    ElMessage.error(message);
  }
  return res;
};
复制代码

服务端校验token

// 如同session一样,添加token校验中间件,对必要接口进行身份验证
const verResult = (req, res, next) => {
  const { url, headers: { token } } = req
  // 登录、通用接口跳过校验
  if(url.indexOf('/user/') > -1 || url.indexOf('/common/') > -1) {
    next()
  } else {
    if(!token) {
      // token不存在重新登录
      setJson(res, 403, "请登录", null)
    } else {
      const { iat, exp, message = '' } = jwt.verify(token, 'secret', (error, decoded) => {
        if(error) {
          if(error.name === 'TokenExpiredError') {
            return {
              iat: 1,
              exp: 0,
              message: "token已过期"
            }
          } else if (error.name === 'JsonWebTokenError') {
            return {
              iat: 1,
              exp: 0,
              message: "token无效"
            }
          }
        } else {
          return decoded
        }
      })
      // token过期或者无效重新登录
      if(iat < exp) {
        next()
      } else {
        setJson(res, 403, message, null)
      }
    }
  }
}
app.use(verResult)
复制代码

更新token

这里有一些问题:

  1. token作为一种权限验证指标时间越短越好
  2. token一旦生成过期时间无法更改

但实际问题是,频繁登录一定会打击用户的使用积极性,所以就必须针对活跃用户去做一些无感更新token的操作,期初的想法当然是想和session一样,当用户调用一次接口就返回一个新token,看起来好像没有什么大问题,但是却引起了更大的安全问题,假设token在一分钟后过即将过期,再一分钟内接口被调用了10000次,那这里就会产生10000个新的token,也就是同时存在了10000把钥匙可以打开服务端的大门,所以这个方案是不可行的,这里分享个优化方案

refresh_token

在用户初始登录时生成两个token: access_tokenrefresh_tokenaccess_token过期时间较短,比如只有5分钟(毕竟越短越安全),refresh_token过期时间有7天,接口校验使用access_token,如果access_token过期的话使用refresh_token获取新的access_token,后续使用新的access_token,但如果refresh_token也过期了,用户必须重新登录

简单总结一下:

  • 无状态,符合resetful api原则
  • 扩展新好,对分布式较为友好
  • 一次性,一旦token签发不可更改,若需要做更新续签,需要额外逻辑,需要自行实现
  • 数据较大、不宜存储敏感数据

结语

最后自己也有个问题,req.session._garbage方法具体是做什么的,看起来好像是更新session有效期开始时间,但是把express-session源码down下来也没有找到该方法,期望大佬得解答

文章分类
前端
文章标签