express基于JWT实现用户登陆授权控制

4,346 阅读5分钟

你是否和我一样,在对接后端大佬的接口时,对于请求头authorization认证感到疑惑;
你是否和我一样,在向node后端领域扩展时,对于用户登陆注册授权感到挠头;
你是否和我一样,在浏览器访问某个页面时,对于访问权限控制感到好奇;
那么,请花上几分钟时间阅读,让下文来帮你解惑。

本文主要通过express来实现用户登陆授权的逻辑,这里的JWT只是一个标准,全称:JSON Web Token。有兴趣的小伙伴可以去官方说明加深了解。

初识JWT

了解http协议的同学都知道,http协议是无状态的,所以就需要客户端在每次请求的时候携带一些标识来表明身份,所以就有了CookieAuthorizationTokensession_id等,客户端认证访问服务端的模式一般如下:

  1. 客户端提交用户信息登陆
  2. 服务端验证通过后,存储相关对话信息,生成对应的标识字符(token、Authorization、session_id),返回给客户端
  3. 客户端接收服务器返回内容,存储到对应的位置(cookie、localstorage)
  4. 客户端每次都携带这个标识字符进行服务端数据请求
  5. 服务端根据标识字符校验身份,进行数据处理

JWT字符由三部分组成,例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTU3NzM5NjM1LCJleHAiOjE1NTgzNDQ0MzV9.L4PqLf7PatEf_TVrNG2GgyBlU7YR8iuEoXOOeu7i15g
按照格式排列就是这样Header.Payload.Signature

Header: JWT的元数据,一般是一些要加密的json对象,比如{username: 'Tom'}
Payload: JWT的负载对象,一般是JWT官方规定的字段,比如exp(过期时间),详见字段说明
Signature: 对前两部分和私有秘钥进行签名

本文案例使用npm包jsonwebtoken,点击查看用法

项目初始配置

初始化一个express项目,配置数据库连接和加载bodyParser插件。

//connect mongoDB
let mongoose = require('mongoose');
let mongoURL = 'mongodb://localhost/dataBase';
mongoose.connect(mongoURL);
mongoose.Promise = global.Promise;
let db = mongoose.connection;
db.on('error',console.error.bind(console, 'MongoDB connection error:'));

let bodyParser = require('body-parser');
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// parse application/json
app.use(bodyParser.json());

用户注册

编写用户model

用于连接数据库的数据Schema模型

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var AuthSchema = new Schema({
  username: String,
  userpswd: String
});

// 参数:导出模块名称、Schema实例、数据表名称
module.exports = mongoose.model('AuthInfo', AuthSchema, 'authinfo');

编写注册route

基于restful API的接口路由

var express = require('express');
var router = express.Router();
var bodyParser = require('body-parser');

var mongoose = require('mongoose');
var AuthInfo = require('../models/authModel'); // 导入model模块

router.post('/',function(req, res, next){
  console.log('open post register');

  var username = req.body.username;
  var password = req.body.password;

  //是否合法的参数
  if (username == null || username.trim() == '' || password == null || password.trim() == '') {
    res.send({code: 500, message: '用户名密码不能为空'})
    return
  }
  
  // md5
  var md5String = require('crypto').createHash('md5').update(password).digest('hex');

  //验证账号是否存在
  var queryString = {username: username};
  res.set({'Content-type': 'application/json;charset=utf-8'});

  AuthInfo.findOne(queryString).then(data => {
    return new Promise((resolve, reject)=>{
      if(data){
        res.send({code: 500, message: '用户已经注册'});
        reject();
      }else{
        resolve();
      }
    }).then(()=>{
      //保存
      return new AuthInfo({
        username: username,
        password: md5String
      }).save();
    }).then(data => {
      if(data){
        //返回
        res.send({code: 1, message: '注册成功'})
        return;
      }
      // 返回
      res.send({code: 500, message: '注册失败'});
    }).catch(err => {
      // 异常
      if(err){
        res.status(500).send(err);
        console.log(err);
      }
    })
  })

});

module.exports = router;

编写登陆route

基于restful API的接口路由

var express = require('express');
var router = express.Router();
var bodyParser = require('body-parser');

var mongoose = require('mongoose');
var AuthInfo = require('../models/authModel');

