nodejs实现jwt token认证登录

在本文中采用koa2框架编写接口的方式实现一遍jwt token登录认证的流程,主要涉及的知识点有如何生成登录校验码,koa2如何配置session以及前后端对token的处理.

githup项目地址

session配置

在koa2项目中安装一个依赖包 koa-session 能够方便的操作session.配置如下:

const session = require('koa-session');

const key = 'koa:sess';

exports.sessionKey = key;

exports.configSession = (app) => {
  // 使用session
  app.keys = ['secret'];
  const CONFIG = {
    key, // cookie key (默认koa:sess)
    maxAge: 60000, // cookie的过期时间,毫秒,默认为1分钟
    overwrite: true, // 是否覆盖    (默认default true)
    httpOnly: false, // cookie是否只有服务器端可以访问,默认为true
    signed: true, // 签名默认true
    rolling: false, // 在每次请求时强行设置cookie,这将重置cookie过期时间(默认:false)
    renew: false, // (boolean) 会话即将到期时,续订会话
  };
  app.use(session(CONFIG, app));
};

再将此配置文件引入到项目入口文件中运行.接下来使用session就变的异常简单.比如给session加入一条用户数据,只需调用 ctx.session.user_info = {name:"张三"}即可.获取session中该用户的值直接使用ctx.session.user_info.

生成登录校验码

                                                       

为了生成类似上方中的图片检验码,首先需要安装一个第三方依赖包 svg-captcha.编写一个工具方法如下:

const vCode = require('svg-captcha');

exports.genCaptcha = () => {
  const captcha = vCode.create({ fontSize: 50, width: 100, height: 40 }); //{text:"",data:""}
  return captcha;
};

调用genCaptcha方法会返回形似{text:"",data:""}结构的数据.data是生成图片的二进制数据,而text是对应的值. 

现在结合前面的介绍的session相关知识编写一个生成登录校验码的接口,代码如下:

/**
 * 获取登录校验码
 */
router.get('/getCaptcha', async (ctx) => {
  const { text, data } = genCaptcha();
  ctx.session.captcha = text.toLowerCase(); //转化成小写字母
  ctx.set('Content-Type', 'image/svg+xml');
  ctx.body = String(data);
});

打开浏览器访问接口地址 http://localhost:3000/getCaptcha,就能出现下方图片了.

                                                                

后端登录

用户向后端发送三个字段,分别是用户名,密码和校验码.首先从session中获取校验码判断用户发送过来的值是否正确,只有当校验码相等时才进行下一步.

/**
 * 登录
 */
router.post('/login', async (ctx) => {
  const { user_name, password, captcha } = ctx.request.body;
  if (ctx.session.captcha != captcha.toLowerCase()) {
    ctx.body = {
      error_no: 50,
      message: '验证码不正确',
    };
    return false;
  }
  ctx.body = await login({ user_name, password }, ctx);
});

userIsExist函数是持久层的一个方法,通过给它传递user_name和password参数让其去数据库中查询,判断该用户传递过来的用户名和密码是否正确.

如果发现用户名和密码都输入正确,我们就判定该用户为登录成功的状态.用户信息userInfo此时需要存储在session当中,为了方便用户下一次调用其他接口时可以快速获取用户数据.但在实际开发中,用户信息的数据建议存放在redis里,如果用户调用其他接口时,就从redis里获取身份信息.

用户信息的id需要单独取出来做一层加密生成一个随机字符串token,生成token的方式通过调用generateToken函数实现.将得到的token赋予用户信息上返回给前端.那么前端就能获取并保存token信息了.

exports.login = async ({ user_name, password }, ctx) => {
  //验证用户名密码是否正确
  const userInfo = await userIsExist({ user_name, password: md5(password) });
  if (userInfo == null) {
    return {
      error_no: 51,
      message: '用户名或密码错误',
    };
  }
  ctx.session[(`user_id_${userInfo.user_id}`, userInfo)];
  const token = generateToken(userInfo.user_id, 1);
  userInfo.token = token;
  return {
    error_no: 0,
    message: userInfo,
  };
};

 

生成token

generateToken函数的代码如下.通过调用jwt.sign函数,传入需要加密的数据data就可以生成字符串token并返回.其中scope是自定义参数,可以传递任意数值服务于业务上的需要.

