代码改变世界

Nodejs之MEAN栈开发(八)---- 用户认证与会话管理详解

2016-07-22 08:30  stoneniqiu  阅读(5630)  评论(20编辑  收藏

用户认证与会话管理基本上是每个网站必备的一个功能。在Asp.net下做的比较多,大体的思路都是先根据用户提供的用户名和密码到数据库找到用户信息,然后校验,校验成功之后记住用户的姓名和相关信息,这个信息经过处理之后会保存在cookie、缓存、Session等地方,然后还有一个过期时间,避免每次都要去捞数据库。在node下基本上也是这个思路,这一节的内容会涉及到user模型的加密方式、如何生成一个Json Web Token(JWT)、以及在客户端用Angular创建注册和登录页面,在请求需要认证的api时如何传递JWT到服务端等等,下面一一道来。

开始之前,先熟悉下整个流程。当用户第一次认证时,整个过程如下。

而当用户端获取到token之后,再访问需要认证的页面时,只需要在请求中带上token即可,然后由服务端校验这个token是否有效。

当token正确再更新用户的数据或者返回页面。接下来一步一步实现这个过程。

一、增加用户模型与扩展方法

这里就需要用到第三节的知识了,在创建用户模型之前,我们先考虑加密方式的问题,一般我们都会使用单向(不可逆)加密的方式,比如MD5。但有很多用户的密码都比较弱,比如123456,love1314等等,这样会出现很多相同的密码,容易被识破。为避免这个情况,引入一个salt(加点'盐')值。将用户的密码和salt值合并之后再进行加密。得到一个hash值。这样密码强度就高了很多。

1.userSchema 

因此,salt和hash值都要存进数据库。所以我们的Mongoose模型如下(位于app_api/models/books.js):

var userSchema = new mongoose.Schema({
    name: { type: String, required: true },
    email: { type: String, unique: true, required: true },
    hash: String,
    salt:String,
    createdOn: {
        type: Date,
        default: Date.now
    }
});
mongoose.model('User', userSchema);

设定了email字段不可重复,并注册这个模型。

2.setPassword

Mongoose支持直接在Schema上面扩展方法,比如增加一个设置密码的方法。

userSchema.methods.setPassword = function(password) {

};

需要将setPassword这个方法加入methods这个对象中,Mongoose支持通过this获取或到模型的字段。实现这个方法,我们还需要安装一个常用模块:crypto

 

我们将用到crypto的两个方法,randomBytes和pbkdf2Sync,前者会生成一个字符串,后者生成密码和salt的哈希值。因此上面的setPassword方法如下:

var crypto = require('crypto');
userSchema.methods.setPassword = function(password) {
    this.salt = crypto.randomBytes(16).toString('hex');
    //1000代表迭代次数 64代表长度
    this.hash = crypto.pbkdf2Sync(password, this.salt,1000,64).toString('hex');
};

先引用crypto模块,生成一个16位的随机字符串作为salt,然后调用pbkdf2Sync方法生成哈希值。

3.validPassword

再增加一个验证方法:

userSchema.methods.validPassword = function(password) {
    var hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64).toString('hex');
    return this.hash === hash;
};

4.Json Web Token

Json Web Token简称JWT,用来在服务器端和客户端传递数据。JWT是由三段处理后的字符串通过点号组成,看起来有点长,如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NTZiZWRmNDhmOTUzOTViMTlhNjc1ODgiLCJlbWFpbCI6InNpbW9uQGZ1bGxzdGFja3RyYWluaW5nLmNvbSIsIm5hbWUiOiJTaW1vbiBIb
2xtZXMiLCJleHAiOjE0MzUwNDA0MTgsImlhdCI6MTQzNDQzNTYxOH0.GD7UrfnLk295rwvIrCikbkAKctFFoRCHotLYZwZpdlE

第一段是一个编码之后的json对象,这个json对象包含了hash算法和类型。第二段也是一个编码之后的json对象,也就是我们需要的令牌数据。第三段是一个签名,签名的密码保存在服务端。 所以这个长长的字符串有两段是没有加密的,只是编码。这样便于浏览器可以方便的解码而获取到信息,现代的浏览器会有一个atob()的方法来解码Base64的字符串,对应的有一个btoa()方法来编码。而第三部分的签名可以确保信息没有被篡改,前提是保护好服务器端的密钥。 我们将用它来传递认证之后的用户信息。

