跳到主要內容

Nodejs、momgodb user authentication with JSON Web Tokens


目標

  • 以RESTful API實作用戶註冊、登入、取得用戶資訊及驗證用戶身分
  • 使用mongodb資料庫及mongoose產生Schema
  • 使用passport進行中間件擴展,支援多種驗證方式,如OAuth、本地驗證...等
  • 以jwt進行token的產生及識別
  • 使用bcrypt對用戶密碼加解密

2016/9/24 新增
  • user權限(role)
  • 編輯user
待完成

  • 加入刪除user的權限
  • 加入swagger ui
Source Code:https://github.com/weihanchen/NodeJS_User_Authentication

Installation

Package Dependence

{
"name": "user_authentication_api",
"description": "api server",
"node-main": "./run.js",
"dependencies": {
"bcrypt-nodejs": "^0.0.3",
"body-parser": "^1.15.2",
"express": "^4.14.0",
"jwt-simple": "^0.5.0",
"morgan": "^1.7.0",
"moment": "^2.14.1",
"mongoose": "^4.6.1",
"passport": "^0.3.2",
"passport-jwt": "^2.1.0"
},
"devDependencies": {
"mocha": "^3.0.2",
"nodemon": "^1.9.1",
"supertest": "^2.0.0",
"should": "^11.1.0"
},
"scripts": {
"dev": "node dev.js",
"test": "./node_modules/.bin/mocha test",
"product": "node run.js"
},
"author": "will.chen",
"license": "ISC"
}
view raw package.json hosted with ❤ by GitHub

  1. PassportJS - for user password security
  2. Simple JWT - token use
  3. Mongoose -  mongodb object modeling

目錄結構

NodeJS_User_Authentication
 |-config
    |-database.js - token密鑰及db連線訊息
    |-initial.js - 初始化配置(role permissions)
 |-middleware
    |-jwtauth.js - 以passport-jwt建構本地jwt驗證策略
 |-models
    |- role.js - 創建role schema
    |- users.js - 創建user schema並提供儲存用戶、密碼加密、密碼比對方法
 |-node_modules - package dependences 
 |-routes
    |- initial.js - 初始化role permissions
    |- users.js - 用戶路由,包括登入、註冊取得用戶資訊
 |-services
    |- error
        |-builder.js - 錯誤訊息、狀態碼定義
    |- permissions
        |- validator.js - 驗證user操作權限的相關邏輯
 |- test
    |- routes
        |- users.js
    |- all.test.js 測試入口
 package.json
 readme.md
 run.js

Step By Step


config/database.js,配置db連線字串及token密鑰,建議使用環境變數避免hard code於程式碼內

module.exports = {
'secret': process.env.SECRET_KEY || 'user_auth_demo',
'database': process.env.MONGO_CONNECTION || 'mongodb://username:password@localhost:27017/user_auth_demo'
};
config/initial.js, 配置初始化帳戶、權限

let adminRoleLevel = Number.MAX_SAFE_INTEGER;
let userRoleLevel = 0;
module.exports = {
'admin_account': process.env.ADMIN_ACCOUNT || 'superadmin',
'admin_password': process.env.ADMIN_PASSWORD || 'superadmin',
'admin_role_level': adminRoleLevel,
'roles': [
{
'role': 'admin',
'level': adminRoleLevel
},
{
'role': 'user',
'level': userRoleLevel
}
],
'user_role_level': userRoleLevel
};
創建models/user.js,建立用戶模型,使用mongoose對mongodb進行schema定義

