Nodejs之MEAN棧開發(八)---- 用戶認證與會話管理詳解


用戶認證與會話管理基本上是每個網站必備的一個功能。在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技術棧的探索我會繼續,謝謝你的關注。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM