Node.js+MongoDB对于RestfulApi中用户token认证的实践

最近在新的项目中开发中需要使用到RESTful API的对接IOS与Android应用开发,所以一不小心就踩进了REST的坑里面……在开发过程中还是有许多收货的。这次就来看看API设计中的token的思路与实践。

技术栈


使用Node.js上的Express框架进行我们的路由设计,Mongoose来与Mongodb数据库连接交互,使用Postman对我们设计的Api进行调试,快动起手来吧!

关于RESTful API


网上已经有了很多关于RESTful的介绍,我这里也不过多重复了。想说的就是它的主要作用,就是对于现如今的网络应用程序,分为前端和后端两个部分,然而当前的发展趋势就是应用平台需求的扩大(IOS、Android、Webapp等等)

因此,就需要一种统一的机制,方便不同的应用平台的前端设备与后端进行通信,也就是前后端的分离。这导致了API架构的流行,甚至出现"API First"的设计思想。RESTful API则是目前比较成熟的一套互联网应用程序的API设计理论。

API设计中的token的思路


在API设计中,TOKEN用来判断用户是否有权限访问API.TOKEN首先不需要编解码处理. 一般TOKEN都是一些用户名+时间等内容的MD5的不可逆加密.然后通过一个USER_TOKEN表来判断用户请求中包含的TOKEN与USER_TOKEN表中的TOKEN是否一致即可.

具体实践过程主要为:

1.设定一个密钥比如key = ‘2323dsfadfewrasa3434’。
2.这个key 只有发送方和接收方知道。
3.调用时,发送方,组合各个参数用密钥 key按照一定的规则(各种排序,MD5,ip等)生成一个access_key。一起post提交到API接口。
4.接收方拿到post过来的参数以及这个access_key。也和发送一样,用密钥key 对各个参数进行一样的规则(各种排序,MD5,ip等)也生成一个access_key2。
5.对比 access_key 和 access_key2 。一样。则允许操作,不一样,报错返回或者加入黑名单。

token设计具体实践

废话不多说,先进入看我们的干货,这次选用Node.js+experss配合Mongoose来进入REST的token实践

项目地址: 完整项目在这
或git clone https://github.com/Nicksapp/nAuth-restful-api.git

整体构架


开发前先进行我们设计的构想

  • 路由设计
    • POST /api/signup: 用户注册
    • POST /api/user/accesstoken: 账号验证,获取token
    • GET /api/users/info: 获得用户信息,需验证
  • user 模型设计
    • name : 用户名
    • password: 密码
    • token: 验证相关token

新建项目

先看看我们的项目文件夹

- routes/
---- index.js
---- users.js
- models/
---- user.js
- config.js
- package.json
- passport.js
- index.js

npm init创建我们的package.json

接着在项目根文件夹下安装我们所需的依赖

npm install express body-parser morgan mongoose jsonwebtoken bcrypt passport passport-http-bearer --save 
  • express: 我们的主要开发框架
  • mongoose: 用来与MongoDB数据库进行交互的框架,请提前安装好MongoDB在PC上
  • morgan: 会将程序请求过程的信息显示在Terminal中,以便于我们调试代码
  • jsonwebtoken: 用来生成我们的token
  • passport: 非常流行的权限验证库
  • bcrypt: 对用户密码进行hash加密

– save会将我们安装的库文件写入package.json的依赖中,以便其他人打开项目是能够正确安装所需依赖.

用户模型


定义我们所需用户模型,用于moogoose,新建models/user.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const bcrypt = require('bcrypt');

const UserSchema = new Schema({
  name: {
    type: String,
    unique: true, // 不可重复约束
    require: true // 不可为空约束
  },
  password: {
    type: String,
    require: true // 不可为空约束
  },
  token: {
    type: String
  }
});

// 添加用户保存时中间件对password进行bcrypt加密,这样保证用户密码只有用户本人知道
UserSchema.pre('save', function (next) {
    var user = this;
    if (this.isModified('password') || this.isNew) {
        bcrypt.genSalt(10, function (err, salt) {
            if (err) {
                return next(err);
            }
            bcrypt.hash(user.password, salt, function (err, hash) {
                if (err) {
                    return next(err);
                }
                user.password = hash;
                next();
            });
        });
    } else {
        return next();
    }
});
// 校验用户输入密码是否正确
UserSchema.methods.comparePassword = function(passw, cb) {
    bcrypt.compare(passw, this.password, (err, isMatch) => {
        if (err) {
            return cb(err);
        }
        cb(null, isMatch);
    });
};

module.exports = mongoose.model('User', UserSchema);

配置文件


./config.js 用来配置我们的MongoDB数据库连接和token的密钥。

module.exports = {
  'secret': 'learnRestApiwithNickjs', // JSON Web Token 加密密钥设置
  'database': 'mongodb://localhost:27017/test' // 填写本地自己 mongodb 连接地址,xxx为数据表名
};

本地服务器配置


./index.js 服务器配置文件,也是程序的入口。

这里我们主要用来包含我们程序需要加载的库文件,调用初始化程序所需要的依赖。

