1. 程式人生 > >JWT(JSON Web Tokens)的使用

JWT(JSON Web Tokens)的使用

由來

做了這麼長時間的web開發,從JAVA EE中的jsf,spring,hibernate框架,到spring web MVC,到用php框架thinkPHP,到現在的nodejs,我自己的看法是越來越喜歡乾淨整潔的web層,之前用jsf開發做view層的時候,用的primefaces做的介面顯示,雖然primefaces的確提供了很大的便利,可以讓開發人員專注於業務邏輯開發,這樣其實就省去了前端開發的工作。而後來發現有些客戶需要的展現形式很難實現,或者通過拼湊的方法實現的結果效率不高。使用不靈活,後來自己漸漸的轉向了做前端工程師。spring WEB MVC可以做到乾淨整潔的web層,可以做到web層分離,通過ajax和服務端通訊。現在在學習AngularJS框架,後臺資料服務端打算用REST風格的介面來做,這個在前後臺互動上就要考慮資料通訊的安全問題,關於這個在

關於SESSION的理解一文中其實有提到的。



轉載請註明出處:http://www.haomou.net/2014/08/13/2014_web_token/

來龍去脈

諸如Ember,Angular,Backbone之類的前端框架類庫正隨著更加精細的Web應用而日益壯大。正因如此,伺服器端的組建也正正在從傳統的任務中解脫,轉而變的更像API。API使得傳統的前端和後端的概念解耦。開發者可以脫離前端,獨立的開發後端,在測試上獲得更大的便利。這種途徑也使得一個移動應用和網頁應用可以使用相同的後端。

當使用一個API時,其中一個挑戰就是認證(authentication)。在傳統的web應用中,服務端成功的返回一個響應(response)依賴於兩件事。一是,他通過一種儲存機制儲存了會話資訊(Session)。每一個會話都有它獨特的資訊(id),常常是一個長的,隨機化的字串,它被用來讓未來的請求(Request)檢索資訊。其次,包含在響應頭(Header)裡面的資訊使客戶端儲存了一個Cookie。伺服器自動的在每個子請求裡面加上了會話ID,這使得伺服器可以通過檢索Session中的資訊來辨別使用者。這就是傳統的web應用逃避HTTP面向無連線的方法(This is how traditional web applications get around the fact that HTTP is stateless)。

API應該被設計成無狀態的(Stateless)。這意味著沒有登陸,登出的方法,也沒有sessions,API的設計者同樣也不能依賴Cookie,因為不能保證這些request是由瀏覽器所發出的。自然,我們需要一個新的機制。這篇文章關注於JSON Web Tokens,簡寫為JWTs,一個可能的解決這個問題的機制。這篇文章利用Node的Express框架作為後端,以及Backbone作為前端。

常用方法

第一個是使用在HTTP規範中所制定的Basic Auth, 它需要在在響應中設定一個驗證身份的Header。客戶端必須在每個子響應是附加它們的憑證(credenbtial),包括它的密碼。如果這些憑證通過了,那麼使用者的資訊就會被傳遞到服務端應用。

第二個方面有點類似,但是使用應用自己的驗證機制。通常包括將傳送的憑證與儲存的憑證進行檢查。和Basic Auth相比,這種需要在每次請求(call)中傳送憑證。

第三種是OAuth(或者OAuth2)。為第三方的認證所設計,但是更難配置。至少在伺服器端更難。

在使用中,並不會每次都讓使用者提交使用者名稱和密碼,通常的情況是客戶端通過一些可靠資訊和伺服器交換取token,這個token作為客服端再次請求的許可權鑰匙。Token通常比密碼更加長而且複雜。比如說,JWTs通常會長達150個字元。一旦獲得了token,在每次呼叫API的時候都要附加上它。然後,這仍然比直接傳送賬戶和密碼更加安全,哪怕是HTTPS。
把token想象成一個安全的護照。你在一個安全的前臺驗證你的身份(通過你的使用者名稱和密碼),如果你成功驗證了自己,你就可以取得這個。當你走進大樓的時候(試圖從呼叫API獲取資源),你會被要求驗證你的護照,而不是在前臺重新驗證。

JWTs

JWTs是一份草案,儘管在本質上它是一個老生常談的一種更加具體的認證授權的機制。一個JWT被週期(period)分成了三個部分。JWT是URL-safe的,意味著可以用來查詢字元引數。(譯者注:也就是可以脫離URL,不用考慮URL的資訊)。關於Json Web Token,參考 http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html

