Node中实现一个简易的图片验证码流程

2,073 阅读5分钟

前言

最近在实现一个登录中常见的功能:图片验证码。这个功能非常有意思,但是你说难又不会太难,简单又不会太简单。不会太难之处在于你最方便可以客户端本地存储验证码图片(不排除被打的可能),不会太简单的是要做到非常完整的流程还是要点精力,比如过期时间,加密验证,签发私钥...。本文从一个不是非常主后端的角度去实现一个标准图片验证码的流程。

已用技术:

  • koa
  • koa-session(express 的原理一样)
  • svg-captcha
  • redis

原理

我们都知道正常的登录验证码都是存在一定过期时间的,那这个过期怎么算是过期?那我们可以通过 cookie,session,redis 中去判断过期与否。因为这三者在 web 和 app 都可以设定过期时间限制登录凭证。考虑用 session 和 redis 来实现这个功能,因为 cooile 太容易被篡改了。

基本思路如下:

  1. 用户进入登录页面后请求接口从后端获取图片验证码标签
  2. 后端生成验证码的同时,设置一个 session,同时拿到响应的 session 值作为 key 值,图片验证码的正确字符串作为 value,映射到 redis 中存储。
  3. 用户确定登录后,携带客户端的 cooile 和验证码一起发送给后台
  4. 后台拿到请求头中 cooile 中的 session 值和验证码,用 session 值作为 key 获取 redis 中的对应的 value 值,进行验证比对正确。

实现

后端服务器下载koa-session,redis,svg-captcha插件依赖。

后端: 在app.js中引入koa-session

const Koa = require("koa");
const session = require("koa-session");

const session_signed_key = ["appletSystem"];
const app = new Koa();
const CONFIG = {
  name: "sessionId",
  maxAge: 180000, // session 过期时间,以毫秒ms为单位计算 
  overwrite: true,//是否允许重写 。(默认是 true) 
  httpOnly: true,//是否设置HttpOnly,如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。
  signed: true,//是否签名。(默认是 true) 
  rolling: false,//是否每次响应时刷新Session的有效期
  renew: false,//是否在Session快过期时刷新Session的有效期
};
app.keys = session_signed_key;
app.use(session(CONFIG, app));

app.listen(8080, () => {
  console.log(`服务器8080启动成功`);
});

在这里,session_signed_key为开启sign签发的公钥,CONFIG为session的配置文件,signed为了防止客户端的cookie被篡改,必须为true;maxAge为过期时间,最后设置app.keys,use(session(CONFIG, app))就ok了。

使用redis:

确保本地安装了redis并且启动服务

const { createClient } = require("redis");
const client = createClient({
  host: "localhost", //	redis地址
  port: 6379, // 端口号
});
client.on("connect", (error) => {
  if (!error) {
    console.log("connect to redis");
  }
});
function setString(key, value, expire = 180) { //存储key和value键值对
  return new Promise((resolve, reject) => {
    client.set(key, value, (error, replay) => {
      if (error) {
        reject("设置失败");
      }
      if (expire) {//默认expire过期时间为180s
        client.expire(key, expire); //给当前的key设置过期时间
      }
      resolve("设置成功");
    });
  });
}
function getString (key) => {
  return new Promise(async (resolve, reject) => {
    client.get(key, function (err, result) {
      resolve(result);//直接获取value值,如果没有key,直接返回null
    });
  });
};

引入redis并且创建client代理,通过封装两个方法get和set去获取redis的key-value。方法可直接复制使用。

在验证码接口:

const svgCaptcha = require("svg-captcha");