const express = require('express');
const app = express();
const bodyParser = require('body-parser');// 解析body字段模块
const morgan = require('morgan'); // 命令行log显示
const mongoose = require('mongoose');
const passport = require('passport');// 用户认证模块passport
const Strategy = require('passport-http-bearer').Strategy;// token验证模块
const routes = require('./routes'); //路由配置
const config = require('./config'); //全局配置

let port = process.env.PORT || 8080;

app.use(passport.initialize());// 初始化passport模块
app.use(morgan('dev'));// 命令行中显示程序运行日志,便于bug调试
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json()); // 调用bodyParser模块以便程序正确解析body传入值

routes(app); // 路由引入

mongoose.Promise = global.Promise;
mongoose.connect(config.database); // 连接数据库

app.listen(port, () => {
  console.log('listening on port : ' + port);
})

路由配置


./routes 主要存放路由相关文件

./routes/index.js 路由总入口,引入所使用路由

module.exports = (app) => {
  app.get('/', (req, res) => {
    res.json({ message: 'hello index!'});
  });

  app.use('/api', require('./users')); // 在所有users路由前加/api
};

./routes/users.js

const express = require('express');
const User = require('../models/user');
const jwt = require('jsonwebtoken');
const config = require('../config');
const passport = require('passport');
const router = express.Router();

require('../passport')(passport);

// 注册账户
router.post('/signup', (req, res) => {
  if (!req.body.name || !req.body.password) {
    res.json({success: false, message: '请输入您的账号密码.'});
  } else {
    var newUser = new User({
      name: req.body.name,
      password: req.body.password
    });
    // 保存用户账号
    newUser.save((err) => {
      if (err) {
        return res.json({success: false, message: '注册失败!'});
      }
      res.json({success: true, message: '成功创建新用户!'});
    });
  }
});

// 检查用户名与密码并生成一个accesstoken如果验证通过
router.post('/user/accesstoken', (req, res) => {
  User.findOne({
    name: req.body.name
  }, (err, user) => {
    if (err) {
      throw err;
    }
    if (!user) {
      res.json({success: false, message:'认证失败,用户不存在!'});
    } else if(user) {
      // 检查密码是否正确
      user.comparePassword(req.body.password, (err, isMatch) => {
        if (isMatch && !err) {
          var token = jwt.sign({name: user.name}, config.secret,{
            expiresIn: 10080  // token到期时间设置
          });
          user.token = token;
          user.save(function(err){
            if (err) {
              res.send(err);
            }
          });
          res.json({
            success: true,
            message: '验证成功!',
            token: 'Bearer ' + token,
            name: user.name
          });
        } else {
          res.send({success: false, message: '认证失败,密码错误!'});
        }
      });
    }
  });
});

// passport-http-bearer token 中间件验证
// 通过 header 发送 Authorization -> Bearer  + token
// 或者通过 ?access_token = token
router.get('/users/info',
  passport.authenticate('bearer', { session: false }),
  function(req, res) {
    res.json({username: req.user.name});
});

module.exports = router;

};

passport配置


./passport.js 配置权限模块所需功能

const passport = require('passport');
const Strategy = require('passport-http-bearer').Strategy;

const User = require('./models/user');
const config = require('./config');

module.exports = function(passport) {
    passport.use(new Strategy(
        function(token, done) {
            User.findOne({
                token: token
            }, function(err, user) {
                if (err) {
                    return done(err);
                }
                if (!user) {
                    return done(null, false);
                }
                return done(null, user);
            });
        }
    ));
};

主要验证发送的token值与用户服务器端token值是否匹配,进行信息验证。

具体调试


现在就可以运行我们的代码看具体运作过程了!为了便于调试与参数的收发,我们使用postman(可在Chrome上或Mac上安装)来操作.
node index
运行我们的本地服务器,记得先启动Mongodb数据服务,访问 localhost:8080/应该就可以看到我们所返回的初始json值了,然我们继续深入测试。
POST访问localhost:8080/api/signup,我们来注册一个新用户,注意要设置body
的Content-Type为x-www-form-urlencoded以便我们的body-parser能够正确解析,好的我们成功模拟创建了我们的新用户。


连接一下数据库看下我们的用户信息是否也被正确存储(注:我使用的是MongoChef,十分强大MongoDB数据库管理软件),我们可以看到,我的password也被正确加密保存了。

接着POST访问localhost:8080/api/user/accesstoken,来为我的用户获得专属token,POST过程与注册相关,可以看到也正确生成我们的token值。

再看下我们的数据库中的用户信息,token值也被存入了进来,便于我们之后进行权限验证。

GET访问localhost:8080/api/users/info,同时将我们的token值在Header
中以Authorization: token
传入,正确获得用户名则表示我们访问请求通过了验证。

如果token值不正确,则返回 Unauthorized 并拒绝访问请求。到这里我们的权限验证功能也就基本实现了(喜大普奔~~~)。

总结


希望在看完这篇教程后能够对你在RESTful Api开发上有所启发,小生才疏学浅,过程中有什么不足的地方也欢迎指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容