JWT的第一部分是對一個簡單js物件的編碼後的字串,這個js物件是用來描述這個token型別以及使用的hash演算法。下面的例子展示的是一個使用了HMAC SHA-256演算法的JWT token。

1
2
3
4
{
  "typ" : "JWT",
  "alg" : "HS256"
}

在加密之後,這個物件變成了一個字串:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
JWT的第二部分是token的核心,這部分同樣是對一個js物件的編碼,包含了一些摘要資訊。有一些是必須的,有一些是選擇性的。例項如下:

1
2
3
4
5
{
  "iss": "joe",
  "exp": 1300819380,
  "http://example.com/is_root": true
}

這個結構被稱為JWT Claims Set。這個iss是issuer的簡寫,表明請求的實體,可以是發出請求的使用者的資訊。exp是expires的簡寫,是用來指定token的生命週期。(相關引數參看:the document)加密編碼之後如下:

1
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ

JWT的第三個部分,是JWT根據第一部分和第二部分的簽名(Signature)。像這個樣子:

1
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

最後將上面的合併起來,JWT token如下:

1
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

處理Tokens

我們將用JWT simple模組去處理token,它將使我們從鑽研如何加密解密中解脫出來。如果你有興趣,可以閱讀這篇說明,或者讀這個倉庫的原始碼。
首先我們將使用下面的命令安裝這個庫。記住你可以在命令中加入—save,讓其自動的讓其加入到你的package.json檔案裡面。

npm install jwt-simple

在你應用的初始環節,加入以下程式碼。這個程式碼引入了Express和JWT simple,而且建立了一個新的Express應用。最後一行設定了app的一個名為jwtTokenSecret的變數,其值為‘YOUR_SECRET_STRING’(記得把它換成別的)。

var express = require('express');
var jwt = require('jwt-simple');
var app = express();

app.set('jwtTokenSecret', 'YOUR_SECRET_STRING');

獲取token

我們需要做的第一件事就是讓客戶端通過他們的賬號密碼交換token。這裡有2種可能的方法在RESTful API裡面。第一種是使用POST請求來通過驗證,使服務端傳送帶有token的響應。除此之外,你可以使用GET請求,這需要他們使用引數提供憑證(指URL),或者更好的使用請求頭。
這篇文章的目的是為了解釋token驗證的方法而不是基本的使用者名稱/密碼驗證機制。所以我們假設我們已經通過請求得到了使用者名稱和密碼:

1
User.findOne({ username: username }, function(err, user) {
  if (err) {
    // user not found
    return res.send(401);
  }

  if (!user) {
    // incorrect username
    return res.send(401);
  }

  if (!user.validPassword(password)) {
    // incorrect password
    return res.send(401);
  }

  // User has authenticated OK
  res.send(200);
});

如果使用者成功驗證賬號和密碼,然後我們生成一個token,返回給使用者。

1
var expires = moment().add('days', 7).valueOf();
var token = jwt.encode({
  iss: user.id,
  exp: expires
}, app.get('jwtTokenSecret'));

res.json({
  token : token,
  expires: expires,
  user: user.toJSON()
});

注意到jwt.encode()函式有2個引數。第一個就是一個需要加密的物件,第二個是一個加密的金鑰。這個token是由我們之前提到的iss和exp組成的。注意到Moment.js被用來設定token將在7天之後失效。而res.json()方法用來傳遞這個JSON物件給客戶端。

驗證Token

客戶端獲取到token後,應該在每次向伺服器請求資料時附帶這個token,然後服務端驗證token。
為了驗證JWT,我們需要寫出一些可以完成這些功能的中介軟體(Middleware):

  • 檢查附上的token
  • 試圖解密
  • 驗證token的可用性
  • 如果token是合法的,檢索裡面使用者的資訊,以及附加到請求的物件上
    我們來寫一箇中間件的框架
    1
    
    // @file jwtauth.js
    
    var UserModel = require('../models/user');
    var jwt = require('jwt-simple');
    
    module.exports = function(req, res, next) {
      // code goes here
    };
    

為了獲得最大的可擴充套件性,我們允許客戶端使用一下3個方法附加我們的token:作為請求連結(query)的引數,作為主體的引數(body),和作為請求頭(Header)的引數。對於最後一個,我們將使用Header x-access-token。

下面是我們的允許在中介軟體的程式碼,試圖去檢索token:

1
var token = (req.body && req.body.access_token) || (req.query && req.query.access_token) || req.headers['x-access-token'];

注意到他為了訪問req.body,我們需要首先使用express.bodyParser()中介軟體(譯者注,這個是Express 3.x的中介軟體)。
下一步,我們講解析JWT:

1
if (token) {
  try {
    var decoded = jwt.decode(token, app.get('jwtTokenSecret'));

    // handle token here

  } catch (err) {
    return next();
  }
} else {
  next();
}

如果解析的過程失敗,那麼JWT Simple元件將會丟擲一段異常。如果異常發生了,或者沒有token,我們將會呼叫next()來繼續處理請求。這代表喆我們無法確定使用者。如果一個合格的token合法並且被解碼,我們應該得到2個屬性,iss包含著使用者ID以及exp包含token過期的時間戳。我們將首先處理後者,如果它過期了,我們就拒絕它:

1
if (decoded.exp <= Date.now()) {
  res.end('Access token has expired', 400);
}

如果token依舊合法,我們可以從中檢索出使用者資訊,並且附加到請求物件裡面去:

1
User.findOne({ _id: decoded.iss }, function(err, user) {
  req.user = user;
});

最後,將這個中介軟體附加到路由裡面:

1
var jwtauth = require('./jwtauth.js');

app.get('/something', [express.bodyParser(), jwtauth], function(req, res){
  // do something
});

或者匹配一些路由

1
app.all('/api/*', [express.bodyParser(), jwtauth]);

客戶端請求

我們提供了一個簡單的get端去獲得一個遠端的token。這非常直接了,所以我們不用糾結細節,就是發起一個請求,傳遞使用者名稱和密碼,如果請求成功了,我們就會得到一個包含著token的響應。

我們現在研究的是後續的請求。一個方法是通過JQuery的ajaxSetup()方法。這可以直接用來做Ajax請求,或者通過前端框架使用包裝過的Ajax方法。比如,假設我們將我們的請求使用window.localStorage.setItem(‘token’, ‘the-long-access-token’);放在本地儲存(Local Storage)裡面,我們可以通過這種方法將token附加到請求頭裡面:

1
var token = window.localStorage.getItem('token');

if (token) {
  $.ajaxSetup({
    headers: {
      'x-access-token': token
    }
  });
}

很簡單,但是這會劫持所有Ajax請求,如果這裡有一個token在本地儲存裡面。它將會附加到一個名為x-access-token的Header裡面。

bear token

OAuth 2.0 (RFC 6749) 定義了 Client 如何取得 Access Token 的方法。Client 可以用 Access Token 以 Resource Owner 的名義來向 Resource Server 取得 Protected Resource ,例如我 (Resource Owner) 授權一個手機 App (Client) 以我 (Resource Owner) 的名義去 Facebook (Resource Server) 取得我的朋友名單 (Protected Resource)。OAuth 2.0 定義Access Token 是 Resource Server 用來認證的唯一方式,有了這個, Resource Server 就不需要再提供其他認證方式,例如賬號密碼。

然而在 RFC 6749 裡面只定義抽象的概念,細節如 Access Token 格式、怎麼傳到 Resource Server ,以及 Access Token 無效時, Resource Server 怎麼處理,都沒有定義。所以在 RFC 6750 另外定義了 Bearer Token 的用法。Bearer Token 是一種 Access Token ,由 Authorization Server 在 Resource Owner 的允許下核發給 Client ,Resource Server 只要認在這個 Token 就可以認定 Client 已經獲取 Resource Owner 的許可,不需要用密碼學的方式來驗證這個 Token 的真偽。關於Token 被偷走的安全性問題,另一篇再說。

Bearer Token 的格式

1
Bearer XXXXXXXX

其中 XXXXXXXX 的格式為 b64token ,ABNF 的定義:

1
b64token = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="

寫成 Regular Expression 即是:

1
/[A-Za-z0-9\-\._~\+\/]+=*/

關於Bear Token還是打算另起一篇,詳細說明:Bearer Token

express-jwt例項

下面給一個具體的例項,這個例子的客戶端是web app,使用AngularJS框架。服務端使用NodeJS做的RESTful API介面,客戶端直接呼叫介面資料,其中使用了token認證機制。
當用戶把他的授權資訊發過來的時候, Node.js 服務檢查是否正確,然後返回一個基於使用者資訊的唯一 token 。 AngularJS 應用把 token 儲存在使用者的 SessionStorage ,之後的在傳送請求的時候,在請求頭裡面加上包含這個 token 的 Authorization。如果 endpoint 需要確認使用者授權,服務端檢查驗證這個 token,然後如果成功了就返回資料,如果失敗了返回 401 或者其它的異常。
用到的技術:

  • AngularJS
  • NodeJS ( express.js, express-jwt 和 moongoose)
  • MongoDB
  • Redis (備用,用於記錄使用者退出登入時候還沒有超時的token)

    客戶端 : AngularJS 部分

    首先,我們來建立我們的 AdminUserCtrl controller 和處理 login/logout 動作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    appControllers.controller('AdminUserCtrl', ['$scope', '$location', '$window', 'UserService', 'AuthenticationService',
        function AdminUserCtrl($scope, $location, $window, UserService, AuthenticationService) {
    
            //Admin User Controller (login, logout)
            $scope.logIn = function logIn(username, password) {
                if (username !== undefined && password !== undefined) {
    
                    UserService.logIn(username, password).success(function(data) {
                        AuthenticationService.isLogged = true;
                        $window.sessionStorage.token = data.token;
                        $location.path("/admin");
                    }).error(function(status, data) {
                        console.log(status);
                        console.log(data);
                    });
                }
            }
    
            $scope.logout = function logout() {
                if (AuthenticationService.isLogged) {
                    AuthenticationService.isLogged = false;
                    delete $window.sessionStorage.token;
                    $location.path("/");
                }
            }
        }
    ]);
    

這個 controller 用了兩個 service: UserService 和 AuthenticationService。第一個處理呼叫 REST api 用證書。後面一個處理使用者的認證。它只有一個布林值,用來表示使用者是否被授權。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
appServices.factory('AuthenticationService', function() {
    var auth = {
        isLogged: false
    }

    return auth;
});
appServices.factory('UserService', function($http) {
    return {
        logIn: function(username, password) {
            return $http.post(options.api.base_url + '/login', {username: username, password: password});
        },

        logOut: function() {

        }
    }
});

好了,我們需要做張登陸頁面:

1
<form class="form-horizontal" role="form">
    <div class="form-group">
        <label for="inputUsername" class="col-sm-4 control-label">Username</label>
        <div class="col-sm-4">
            <input type="text" class="form-control" id="inputUsername" placeholder="Username" ng-model="login.email">
        </div>
    </div>
    <div class="form-group">
        <label for="inputPassword" class="col-sm-4 control-label">Password</label>
        <div class="col-sm-4">
            <input type="password" class="form-control" id="inputPassword" placeholder="Password" ng-model="login.password">
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-offset-4 col-sm-10">
            <button type="submit" class="btn btn-default" ng-click="logIn(login.email, login.password)">Log In</button>
        </div>
    </div>
</form>

當用戶傳送他的資訊過來,我們的 controller 把內容傳送到 Node.js 伺服器,如果資訊可用,我們把 AuthenticationService裡面的 isLogged 設為 true。我們把從服務端發過來的 token 存起來,以便下次請求的時候使用。等講到 Node.js 的時候我們會看看怎麼處理。

好了,我們要往每個請求裡面追加一個特殊的頭資訊了:[Authorization: Bearer ] 。為了實現這個需求,我們建立一個服務,叫 TokenInterceptor。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
appServices.factory('TokenInterceptor', function ($q, $window, AuthenticationService) {
    return {
        request: function (config) {
            config.headers = config.headers || {};
            if ($window.sessionStorage.token) {
                config.headers.Authorization = 'Bearer ' + $window.sessionStorage.token;
            }
            return config;
        },

        response: function (response) {
            return response || $q.when(response);
        }
    };
});

然後我們把這個interceptor 追加到 $httpProvider :

1
2
3
app.config(function ($httpProvider) {
    $httpProvider.interceptors.push('TokenInterceptor');
});

然後,我們要開始配置路由了,讓 AngularJS 知道哪些需要授權,在這裡,我們需要檢查使用者是否已經被授權,也就是檢視 AuthenticationService 的 isLogged 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
app.config(['$locationProvider', '$routeProvider', 
  function($location, $routeProvider) {
    $routeProvider.
        when('/', {
            templateUrl: 'partials/post.list.html',
            controller: 'PostListCtrl'
        }).
        when('/post/:id', {
            templateUrl: 'partials/post.view.html',
            controller: 'PostViewCtrl'
        }).
        when('/tag/:tagName', {
            templateUrl: 'partials/post.list.html',
            controller: 'PostListTagCtrl'
        }).
        when('/admin', {
            templateUrl: 'partials/admin.post.list.html',
            controller: 'AdminPostListCtrl',
            access: { requiredLogin: true }
        }).
        when('/admin/post/create', {
            templateUrl: 'partials/admin.post.create.html',
            controller: 'AdminPostCreateCtrl',
            access: { requiredLogin: true }
        }).
        when('/admin/post/edit/:id', {
            templateUrl: 'partials/admin.post.edit.html',
            controller: 'AdminPostEditCtrl',
            access: { requiredLogin: true }
        }).
        when('/admin/login', {
            templateUrl: 'partials/admin.login.html',
            controller: 'AdminUserCtrl'
        }).
        when('/admin/logout', {
            templateUrl: 'partials/admin.logout.html',
            controller: 'AdminUserCtrl',
            access: { requiredLogin: true }
        }).
        otherwise({
            redirectTo: '/'
        });
}]);

app.run(function($rootScope, $location, $window, AuthenticationService) {
    $rootScope.$on("$routeChangeStart", function(event, nextRoute, currentRoute) {
        //redirect only if both isLogged is false and no token is set
        if (nextRoute != null && nextRoute.access != null && nextRoute.access.requiredLogin 
            && !AuthenticationService.isLogged && !$window.sessionStorage.token) {

            $location.path("/admin/login");
        }
    });
});

服務端: Node.js + MongoDB 部分

為了在我們的 RESTful api 處理授權資訊,我們要用到 express-jwt (JSON Web Token) 來生成一個唯一 Token,基於使用者的資訊。以及驗證 Token。

首先,我們在 MongoDB 裡面建立一個使用者的 Schema。我們還要建立呼叫一箇中間件,在建立和儲存使用者資訊到資料庫之前,用於加密密碼。還有我們需要一個方法來解密密碼,當收到使用者請求的時候,檢查是否在資料庫裡面有匹配的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var Schema = mongoose.Schema;

// User schema
var User = new Schema({
    username: { type: String, required: true, unique: true },
    password: { type: String, required: true}
});

// Bcrypt middleware on UserSchema
User.pre('save', function(next) {
  var user = this;

  if (!user.isModified('password')) return next();

  bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
    if (err) return next(err);

    bcrypt.hash(user.password, salt, function(err, hash) {
        if (err) return next(err);
        user.password = hash;
        next();
    });
  });
});

//Password verification
User.methods.comparePassword = function(password, cb) {
    bcrypt.compare(password, this.password, function(err, isMatch) {
        if (err) return cb(err);
        cb(isMatch);
    });
};

然後我們開始寫授權使用者和建立 Token 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
exports.login = function(req, res) {
    var username = req.body.username || '';
    var password = req.body.password || '';

    if (username == '' || password == '') {
        return res.send(401);
    }

    db.userModel.findOne({username: username}, function (err, user) {
        if (err) {
            console.log(err);
            return res.send(401);
        }

        user.comparePassword(password, function(isMatch) {
            if (!isMatch) {
                console.log("Attempt failed to login with " + user.username);
                return res.send(401);
            }

            var token = jwt.sign(user, secret.secretToken, { expiresInMinutes: 60 });

            return res.json({token:token});
        });

    });
};

最後,我們需要把 jwt 中介軟體加到所有的,訪問時需要授權的路由上面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
Get all published posts
*/
app.get('/post', routes.posts.list);
/*
    Get all posts
*/
app.get('/post/all', jwt({secret: secret.secretToken}), routes.posts.listAll);

/*
    Get an existing post. Require url
*/
app.get('/post/:id', routes.posts.read);

/*
    Get posts by tag
*/
app.get('/tag/:tagName', routes.posts.listByTag);

/*
    Login
*/
app.post('/login', routes.users.login);

/*
    Logout
*/
app.get('/logout', routes.users.logout);

/*
    Create a new post. Require data
*/
app.post('/post', jwt({secret: secret.secretToken}), routes.posts.create);

/*
    Update an existing post. Require id
*/
app.put('/post', jwt({secret: secret.secretToken}), routes.posts.update);

/*
    Delete an existing post. Require id
*/
app.delete('/post/:id', jwt({secret: secret.secretToken}), routes.posts.delete);