要生成JWT,还需要安装一个模块jsonwebtoken。

并引用:

var mongoose = require( 'mongoose' );
var crypto = require('crypto');
var jwt = require('jsonwebtoken');

token将包含用户_id,email,name和一个过期时间。 接下来添加一个 generateJwt 方法

userSchema.methods.generateJwt = function() {
    var expiry = new Date();
    expiry.setDate(expiry.getDate() + 7);
    return jwt.sign({
        _id: this._id,
        email: this.email,
        name: this.name,
        exp:parseInt(expiry.getTime()/1000)}, 'ReadingClubSecret');
};

这里我们调用了jwt的sign方法,并定义了一个密钥:ReadingClubSecret.

5.dotenv

4中的密钥还需要在别的地方调用,所以最好还是用文件管理起来,node有一个dotenv的模块,可以将这个密钥设置成环境变量。在根目录下创建一个.env的文件,并设置密码:

JWT_SECRET=ReadingClubSecret

同时还要注意,在gitignore 中增加这个文件的忽略,不必上传到git上。然后我们安装dotenv模块:

在app.js最顶端引用:

require('dotenv').load();
var express = require('express');

然后修改4中的方法:

  exp:parseInt(expiry.getTime()/1000)}, process.env.JWT_SECRET);

二、Passport 认证管理

第一部分增加了几个模型的扩展方法,接下来我们用passport来做认证管理。Passport 是Jared Hanson 开发的一个node模块,支持多种不同的认证,包括Facebook,Twitter,OAuth以及本地用户名和密码。 每一种方式相当于是一种策略,安装需要的策略即可:

我们安装了本地策略,也就是用户名加密码登录的方式。接下来就是如何使用passport,也就是配置认证策略。

1.passport.js

在app_api目录下创建一个config文件夹,并在其中创建一个passport.js文件。

var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var mongoose = require('mongoose');
var User = mongoose.model('User');

使用passport.use方法配置策略,参数是一个策略的构造函数,代码结构如下:

passport.use(new LocalStrategy({},
 function(username, password, done) {
 }
));

本地策略默认使用的字段是‘username’ 和‘password’,但我们是把email作为登录名,所以需要重载一下。

passport.use(new LocalStrategy(
usernameField: 'email'
},
function(username, password, done) {
}
));

参数中的done是一个回调函数,那么接下来我们要用Mongoose来实现以下步骤

  1. 通过email找到用户。
  2. 验证密码是否正确
  3. 如果密码正确返回用户对象。
  4. 否则的话返回一个错误提示信息。

第一步,可以使用Mongoose的findOne(),验证密码就使用前面创建的validPassword()。

passport.use(new LocalStrategy({
    usernameField:'email'
},function(username, password, done) {
    User.findOne({ email: username }, function(err, user) {
        if (err) {
            return done(err);
        }
        if (!user) {
            return done(null, false, { message: '用户不存在' });
        }
        if (!user.validPassword(password)) {
            return done(null, false, { message: '密码错误!' });
        }
        return done(null, user);

    });
}))

2.使用passport

现在方法定义好了,接下来就是如何在应用中使用。我们需要在app.js分三步处理

  1. 引用passport。
  2. 引用策略配置
  3. 初始化通行证。

这也没有什么复杂的,关键就是在哪儿设置。修改app.js

var passport = require('passport');
require('./app_api/config/passport');

app.use(passport.initialize());
//app.use('/', routes);
app.use('/api', routesApi);

这些操作都是要位于api路由之前,并调用passport的初始化方法。什么时候使用通行证呢,后面会继续。

3.register

为了让用户可以登录和注册我们的系统。还需要2个新的方法,先增加路由,位于app_api/routes下。创建authentication.js, 先加上必要的引用和方法:

var passport = require('passport');
var mongoose = require('mongoose');
var User = mongoose.model('User');
var sendJSONresponse = function (res, status, content) {
    res.status(status);
    res.json(content);
};

registration 方法需要做以下事情

  1. 验证必填的字段。
  2. 创建一个user的实例。
  3. 设置用户的name和email。
  4. 使用setPassword方法创建salt和hash。
  5. 保存数据
  6. 返回一个JWT。
