auth的目录结构如下:
- auth
- views
- foot.ejs
- head.ejs
- login.ejs
- index.js
- views
一、代码介绍
login.ejs代码如下:
<%- include('head', { title: 'Authentication Example' }) -%>
<h1>Login</h1>
<%- message %>
Try accessing <a href="/restricted">/restricted</a>, then authenticate with "tj" and "foobar".
<form method="post" action="/login">
<p>
<label for="username">Username:</label>
<input type="text" name="username" id="username">
</p>
<p>·
<label for="password">Password:</label>
<input type="text" name="password" id="password">
</p>
<p>
<input type="submit" value="Login">
</p>
</form>
<%- include('foot') -%>
head.ejs代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><%= title %></title>
<style>
body {
padding: 50px;
font: 13px Helvetica, Arial, sans-serif;
}
.error {
color: red
}
.success {
color: green;
}
</style>
</head>
<body>
foot.ejs代码如下:
</body>
</html>
index.js代码如下:
'use strict'
/**
* Module dependencies.
*/
var express = require('../..');
var hash = require('pbkdf2-password')()
var path = require('path');
var session = require('express-session');
var app = module.exports = express();
// config
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// middleware
app.use(express.urlencoded({ extended: false }))
app.use(session({
resave: false, // don't save session if unmodified
saveUninitialized: false, // don't create session until something stored
secret: 'shhhh, very secret'
}));
// Session-persisted message middleware
app.use(function(req, res, next){
var err = req.session.error;
var msg = req.session.success;
delete req.session.error;
delete req.session.success;
res.locals.message = '';
if (err) res.locals.message = '<p class="msg error">' + err + '</p>';
if (msg) res.locals.message = '<p class="msg success">' + msg + '</p>';
next();
});
// dummy database
var users = {
tj: { name: 'tj' }
};
// when you create a user, generate a salt
// and hash the password ('foobar' is the pass here)
hash({ password: 'foobar' }, function (err, pass, salt, hash) {
if (err) throw err;
// store the salt & hash in the "db"
users.tj.salt = salt;
users.tj.hash = hash;
});
// Authenticate using our plain-object database of doom!
function authenticate(name, pass, fn) {
if (!module.parent) console.log('authenticating %s:%s', name, pass);
var user = users[name];
// query the db for the given username
if (!user) return fn(null, null)
// apply the same algorithm to the POSTed password, applying
// the hash against the pass / salt, if there is a match we
// found the user
hash({ password: pass, salt: user.salt }, function (err, pass, salt, hash) {
if (err) return fn(err);
if (hash === user.hash) return fn(null, user)
fn(null, null)
});
}
function restrict(req, res, next) {
if (req.session.user) {
next();
} else {
req.session.error = 'Access denied!';
res.redirect('/login');
}
}
app.get('/', function(req, res){
res.redirect('/login');
});
app.get('/restricted', restrict, function(req, res){
res.send('Wahoo! restricted area, click to <a href="/logout">logout</a>');
});
app.get('/logout', function(req, res){
// destroy the user's session to log them out
// will be re-created next request
req.session.destroy(function(){
res.redirect('/');
});
});
app.get('/login', function(req, res){
res.render('login');
});
app.post('/login', function (req, res, next) {
authenticate(req.body.username, req.body.password, function(err, user){
if (err) return next(err)
if (user) {
// Regenerate session when signing in
// to prevent fixation
req.session.regenerate(function(){
// Store the user's primary key
// in the session store to be retrieved,
// or in this case the entire user object
req.session.user = user;
req.session.success = 'Authenticated as ' + user.name
+ ' click to <a href="/logout">logout</a>. '
+ ' You may now access <a href="/restricted">/restricted</a>.';
res.redirect('back');
});
} else {
req.session.error = 'Authentication failed, please check your '
+ ' username and password.'
+ ' (use "tj" and "foobar")';
res.redirect('/login');
}
});
});
/* istanbul ignore next */
if (!module.parent) {
app.listen(3000);
console.log('Express started on port 3000');
}
二、代码逐行解读
head.ejs解读
<%= title %> //等待传入参数
login.ejs解读
<%- include('head', { title: 'Authentication Example' }) -%>
<!-->
引入head.ejs文件,传入参数title。
注意<%=><%->的区别:
`<%= ... %>`: 这个标签会输出变量的值,并且会对该值进行 HTML 转义。
`<%- ... %>`: 这个标签也会输出变量的值,但是不会进行 HTML 转义。
简单来说就是-会执行js脚本而=会把脚本文本化。
<-->
<h1>Login</h1>
<%- message %>
Try accessing <a href="/restricted">/restricted</a>, then authenticate with "tj" and "foobar".
<!--这是一段文本提示信息,不是可执行代码。-->
<form method="post" action="/login">
<p>
<label for="username">Username:</label>
<input type="text" name="username" id="username">
</p>
<p>
<label for="password">Password:</label>
<input type="text" name="password" id="password">
</p>
<p>
<input type="submit" value="Login">
</p>
</form>
<%- include('foot') -%>
index.js解读
1、 引入模块解读
var express = require('../..'); //向上两层目录,不填写引入文件是默认引入index.js
var hash = require('pbkdf2-password')()
/*
PBKDF2(Password-Based Key Derivation Function 2)算法作为一种基于密码的密钥生成方法。
官方使用说明文档下面介绍
*/
var path = require('path'); // path包
var session = require('express-session');
1、1 pbkdf2-password包解读
// 官方包使用说明 Creates a new hasher functions,with the specified options.
var bkfd2Password = require("pbkdf2-password");
var hasher = bkfd2Password();
var assert = require("assert");
var opts = {
password: "helloworld"
};
hasher(opts, function(err, pass, salt, hash) {
opts.salt = salt;
hasher(opts, function(err, pass, salt, hash2) {
assert.deepEqual(hash2, hash);
// password mismatch
opts.password = "aaa";
hasher(opts, function(err, pass, salt, hash2) {
assert.notDeepEqual(hash2, hash);
console.log("OK");
});
});
});
1、2 express-session解读
服务器创建一个session时,同时会创建一个sessionID。服务器会把这个sessionID存储的在客户端的cookie中。客户端通过sessionID可以找到服务器的session。实现登陆状态保持的效果。通过session中间件,服务器将创建的session对象挂载到req.session上。
参考文章:zhuanlan.zhihu.com/p/409813376
2、 各个中间件解读
var app = module.exports = express(); // 引入express模块并实例化
app.set('view engine', 'ejs'); // 设置ejs解析引擎
app.set('views', path.join(__dirname, 'views')); // 设置views为绝对路径
app.use(express.urlencoded({ extended: false })) //urlencoded用于解析 URL 编码的请求体
/*
1、urlencoded()主要用于解析 URL 编码的请求体,如表单的POST请求,而不用于GET,对于 GET 请求,
数据通常不是通过请求体传输的,而是通过 URL 的查询字符串(query string)部分。
2、{extended: false}使用querystring库解析URL 编码的数据。
这个库主要处理一维对象,不支持嵌套结构,但更安全高效。
{extended:true}使用更强大的qs库,支持嵌套对象和其他复杂数据结构,适用于更复杂的表单场景。
*/
app.use(session({ //创建session对象
resave: false, // 强制将会话保存到会话存储中:否
saveUninitialized: false, // 强制将会话初始化:否(会话尚未初始化(即req.session对象为空))
/*
`saveUninitialized: true` 是 Express.js 中 `express-session` 中间件的一个选项。
当设置为 `true` 时,这个选项允许中间件保存那些尚未初始化的会话。
换句话说,即使客户端没有发送会话标识符(例如,cookie),服务器也会为客户端创建一个新的会话。
*/
secret: 'shhhh, very secret' //密钥,可以为常见的任意类型值
}));
app.use(function(req, res, next){
var err = req.session.error;
var msg = req.session.success; //接受自定义的session.error、success属性
delete req.session.error;
delete req.session.success;
res.locals.message = '';
if (err) res.locals.message = '<p class="msg error">' + err + '</p>';
if (msg) res.locals.message = '<p class="msg success">' + msg + '</p>';
//res.locals是express自带的对象,用于服务器端临时存储数据,它会在响应发送后被清除。
next();
});
hash({ password: 'foobar' }, function (err, pass, salt, hash) {
if (err) throw err;
// store the salt & hash in the "db"
users.tj.salt = salt;
users.tj.hash = hash;
});
function authenticate(name, pass, fn) {
if (!module.parent) console.log('authenticating %s:%s', name, pass);
/*
`module.parent`是一个引用,指向当前模块被`require`时所在的父模块。如果当前模块是直接通过命令行使用
Node.js执行(而不是被其他模块`require`),那么`module.parent`将是`null`。
*/
var user = users[name];
// query the db for the given username
if (!user) return fn(null, null)
// apply the same algorithm to the POSTed password, applying
// the hash against the pass / salt, if there is a match we
// found the user
hash({ password: pass, salt: user.salt }, function (err, pass, salt, hash) {
// 根据password和salt可以生成唯一的hash值。这里数据只需要保存salt和hash值,而不保存密码。
if (err) return fn(err);
if (hash === user.hash) return fn(null, user)
fn(null, null)
});
}
function restrict(req, res, next) {
if (req.session.user) {
next();
} else {
req.session.error = 'Access denied!';
res.redirect('/login');
}
}
app.get('/', function(req, res){
res.redirect('/login');
});
app.get('/restricted', restrict, function(req, res){
//引用局部中间件,当引用多个中间件时,可以用middleware1,middleware2,...或者[middleware1,middleware2,...]
res.send('Wahoo! restricted area, click to <a href="/logout">logout</a>');
});
app.get('/logout', function(req, res){
// destroy the user's session to log them out
// will be re-created next request
req.session.destroy(function(){ //销毁session,并执行回调函数
res.redirect('/');
});
});
app.get('/login', function(req, res){
res.render('login'); //渲染名为 'login' 的视图模板
});
app.post('/login', function (req, res, next) {
authenticate(req.body.username, req.body.password, function(err, user){
if (err) return next(err)
if (user) {
// Regenerate session when signing in
// to prevent fixation
req.session.regenerate(function(){ // 再生成session对象
// Store the user's primary key
// in the session store to be retrieved,
// or in this case the entire user object
req.session.user = user;
req.session.success = 'Authenticated as ' + user.name
+ ' click to <a href="/logout">logout</a>. '
+ ' You may now access <a href="/restricted">/restricted</a>.';
res.redirect('back'); //将用户重定向到他们之前所在的页面
});
} else {
req.session.error = 'Authentication failed, please check your '
+ ' username and password.'
+ ' (use "tj" and "foobar")';
res.redirect('/login');
}
});
});
// 一个页面又可以触发get、又可以触发post。那能不能触发多次get、post呢?
// 如果一个页面需要处理多个get或者多个post请求,一般会用参数区分开进行处理。
if (!module.parent) {
app.listen(3000);
console.log('Express started on port 3000');
}
三、代码整体总结
主要对index.js进行总结:
1、引入各种包:express、pbkdf2-password、path、express-session;
2、初始化express对象;
3、设置模板引擎和views的绝对路径;
4、使用urlencoded中间件解析post请求;
5、使用中间件创建session对象;
6、设置错误捕获和消息反馈中间件;
为什么要在这里设置这个?这一步session还是为null啊。答:session是空对象。JS中,当属性值为空或者不存在这个属性值时,获取的值为undefined。
7、根据密码创建salt和hash值,并存储在用户名对象中;
8、定义一个验证hash的函数和定义一个验证登陆成功与否的函数;
9、设置get监听各个路由;
10、设置post监听/login路由,设置处理验证成功与验证失败的处理回调函数
11、区分引用还是命令行调用,如果是命令行调用,使用3000端口。
再次梳理一下index.js的逻辑思路:
引入必要模块、设置解析引擎 -> 引入session中间件和成功失败消息处理中间件 -> 初始化hash对象 -> 定义验证hash值的函数和验证登陆状态的函数 -> 各种get请求和post请求的处理及页面渲染(post请求通过回调函数设置登陆状态相关参数)
欢迎各位大佬指正!