class svg {
  async getsvg(ctx, next) {
    const { time } = ctx.request.query;
    const c = svgCaptcha.create({
      size: 4, // 验证码长度
      ignoreChars: "0o1iLl", // 验证码字符中排除 0o1i
      color: true, // 验证码是否有彩色
      noise: 0, //干扰线
      background: "#666", // 背景颜色
      height: 50,
      fontSize: 60,
      width: 100,
    });
    ctx.session.sessionId= { time: "svgIsMjc" + time  };
    await ctx.session.manuallyCommit();
    const cookie = ctx.response.headers["set-cookie"];
    if (cookie) {
      let cookies = cookie[0]; //获取第一个值
      const newSessionKey = getCookie(cookies); //传字符串
      try {
        const result = await setString(newSessionKey, c.text.toLowerCase());
      } catch (error) {
        throw new Error(error);
      }
    }
    ctx.body = c.data;
  }
}

在这里遇到问题比较多,既然返回图片的时候设置一个过期的session,那么该怎么设置?起初我设置了ctx.session.sessionId= { time: "svgIsMjc" + time +ip },time为当前时间毫秒和获取的ip地址来确保session的值各不相同。

如图,确实能在图片返回的同时客户端存储了cookie值,并且为session。

并且每次请求客户端的sessionId会不同

如图

在验证图片码过期中间件接口:

const verifySvgCode = async (ctx, next) => {
  const cookie = ctx.request.headers.cookie;//获取cookie
  const { vaildcode } = ctx.request.body;//获取客户端的验证码
  if (!ctx.session.sessionId) {//验证是否过期或者被篡改
    const error = new Error(erroType.SVG_COOKIE_IS_TIMEOUT);

    return ctx.app.emit("error", error, ctx);
  }
  if (cookie) {
    //存在就是cookie没有过期,验证cookie的redis存储的值
    const newSessionKey = getCookie(cookie);//转化session字符串截取sessionid值
    const oldvaildcode = await getString(newSessionKey);//获取value值
    if (oldvaildcode && vaildcode.toLowerCase() == oldvaildcode) {
    //校验两者验证码的相等与否,否则抛出错误
      await next();
      return;
    } else {
      const error = new Error(erroType.SVG_COOKIE_IS_FALSE);
      return ctx.app.emit("error", error, ctx);
    }
  } else {
    const error = new Error(erroType.SVG_COOKIE_IS_TIMEOUT);
    return ctx.app.emit("error", error, ctx);
  }
};

通过ctx.headers获取cookie,如果过期当前肯定为空值。在这里,我一开始是犯错误。ctx.session.sessionId是一个undefined。因为在CONFIG中我少配置了一个key属性,所以拿不到,搞得排查了大半天!。

const CONFIG = {
  ...
  key: "sessionId", //cookie的key。 (默认是 koa:sess) */,必须加key值,不然无法获取session!
};

而且,还有一个npm仓库中redis工具的问题。目前redis最高为4.0版本,第二个版本就是3.1.2。在node里面使用最高4.0版本是连接不上去redis的,所以我采用3.1.2版本。

而3.1.2版本的redis工具get方法我是出现问题。 直接可以set但是get的时候总是null 我在redis-cli下却可以get到值。排查了半天,我发现是get的时候key值长度问题。

封装的截取sessionId的方法:

const getCookie = (cookie) => {
  //接收字符串
  const sessionId = cookie;
  const start = sessionId.indexOf("=") + 1;
  const end = sessionId.indexOf(";") - 2;
  const newSessionKey = sessionId.substring(start, 110);
  console.log(newSessionKey);
  return JSON.stringify(newSessionKey);
};

我多次调试完,发现client.get的时候传入的key值也就是sessionId的值如果超过110长度,那么redis预期这个key值不存在就给我返回null了。

所以我妥协了,截取了110长度作为key存储,再用get的时候能得到存储的value值。 如图成功拿到了。问题都在百度搜查过,存在但是没有解决办法。

总结

  1. redis的创建以及使用
  2. koa-session的使用方法以及注意事项
  3. npm中的redis工具的get问题
  4. 图片验证码的流程以及校验

来源

[1] 博客 blog
mjcelaine.top