express案例auth模块的学习

48 阅读7分钟

auth的目录结构如下:

  • auth
    • views
      • foot.ejs
      • head.ejs
      • login.ejs
    • index.js

一、代码介绍

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请求通过回调函数设置登陆状态相关参数)

欢迎各位大佬指正!