JWT认证(理论+koa2实践入门) - 我确定这已经是个懒人手册了

164 阅读3分钟

简言

最近在做前端知识的复习和整理,有了一些自己新的体会。更多在于记录,通过反复的温习,写笔记消除自己以前学习知识点的误区

什么是JWT

Json Web Token, 一种基于JSON格式的认证校验Token

JWT与cookie/session认证有什么不同?

Cookie/Session认证方式

image.png

缺点:

  1. 需要在服务端搭建更多的额外存储服务,不是必须操作
  2. 如果是服务器集群模式下(负载均衡),需要每个服务器都能够读取到session,维护工程量大
  3. 如果在浏览器外的终端中(IOS,Android),需要手动进行Cookie本地化,程序被kill时,会清空cookie
  4. Cookie在浏览器端,同域下存储大小为4KB,不适用于大型复杂场景
  5. Cookie是明文传递的,容易引发安全问题

JWT生成Token和认证原理

JWT由三部分组成Header, Payload, Signature

  • Header(用以存储JWT的元数据,例如配置加密算法)
// json配置(下面只是伪代码)
{
    xxx: 'RS256', // 设置加密方式
    expires: '过期时间' // 设置过期时间
    ....
}

// 然后使用base64UrlEncode(header json) 处理,下图有操作
  • Payload(用以携带用户信息)
// json配置
{
    name: 'xxx', 
    password: 'xxxx',
    xxxxx
}

// 然后使用base64UrlEncode(payload json) 处理,下图有操作
  • Signature(用以对前两部分签名,防止篡改)

过程就是首先指定一个秘钥secret,然后通过Header中指定的算法,对其进行加密

image.png 最后算出签名之后,使用Header.Payload.Signature的格式拼接起来丢给前端(这个拼接串就是token)

这里有一个小点:base64Url和base64不同之处,=被省略,+替换成-,/替换成_,这是因为有的请求会将token放在get请求

实践一下

在nodejs中实现JWT,需要使用jsonwebtoken 这个npm依赖

npm install jsonwebtoken

用户注册阶段

image.png

  1. 编写路由匹配

image.png

  • REGISTER: 枚举值,匹配路由 /register
  • adminValidator: 校验是否存在用户名和密码
const adminValidator = async (ctx, next) => {
  const { username, password } = ctx.request.body;

  if (!username || !password) {
    commonResult(ctx, {
      message: "用户名或密码为空",
      status: 500,
    });

    return;
  }
  await next();
};

  • cryptPassword: 加密密码
const cryptPassword = async (ctx, next) => {
  const { password } = ctx.request.body;

  let salt = bcryptjs.genSaltSync(10);
  // hash保存的是加密后的password
  let hash = bcryptjs.hashSync(password, salt);
  ctx.request.body.password = hash;

  await next();
};
  • verifyAdmin: 检测用户是否存在(这一步优化可以放在第二步)
const verifyAdmin = async (ctx, next) => {
  const { username } = ctx.request.body;

  const hasAdmin = await queryAdmin(username); // 这里执行sql

  if (hasAdmin && hasAdmin.length) {
    commonResult(ctx, {
      message: `管路员 ${username} 已存在,请勿重复注册`,
      status: 409,
    });
    return;
  }

  await next();
};
  • register: 将用户名和密码写入数据库或redis
const register = async (ctx) => {
    const { username, password } = ctx.request.body;

    const res = await createAdmin({ username, password }); // 这里执行sql

    // redis更新 - 使用ioredis库
    // redis.setExpire

    commonResult(ctx, res); // commonResult,封装响应body
 };

登录阶段

image.png

  1. 编写路由匹配 image.png
  • Login:同上
  • adminValidator: 同上
  • verifyLogin:校验用户是否存在以及密码是否正确
// 校验登陆
const verifyLogin = async (ctx, next) => {
  const { username, password } = ctx.request.body;

  try {
    const res = await queryAdmin(username); // 这里执行sql

    if (!res) {
      commonResult(ctx, {
        message: `用户名${username}不存在`,
        status: 500,
      });
      return;
    } else {
      if (!bcryptjs.compareSync(password, res?.[0]?.password)) {
        commonResult(ctx, {
          message: `密码输入错误`,
          status: 500,
        });
        return;
      } else {
        ctx.response.body = res;
      }
    }
  } catch (err) {
    commonResult(ctx, {
      message: `用户登录失败:${err}`,
      status: 500,
    });
    return;
  }
  • login:执行登录操作,将token写入cookie或是通过响应体返回token
const login = async (ctx) => {
    const res = ctx.response.body;
    const admin = res[0];

    // 1. 获取用户信息(在token的payload中,记录id,username,admin_type)
    try {
      const { password, account, ...rest } = admin;
      ctx.cookies.set("username", encodeURIComponent(`${account}`), {
        sameSite: "strict",
      });

      const token = encodeURIComponent(
        `${jwt.sign(rest, JWT_SECRET, { expiresIn: "1d" })}` // JWT_SECRET是服务器自己的秘钥,只有自己知道
      );
      ctx.cookies.set("token", token, {
        expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
        sameSite: "strict",
      });

      commonResult(ctx, {
        message: `欢迎回来, ${admin.account}`,
        status: 200,
      });
    } catch (err) {
      console.log(err);
    }
  };

校验权限(除注册,登录以外的接口,都可以加上权限校验)

image.png

  1. 编写路由匹配 image.png
  • GETCURRENTUSER: 同上
  • verifyAuth:校验token
// 验证授权
const verifyAuth = async (ctx, next) => {
  const token = ctx.cookies.get("token");

  if (!token) {
    commonResult(ctx, {
      status: 401,
      message: "登录过期,请重新登录",
    });
  } else {
    try {
      const result = jwt.verify(token, JWT_SECRET);
      await next();
    } catch (err) {
      console.log(err);  // 如果token过期或者是不合法,则会进入这里
      commonResult(ctx, {
        status: 401,
        message: "登录过期,请重新登录",
      });
    }
  }
};
  • getCurrentUser:获取当前管理员 (这个就不写了,就是一个controller._fn了)

这样,对于一个新用户的整个JWT的授权认证过程就已完成