const jwt = require('jsonwebtoken');
const { security } = require('../config');

exports.generateToken = (data, scope) => {
  const { secretKey, expiresIn } = security;
  const token = jwt.sign(
    {
      ...data,
      scope,
    },
    secretKey,
    {
      expiresIn,
    }
  );
  return token;
};

security的值如下,secretKey是自定义的加密或者解密的盐值.expiresIn是过期时间.

module.exports = {
   security: {secretKey:"xxxxxxx",expiresIn: 60 * 60 * 24 *30}
}

 token验证

现在前端已经登录成功了,它想访问其他接口比如修改用户信息,后端就要先对请求进行拦截判断是否真的登录了.如何判断该请求是否登录了呢?

在前面介绍登录的流程时,后端最终将token返回给了前端.前端会携带token再次发起请求,后端获取token之后进行解密.如果发现解密成功得到了user_id,那就可以判定该请求是合法的.通过user_id就可以在session中寻找到用户信息.

编写一个校验用户是否登录的中间件(代码如下)

  1. basicAuth认证:basicAuth函数执行完后获取的userToken不存在说明前端没有采用basicAuth相应的规范包装token.
  2. jwt认证:经过第一轮basicAuth认证后,将userToken.name和盐值传入jwt校验函数中进行认证.如果最终能解密出decode的值就判断该请求合法并放行.
const basicAuth = require('basic-auth');
const jwt = require('jsonwebtoken');
const { security } = require('../config');

class Auth {
  constructor() {}

  get m() {
    return async (ctx, next) => {
      const userToken = basicAuth(ctx.req);
      //这个userToken的name属性是前端自定义的,userToken.name就是token值
      if (!userToken || !userToken.name) {
        ctx.body = {
          error_no: 54,
          message: 'token无效',
        };
        return false;
      }
      const { secretKey } = security; //加密或者解密的盐值
      try {
        var decode = jwt.verify(userToken.name, secretKey);
      } catch (error) {
        if (error.name == 'TokenExpiredError') {
          ctx.body = {
            error_no: 54,
            message: 'token已过期',
          };
        }
        return false;
      }

      //decode只是user_id
      ctx.auth = ctx.session[`user_id_${decode}`]; //获取存储在session中的用户数据

      await next();
    };
  }
}

module.exports = Auth;

在更新用户信息的接口中可以引入上方定义的中间件.如果用户通过了token认证,那么在ctx.auth中就能获取到用户数据.

/**
 * 更新用户信息
 */
router.post('/update_user', new Auth().m, async (ctx) => {
  /**
   * 测试一下用户是否登录,如果登录了就能得到用户数据
   */
  const { user_name, user_id } = ctx.auth;
  ctx.body = {
    error_no: 0,
    message: {
      user_id,
      user_name,
    },
  };
});

 

前端包装处理token

前端调用登录接口后会接收到后端返回的token字符串(如下图).token字符串一般需要存储在localstorage和数据框架(vuex或者redux)当中.另外前端还需要对token进行包装处理装载到http的请求头里,这样才能畅通不足的访问后端其他需要登录认证的接口.

下面以axios为例,介绍如何封装处理token.

后端返回的token值加上一个冒号会组成一个新的字符串,对这个新字符串做base64编码赋予变量baseCode.`Basic ${baseCode}`组成的字符串就是最终需要发送给后端的值.token值还需要使用属性名Authorization装载到http请求头上.后端分别通过basicAuth认证和jwt认证才能判断该请求是否合法.

import axios from 'axios';
import { service_ip } from './tool';
import { Base64 } from 'js-base64';

const _axios = axios.create({
  baseURL: service_ip, //请求的公共地址
  timeout: 5000, // 请求超时时间
});
/**
 * 将token装载在http请求头上
/
_axios.interceptors.request.use(  (params) => {
    // 在发送请求之前做些什么
    params.data = { data: params.data };
    params.headers['Authorization'] = getEncode();
    return params;
  },
  (error) => {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
);

/**
 * 获取处理后的token值
/
function getEncode() {
  const token = getToken();//后端返回的token值
  const baseCode = Base64.encode(token + ':');
  return `Basic ${baseCode}`;
}

猜你喜欢

转载自blog.csdn.net/brokenkay/article/details/108284390
今日推荐