看起来有点多,但其实大部分我们已经实现了。现在调用就是
module.exports.register=function(req, res) {
    if (!req.body.name || !req.body.email || !req.body.password) {
        sendJSONresponse(res, 400, { message: "请完成所有字段" });
        return;
    }
    var user = new User();
    user.name = req.body.name;
    user.email = req.body.email;
    user.setPassword(req.body.password);
    user.save(function(err) {
        var token;
        if (err) {
            sendJSONresponse(res, 404, err);
        } else {
            token = user.generateJwt();
            sendJSONresponse(res, 200, { 'token': token });
        }
    });
}

4.login

login和register不同的是,login需要使用passport来认证了。

module.exports.login = function(req, res) {
    if (!req.body.email || !req.body.password) {
        sendJSONresponse(res, 400, { message: '请输入邮箱和密码!' });
        return;
    }
    passport.authenticate('local', function(err, user, info) {
        var token;
        if (err) {
            sendJSONresponse(err, 404, err);
            return;
        }
        if (user) {
            token = user.generateJwt();
            sendJSONresponse(res, 200, { token: token });
        } else {
            sendJSONresponse(res, 401, info);
        }

    })(req,res);
};

先对email和password进行判断,然后再调用passport的认证方法,参数local表示采用local策略。如果认证成功,我们再创建一个token并返回。否则就返回401。而这里的info就是passport.js中传过来的错误信息。

5.postman

注册方法写好了,我们可以用postmen测试一下。 下载postmen http://files.cnblogs.com/files/stoneniqiu/Postman.rar  解压之后按照提示安装即可。chrome扩展安装成功之后会有一下提示:

你也可以创建一个桌面快捷方式:

接下来可以方便的测试我们的注册方法了:

打开postmen,选择post方法,地址栏中输入http://localhost:3000/api/register ,选择x-www-form-urlencode 然后输入我们的数据。 完成之后,点击send,可以看到下方出现了token。说明注册成功!

查询一下mongodb:

可以看见,数据库中多了一个name为stoneniqiu的用户。

6.express-jwt  

还有一个问题,我们在第三节公布了好些api,但并不是所有的都需要认证,特别是一些get方式的请求可以是匿名的。所以接下来需要做的一件事就是配置路由,用以阻止那些没有认证的请求到达我们指定的控制器。相当于是一个介于路由和控制器之间的中间件,当路由被调用了时,这个中间件在控制器之前激活,中间件验证之后再决定请求是否能到达控制器。这个模块就是express-jwt。

如果是在Asp.net MVC可能比较好理解,就是AOP,增加一个Filter就好了。其实是一样的。

使用express-jwt 需要引用和配置,在app_api/routes/index.js, 顶部增加下面的代码

var express = require('express');
var router = express.Router();
var jwt = require('express-jwt');
var auth = jwt({
    secret: process.env.JWT_SECRET,
    userProperty: 'payload'
});

 以上代码定义了一个auth对象,jwt方法中的secret参数就是之前定义在文件中的密码。而这个userProperty指的是认证成功后附带用户信息的对象名称,一般是用user的,但在这我们用了payload,主要是为了避免与Mongoose中的user模型对象混淆。

接下来将认证增加到特定的路由上。只需要添加在路由和控制器方法之间即可,在post,put,delete这些请求上增加了auth。忽略get请求。

router.get('/books', bookCtrl.books);
router.post('/book', auth, bookCtrl.bookCreate);
router.get('/book/:bookid', bookCtrl.bookReadOne);
router.put('/books/:bookid', auth, bookCtrl.bookUpdateOne);
router.delete('/book/:bookid', auth, bookCtrl.bookDeleteOne);

刚好介于路由和控制器之间。如果请求的token是非法的或者根本不存在,中间件将抛出错误并阻止代码继续执行。所以我们应该捕获到错误并返回一个未认证的消息和一个401的状态。而最适合做这件事的地方就是在app.js中。

// error handlers
app.use(function(err, req, res, next) {
    if (err.name == 'UnauthorizedError') {
        res.status(401);
        res.json({ message: err.name + ":" + err.message });
    }
});

接下来测试下api/book的post方法。

这个时候回返回一个认证失败的错误。说明auth发挥作用了。以上是验证失败的情况,如果验证成功,那如何使用JWT数据呢?还需要实现一个getAuthor的方法,用来验证token,并获取当前用户信息。在app_api/controller/book.js添加