上面這個例項就採用了token的驗證方式構建了api介面,但是有兩個問題需要解決:

  • 使用者退出登入,但是token並沒有失效,因為服務端沒有刪除這個token
  • token失效了,怎麼辦,如果還是讓用於登入重新獲取token,會體驗不好。應該有token重新整理機制。

    使用Redis解決問題1

    解決方法是:當用戶點了 logout 按鈕的時候,Token 只會儲存一段時間,就是你用 jsonwebtoken 登陸之後,token 有效的這段時間,我們將這個token存放在Redis中,生存時間也是jwt獲取這個token的時間。這個時間到期後,token 會被 redis 自動刪掉。最後,我們建立一個 nodejs 的中介軟體,檢查所有受限 endopoint 用的 token 是否存在 Redis 資料庫中。

    NodeJS 配置 Reids

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    var redis = require('redis');
    var redisClient = redis.createClient(6379);
    
    redisClient.on('error', function (err) {
        console.log('Error ' + err);
    });
    
    redisClient.on('connect', function () {
        console.log('Redis is ready');
    });
    
    exports.redis = redis;
    exports.redisClient = redisClient;
    

然後,我們來建立一個方法,用來檢查提供的 token 是不是被

Token 管理和中介軟體

為了在 Redis 中儲存 Token,我們要建立一個方法來拿到請求中的 Header 的 Token 引數,然後把它作為 Redis 的 key 儲存起來。值是什麼我們不管它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var redisClient = require('./redis_database').redisClient;
var TOKEN_EXPIRATION = 60;
var TOKEN_EXPIRATION_SEC = TOKEN_EXPIRATION * 60;

exports.expireToken = function(headers) {
    var token = getToken(headers);

    if (token != null) {
        redisClient.set(token, { is_expired: true });
        redisClient.expire(token, TOKEN_EXPIRATION_SEC);
    }
};

var getToken = function(headers) {
    if (headers && headers.authorization) {
        var authorization = headers.authorization;
        var part = authorization.split(' ');

        if (part.length == 2) {
            var token = part[1];

            return part[1];
        }
        else {
            return null;
        }
    }
    else {
        return null;
    }
};

然後,再建立一箇中間件來驗證一下 token,當用戶發起請求的時候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Middleware for token verification
exports.verifyToken = function (req, res, next) {
    var token = getToken(req.headers);

    redisClient.get(token, function (err, reply) {
        if (err) {
            console.log(err);
            return res.send(500);
        }

        if (reply) {
            res.send(401);
        }
        else {
            next();
        }

    });
};

verifyToken 這個方法,是一箇中間件,用來拿到請求頭中的 token,然後在 Redis 裡面查詢它。如果 token 被發現了,我們就發 HTTP 401.否則我們就繼續工作流,讓請求訪問 API。

我們要在使用者點 logout 的時候,執行 expireToken 方法:

1
2
3
4
5
6
7
8
9
10
11
exports.logout = function(req, res) {
    if (req.user) {
        tokenManager.expireToken(req.headers);

        delete req.user;
        return res.send(200);
    }
    else {
        return res.send(401);
    }
}

最後我們更新路由,用上新的中介軟體:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Login
app.post('/user/signin', routes.users.signin);

//Logout
app.get('/user/logout', jwt({secret: secret.secretToken}), routes.users.logout);

//Get all posts
app.get('/post/all', jwt({secret: secret.secretToken}), tokenManager.verifyToken, routes.posts.listAll);

//Create a new post
app.post('/post', jwt({secret: secret.secretToken}), tokenManager.verifyToken , routes.posts.create);

//Edit the post id
app.put('/post', jwt({secret: secret.secretToken}), tokenManager.verifyToken, routes.posts.update);

//Delete the post id
app.delete('/post/:id', jwt({secret: secret.secretToken}), tokenManager.verifyToken, routes.posts.delete);

好了,現在我們每次傳送請求的時候,我們都去解析 token, 然後看看是不是有效的。
這裡有整個專案的原始碼

refresh token解決問題2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
appServices.factory('TokenInterceptor', function ($q, $window, $location, AuthenticationService) {
    return {
        request: function (config) {
            config.headers = config.headers || {};
            if ($window.sessionStorage.token) {
                config.headers.Authorization = 'Bearer ' + $window.sessionStorage.token;
            }
            return config;
        },

        requestError: function(rejection) {
            return $q.reject(rejection);
        },

        /* Set Authentication.isAuthenticated to true if 200 received */
        response: function (response) {
            if (response != null && response.status == 200 && $window.sessionStorage.token && !AuthenticationService.isAuthenticated) {
                AuthenticationService.isAuthenticated = true;
            }
            return response || $q.