本文介绍基于Node.js和MySQL搭建一个简单的Rest API应用,认证基于JWT(JSON Web Token)。其他用到的技术包括:路由:Express,ORM/数据库:Sequelize, 认证:Passport。
源码的Github地址:https://github.com/brianalois/node_rest_api_mysql
App结构
这里结合了标准的express app结构和sequelize的组织代码的结构,以及使用了一些Laravel结构。
— bin
— config
- - - config.js
— controllers
- - - CompanyController.js
- - - HomeController.js
— — — UserController.js
— middleware
- - - custom.js
- - - passport.js
— models
— — — index.js
— — — company.js
— — — user.js
— public
— routes
— — — v1.js
- seeders
- services
- - - AuthService.js
.env
app.js
global_functions.js
查阅代码
从.env开始
这个文件用于配置环境变量:
APP=dev
PORT=3000
DB_DIALECT=mysql
DB_HOST=localhost
DB_PORT=3306
DB_NAME=dbNameChange
DB_USER=rootChange
DB_PASSWORD=passwordChange
JWT_ENCRYPTION=PleaseChange
JWT_EXPIRATION=10000
主文件app.js
引入定义全局变量和全局函数的文件。
require('./config/config');
require('./global_functions');
添加依赖和初始化server:
const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
const passport = require('passport');
const v1 = require('./routes/v1');
const app = express();
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
//Passport
app.use(passport.initialize());
连接数据库和加载数据模型:
const models = require("./models");
models.sequelize.authenticate().then(() => {
console.log('Connected to SQL database');
})
.catch(err => {
console.error('Unable to connect to SQL database:', err);
});
if(CONFIG.app==='development'){
models.sequelize.sync();//creates tables from models
// models.sequelize.sync({ force: true });//good for testing
}
配置CORS,以便让其他网站可以请求api:
app.use(function (req, res, next) {
// 设置允许请求的域名
res.setHeader('Access-Control-Allow-Origin', '*');
// 允许请求的方法
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
// 允许的请求头
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, content-type, Authorization, Content-Type');
// 允许请求带有cookie信息
res.setHeader('Access-Control-Allow-Credentials', true);
next();
});
搭建路由和处理错误
app.use('/v1', v1);
app.use('/', function(req, res){
res.statusCode = 200;// 返回成功的请求码
res.json({status:"success", message:"Parcel Pending API", data:{}})
});
// 捕获404,并处理错误
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// 错误处理
app.use(function(err, req, res, next) {
//设置国际化,只在开发模式下提供错误信息
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// 渲染错误页面
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
config/config.js
app.js需要先加载此文件。它配置了全局的变量。一旦加载完config.js后就可以在所有的地方使用它,且不用再次引入。
require('dotenv').config();//初始化环境变量
CONFIG = {} //Make this global to use all over the application
CONFIG.app = process.env.APP || 'development';
CONFIG.port = process.env.PORT || '3000';
CONFIG.db_dialect = process.env.DB_DIALECT || 'mysql';
CONFIG.db_host = process.env.DB_HOST || 'localhost';
CONFIG.db_port = process.env.DB_PORT || '3306';
CONFIG.db_name = process.env.DB_NAME || 'name';
CONFIG.db_user = process.env.DB_USER || 'root';
CONFIG.db_password = process.env.DB_PASSWORD || 'db-password';
CONFIG.jwt_encryption = process.env.JWT_ENCRYPTION || 'jwt_please_change';
CONFIG.jwt_expiration = process.env.JWT_EXPIRATION || '10000';
global_functions.js
这个文件定义了一些全局函数,在app.js加载完后,我们就可以在任何地方使用这些全局函数,且不用再次引入。
to函数用于处理promise和错误。
to = function(promise) {
return promise
.then(data => {
return [null, data];
}).catch(err =>
[pe(err)]
);
}
pe = require('parse-error');
TE = function(err_message, log){ // TE 表示抛出错误
if(log === true){
console.error(err_message);
}
throw new Error(err_message);
}
ReE = function(res, err, code){ // 错误的web响应
if(typeof err == 'object' && typeof err.message != 'undefined'){
err = err.message;
}
if(typeof code !== 'undefined') res.statusCode = code;
return res.json({success:false, error: err});
}
ReS = function(res, data, code){ // 成功的web响应
let send_data = {success:true};
if(typeof data == 'object'){
send_data = Object.assign(data, send_data);//合并对象
}
if(typeof code !== 'undefined') res.statusCode = code;
return res.json(send_data)
};
//这里用于处理所有被拒绝的promise
process.on('unhandledRejection', error => {
console.error('Uncaught Error', pe(error));
});
搭建数据库和加载模型
models/index.js
'use strict';
var fs = require('fs');
var path = require('path');
var Sequelize = require('sequelize');
var basename = path.basename(__filename);
var db = {};
const sequelize = new Sequelize(CONFIG.db_name, CONFIG.db_user, CONFIG.db_password, {
host: CONFIG.db_host,
dialect: CONFIG.db_dialect,
port: CONFIG.db_port,
operatorsAliases: false
});
使用环境变量连接sequelize。
在model目录下加载所有的模型
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
var model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
导出Sequelize
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
User模型
models/user.js
导入模型
'use strict';
const bcrypt = require('bcrypt');
const bcrypt_p = require('bcrypt-promise');
const jwt = require('jsonwebtoken');
使用钩子函数和自定义的方法来构建Schema。在这里我们使用beforeSave钩子函数在每次修改密码时对密码进行哈希散列。
在User模型上自定义了一个方法用产生用户的JWT token。
'use strict';
const bcrypt = require('bcrypt');
const bcrypt_p = require('bcrypt-promise');
const jwt = require('jsonwebtoken');
module.exports = (sequelize, DataTypes) => {
var Model = sequelize.define('User', {
first : DataTypes.STRING,
last : DataTypes.STRING,
email : {type: DataTypes.STRING, allowNull: true, unique: true, validate: { isEmail: {msg: "Phone number invalid."} }},
phone : {type: DataTypes.STRING, allowNull: true, unique: true, validate: { len: {args: [7, 20], msg: "Phone number invalid, too short."}, isNumeric: { msg: "not a valid phone number."} }},
password : DataTypes.STRING,
});
Model.associate = function(models){
this.Companies = this.belongsToMany(models.Company, {through: 'UserCompany'});
};
Model.beforeSave(async (user, options) => {
let err;
if (user.changed('password')){
let salt, hash
[err, salt] = await to(bcrypt.genSalt(10));
if(err) TE(err.message, true);
[err, hash] = await to(bcrypt.hash(user.password, salt));
if(err) TE(err.message, true);
user.password = hash;
}
});
Model.prototype.comparePassword = async function (pw) {
let err, pass
if(!this.password) TE('password not set');
[err, pass] = await to(bcrypt_p.compare(pw, this.password));
if(err) TE(err);
if(!pass) TE('invalid password');
return this;
}
Model.prototype.getJWT = function () {
let expiration_time = parseInt(CONFIG.jwt_expiration);
return "Bearer "+jwt.sign({user_id:this.id}, CONFIG.jwt_encryption, {expiresIn: expiration_time});
};
Model.prototype.toWeb = function (pw) {
let json = this.toJSON();
return json;
};
return Model;
};
Company模型
models/company.js
'use strict';
module.exports = (sequelize, DataTypes) => {
var Model = sequelize.define('Company', {
name: DataTypes.STRING
});
Model.associate = function(models){
this.Users = this.belongsToMany(models.User, {through: 'UserCompany'});
};
Model.prototype.toWeb = function (pw) {
let json = this.toJSON();
return json;
};
return Model;
};
App的路由
routes/v1.js
导入模块以及配置passport中间件。
const express = require('express');
const router = express.Router();
const UserController = require('./../controllers/UserController');
const CompanyController = require('./../controllers/CompanyController');
const HomeController = require('./../controllers/HomeController');
const custom = require('./../middleware/custom');
const passport = require('passport');
const path = require('path');
基本的增删查改的路由,可以使用postman或者curl来测试。在app.js里给api限定了版本,所以需要添加/v1/{api}来访问。如:
url: localhost:3000/v1/users
User路由
router.post('/users', UserController.create); //create
router.get('/users',passport.authenticate('jwt', {session:false}), UserController.get); //read
router.put('/users',passport.authenticate('jwt', {session:false}), UserController.update); //update
router.delete('/users',passport.authenticate('jwt',{session:false}), UserController.remove); //delete
router.post( '/users/login', UserController.login);
Company路由
router.post( '/companies',
passport.authenticate('jwt', {session:false}), CompanyController.create);
router.get( '/companies', passport.authenticate('jwt', {session:false}), CompanyController.getAll);
router.get( '/companies/:company_id', passport.authenticate('jwt', {session:false}), custom.company, CompanyController.get);
router.put( '/companies/:company_id', passport.authenticate('jwt', {session:false}), custom.company, CompanyController.update);
router.delete( '/companies/:company_id', passport.authenticate('jwt', {session:false}), custom.company, CompanyController.remove);
导出路由
module.exports = router;
middleware/passport.js
引入模块
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const User = require('../models').User;
这里使用了passport中间件来对所有的路由定义用户信息。我们把userId存放在jwt token里,它包含在头信息Authorization里,如Bearer a23uiabsdkjd….。
passport中间件读取token里的user id,然后获取用户信息,并把它发送给controller。
module.exports = function(passport){
var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = CONFIG.jwt_encryption;
passport.use(new JwtStrategy(opts, async function(jwt_payload, done){
let err, user;
[err, user] = await to(User.findById(jwt_payload.user_id));
console.log('user', user.id);
if(err) return done(err, false);
if(user) {
return done(null, user);
}else{
return done(null, false);
}
}));
}
自定义中间件
middleware/custom.js
const Company = require('./../models').Company;
let company = async function (req, res, next) {
let company_id, err, company;
company_id = req.params.company_id;
[err, company] = await to(Company.findOne({where:{id:company_id}}));
if(err) return ReE(res, "err finding company");
if(!company) return ReE(res, "Company not found with id: "+company_id);
let user, users_array, users;
user = req.user;
[err, users] = await to(company.getUsers());
users_array = users.map(obj=>String(obj.user));
if(!users_array.includes(String(user._id))) return ReE(res, "User does not have permission to read app with id: "+app_id);
req.company = company;
next();
}
module.exports.company = company;
Controller
ontrollers/UserController.js
引入模块
const User = require('../models').User;
const authService = require('./../services/AuthService');
新建create
记住ReE是一个辅助函数,它让所有的错误响应格式相同。 使用service来执行实际的创建用户。
const create = async function(req, res){
res.setHeader('Content-Type', 'application/json');
const body = req.body;
if(!body.unique_key && !body.email && !body.phone){
return ReE(res, 'Please enter an email or phone number to register.');
} else if(!body.password){
return ReE(res, 'Please enter a password to register.');
}else{
let err, user;
[err, user] = await to(authService.createUser(body));
if(err) return ReE(res, err, 422);
return ReS(res, {message:'Successfully created new user.', user:user.toWeb(), token:user.getJWT()}, 201);
}
}
module.exports.create = create;
查询get
从中间件passport的req.user返回用户信息。需要在请求的头信息包含token,Authorization: Bearer Jasud2732r…
const get = async function(req, res){
res.setHeader('Content-Type', 'application/json');
let user = req.user;
return ReS(res, {user:user.toWeb()});
}
module.exports.get = get;
更新update
const update = async function(req, res){
let err, user, data
user = req.user;
data = req.body;
user.set(data);
[err, user] = await to(user.save());
if(err){
if(err.message=='Validation error') err = 'The email address or phone number is already in use';
return ReE(res, err);
}
return ReS(res, {message :'Updated User: '+user.email});
}
module.exports.update = update;
删除remove
const remove = async function(req, res){
let user, err;
user = req.user;
[err, user] = await to(user.destroy());
if(err) return ReE(res, 'error occured trying to delete user');
return ReS(res, {message:'Deleted User'}, 204);
}
module.exports.remove = remove;
登陆Login
返回认证的token
const login = async function(req, res){
const body = req.body;
let err, user;
[err, user] = await to(authService.authUser(req.body));
if(err) return ReE(res, err, 422);
return ReS(res, {token:user.getJWT(), user:user.toWeb()});
}
module.exports.login = login;
AuthService
services/AuthService.js
引入模块
const User = require('./../models').User;
const validator = require('validator');
如果用户可以使用电子邮件或电话号码,我们很乐意。这个方法把它组合在unique_key变量里。我们将在创建用户函数中使用它。
const getUniqueKeyFromBody = function(body){// 这里提供了三种选择,unique_key,email和phone
let unique_key = body.unique_key;
if(typeof unique_key==='undefined'){
if(typeof body.email != 'undefined'){
unique_key = body.email
}else if(typeof body.phone != 'undefined'){
unique_key = body.phone
}else{
unique_key = null;
}
}
return unique_key;
}
module.exports.getUniqueKeyFromBody = getUniqueKeyFromBody;
新建用户
验证email是否合法,或者phone是否合法,以及它们是否唯一,然后把它保存在数据库。
const createUser = async function(userInfo){
let unique_key, auth_info, err;
auth_info={}
auth_info.status='create';
unique_key = getUniqueKeyFromBody(userInfo);
if(!unique_key) TE('An email or phone number was not entered.');
if(validator.isEmail(unique_key)){
auth_info.method = 'email';
userInfo.email = unique_key;
[err, user] = await to(User.create(userInfo));
if(err) TE('user already exists with that email');
return user;
}else if(validator.isMobilePhone(unique_key, 'any')){
auth_info.method = 'phone';
userInfo.phone = unique_key;
[err, user] = await to(User.create(userInfo));
if(err) TE('user already exists with that phone number');
return user;
}else{
TE('A valid email or phone number was not entered.');
}
}
module.exports.createUser = createUser;
验证用户
const authUser = async function(userInfo){//returns token
let unique_key;
let auth_info = {};
auth_info.status = 'login';
unique_key = getUniqueKeyFromBody(userInfo);
if(!unique_key) TE('Please enter an email or phone number to login');
if(!userInfo.password) TE('Please enter a password to login');
let user;
if(validator.isEmail(unique_key)){
auth_info.method='email';
[err, user] = await to(User.findOne({where:{email:unique_key}}));
console.log(err, user, unique_key);
if(err) TE(err.message);
}else if(validator.isMobilePhone(unique_key, 'any')){//checks if only phone number was sent
auth_info.method='phone';
[err, user] = await to(User.findOne({where:{phone:unique_key }}));
if(err) TE(err.message);
}else{
TE('A valid email or phone number was not entered');
}
if(!user) TE('Not registered');
[err, user] = await to(user.comparePassword(userInfo.password));
if(err) TE(err.message);
return user;
}
module.exports.authUser = authUser;
Company Controller
controllers/CompanyController.js
这是和UserController的结构是一样的。
Create
const Company = require('../models').Company;
const create = async function(req, res){
res.setHeader('Content-Type', 'application/json');
let err, company;
let user = req.user;
let company_info = req.body;
[err, company] = await to(Company.create(company_info));
if(err) return ReE(res, err, 422);
company.addUser(user, { through: { status: 'started' }})
[err, company] = await to(company.save());
if(err) return ReE(res, err, 422);
let company_json = company.toWeb();
company_json.users = [{user:user.id}];
return ReS(res,{company:company_json}, 201);
}
module.exports.create = create;
获取属于用户的所有公司
const getAll = async function(req, res){
res.setHeader('Content-Type', 'application/json');
let user = req.user;
let err, companies;
[err, companies] = await to(user.getCompanies());
let companies_json =[]
for( let i in companies){
let company = companies[i];
let users = await company.getUsers()
let company_info = company.toWeb();
let users_info = []
for (let i in users){
let user = users[i];
// let user_info = user.toJSON();
users_info.push({user:user.id});
}
company_info.users = users_info;
companies_json.push(company_info);
}
return ReS(res, {companies:companies_json});
}
module.exports.getAll = getAll;
Get
const get = function(req, res){
res.setHeader('Content-Type', 'application/json');
let company = req.company;
return ReS(res, {company:company.toWeb()});
}
module.exports.get = get;
更新Upate
const update = async function(req, res){
let err, company, data;
company = req.company;
data = req.body;
company.set(data);
[err, company] = await to(company.save());
if(err){
return ReE(res, err);
}
return ReS(res, {company:company.toWeb()});
}
module.exports.update = update;
删除Remove
const remove = async function(req, res){
let company, err;
company = req.company;
[err, company] = await to(company.destroy());
if(err) return ReE(res, 'error occured trying to delete the company');
return ReS(res, {message:'Deleted Company'}, 204);
}
module.exports.remove = remove;
这里简单介绍了整个代码,到此就结束了。
意译原文:https://codeburst.io/build-a-rest-api-for-node-mysql-2018-jwt-6957bcfc7ac9