var User = mongoose.model('User');
var
getAuthor = function (req, res, callback) { if (req.payload && req.payload.email) { User.findOne({ email: req.payload.email }) .exec(function (err, user) { if (!user) { sendJSONresponse(res, 404, { message: "User not found" }); return; } else if (err) { console.log(err); sendJSONresponse(res, 404, err); return; } callback(req, res,user); }); } else { sendJSONresponse(res, 404, { message : "User not found" }); return; } };

注意到这里的payload对象,正是我们在auth中定义的。然后通过邮箱去查找用户。最后传递给回调函数。而这儿的回调函数正是那些需要认证的控制器,修改bookCreate:

module.exports.bookCreate = function (req, res) {
    getAuthor(req, res, function(req, res,user) {
        console.log("imgurl:", req.body.img);
        BookModel.create({
            title: req.body.title,
            info: req.body.info,
            img: req.body.img,
            tags: req.body.tags,
            brief: req.body.brief,
            ISBN: req.body.ISBN,
            rating: req.body.rating,
            username: user.name,
            userId:user._id
        }, function (err, book) {
            if (err) {
                console.log(err);
                sendJSONresponse(res, 400, err);
            } else {
                console.log("新增书籍:", book);
                sendJSONresponse(res, 201, book);
            }
        });
    });
};

相当于是在原来的bookCreate方法上包裹一层(这样嵌套的写法看着有点难受。关于函数组织的方式以后专门讨论)。而且注意到我给book模型增加了username和userId两个属性。便于是记录是谁新增或更新了数据。

三、创建Angular认证服务

到目前为止,后台的所有准备工作已经做完了。包括给模型增加扩展方法、创建登录、注册的api,给路由设置认证等等。接下来的工作转移到前端,先用Angular创建认证相关的服务,这个服务应该负责所有和认证相关的事情,包括保存和读取JWT,返回当前用户的信息,以及调用登录和注册方法。

假设用户已经登录,api返回了一个jwt,但我们应该如何处理这个token呢,如果保存在内存中,用户一刷新就没了,那我们应该是用cookies还是ocal storage呢?

传统的做法是将用户数据保存在一个cookie中,cookie多用于服务端,每个到服务端的请求都会在http头中带上cookie。在SPA中,我们不需要这样,api是无状态的,不需要获取或设置cookie。所以我们选择本地存储。本地存储使用起来也很方便:

window.localStorage['my-data'] = 'Some information';
window.localStorage['my-data']; // Returns 'Some information'

所以接下来我们创建一个服务包含两个方法,saveToken和getToken。创建一个authentication.service.js,位于app_client/common/services。

(function () {
    angular
        .module('readApp')
        .service('authentication', authentication);

    authentication.$inject = ['$window'];
    function authentication($window) {
        var saveToken = function (token) {
            $window.localStorage['read-token'] = token;
        };
        var getToken = function () {
            return $window.localStorage['read-token'];
        };
        return {
            saveToken: saveToken,
            getToken: getToken
        };
    }
})();

这里使用了一个$window对象代替了原生的window对象,创建了两个方法并返回。不要忘记加入appClientFiles。 登录和注册我们已经在api中写好了。现在还需要在服务中创建登录,注册和退出三个方法:

     var register = function(user) {
            return $http.post('/api/register', user).success(function(data) {
                saveToken(data.token);
            });
        };
        var login = function(user) {
            return $http.post('/api/login', user).success(function(data) {
                saveToken(data.token);
            });
        };
        var logout = function() {
            $window.localStorage.removeItem('read-token');
        };

        return {
            saveToken: saveToken,
            getToken: getToken,
            register: register,
            login: login,
            logout: logout
        };

接下来的问题是 如何获得用户登录之后的数据,比如显示姓名。 保存在localStorage中的数据包含了用户信息,我们需要解析jwt,不是简单的判断token是否存在,还要判断是否过期。所以我们还需要增加一个方法:isLoggedIn

 var isLoggedIn = function() {
            var token = getToken();
            if (token) {
                var payload = JSON.parse($window.atob(token.split('.')[1]));
                return payload.exp > Date.now() / 1000;
            } else {
                return false;
            }
        };

通过atob方法解码字符串,再转换为json。别忘记加入return中。只有isloggedIn还不够,我们希望直接获取到用户的信息,比如email和name。因此还需要增加一个currentUser方法。

