├── .gitignore ├── README.md ├── config.js ├── index.js ├── models └── user.js ├── package.json ├── passport.js └── routes ├── index.js └── users.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | md/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Node.js+Mongoose的RestfulApi的用户token权限验证 2 | 3 | ### 关于安全性方面的建议 4 | 5 | 可以参考这篇总结 [开发安全的 API 所需要核对的清单](https://github.com/shieldfy/API-Security-Checklist/blob/master/README-zh.md) 6 | 7 | ### 安装 8 | `git clone https://github.com/Nicksapp/nAuth-restful-api.git` 9 | 10 | ### 运行 11 | `npm install` 12 | 13 | 具体数据库配置信息在config.js中设置 14 | 15 | ### 整体构架 16 | 开发前先进行我们设计的构想 17 | 18 | * 路由设计 19 | * POST /api/signup: 用户注册 20 | * POST /api/user/accesstoken: 账号验证,获取token 21 | * GET /api/user/user_info: 获得用户信息,需验证 22 | 23 | * user 模型设计 24 | * name : 用户名 25 | * password: 密码 26 | * token: 验证相关token 27 | 28 | ### 关于RESTful API 29 | 网上已经有了很多关于RESTful的介绍,我这里也不过多重复了。想说的就是它的主要作用,就是对于现如今的网络应用程序,分为前端和后端两个部分,然而当前的发展趋势就是应用平台需求的扩大(IOS、Android、Webapp等等) 30 | 31 | 因此,就需要一种统一的机制,方便不同的应用平台的前端设备与后端进行通信,也就是前后端的分离。这导致了API架构的流行,甚至出现"API First"的设计思想。RESTful API则是目前比较成熟的一套互联网应用程序的API设计理论。 32 | 33 | ### 技术栈 34 | 使用Node.js上的[Express](http://www.expressjs.com.cn/)框架进行我们的路由设计,[Mongoose](https://cnodejs.org/topic/504b4924e2b84515770103dd)来与Mongodb数据库连接交互,使用Postman对我们设计的Api进行调试,快动起手来吧! 35 | 36 | 37 | ### API设计中的token的思路 38 | 在API设计中,TOKEN用来判断用户是否有权限访问API.TOKEN首先不需要编解码处理. 一般TOKEN都是一些用户名+时间等内容的MD5的不可逆加密.然后通过一个USER_TOKEN表来判断用户请求中包含的TOKEN与USER_TOKEN表中的TOKEN是否一致即可. 39 | 40 | 具体实践过程主要为: 41 | 42 | 1. 设定一个密钥比如key = ‘2323dsfadfewrasa3434'。 43 | 2. 这个key 只有发送方和接收方知道。 44 | 3. 调用时,发送方,组合各个参数用密钥 key按照一定的规则(各种排序,MD5,ip等)生成一个access_key。一起post提交到API接口。 45 | 4. 接收方拿到post过来的参数以及这个access_key。也和发送一样,用密钥key 对各个参数进行一样的规则(各种排序,MD5,ip等)也生成一个access_key2。 46 | 5. 对比 access_key 和 access_key2 。一样。则允许操作,不一样,报错返回或者加入黑名单。 47 | 48 | ### token设计具体实践 49 | 50 | > 废话不多说,先进入看我们的干货,这次选用Node.js+experss配合Mongoose来进入REST的token实践 51 | 52 | 项目地址: [GitHub地址](https://github.com/Nicksapp/nAuth-restful-api) 53 | 54 | 或 `git clone https://github.com/Nicksapp/nAuth-restful-api.git` 55 | 56 | 57 | 58 | ### 新建项目 59 | 先看看我们的项目文件夹 60 | 61 | ``` javascript 62 | - routes/ 63 | ---- index.js 64 | ---- users.js 65 | - models/ 66 | ---- user.js 67 | - config.js 68 | - package.json 69 | - passport.js 70 | - index.js 71 | ``` 72 | 73 | `npm init`创建我们的`package.json` 74 | 75 | 接着在项目根文件夹下安装我们所需的依赖 76 | 77 | ``` 78 | npm install express body-parser morgan mongoose jsonwebtoken bcrypt passport passport-http-bearer --save 79 | 80 | ``` 81 | * express: 我们的主要开发框架 82 | * mongoose: 用来与MongoDB数据库进行交互的框架,请提前安装好MongoDB在PC上 83 | * morgan: 会将程序请求过程的信息显示在Terminal中,以便于我们调试代码 84 | * jsonwebtoken: 用来生成我们的token 85 | * passport: 非常流行的权限验证库 86 | * bcrypt: 对用户密码进行hash加密 87 | 88 | -- save会将我们安装的库文件写入`package.json`的依赖中,以便其他人打开项目是能够正确安装所需依赖. 89 | 90 | ### 用户模型 91 | 定义我们所需用户模型,用于moogoose,新建`models/user.js` 92 | 93 | ``` javascript 94 | const mongoose = require('mongoose'); 95 | const Schema = mongoose.Schema; 96 | const bcrypt = require('bcrypt'); 97 | 98 | const UserSchema = new Schema({ 99 | name: { 100 | type: String, 101 | unique: true, // 不可重复约束 102 | require: true // 不可为空约束 103 | }, 104 | password: { 105 | type: String, 106 | require: true 107 | }, 108 | token: { 109 | type: String 110 | } 111 | }); 112 | 113 | // 添加用户保存时中间件对password进行bcrypt加密,这样保证用户密码只有用户本人知道 114 | UserSchema.pre('save', function (next) { 115 | var user = this; 116 | if (this.isModified('password') || this.isNew) { 117 | bcrypt.genSalt(10, function (err, salt) { 118 | if (err) { 119 | return next(err); 120 | } 121 | bcrypt.hash(user.password, salt, function (err, hash) { 122 | if (err) { 123 | return next(err); 124 | } 125 | user.password = hash; 126 | next(); 127 | }); 128 | }); 129 | } else { 130 | return next(); 131 | } 132 | }); 133 | // 校验用户输入密码是否正确 134 | UserSchema.methods.comparePassword = function(passw, cb) { 135 | bcrypt.compare(passw, this.password, (err, isMatch) => { 136 | if (err) { 137 | return cb(err); 138 | } 139 | cb(null, isMatch); 140 | }); 141 | }; 142 | 143 | module.exports = mongoose.model('User', UserSchema); 144 | 145 | ``` 146 | 147 | ### 配置文件 148 | `./config.js` 用来配置我们的MongoDB数据库连接和token的密钥。 149 | 150 | ```javascript 151 | module.exports = { 152 | 'secret': 'learnRestApiwithNickjs', // used when we create and verify JSON Web Tokens 153 | 'database': 'mongodb://localhost:27017/test' // 填写本地自己 mongodb 连接地址,xxx为数据表名 154 | }; 155 | 156 | ``` 157 | 158 | ### 本地服务器配置 159 | `./index.js` 服务器配置文件,也是程序的入口。 160 | 161 | 这里我们主要用来包含我们程序需要加载的库文件,调用初始化程序所需要的依赖。 162 | 163 | ```javascript 164 | const express = require('express'); 165 | const app = express(); 166 | const bodyParser = require('body-parser');// 解析body字段模块 167 | const morgan = require('morgan'); // 命令行log显示 168 | const mongoose = require('mongoose'); 169 | const passport = require('passport');// 用户认证模块passport 170 | const Strategy = require('passport-http-bearer').Strategy;// token验证模块 171 | const routes = require('./routes'); 172 | const config = require('./config'); 173 | 174 | let port = process.env.PORT || 8080; 175 | 176 | app.use(passport.initialize());// 初始化passport模块 177 | app.use(morgan('dev'));// 命令行中显示程序运行日志,便于bug调试 178 | app.use(bodyParser.urlencoded({ extended: false })); 179 | app.use(bodyParser.json()); // 调用bodyParser模块以便程序正确解析body传入值 180 | 181 | routes(app); // 路由引入 182 | 183 | mongoose.Promise = global.Promise; 184 | mongoose.connect(config.database); // 连接数据库 185 | 186 | app.listen(port, () => { 187 | console.log('listening on port : ' + port); 188 | }) 189 | 190 | ``` 191 | 192 | ### 路由配置 193 | `./routes` 主要存放路由相关文件 194 | 195 | `./routes/index.js` 路由总入口,引入所使用路由 196 | 197 | ```javascript 198 | module.exports = (app) => { 199 | app.get('/', (req, res) => { 200 | res.json({ message: 'hello index!'}); 201 | }); 202 | 203 | app.use('/api', require('./users')); // 在所有users路由前加/api 204 | }; 205 | ``` 206 | 207 | `./routes/users.js` 208 | 209 | ``` javascript 210 | 211 | const express = require('express'); 212 | const User = require('../models/user'); 213 | const jwt = require('jsonwebtoken'); 214 | const config = require('../config'); 215 | const passport = require('passport'); 216 | const router = express.Router(); 217 | 218 | require('../passport')(passport); 219 | 220 | // 注册账户 221 | router.post('/signup', (req, res) => { 222 | if (!req.body.name || !req.body.password) { 223 | res.json({success: false, message: '请输入您的账号密码.'}); 224 | } else { 225 | var newUser = new User({ 226 | name: req.body.name, 227 | password: req.body.password 228 | }); 229 | // 保存用户账号 230 | newUser.save((err) => { 231 | if (err) { 232 | return res.json({success: false, message: '注册失败!'}); 233 | } 234 | res.json({success: true, message: '成功创建新用户!'}); 235 | }); 236 | } 237 | }); 238 | 239 | // 检查用户名与密码并生成一个accesstoken如果验证通过 240 | router.post('/user/accesstoken', (req, res) => { 241 | User.findOne({ 242 | name: req.body.name 243 | }, (err, user) => { 244 | if (err) { 245 | throw err; 246 | } 247 | if (!user) { 248 | res.json({success: false, message:'认证失败,用户不存在!'}); 249 | } else if(user) { 250 | // 检查密码是否正确 251 | user.comparePassword(req.body.password, (err, isMatch) => { 252 | if (isMatch && !err) { 253 | var token = jwt.sign({name: user.name}, config.secret,{ 254 | expiresIn: 10080 // token 过期销毁时间设置 255 | }); 256 | user.token = token; 257 | user.save(function(err){ 258 | if (err) { 259 | res.send(err); 260 | } 261 | }); 262 | res.json({ 263 | success: true, 264 | message: '验证成功!', 265 | token: 'Bearer ' + token, 266 | name: user.name 267 | }); 268 | } else { 269 | res.send({success: false, message: '认证失败,密码错误!'}); 270 | } 271 | }); 272 | } 273 | }); 274 | }); 275 | 276 | // passport-http-bearer token 中间件验证 277 | // 通过 header 发送 Authorization -> Bearer + token 278 | // 或者通过 ?access_token = token 279 | router.get('/user/user_info', 280 | passport.authenticate('bearer', { session: false }), 281 | function(req, res) { 282 | res.json({username: req.user.name}); 283 | }); 284 | 285 | module.exports = router; 286 | 287 | ``` 288 | 289 | ### passport配置 290 | `./passport.js` 配置权限模块所需功能 291 | 292 | ``` javascript 293 | const passport = require('passport'); 294 | const Strategy = require('passport-http-bearer').Strategy; 295 | 296 | const User = require('./models/user'); 297 | const config = require('./config'); 298 | 299 | module.exports = function(passport) { 300 | passport.use(new Strategy( 301 | function(token, done) { 302 | User.findOne({ 303 | token: token 304 | }, function(err, user) { 305 | if (err) { 306 | return done(err); 307 | } 308 | if (!user) { 309 | return done(null, false); 310 | } 311 | return done(null, user); 312 | }); 313 | } 314 | )); 315 | }; 316 | 317 | ``` 318 | 319 | 主要验证发送的token值与用户服务器端token值是否匹配,进行信息验证。 320 | 321 | ### 具体调试 322 | 323 | 现在就可以运行我们的代码看具体运作过程了!为了便于调试与参数的收发,我们使用[postman](https://www.getpostman.com/)(可在Chrome上或Mac上安装)来操作. 324 | 325 | `node index`运行我们的本地服务器,访问 [localhost:8080/]() 326 | 应该就可以看到我们所返回的初始json值了,然我们继续深入测试。 327 | 328 | ![](https://haitao.nos.netease.com/942f5170-3e46-4214-9841-dbb60344366f_1030_680.jpg) 329 | 330 | POST访问[localhost:8080/api/signup](),我们来注册一个新用户,注意要设置`body`的`Content-Type`为`x-www-form-urlencoded` 以便我们的`body-parser`能够正确解析,好的我们成功模拟创建了我们的新用户。 331 | 332 | ![](https://haitao.nos.netease.com/3d7edd98-070f-495f-a7fd-270e9eab7133_1030_680.jpg) 333 | 334 | 连接一下数据库看下我们的用户信息是否也被正确存储(注:我使用的是MongoChef,十分强大MongoDB数据库管理软件),我们可以看到,我的password也被正确加密保存了。 335 | 336 | ![](https://haitao.nos.netease.com/32ddbcae-0029-48f9-bf62-0b90ff8d5948_1050_712.jpg) 337 | 338 | 接着POST访问[localhost:8080/api/user/accesstoken](),来为我的用户获得专属token,POST过程与注册相关,可以看到也正确生成我们的token值。 339 | 340 | ![](https://haitao.nos.netease.com/854a96d4-0785-4631-b67a-790d13e73bd7_1030_680.jpg) 341 | 342 | 再看下我们的数据库中的用户信息,token值也被存入了进来,便于我们之后进行权限验证。 343 | 344 | ![](https://haitao.nos.netease.com/e8ce921f-d003-494e-aff5-06057a14e8bc_1050_712.jpg) 345 | 346 | GET访问[localhost:8080/api/user/user_info](),同时将我们的token值在`Header`中以 `Authorization: token` 传入,正确获得用户名则表示我们访问请求通过了验证。 347 | 348 | ![](https://haitao.nos.netease.com/e53d21d8-0f60-40d1-b698-ee966657dc56_1030_680.jpg) 349 | 350 | 如果token值不正确,则返回HTTP状态码 401 Unauthorized 并拒绝访问请求。到这里我们的权限验证功能也就基本实现了。 351 | ![](https://haitao.nos.netease.com/a1b2811d-84af-4d0d-b0e9-5076ba91b261_1030_680.jpg) 352 | 353 | ### 总结 354 | 希望在看完这篇教程后能够对你在RESTful Api开发上有所启发,小生才疏学浅,过程中有什么不足的地方也欢迎指正。 355 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'secret': 'learnRestApiwithNickjs', // used when we create and verify JSON Web Tokens 3 | 'database': 'mongodb://localhost:27017/test' // 填写本地自己 mongodb 连接地址,xxx为数据表名 4 | }; 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const bodyParser = require('body-parser');// 解析body字段模块 4 | const morgan = require('morgan'); // 命令行log显示 5 | const mongoose = require('mongoose'); 6 | const passport = require('passport');// 用户认证模块passport 7 | const Strategy = require('passport-http-bearer').Strategy;// token验证模块 8 | const routes = require('./routes'); 9 | const config = require('./config'); 10 | 11 | let port = process.env.PORT || 8080; 12 | 13 | app.use(passport.initialize());// 初始化passport模块 14 | app.use(morgan('dev'));// 命令行中显示程序运行日志,便于bug调试 15 | app.use(bodyParser.urlencoded({ extended: false })); 16 | app.use(bodyParser.json()); // 调用bodyParser模块以便程序正确解析body传入值 17 | 18 | routes(app); 19 | 20 | mongoose.Promise = global.Promise; 21 | mongoose.connect(config.database); // 连接数据库 22 | 23 | app.listen(port, () => { 24 | console.log('listening on port : ' + port); 25 | }) 26 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const bcrypt = require('bcrypt'); 4 | 5 | const UserSchema = new Schema({ 6 | name: { 7 | type: String, 8 | unique: true, 9 | require: true 10 | }, 11 | password: { 12 | type: String, 13 | require: true 14 | }, 15 | token: { 16 | type: String 17 | } 18 | }); 19 | 20 | // 添加用户保存时中间件对password进行bcrypt加密,这样保证用户密码只有用户本人知道 21 | UserSchema.pre('save', function (next) { 22 | var user = this; 23 | if (this.isModified('password') || this.isNew) { 24 | bcrypt.genSalt(10, function (err, salt) { 25 | if (err) { 26 | return next(err); 27 | } 28 | bcrypt.hash(user.password, salt, function (err, hash) { 29 | if (err) { 30 | return next(err); 31 | } 32 | user.password = hash; 33 | next(); 34 | }); 35 | }); 36 | } else { 37 | return next(); 38 | } 39 | }); 40 | // 校验用户输入密码是否正确 41 | UserSchema.methods.comparePassword = function(passw, cb) { 42 | bcrypt.compare(passw, this.password, (err, isMatch) => { 43 | if (err) { 44 | return cb(err); 45 | } 46 | cb(null, isMatch); 47 | }); 48 | }; 49 | 50 | module.exports = mongoose.model('User', UserSchema); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 3 | "version": "1.0.0", 4 | "description": "基于node.js+express+mongodb的restful api开发用户模型认证相关", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Nickj", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bcrypt": "^0.8.7", 13 | "body-parser": "^1.15.2", 14 | "express": "^4.14.0", 15 | "jsonwebtoken": "^7.1.9", 16 | "mongoose": "^4.7.1", 17 | "morgan": "^1.7.0", 18 | "passport": "^0.3.2", 19 | "passport-http-bearer": "^1.0.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /passport.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const Strategy = require('passport-http-bearer').Strategy; 3 | 4 | const User = require('./models/user'); 5 | const config = require('./config'); 6 | 7 | module.exports = function(passport) { 8 | passport.use(new Strategy( 9 | function(token, done) { 10 | User.findOne({ 11 | token: token 12 | }, function(err, user) { 13 | if (err) { 14 | return done(err); 15 | } 16 | if (!user) { 17 | return done(null, false); 18 | } 19 | return done(null, user); 20 | }); 21 | } 22 | )); 23 | }; 24 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (app) => { 2 | app.get('/', (req, res) => { 3 | res.json({ message: 'hello index!'}); 4 | }); 5 | 6 | app.use('/api', require('./users')); // 在所有users路由前加/api 7 | }; 8 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const User = require('../models/user'); 3 | const jwt = require('jsonwebtoken'); 4 | const config = require('../config'); 5 | const passport = require('passport'); 6 | const router = express.Router(); 7 | 8 | require('../passport')(passport); 9 | 10 | // 注册账户 11 | router.post('/signup', (req, res) => { 12 | if (!req.body.name || !req.body.password) { 13 | res.json({success: false, message: '请输入您的账号密码.'}); 14 | } else { 15 | var newUser = new User({ 16 | name: req.body.name, 17 | password: req.body.password 18 | }); 19 | // 保存用户账号 20 | newUser.save((err) => { 21 | if (err) { 22 | return res.json({success: false, message: '注册失败!'}); 23 | } 24 | res.json({success: true, message: '成功创建新用户!'}); 25 | }); 26 | } 27 | }); 28 | 29 | // 检查用户名与密码并生成一个accesstoken如果验证通过 30 | router.post('/user/accesstoken', (req, res) => { 31 | User.findOne({ 32 | name: req.body.name 33 | }, (err, user) => { 34 | if (err) { 35 | throw err; 36 | } 37 | if (!user) { 38 | res.json({success: false, message:'认证失败,用户不存在!'}); 39 | } else if(user) { 40 | // 检查密码是否正确 41 | user.comparePassword(req.body.password, (err, isMatch) => { 42 | if (isMatch && !err) { 43 | var token = jwt.sign({name: user.name}, config.secret,{ 44 | expiresIn: 10080 // token 过期销毁时间设置 45 | }); 46 | user.token = token; 47 | user.save(function(err){ 48 | if (err) { 49 | res.send(err); 50 | } 51 | }); 52 | res.json({ 53 | success: true, 54 | message: '验证成功!', 55 | token: 'Bearer ' + token, 56 | name: user.name 57 | }); 58 | } else { 59 | res.send({success: false, message: '认证失败,密码错误!'}); 60 | } 61 | }); 62 | } 63 | }); 64 | }); 65 | 66 | // passport-http-bearer token 中间件验证 67 | // 通过 header 发送 Authorization -> Bearer + token 68 | // 或者通过 ?access_token = token 69 | router.get('/user/user_info', 70 | passport.authenticate('bearer', { session: false }), 71 | function(req, res) { 72 | res.json({username: req.user.name}); 73 | }); 74 | 75 | module.exports = router; 76 | --------------------------------------------------------------------------------