let mongoose = require('mongoose');
let Schema = mongoose.Schema;
let bcrypt = require('bcrypt-nodejs');
// set up a mongoose model
let UserSchema = new Schema({
username: {
type: String,
unique: true,
required: true
},
displayName: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
},
roleId: {
type: String,
required: true
}
});
UserSchema.pre('save', function(next) {
let user = this;
//密碼變更或新密碼時
if (user.isModified('password') || this.isNew) {
bcrypt.genSalt(10, function(err, salt) {
if (err) {
return next(err);
}
bcrypt.hash(user.password, salt, null, function(err, hash) {
if (err) {
return next(err);
}
//使用hash取代明文密碼
user.password = hash;
next();
});
});
} else {
return next();
}
});
/**
* mongoose支持擴展方法,因此撰寫密碼驗證
* @param {[string]} password [密碼]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
UserSchema.methods.comparePassword = function(candidatePassword, callback) {
bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
if (err) {
return callback(err);
}
callback(null, isMatch);
});
};
module.exports = mongoose.model('User', UserSchema);
});
} else {
return next();
}
});
/**
* mongoose支持擴展方法,因此撰寫密碼驗證
* @param {[string]} password [密碼]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
UserSchema.methods.comparePassword = function(candidatePassword, callback) {
bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
if (err) {
return callback(err);
}
callback(null, isMatch);
});
};
module.exports = mongoose.model('User', UserSchema);
view raw models-user.js hosted with ❤ by GitHub
創建models/role.js,建立角色模型

let mongoose = require('mongoose');
let Schema = mongoose.Schema;
let RoleSchema = new Schema({
role: {
type: String,
unique: true,
required: true
},
level: {
type: Number,
unique: true,
required: true
}
});
module.exports = mongoose.model('Role', RoleSchema);
view raw models-role.js hosted with ❤ by GitHub
middleware/jwtauth.js設計驗證權限的中間件,這裡使用ExtractJwt對request進行解析token


let passport = require("passport");
let passportJWT = require("passport-jwt");
let User = require(__base + 'models/user');
let config = require(__base + 'config/database');
let errorBuilder = require(__base + 'services/error/builder');
let ExtractJwt = passportJWT.ExtractJwt;//extract jwt token
let Strategy = passportJWT.Strategy;//策略選擇為jwt
let params = {
secretOrKey: config.secret,
jwtFromRequest: ExtractJwt.fromAuthHeader() //creates a new extractor that looks for the JWT in the authorization header with the scheme 'JWT',e.g JWT + 'token'
};
module.exports = function() {
let strategy = new Strategy(params, function(payload, done) {
//驗證token是否失效
if (payload.exp <= Date.now()) {
return done(errorBuilder.unauthorized('Access token has expired'), false);
}
//根據解析後id取得user,並驗證user是否存在
User.findOne({ _id: payload.iss }, function(err, user) {
if (err) return done(err, false);
if (user) done(null, user);
else done(null, false);
});
});
passport.use(strategy);
return {
initialize: function() {
return passport.initialize();
},
authenticate: function() {
return passport.authenticate("jwt", { session: false });
}
};
};
routes/initial.js, 設定最高管理者、各個角色

'use strict';
let initial_config = require(__base + 'config/initial'); // get initial config file
let errorBuilder = require(__base + 'services/error/builder');
// get the mongoose model
let User = require(__base + 'models/user');
let Role = require(__base + 'models/role');
exports.initialize = (req, res, next) => {
let errorHandler = (error) => {
next(error);
}
setRoles().then(() => {
setAdminUser().then(() => {
res.json({
success: true,
message: 'Successful initialize.'
})
}, errorHandler)
}, errorHandler)
}
//private methods
function setAdminUser() {
let deferred = Promise.defer();
let dbErrorHandler = (error) => {
deferred.reject(errorBuilder.badRequest(err.errmsg));
}
Role.findOne({ $query: {}, $orderby: { level: -1 } }).exec().then(role => {
let adminUser = new User({
displayName: initial_config.admin_account,
username: initial_config.admin_account,
password: initial_config.admin_password,
roleId: role._id
})
adminUser.save().then(() => {
deferred.resolve();
}).catch(dbErrorHandler);
}).catch(dbErrorHandler);
return deferred.promise;
}
function setRoles() {
let promises = [];
let result = Promise.defer();
let roles = initial_config.roles;
roles.forEach(role => {
let deferred = Promise.defer();
let newRole = new Role(role);
newRole.save(error => {
if (error) deferred.reject(errorBuilder.badRequest(error.errmsg));
else deferred.resolve();
});
promises.push(deferred.promise);
})
Promise.all(promises).then(() => {
result.resolve();
}, error => {
result.reject(error);
})
return result.promise;
}
routes/users.js,包括登入、註冊、取得用戶訊息

'use strict';
let config = require(__base + 'config/database'); // get db config file
let errorBuilder = require(__base + 'services/error/builder');
let User = require(__base + 'models/user.js'); // get the mongoose model
let jwt = require('jwt-simple');
let moment = require('moment');
exports.login = (req, res, next) => {
User.findOne({
username: req.body.username
}, (error, user) => {
if (error) next(errorBuilder.badRequest(error));
if (!user) next(errorBuilder.badRequest('User not found.'));
else {
user.comparePassword(req.body.password, (error, isMatch) => { //使用user schema中定義的comparePassword檢查請求密碼是否正確
if (isMatch && !error) {
let expires = moment().add(1, 'day').valueOf();
let token = jwt.encode({
iss: user.id, //加密對象
exp: expires
}, config.secret);
res.json({ success: true, token: 'JWT ' + token }); //JWT for passport-jwt extract fromAuthHeader
} else {
next(errorBuilder.badRequest('Wrong password.'));
}
})
}
})
}
exports.me = (req, res, next) => { //get users/me之前經過中間件驗證用戶權限,當驗證通過便取得正確用戶訊息,直接回傳即可
let responseBody = {
username: req.user.username,
displayName: req.user.displayName
}
res.send(responseBody);
}
exports.signup = (req, res, next) => {
let requireProperties = ['displayName', 'password', 'username'];
let propertyMissingMsg = '';
let requireValid = requireProperties.every(property => {
if (!req.body.hasOwnProperty(property)) {
propertyMissingMsg = 'Please pass ' + property;
return false;
}
return true;
})
if (!requireValid) {
next(errorBuilder.badRequest(propertyMissingMsg));
return;
}
if (!req.body.username || !req.body.password) next(errorBuilder.badRequest('Please pass username and password'))
else {
let newUser = new User({
username: req.body.username,
displayName: req.body.displayName,
password: req.body.password
})
User.findOne({ username: newUser.username }, (error, user) => {
if (error) next(errorBuilder.internalServerError());
else if (user) {
next(errorBuilder.badRequest('username already exist.'));
} else {
newUser.save((error) => {
if (error) next(errorBuilder.internalServerError());
else res.json({ success: true, message: 'Successful signup.' });
})
}
})
}
}
view raw routes-users.js hosted with ❤ by GitHub

  • 登入 - 檢查用戶是否存在、解密並檢查密碼正確性、產生jwt token
  • 註冊 - 檢查request body 是否包含username、password、displayName,檢查username是否重複
  • 取得用戶資訊- 中間件jwtauth.js通過驗證時設定req.user,因此直接回傳req.user
run.js,路由及相關中間件設定,其中/users/me加入jwtauth.authenticate驗證token是否具有存取權限
global.__base = __dirname + '/';
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
//the routing modules
const users = require(__base + 'routes/users');
const initial = require(__base + 'routes/initial');
app.set('port', process.env.PORT || 3000);
let config = require(__base + 'config/database'); // get db config file
let morgan = require('morgan');
let mongoose = require('mongoose');
let jwtauth = require(__base + 'middleware/jwtauth')();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// log to console
app.use(morgan('dev'));
app.use(jwtauth.initialize());
mongoose.Promise = global.Promise;
mongoose.connect(config.database);
let apiRoutes = express.Router();
apiRoutes.route('/initialize')
.post(initial.initialize)
apiRoutes.route('/users')
.post(users.signup)
apiRoutes.route('/users/login')
.post(users.login)
apiRoutes.use(jwtauth.authenticate()).route('/users/me')
.get(users.me)
apiRoutes.use(jwtauth.authenticate()).route('/users/:id')
.delete(users.delete)
.get(users.info)
.put(users.edit)
app.use('/api', apiRoutes);
app.use(errorHandler);
app.listen(app.get('port'), () => {
console.log('Express server listening on port ' + app.get('port'));
});
function errorHandler(err, req, res, next) {
res.status(err.status || 500).json(err);
}
module.exports = app;
view raw run.js hosted with ❤ by GitHub

使用postman工具進行API驗證


* 註冊用戶


* 登入 - 並取得token


* 取得用戶資訊-  未帶入token


* 取得用戶資訊 - 帶入token


留言

這個網誌中的熱門文章

C#資料庫操作(新增、修改、刪除、查詢)

java西元民國轉換_各種不同格式