1. 程式人生 > >使用json web token

使用json web token

undefined this dmi ber sub 數據庫 說明 nat publish

由來

做了這麽長時間的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
2
3
4
5
6
7
8
9
10
11
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
    2
    3
    4
    5
    6
    7
    8
    // @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
2
3
4
5
6
7
8
9
10
11
12
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
2
3
if (decoded.exp <= Date.now()) {
res.end(‘Access token has expired‘, 400);
}

如果token依舊合法,我們可以從中檢索出用戶信息,並且附加到請求對象裏面去:

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

最後,將這個中間件附加到路由裏面:

1
2
3
4
5
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
2
3
4
5
6
7
8
9
var token = window.localStorage.getItem(‘token‘);

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

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

bear token

關於bear token,參看 RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage , 目前國內各大網站都是用不同的token,也沒說必須使用bear token,只有twitter明確說明的是使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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.when(response);
},

/* Revoke client authentication if 401 is received */
responseError: function(rejection) {
if (rejection != null && rejection.status === 401 && ($window.sessionStorage.token || AuthenticationService.isAuthenticated)) {
delete $window.sessionStorage.token;
AuthenticationService.isAuthenticated = false;
$location.path("/admin/login");
}

return $q.reject(rejection);
}
};
});

上面代碼中的最後一部分responseError其實就是授權失敗的部分,這裏面的處理方法是返回到登錄授權頁面。
這裏面考慮的方法是,如果是token超時,使用refresh_token來換取新的token。這個refresh_token,是一開始核發的時候一塊發布給客戶端的,這裏就不能使用上面這個bear token了,要自己處理一下token的問題。
思路1:在user中記錄token超時時間,計算一下剩余時間,如果剩余時間比如說小於1分鐘,開始核發新的token,客戶端自動使用新的token,等退出時,就不核發新的token。

謝謝!

使用json web token