Express
Express,高度包容、快速而极简的 Node.js Web 框架。通过本章节,你将了解到一下内容:
- 使用 express-generator 快速搭建项目框架
- 使用 nunjucks 作为视图模版
- 使用 favicon.ico 设置图标
- 使用 axios 请求第三方接口
- 使用 cors 允许跨域请求
- 使用 knex 增删改查数据库
- 使用 Base 公共模型优化
- 使用 session 完成 csrf 防御
- 使用 cookie 完成登录权限控制
本章主要为使用 Express Web 来体验 Nodejs Web,更多依赖包及使用方法,数据库SQL的运用及后端安全知识,请自行查阅相关专业信息。
express-generator
express-generator 是 Express 应用程序生成器工具,我们可以使用它来快速创建应用程序框架。
- 全局安装 express-generator 项目构建工具,express-generator 会帮忙我们快速创建一个基于 express 的 web后端服务代码,已经完成了基础需要依赖及配置。
npm install -g express-generator
- 初始化项目,进入桌面使用 express appName,这时候桌面就创建 expressApp 文件夹,里面包含基本的项目文件
cd ~/Desktop && express expressApp && cd expressApp
- 下载相关依赖,我们打开 expressApp/package.json 会发现包含有以下依赖,可以点击查看依赖包的作用及使用方法:
- cookie-parser 用于管理 cookie
- debug 用于打印调试
- express Nodejs Web 框架
- http-errors 网络错误管理
- jade 视图模版
- morgan 日志组件
npm install
- 启动项目
npm start
浏览器打开http://localhost:3000/,可以看到 Welcome to Express 啦!
- 目录结构分析
express-generator 帮助我们创建及配置好项目文件,主要有以下:
- app.js 主文件
- bin/www 启动入口文件
- package.json 依赖包管理文件
- public 静态资源目录
- routes 路由目录
- views 模版目录
.
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.pug
├── index.pug
└── layout.pug
app.js
// 各个依赖包
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
// 路由文件引用
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
// Express 引用实例化
var app = express();
// 视图模版设置
// 设置视图模版目录,设置视图模版后缀为 jade 文件
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// 使用 morgan 日志打印
app.use(logger('dev'));
// 使用对 Post 来的数据 json 格式化
app.use(express.json());
// 使用对 表单提交的数据 进行格式化
app.use(express.urlencoded({ extended: false }));
// 使用 cookie
app.use(cookieParser());
// 设置静态文件地址路径为 public
app.use(express.static(path.join(__dirname, 'public')));
// 使用配置好的路由
app.use('/', indexRouter);
app.use('/users', usersRouter);
// 捕捉404错误
app.use(function(req, res, next) {
next(createError(404));
});
// 监听异常如果有,立刻返回异常
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;
nunjucks
Nunjucks是Mozilla开发的一个纯JavaScript编写的模板引擎,既可以用在Node环境下,又可以运行在浏览器端。但是,主要还是运行在Node环境下,因为浏览器端有更好的模板解决方案,例如MVVM框架。
如果你使用过Python的模板引擎jinja2,那么使用Nunjucks就非常简单,两者的语法几乎是一模一样的,因为Nunjucks就是用JavaScript重新实现了jinjia2。
为什么要使用 nunjucks ?
在 express-generator 默认为我们配置了 jade 模版(现在改名字为 pug ),jade 模版语法如下:
extends layout
block content
h1= title
p Welcome to #{title}
语法简洁,但是不利于对完整的 HTML 代码进行复制,有时候我们很多代码是从别的地方拷贝服用过来,统一的模版结构可以减少我们转换的工作,因此我们切换为 nunjucks 模版,nunjucks 模版和 swig 模版都可以,但是他们之间还是有语法的不一致,建议专注使用一个 。nunjucks 的语法如下,可以在 nunjucks 中直接书写 HTML 结构:
{% block header %}
This is the default content
{% endblock %}
<section class="left">
{% block left %}{% endblock %}
</section>
<section class="right">
{% block right %}
This is more content
{% endblock %}
</section>
接下来我们使用 nunjucks 并修改模版:
- 安装 nunjucks 依赖
npm install --save nunjucks
- 参照使用文档修改配置信息
app.js
// 引入 nunjucks
var nunjucks = require('nunjucks');
...
// 视图模版设置
// 1. 设置视图模版后缀修改为 tpl 文件
// 2. 添加 nunjucks 在 express 中的自动配置
// 3. 注释 设置 views 代码,在 nunjucks 自动配置中有设置
// app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'tpl');
nunjucks.configure('views', {
autoescape: true,
express: app,
watch: true
});
- 在 views 目录中添加 layout.tpl、index.tpl、error.tpl
layout.tpl
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{title}}</title>
{% block css %}
{% endblock %}
</head>
<body>
{% block content %}
{% endblock %}
{% block js %}
{% endblock %}
</body>
</html>
index.tpl
{% extends './layout.tpl' %}
{% block css %}
<link rel="stylesheet" href="/stylesheets/style.css">
{% endblock %}
{% block content %}
<h1>{{title}}</h1>
<p>Welcome to {{title}} with nunjucks!</p>
{% endblock %}
error.tpl
{% extends './layout.tpl' %}
{% block css %}
<link rel="stylesheet" href="/stylesheets/style.css">
{% endblock %}
{% block content %}
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
{% endblock %}
- 跑起来
npm start
favicon.ico
配置 favicon.ico 图标
- 下载serve-favicon 依赖
npm install -save serve-favicon
把 favicon.ico文件放置到 public 目录下
配置app.js
// 引入 serve-favicon 依赖包
var favicon = require('serve-favicon');
...
// 设置 favicon.ico 地址
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
- 可能缓存问题,关闭浏览器,重启服务多刷新几次就可以看到浏览器左上角的 favicon.ico 了
axios
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
本教材中因为使用 axios 作为网络请求,因为前端开发中逐渐的走向数据驱动视图化,慢慢的移除 jQuery框架,这时候 Ajax 之外的选择,axiso应运而生,不仅可以作为客户端网络请求,还可以作为服务端网络请求。
Node.js 中优秀的网络请求插件还有 request ,如果在某些条件下 axios 不能满足需求,我们可以考虑使用 request 。
- axios
- request(https://www.npmjs.com/package/request)
以下我们使用 axios 来实现一个 isbn 编码的信息查询接口,同时完善 MVC 的开发结构。
- 下载 axios
npm install -save axios
修改路由文件,把 routes/user.js 重命名为 routers/api.js
修改路由配置,因为我们是个接口地址,所以前缀应该为 /api ,而不是 /users/
app.js
// var usersRouter = require('./routes/users'); 修改为以下
var apiRouter = require('./routes/api');
// app.use('/users', usersRouter); 修改为以下
app.use('/api', apiRouter);
- 创建 models/douban.js ,豆瓣的数据管理Model
models/douban.js
// 引用 axios
const axios = require('axios');
// 定义豆瓣接口API
const ISBNAPI = 'https://api.douban.com/v2/book/isbn/';
const douban = {
// 更具 isbn 查询内容的方法
isbn:function(isbn){
// 返回一个 Promise
return new Promise((resolve,reject) => {
// 发起请求
axios.get(ISBNAPI + isbn + '?apikey=0df993c66c0c636e29ecbb5344252a4a')
.then( res => {
resolve(res.data)
}).catch( err => {
reject(err.response.data)
})
})
}
}
module.exports = douban;
- 创建 controllers/book.js ,书的控制器
controllers/book.js
// 引用豆瓣Model
const doubanModel = require('./../models/douban');
const book = {
// 书详情方法 async 方法
info:async function(req,res,next){
// 从 Get 请求中获取 isbn,如果为空,立即返回提示。
const ISBN = req.query.isbn;
if(!ISBN){
res.json({ code: 0,msg: 'isbn empty!'})
return
}
// 使用 try catch 捕获 reject 异常
try{
// 从豆瓣 Model 中获取数据并返回
const data = await doubanModel.isbn(ISBN);
res.json({ code: 200, data: data })
}catch(err) {
// 如果异常,返回错误信息
res.json({ code: 0, data: err })
}
}
}
module.exports = book;
- 修改 routes/api.js 创建路由入口
routes/api.js
// 引用路由
var express = require('express');
var router = express.Router();
// 引用书控制器
var bookController = require('./../controllers/book');
// 设置路由地址
router.get('/isbn', bookController.info);
module.exports = router;
CORS
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
上述 ISBN 编码查询的接口,如果我们再非同源的地址,例如在本地桌面新建一个文件然后使用 fetch 请求,会被浏览器禁止数据的返回。例如:
<!DOCTYPE html>
<html>
<head>
<title>TEST</title>
</head>
<body>
<script type="text/javascript">
fetch('http://localhost:3000/api/isbn?isbn=9787121317989').then((response)=>{
return response.json();
}).then(res => {
console.log(res)
}).catch( err => {
console.log(err)
})
</script>
</body>
</html>
会报以下错误:
Failed to load http://localhost:3000/api/isbn?isbn=9787121317989: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
- 新建 cors 中间件
middlewares/cors.js
const cors = {
allowAll:function(req,res,next){
res.header("Access-Control-Allow-Headers", "*");
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS');
res.header('Access-Control-Allow-Credentials', true);
next();
}
}
module.exports = cors;
- 在ISBN路由中引用
var express = require('express');
var router = express.Router();
var cors = require('./../middlewares/cors.js');
var bookController = require('./../controllers/book');
router.get('/isbn', cors.allowAll, bookController.info);
module.exports = router;
- 再次使用本地文件发起 fetch 请求测试,这时候就可以正常 console 到数据了。
knex
使用 knex 增删改查数据库
Knex.js是为Postgres,MSSQL,MySQL,MariaDB,SQLite3,Oracle和Amazon Redshift设计的“包含电池”SQL查询构建器,其设计灵活,便于携带并且使用起来非常有趣。它具有传统的节点样式回调以及用于清洁异步流控制的承诺接口,流接口,全功能查询和模式构建器,事务支持(带保存点),连接池 以及不同查询客户和方言之间的标准化响应。Knex的主要目标环境是Node.js,您需要安装该knex库,然后安装适当的数据库库:pg适用于PostgreSQL和Amazon Redshift,mysql适用于MySQL或MariaDB,sqlite3适用于SQLite3或mysql适用于MSSQL。
以下我们使用 knex 使用 mysql 链接操作数据库,使用前请确保安装以下工具:
- XAMPP 集成环境,用于启动数据库
- navicat - 数据库管理工具 - windows 用户安装
- Sequel Pro - 数据库管理工具 - macOS 用户安装
XMAPP 默认用户名为:root , 密码为空 , host 为 127.0.0.1 。请确保 XAMPP 启动,然后使用数据库管理工具连接成功,接着进行以下步骤:
注意:在 Mac 的新版本XAMPP中,host 未必为 127.0.0.1,可能是 192.168.64.2 ,需要按提示修改文件获取权限,同时设置新的用户和密码。
使用数据库管理工具新建数据库 database 名称为: expressapp 。
进入 expressApp 库,新建用户信息表为 users ,默认有 id 字段,需要再添加以下必要字段:
- Field:id
- Field:name Type:VARCHAR LENGTH:255 Comment:姓名
- Field:email Type:VARCHAR LENGTH:255 Comment:邮箱
- Field:password Type:VARCHAR LENGTH:255 Comment:密码
- 手动设置几个默认值,例如:
- name:Jay email:jay@qq.com password:123456
- name:Jeo email:jay@qq.com password:123456
- name:Jax email:jay@qq.com password:123456
- 下载项目相关依赖
npm install -save knex mysql
- 在项目根目录下,新建配置信息 config.js,之后的配置信息涉及到数据库和密码,不上传到 Github 等托管平台,所以需要单独设置,之后使用 .gitignore 避免上传。之后敏感的配置信息,都将在此配置。host 地址为数据库的服务地址,本地为 127.0.0.1 ,也可能在虚拟机上 192.168.64.2 ,以 XAMPP 开放的地址为准。
config.js
const configs = {
mysql: {
host: '127.0.0.1',
port: '3306',
user: 'root',
password: '',
database: 'expressapp'
}
}
module.exports = configs
- 在项目根目录下,新建 .gitignore 避免上传 config.js 及 node_modules 等不需要被上传到 Github 的文件。
gitignore
.DS_Store
.idea
npm-debug.log
yarn-error.log
node_modules
config.js
- 新建 models/knex.js 数据库配置,初始化配置 knex
// 引用配置文件
const configs = require('../config');
// 把配置文件中的信息,设置在初始化配置中
module.exports = require('knex')({
client: 'mysql',
connection: {
host: configs.mysql.host,
port: configs.mysql.port,
user: configs.mysql.user,
password: configs.mysql.password,
database: configs.mysql.database
}
})
- 新建 models/user.js 用户模型,并设置获取所有用户的方法
// 引用 knex
const knex = require('./../models/knex');
// 定义数据库表信息
const TABLE = 'users';
const User = {
// 获取所有用户的方法
all: function(){
// 返回 Promise
return knex.select().table(TABLE)
}
}
module.exports = User
- 新建视图文件 views/user/show.tpl ,用于显示用户控制的页面,视图中循环语法,请参考 nunjucs 文档。
views/user/show.tpl
{% extends './../layout.tpl' %}
{% block css %}
<link rel="stylesheet" href="/stylesheets/style.css">
{% endblock %}
{% block content %}
<div class="page">
<h1>用户管理</h1>
<div class="new-user">
<h2>新建用户</h2>
<input type="text" name="name" id="new-name" placeholder="请输入用户名">
<input type="email" name="email" id="new-email" placeholder="请输入邮箱账号">
<input type="password" name="password" id="new-password" placeholder="请输入密码">
<button id="new-submit">新建用户</button>
</div>
<div class="user-list">
<h2>用户列表</h2>
<ul>
{% for val in users %}
<li>
<span>id: {{val.id}}</span>
<span>email: {{val.email}}</span>
<input class="user-name" type="text" name="name" placeholder="姓名" value="{{val.name}}">
<button class="update-user" data-id="{{val.id}}">修改姓名</button>
<button class="delete-user" data-id="{{val.id}}">删除用户</button>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}
- 新建 controllers/user.js 用户控制器,并设置 show 方法
// 引用用户模版数据
const User = require('./../models/user.js');
const userController = {
// show 获取用户数据并返回到页面
show: async function(req,res,next){
try{
// 从模型中获取所有用户数据
const users = await User.all();
// 把用户数据设置到 res.locals 中
res.locals.users = users;
// 渲染到 views 视图目录的 user/show.tpl 路径中。
res.render('user/show.tpl',res.locals)
}catch(e){
res.locals.error = e;
res.render('error',res.locals)
}
},
}
module.exports = userController;
- 新建路由,修改 routes/index.js
var express = require('express');
var router = express.Router();
var userController = require('./../controllers/user.js');
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
router.get('/user', userController.show);
module.exports = router;
- 重启服务,打开http://localhost:3000/user 你将看到所有用户的信息。
knex 增删改
- 修改用户模型 Models/user.js , 添加操作数据库的方法,添加增、删、改方法。
const knex = require('./../models/knex');
const TABLE = 'users';
const User = {
all: function(){
return knex(TABLE).select()
},
// 添加用户
insert: function(params){
return knex(TABLE).insert(params)
},
// 修改用户
update: function(id, params ){
return knex(TABLE).where('id', '=', id).update( params )
},
// 删除用户
delete: function(id){
return knex(TABLE).where('id', '=', id).del()
}
}
module.exports = User
- 修改用户控制器 controllers/user.js,添加增删改查接受参数及输出的判断逻辑
controllers/user.js
const User = require('./../models/user.js');
const userController = {
show: async function(req,res,next){
try{
const users = await User.all();
res.locals.users = users;
res.render('user/show.tpl',res.locals)
}catch(e){
res.locals.error = e;
res.render('error',res.locals);
}
},
// 新增用户
insert: async function(req,res,next){
let name = req.body.name;
let email = req.body.email;
let password = req.body.password;
console.log(name,email,password);
if(!name || !email || !password){
res.json({ code: 0, data: 'params empty!' });
return
}
try{
const users = await User.insert({name,email,password});
res.json({ code: 200, data: users})
}catch(e){
res.json({ code: 0, data: e })
}
},
update: async function(req,res,next){
let id = req.body.id;
let name = req.body.name;
console.log(id,name);
if(!name || !id){
res.json({ code: 0, data: 'params empty!' });
return
}
try{
const user = await User.update(id,{ name });
res.json({ code: 200, data: user})
}catch(e){
res.json({ code: 0, data: e })
}
},
delete: async function(req,res,next){
let id = req.body.id;
if(!id){
res.json({ code: 0, data: 'params empty!' });
return
}
try{
const user = await User.delete(id);
res.json({ code: 200, data: user})
}catch(e){
res.json({ code: 0, data: e })
}
}
}
module.exports = userController;
- 修改路由 routes/api.js,添加用户增删改的接口。
routes/api.js
var express = require('express');
var router = express.Router();
var cors = require('./../middlewares/cors.js');
var bookController = require('./../controllers/book');
var userController = require('./../controllers/user');
/* GET users listing. */
router.get('/isbn', cors.allowAll,bookController.info);
// 同一个接口地址,但是不同的请求方法
router.post('/user' , userController.insert);
router.put('/user' , userController.update);
router.delete('/user' , userController.delete);
module.exports = router;
- 在视图中添加脚本逻辑,views/user/show.tpl 在底部添加以下脚本代码。
- 引用jQuery
- 绑定新建用户方法
- 绑定修改用户方法
- 绑定删除用户方法
views/user/show.tpl
{% block js %}
<script src="https://lib.baomitu.com/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript">
const indexPage = {
init:function(){
this.bind();
},
bind:function(){
$('#new-submit').on('click',this.newUser);
$('.update-user').on('click',this.update);
$('.delete-user').on('click',this.delete);
},
delete: function(){
let id = $(this).data('id');
$.ajax({
url: '/api/user',
data: { id },
type: 'DELETE',
success: function(data) {
if(data.code === 200){
alert('删除成功!')
location.reload()
}else{
console.log(data)
}
},
error: function(err) {
console.log(err)
}
})
},
update:function(){
let id = $(this).data('id');
let name = $(this).parent().find('.user-name').val();
if(!name || !id){
alert('缺少参数')
return
}
$.ajax({
url: '/api/user',
data: { name, id },
type: 'PUT',
success: function(data) {
if(data.code === 200){
alert('修改成功!')
location.reload()
}else{
console.log(data)
}
},
error: function(err) {
console.log(err)
}
})
},
newUser:function(){
let name = $('#new-name').val();
let email = $('#new-email').val();
let password = $('#new-password').val();
if(!name || !email || !password){
alert('缺少参数')
return
}
$.ajax({
url: '/api/user',
data: { name, email, password },
type: 'POST',
success: function(data) {
if(data.code === 200){
alert('新增成功!')
location.reload()
}else{
console.log(data)
}
},
error: function(err) {
console.log(err)
}
})
}
}
$(function(){
indexPage.init();
})
</script>
{% endblock %}
BaseModel
在完成上一章节的增删改查中,我们会发现不同表中使用 knex 进行增删改查的方法都将差不多,不同的一点只是 table 不一样。目前只是用户表,如果出现其他新闻表、分类表等更多相关的表信息进行查询时,每次都新建一个相关的 model 再构建差不多的方法,这样代码会显得很冗余。
因此我们抽离出一层基础模型,基础模型拥有各个模型可以共有的简单方法,增删改查。只要相关的模型继承基础模型,即可使用其方法。如果相关方法需要使用除了公共的方法之外的方法,例如多个表联合查询,这个时候可以在相关的模版中引用 knex 再配置其特有的方法即可。
- 新建基础模型 models/base.js ,除了 all、insert、update、delete ,方法,我们再新增一个 select 查询方法便于之后查询使用。
models/base.js
const knex = require('./knex');
class Base {
constructor(props) {
this.table = props;
}
all(){
return knex(this.table).select()
}
select(params) {
return knex(this.table).select().where(params)
}
insert(params){
return knex(this.table).insert( params )
}
update(id, params ){
return knex(this.table).where('id', '=', id).update( params )
}
delete(id){
return knex(this.table).where('id', '=', id).del()
}
}
module.exports = Base;
- 修改用户模型 models/user.js ,使其继承基础模型,并设置其 table 表格名称。
models/user.js
// 引用基础模型
const Base = require('./base.js');
// 定义用户模型并基础基础模型
class User extends Base {
// 定义参数默认值为 users 表
constructor(props = 'users') {
super(props);
}
}
module.exports = User
- 修改用户控制器 controller/user.js,修改后的用户模型是一个 class 类而不是对象,因此需要配合 new 来使用。
controller/user.js
const UserModel = require('./../models/user.js');
const User = new UserModel();
CSRF
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF,通过伪装来自受信任用户的请求来利用受信任的网站。
我们针对 CSRF 通过伪造 cookie 的方式,通过在 session 中设置token 对比来防御,主要有一下步骤:
- 在有表单的页面中,生成一段随机的 token ,渲染到页面中,也存储在 session 中。
- 当提交请求时候,除了对 cookie 进行校验以外,增加对 token 的校验。
- 如果请求中带有 csrf 字段,且和 session 中的 token 值匹配,则鉴权成功。
- 下载 express-session
npm install express-session --save
- 在 app.js 中使用
var session = require('express-session');
···
// Use the session middleware
app.use(session({
secret: 'JaxChu',
resave: true,
saveUninitialized: true,
}))
- 新建 CSRF 中间件 middlewares/csrf.js
middlewares/csrf.js
// 使用 Nodejs 中的加密模块
const crypto = require('crypto');
const CSRF = {
// 生成随机数
generateRandom:function(len){
return crypto.randomBytes(Math.ceil(len*3/4)).toString('base64').slice(0,len);
},
// 设置token
getToken:function(req,res,next){
// 获取当前 session 的 csrf_token。
var token = req.session._csrf;
// 获取请求过来的 csrf 参数的 token
var _csrf = req.query.csrf ? req.query.csrf : (req.query.csrf = req.body.csrf);
// 将两个 token 进行比较
// 如果不相等,鉴权失败,禁止下一步。
// 相等就可以下一步走接下来的业务逻辑操作。
if(_csrf !== token){
res.writeHead(403);
res.end("禁止访问");
}else{
next()
}
},
setToken:function(req,res,next){
// 获取当前 session 是否有 csrf_token
// 如果是没有关闭浏览器多次刷新的话,就有,使用原来的
// 如果是新打开浏览器第一次请求的页面,没有就新建一个
var token = req.session._csrf || (req.session._csrf = CSRF.generateRandom(24));
// 把token设置在本地数据中,页面获取本地数据渲染到页面
res.locals.csrf = token;
next();
}
}
module.exports = CSRF;
- 修改路由
routes/index.js
var express = require('express');
var router = express.Router();
var userController = require('./../controllers/user.js');
// 引入 CSRF 中间件
var csrf = require('./../middlewares/csrf.js');
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
// 使用 CSRF 中间件,往页面设置 token
router.get('/user', csrf.setToken, userController.show);
module.exports = router;
routes/api.js
var express = require('express');
var router = express.Router();
var cors = require('./../middlewares/cors.js');
var csrf = require('./../middlewares/csrf.js');
var bookController = require('./../controllers/book');
var userController = require('./../controllers/user');
/* GET users listing. */
router.get('/isbn', cors.allowAll,bookController.info);
// 使用使用 CSRF 中间件,检查token
router.post('/user' ,csrf.getToken, userController.insert);
router.put('/user' ,csrf.getToken, userController.update);
router.delete('/user' ,csrf.getToken, userController.delete);
module.exports = router;
- 修改视图模版 views/user/show.tpl
在HTML中添加一个input的隐藏元素,其value为csrf的token,然后ajax提交之前获取token的值,在参数中一起提交
{% extends './../layout.tpl' %}
{% block css %}
<link rel="stylesheet" href="/stylesheets/style.css">
{% endblock %}
{% block content %}
<div class="page">
<h1>用户管理</h1>
<div class="new-user">
<h2>新建用户</h2>
<input type="text" name="name" id="new-name" placeholder="请输入用户名">
<input type="email" name="email" id="new-email" placeholder="请输入邮箱账号">
<input type="password" name="password" id="new-password" placeholder="请输入密码">
<button id="new-submit">新建用户</button>
</div>
<div class="user-list">
<h2>用户列表</h2>
<ul>
{% for val in users %}
<li>
<span>id: {{val.id}}</span>
<span>email: {{val.email}}</span>
<input class="user-name" type="text" name="name" placeholder="姓名" value="{{val.name}}">
<button class="update-user" data-id="{{val.id}}">修改姓名</button>
<button class="delete-user" data-id="{{val.id}}">删除用户</button>
</li>
{% endfor %}
</ul>
</div>
<!-- csrf token -->
<div class="csrf-contianer">
<input type="text" name="csrf" id="csrf" value="{{csrf}}" hidden>
</div>
</div>
{% endblock %}
{% block js %}
<script src="https://lib.baomitu.com/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript">
const indexPage = {
init:function(){
this.bind();
},
bind:function(){
$('#new-submit').on('click',this.newUser);
$('.update-user').on('click',this.update);
$('.delete-user').on('click',this.delete);
},
delete: function(){
let id = $(this).data('id');
let csrf = $('#csrf').val();
$.ajax({
url: '/api/user',
data: { id,csrf },
type: 'DELETE',
success: function(data) {
if(data.code === 200){
alert('删除成功!')
location.reload()
}else{
console.log(data)
}
},
error: function(err) {
console.log(err)
}
})
},
update:function(){
let id = $(this).data('id');
let name = $(this).parent().find('.user-name').val();
let csrf = $('#csrf').val();
if(!name || !id){
alert('缺少参数')
return
}
$.ajax({
url: '/api/user',
data: { name, id, csrf },
type: 'PUT',
success: function(data) {
if(data.code === 200){
alert('修改成功!')
location.reload()
}else{
console.log(data)
}
},
error: function(err) {
console.log(err)
}
})
},
newUser:function(){
let name = $('#new-name').val();
let email = $('#new-email').val();
let password = $('#new-password').val();
let csrf = $('#csrf').val();
if(!name || !email || !password){
alert('缺少参数')
return
}
$.ajax({
url: '/api/user',
data: { name, email, password, csrf },
type: 'POST',
success: function(data) {
if(data.code === 200){
alert('新增成功!')
location.reload()
}else{
console.log(data)
}
},
error: function(err) {
console.log(err)
}
})
}
}
$(function(){
indexPage.init();
})
</script>
{% endblock %}
cookie
使用 cookie 完成登录权限控制,在登录页面设置登录接口,如果登录成功,为页面设置加密的 cookie。设置页面过滤组件,当每次有请求过来都会走鉴定组件,鉴定组件根据 cookie 的值判断用户的登录状态及身份。
我们需要做以下几件事情:
- cookie 的加密算法
- 登录接口,登录成功设置 cookie
- 用户信息过滤组件,每个用户进来更具 cookie 鉴定身份
- 登录页面,如果登录的话重新定向到首页
- 用户管理页面,如果没有登录的话重定向到登录页面
以下我们逐步完成上述需求:
- 新建算法文件 utils/authCode.js,用于 cookie 的加密和解密。注意:算法文件为敏感文件,请不要上传到托管服务器,需要**.gitignore**。不然被外部发现开源的核心算法代码就会破解用户的 cookie 获取帐号密码。
utils/authCode.js
/**
* 加密解密
*/
const crypto = require('crypto');
const key = Buffer.from('aitschool!@#$123', 'utf8');
const iv = Buffer.from('FnJL7EDzjqWjcaY9', 'utf8');
const authcode = function (str, operation){
operation ? operation : 'DECODE';
if (operation == 'DECODE') {
let src = '';
let cipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
src += cipher.update(str, 'hex', 'utf8');
src += cipher.final('utf8');
return src;
}else {
let sign = '';
let cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
sign += cipher.update(str, 'utf8', 'hex');
sign += cipher.final('hex');
return sign;
}
}
module.exports = authcode;
- 定义登录接口路由 routes/api.js , 走一层 csrf ,再走权限控制器。定义登录页面路由 routes/index.js ,走一层设置 csrf ,在渲染视图模版。
routes/api.js
var authController = require('./../controllers/auth.js');
...
router.post('/login' ,csrf.getToken, authController.login);
routes/index.js
var authController = require('./../controllers/auth.js');
...
router.get('/login', csrf.setToken, authController.renderLogin);
- 新建并定义权限控制器,controllers/auth.js
controllers/auth.js
const UserModel = require('./../models/user.js');
const User = new UserModel();
const authCodeFunc = require('./../utils/authCode.js');
const authController = {
login:async function(req,res,next){
// 获取邮件密码参数
let email = req.body.email;
let password = req.body.password;
// 参数判断
if(!email || !password){
res.json({ code: 0, data: 'params empty!' });
return
}
try{
// 通过用户模型搜索用户
const users = await User.select({ email, password });
// 看是否有用户存在
const user = users[0];
// 如果存在
if(user){
// 将其邮箱、密码、id 组合加密
let auth_Code = email +'\t'+ password +'\t'+ user.id;
auth_Code = authCodeFunc(auth_Code,'ENCODE');
// 加密防止再 cookie 中,并不让浏览器修改
res.cookie('ac', auth_Code, { maxAge: 24* 60 * 60 * 1000, httpOnly: true });
// 返回登录的信息
res.json({ code: 200, message: '登录成功!'})
}else{
res.json({ code: 0, data: { msg: '登录失败,没有此用户!'} })
}
}catch(e){
res.json({ code: 0, data: e })
}
},
// 渲染登录页面的模版
renderLogin:async function(req,res,next){
res.render('login',res.locals)
}
}
module.exports = authController;
- 定义登录页面视图模版 views/login.tpl
{% extends './layout.tpl' %}
{% block css %}
<link rel="stylesheet" type="text/css" href="/stylesheets/style.css">
{% endblock %}
{% block content %}
<div class="form-cells">
<h1>用户登录</h1>
<div class="form-cell">
<input id="email" type="email" name="email" placeholder="邮箱">
</div>
<div class="form-cell">
<input id="password" type="password" name="password" placeholder="密码">
</div>
<div class="form-cell">
<button id="submit">登录</button>
<input type="text" name="csrf" id="csrf" value="{{csrf}}" hidden>
</div>
</div>
{% endblock %}
{% block js %}
<script src="https://lib.baomitu.com/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript">
const PAGE = {
init:function(){
this.bind();
},
bind:function(){
$('#submit').on('click',this.handleSubmit);
},
handleSubmit:function(){
let password = $('#password').val();
let email = $('#email').val();
let csrf = $('#csrf').val();
if(!password || !email){
alert('params empty!')
return
}
console.log(email,password)
$.ajax({
url: '/api/login',
data: { email, password, csrf },
type: 'POST',
success: function(data) {
if(data.code === 200){
alert('登录成功!')
location.reload()
}else{
console.log(data)
}
},
error: function(err) {
console.log(err)
}
})
}
}
PAGE.init();
</script>
{% endblock %}
启动服务,打开http://localhost:3000/login,就可以看到登录页面,我们输出正确的账号和密码,即可完成登录,之后可以打开控制面板 Application 的 Cookies 处看到我们种下的 Cookie。
新建过滤中间件,filters/index.js,filters/loginFilter.js,filters/initFilter.js。filters/index.js 为主文件分别引用 filters/loginFilter.js 及 filters/initFilter.js,分别用于设置用户信息和全局信息。
filters/index.js
// 注册app的过滤器
module.exports = function(app) {
app.use(require('./loginFilter.js'));
app.use(require('./initFilter.js'))
};
filters/loginFilter.js
// 引用加密解密模块
const authCodeFunc = require('./../utils/authCode.js');
module.exports = function (req, res, next) {
res.locals.isLogin = false;
res.locals.userInfo = {};
// 判断是否存在ac cookie
let auth_Code = req.cookies.ac;
if(auth_Code){
// 如果有,对其进行解密
auth_Code = authCodeFunc(auth_Code,'DECODE');
authArr = auth_Code.split("\t");
let email = authArr[0];
let password = authArr[1];
let id = authArr[2];
// 注意:为了防止删改
// 这里其实应该再调用一次用户模型进行登录校验
// 然后把状态保存在 session 中来联合判断。
// 当前为了体验,所以直接把用户名和密码和id直接解密返回
res.locals.isLogin = true;
res.locals.userInfo = {
email,password,id
}
}
next();
}
filters/initFilter.js
module.exports = function (req, res, next) {
res.locals.seo = {
title: 'ExpressApp',
keywords: 'Express、Nodejs',
description: 'ExpressApp to study Nodejs on Web'
}
next();
}
- 引用文件中引用 filters ,app.js,要在定义路由之前定义。
app.js
var filters = require('./filters/index')
...
filters(app);
app.use('/', indexRouter);
app.use('/api', apiRouter);
- 登录页面,如果登录的话重新定向到首页
controller/auth.js
const authController = {
...
renderLogin:async function(req,res,next){
// 如果用户已经登录,重定向到用户管理页面
if(res.locals.isLogin){
res.redirect('/user')
return
}
res.render('login',res.locals)
}
}
- 用户管理页面,如果用户没有登录,重定向到登录页面
controller/user.js
const userController = {
show: async function(req,res,next){
// 如果用户没有登录,重定向到登录页面
if(!res.locals.isLogin){
res.redirect('/login')
return
}
try{
const users = await User.all();
res.locals.users = users;
res.render('user/show.tpl',res.locals)
}catch(e){
res.locals.error = e;
res.render('error',res.locals);
}
},
}
- 调试,打开控制面板清空 cookie ,访问http://localhost:3000/user , 然后就会自动跳转到登录页面。输入账号密码,刷新就会自动跳转到用户管理页面。