網絡傳入安全jwts

使用json web token

發表於
Aug 13 2014
php

由來

作了這麼長時間的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的理解一文中其實有提到的。
html

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

前因後果

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

當使用一個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)。git

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

經常使用方法

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

第二個方面有點相似,可是使用應用本身的驗證機制。一般包括將發送的憑證與存儲的憑證進行檢查。和Basic Auth相比,這種須要在每次請求(call)中發送憑證。ajax

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

在使用中,並不會每次都讓用戶提交用戶名和密碼,一般的狀況是客戶端經過一些可靠信息和服務器交換取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,等退出時,就不

相關文章
相關標籤/搜索