var currentUser = function() {
            if (isLoggedIn()) {
                var token = getToken();
                var payload = JSON.parse($window.atob(token.split('.')[1]));
                return {
                    email: payload.email,
                    name: payload.name,
                };
            }
        };

同上,我们解析jwt的第二段字符串即可。到这儿,authentication服务已经完成了,你可以发现这个代码非常容易提供给别的应用使用。也许需要改变的只是api地址和token的名称而已。现在服务已经可以使用了,接下来还需要创建注册和登录页面。

 四、创建注册和登录页面

1.注册

创建一个注册页面有四步,我们希望用户注册成功之后返回原来的页面。

  1. 定义一个Angular路由
  2. 创建视图。
  3. 创建视图的控制器。
  4. 注册成功之后跳转到之前的页面。

先在app_client/app.js下定义路由。视图文件置于app_client/auth/register/目录下。定义路由如下:

  .when('/register', {
            templateUrl: '/auth/register/register.view.html',
            caseInsensitiveMatch: true,
            controller: 'registerCtrl',
            controllerAs: 'vm'
        })

在创建register.view.html视图:

<navigation></navigation>
<div id="bodycontent" class="container">
    <div class="row">
        <div class="col-md-6 col-sm-12">
            <p class="lead">已有账号?去<a href="/#login">登录</a></p>
            <form ng-submit="vm.onSubmit()">
                <div role="alert" ng-show="vm.formError" class="alert alert-danger">{{vm.formError}}</div>
                <div class="form-group">
                    <label for="name">用户名</label>
                    <input type="text" class="form-control" id="name" name="name" placeholder="输入名称" ng-model="vm.credentials.name" value="" />
                </div>
                <div class="form-group">
                    <label for="email">Email</label>
                    <input type="email" id="email" class="form-control" ng-model="vm.credentials.email" placeholder="邮箱" value="" />
                </div>
                <div class="form-group">
                    <label for="password">密码</label>
                    <input type="password" class="form-control" id="password" placeholder="密码" ng-model="vm.credentials.password" value="" />
                </div>
                <button type="submit" class="btn btn-default" >注册</button>
            </form>
        </div>
    </div>
</div>
<footer-nav></footer-nav>

页面上已经没有多少好讲的,需要注意的是我们将用户的name,email和password绑定到了vm.credentials对象。接下来实现控制器。这个控制器需要提供一个vm.onSubmit方法处理form的提交;初始化credentials对象;另外我们希望用户注册完成之后返回之前的页面,实现这个我们定义一个查询参数,获取当前页面。

registerCtrl控制器:

(function() {
    angular.module('readApp')
        .controller('registerCtrl', registerCtrl);
    registerCtrl.$inject = ['$location','authentication'];
    function registerCtrl($location, authentication) {
        var vm = this;
        vm.credentials = {
            name: "",
            email: '',
            password: ''
        };

        vm.returnPage = $location.search().page || '/';
        vm.onSubmit = function() {

        };
    }
})();

上面用$location来获取参数page的值,然后赋值到returnPage,这样就知道了用户之前的页面。但是用户也有可能在注册页面上点击登录,所以还需要更新下页面:

  <p class="lead">已有账号?去<a href="/#login?page={{vm.returnPage}}">登录</a></p>

接下来完善onSubmit方法。

 vm.onSubmit = function() {
            vm.formError = "";
            if (!vm.credentials.name || !vm.credentials.email || !vm.credentials.password) {
                vm.formError = "需要填完所有字段!";
                return false;
            } else {
                vm.doRegister();
            }
        };
        vm.doRegister = function() {
            vm.formError = "";
            authentication.register(vm.credentials).error(function(err) {
                vm.formError = err;
            }).then(function() {
                $location.search('page', null);
                $location.path(vm.returnPage);
            });
        };

先验证用户信息(验证的比较简单)然后再调用authentication服务的register方法。成功之后跳转页面。同样不要忘记把相关js加入appClientFiles ,这个时候访问http://localhost:3000/Register 页面已经出来。

界面是有点丑,我先承认,但这不是重点。继续往下走。这个时候如果注册成功,会跳转到首页。在页面的Resource下可以看到,localStorage已经存储了一个read-token的值。

如果邮箱重复,会报错:当然这个提示还需要处理一下,不然太难看了。