router.post('/',function(req, res, next){

  var username = req.body.username;
  var password = req.body.password;

  //是否合法的参数
  if (username == null || username.trim() == '' || password == null || password.trim() == '') {
    res.send({code: 500, message: '用户名密码不能为空'})
    return
  }

  var md5String = require('crypto').createHash('md5').update(password).digest('hex'); // md5

  //验证账号是否存在
  var queryString = {username: username, userpswd: md5String};
  res.set({'Content-type': 'application/json;charset=utf-8'});
  
    // md5 token
  var tokenString = require('crypto').createHash('md5').update(JSON.stringify(queryString)).digest('hex');

  AuthInfo.findOne(queryString).then(data => {
    return new Promise((resolve, reject)=>{
      if(data){
        resolve(data);
      }else{
        res.send({code: 500, message: '用户名或密码错误'})
        reject();
      }
    }).then(data => {
      console.log(data);
      res.send({ code: 1, message: '登陆成功', token: tokenString })
    })
  }).catch( err => {
    if(err){
      res.status(500).send(err);
      console.log(err);
    }
  })

});

module.exports = router;

这里和注册不同的是我们需要把从前端页面接收到的密码通过MD5转换才能用于数据库查询,因为数据库的密码字段也正是存着MD5转换过后的字符,当查询成功之后,我们还需要通过对刚才登陆的表单字段对象进行字符串转换,然后再通过MD加密后作为token返回给客户端。

上面是一个简单的使用MD5加密的token用户授权案例,并没有使用JWT,然而,使用JWT认证,我们只需要进行少部分的变动

这里我们需要借助npm包: jsonwebtoken,前端方面需要在axios或者fetch的默认headers配置中配置认证信息,比如:
axios.defaults.headers['Authorization'] = sessionStorage.getItem("token");

注册逻辑不变,我们只需要更改用户登录成功之后的token生成方式为JWT,并且在服务中做路由拦截并对客户端携带过来的认证信息做校验即可。

JWT登陆route改进

var jwt = require('jsonwebtoken'); // 借助 jsonwebtoken

// ...

AuthInfo.findOne(queryString).then(data => {
return new Promise((resolve, reject)=>{
  if(data){
      resolve(data);
  }else{
    res.send({code: 500, message: '用户名或密码错误'})
    reject();
  }
}).then(data => {
  console.log(data);

  /***jwt生成token***/
  let content = {username: username};  // 要生成token的主题信息
  let secretOrPrivateKey= "This is perfect projects."; // 这是加密的key(密钥) 根据个人自定义
  let token = jwt.sign(content, secretOrPrivateKey, {
    expiresIn: 60 * 60 * 24 * 7  // 一周过期
  });

  res.send({ code: 1, message: '登陆成功', token: token })
})
}).catch( err => {
  if(err){
    res.status(500).send(err);
    console.log(err);
  }
})

// ...

JWT项目初始配置改进

var jwt = require('jsonwebtoken'); // 借助 jsonwebtoken

// ...

let allowCrossDomain = function(req, res, next) {
  // 响应头设置 添加Methods: OPTIONS、Headers: Authorization
  res.header('Access-Control-Allow-Origin', 'http://localhost:8082');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'content-type,token,id');
  res.header("Access-Control-Request-Headers", "content-Type, Authorization");
  res.header('Access-Control-Allow-Credentials','true');
  next();
};
app.use(allowCrossDomain);

//添加拦截器
app.use(function(req, res, next){
  // 获取请求头中的Authorization认证字符
  let authorization = req.get('Authorization'); 
  // 排除不需要授权的路由
  if(req.path === '/api/login'){
    next()
  }else if(req.path === '/api/register'){
    next();
  }else{
    let secretOrPrivateKey= "This is perfect projects.";
    jwt.verify(authorization, secretOrPrivateKey, function (err, decode) {
      if (err) {  //  认证出错
        res.status(403).send('认证无效,请重新登录。');
      } else {
        next();
      }
    })
  }
})

//...

JWT认证方案相对于简单token认证方式变动不大,只是变更了token生成方式和用户身份的校验方式,了解JWT认证,对于我们理解前后端交互具有更大的帮助。

一个简单的登录控制demo:github.com/joydezhong/…

本文参考:
juejin.cn/post/684490…
www.ruanyifeng.com/blog/2018/0…