2.登录

登录页面就是套路了,和注册页面一样,我们需要建路由,视图,控制器,很多代码可以copy过来。不细讲了。

路由:

  .when('/login', {
            templateUrl: '/auth/login/login.view.html',
            controller: 'loginCtrl',
            caseInsensitiveMatch: true,
            controllerAs: 'vm'
        })

视图:

<navigation></navigation>
<div id="bodycontent" class="container">
    <div class="row">
        <div  class="page-header">
            <h1>登录</h1>
        </div>
        <div class="col-md-6 col-sm-12 page">
            <p class="lead">没有账号?去<a href="/#register?page={{vm.returnPage}}">注册</a></p>
            <form ng-submit="vm.onSubmit()">
                <div role="alert" ng-show="vm.formError" class="alert alert-danger">{{vm.formError}}</div>
                <div class="form-group">
                    <label for="email">Email</label>
                    <input type="email" id="email" class="form-control" ng-model="vm.credentials.email" placeholder="邮箱" value="" />
                </div>
                <div class="form-group">
                    <label for="password">密码</label>
                    <input type="password" class="form-control" id="password" placeholder="密码" ng-model="vm.credentials.password" value="" />
                </div>
                <button type="submit" class="btn btn-default" >登录</button>
            </form>
        </div>
    </div>
</div>
<footer-nav></footer-nav>

基本上把注册页面复制过来,稍微修改一下。控制器也可以拿过来稍作修改:

(function () {
    angular.module('readApp')
        .controller('loginCtrl', loginCtrl);
    loginCtrl.$inject = ['$location', 'authentication'];
    function loginCtrl($location, authentication) {
        var vm = this;
        vm.credentials = {
            email: '',
            password: ''
        };

        vm.returnPage = $location.search().page || '/';
        vm.onSubmit = function () {
            vm.formError = "";
            if (!vm.credentials.email || !vm.credentials.password) {
                vm.formError = "请输入邮箱和密码!";
                return false;
            } else {
                vm.doLogin();
            }
        };
        vm.doLogin = function () {
            vm.formError = "";
            authentication.login(vm.credentials).error(function (err) {
                vm.formError = err;
            }).then(function () {
                $location.search('page', null);
                $location.path(vm.returnPage);
            });
        };
    }
})();

然加入appClientFiles 数组。访问/login 得到页面

测试一下,登录成功。密码和用户名错误会给出提示。接下来我们还需要更新导航条。当用户登录之后,我们希望显示用户名和一个退出链接。

3.更新导航条

导航条是我们在前面的章节定义好了的一个指令。要实现更新名称的功能还需要增加一个控制器,同时也启用controllerAs语法,为了避免冲突(其他控制器的视图模型都叫vm,而导航条又会一直存在),指定视图模型名称为navvm。

 function navigation() {
        return {
            restrict: 'EA',
            templateUrl: '/common/directive/navigation/navigation.html',
            controller: 'navigationCtrl as navvm'
        };
    }
将视图模型定义为navvm,然后在同目录下创建一个navigation.controller.js,并加入appClientFiles数组。这个控制器有两个任务,一个是获取当前用户,一个是获取当前的地址以便用户登录或注册之后能跳转回来。所以这个控制器会使用到authentication和$location两个服务。

控制器:

(function() {
      angular.module("readApp")
        .controller('navigationCtrl', navigationCtrl);
    navigationCtrl.$inject = ['$location', 'authentication'];
    function navigationCtrl($location, authentication) {
        var vm = this;
        vm.currentPath = $location.path();
    };
})()

在控制器中还是可以继续使用vm名称,只是在视图中换成了navvm:

 <li><a href="/#register?page={{ navvm.currentPath }}">注册</a></li>
 <li><a href="/#login?page={{ navvm.currentPath }}">登录</a></li>

当用户登录后,我们还需要显示用户名称,并可以让用户可以退出。因此增加了isLoggedIn、logout和currentUser。

       vm.isLoggedIn = authentication.isLoggedIn();
        vm.currentUser = authentication.currentUser();
        vm.logout = function () {
            authentication.logout();
            $location.path('/');
        };

整个导航条如下:

<nav class="navbar navbar-default navbar-fixed-top navbar-inverse">
    <div class="container">
        <div class="navbar-header"><a href="/" class="navbar-brand">ReadingClub</a></div>
        <div class="collapse navbar-collapse">

            <ul class="nav navbar-nav pull-right">
                <li><a href="/">首页</a></li>
                <li><a href="/#books">读物</a></li>
                <li><a href="/#about">关于</a></li>
                <li ng-hide="navvm.isLoggedIn"><a href="/#register?page={{ navvm.currentPath }}">注册</a></li>
                <li ng-hide="navvm.isLoggedIn"><a href="/#login?page={{ navvm.currentPath }}">登录</a></li>
                <li ng-show="navvm.isLoggedIn" class="dropdown">
                    <a href="" class="dropdown-toggle" data-toggle="dropdown">{{navvm.currentUser.name }}</a>
                    <ul class="dropdown-menu" role="menu">
                        <li><a href="" ng-click="navvm.logout()">退出</a></li>
                    </ul>
                </li>
            </ul>

        </div>
    </div>
</nav>

使用ng-hide和ng-show指令来切换显示li元素。运行下,看下效果:

大功告成了吗?还没,接下来还有一个问题,新增推荐书目现在是需要用户认证信息的,那么我们如何将用户的jwt通过Service传递到api呢?

jwt是通过一个叫Authorization的http头传递过去,但是有一定的格式,需要在'Bearer ' 单词后加个空格 然后再跟上jwt。修改下booksData

booksData.$inject = ['$http','authentication'];
function booksData($http,authentication) {
    var getBooks = $http.get('/api/books');
    var getbookById = function(bookid) {
        return $http.get('/api/book/' + bookid);
    };
    var addBook = function(data) {
        return $http.post("/api/book", data, {
            headers: {
                Authorization: 'Bearer ' + authentication.getToken()
            }
        });
    };
    var removeBookById = function(bookid) {
        return $http.delete('/api/book/' + bookid);
    };
    return {
        getBooks: getBooks,
        getbookById: getbookById,
        addBook: addBook,
        removeBookById: removeBookById
    };
};

接下来让新增按钮只有在用户登录之后才出现。修改booksCtrl:

 booksCtrl.$inject = ['booksData','$modal', '$location','authentication'];
    function booksCtrl(booksData,$modal, $location, authentication) {
        var vm = this;
        vm.message = "loading...";
        booksData.getBooks.success(function (data) {
            vm.message = data.length > 0 ? "" : "暂无数据";
            vm.books = data;
        }).error(function (e) {
            console.log(e);
            vm.message = "Sorry, something's gone wrong ";
        });
        vm.user = authentication.currentUser();
        vm.isLoggedIn = authentication.isLoggedIn();
        vm.currentPath = $location.path();
//...

视图:books.html 侧边栏

<div class="col-md-3">
            <div class="userinfo">
                <p>{{vm.user.name}}</p>
                <a ng-show="vm.isLoggedIn"  ng-click="vm.popupForm()" class="btn btn-info">新增推荐</a>
                <a ng-hide="vm.isLoggedIn" href="/#/login?page={{ vm.currentPath }}" class="btn btn-default  ">登录后推荐书籍</a>
        </div>
 </div>

测试下登录后新增书籍:

可以看到,用户信息插入到book模型中了。

源码:http://files.cnblogs.com/files/stoneniqiu/ReadingClub0721.zip

github:https://github.com/stoneniqiu/ReadingClub

小结:回顾这章,篇幅很长,信息量大。我们学习了MEAN中如何做用户认证和会话管理,包括加密用户密码,给Mongoose模型增加方法,创建一个json web token,使用passport管理认证,使用了本地存储去保存jwt。创建登录注册页面以及给Angular指令添加控制器等等,知识点比较多,需要理解和连贯起来。到这一节,MEAN系列第一个阶段基本上告一段落了,MEAN栈是一个前后端都使用JavaScript的技术栈,从数据库api到路由到前端,后端采用Express,前端是Angular。node后端还比较有名的还有koa,前端就更多了vue,backbone等等。不能说前后端都采用JavaScript有多好或者有多坏,相对于强类型语言它还有很多不足和不便,但目前来看,它已经很健壮了,请不要没有了解就轻视它,开发一个完整的网站完全不是什么问题,node搭建后台服务更是强项。关于MEAN栈或者其他相关JavaScript技术栈的探索我会继续,谢谢你的关注。