Koa入門教程[2]-經常使用中間件

中間件執行流程

中間件的執行流程,能夠用下面這張圖片來生動的說明(圖片使用了 Koa 2 的 async 語法):javascript

image.png

對於 Koa 1 來講也相似,只是 async 函數換做 generator 函數,await 換做 yield 關鍵字。css

對於前端程序員,能夠把 yield 以前的代碼認爲是捕獲階段,yield 以後的認爲的冒泡階段,從而理解多箇中間件之間代碼的執行流程。html

路由中間件

路由通常屬於業務代碼,咱們通常放在其餘基礎中間件以後來註冊。路由的基本原理就是判斷 url path, 而後決定是否執行某個中間件邏輯。前端

簡單實現能夠相似這樣:java

const Koa = require('koa')
const app = new Koa()

app.use(function *(next) {
    if (this.path === '/home') {
        this.body = '首頁'
    }
    else {
        yield next
    }
    console.log('這裏會執行哦')
})
app.use(function *(next) {
    if (this.path === '/admin') {
        this.body = '管理端'
    }
})
app.listen(3000)

能夠看到,對於不符合本中間件的請求 path, 就直接丟棄,並去執行下一個中間件。若是全部中間件都匹配不到,會返回 404(Koa 默認行爲).node

上面代碼有一個問題,就是 "console.log" 會一直執行,要解決這個也很簡單。由於對於路由中間件來講,全部邏輯都是匹配path的if判斷內部的,因此對於這個不匹配的else代碼,能夠直接當作該 generator 的結束。能夠在 yield 前面加 return 或這樣修改:git

app.use(function* (next) {
  if (this.path !== '/') return yield next;
  this.body = 'we are at home!';
})

koa-router

爲了應對更復雜的路由功能,咱們須要引入第三方的 koa-router 路由模塊。不過 Koa1 須要使用 4.x 版本的。程序員

Issue: You are using koa@1.x but koa-views@5.x needs koa@2 or above. If you are still at v1 please consider using koa-views@4.x. Note however, there are no updates supporting v1
npm i koa-router@4 -d

koa-router 暴露一個 Router 類,像 Vue.js 同樣,只需建立一個 router 實例,就能夠註冊對應的路由規則。github

var app = require('koa')();
var Router = require('koa-router');

var myRouter = new Router();

myRouter.get('/', function *(next) {
  this.response.body = 'Hello World!';
});

app.use(myRouter.routes());

app.listen(3000);

從用法中顯然能看出來,routers 方法返回的應該就是一個 generator 中間件函數,只是內部由 koa-router 進行了路由規則的處理和邏輯執行。開發者只需關注如何向 koa-router 對象上註冊 處理中間件npm

koa-router 像多數路由同樣支持不少 http 方法和匹配規則:

router.get()
router.post()
router.put()
router.del()
router.patch()

視圖渲染中間件

Koa 有不少的中間件 好比 [koa-view](
https://github.com/queckezz/k...

該中間件支持多種模板引擎。

cookie

cookie的獲取和設置是 Koa 內置的context集成的能力,不須要中間件的參與。

this.cookies.get('cookieName')
this.cookies

koa-router

爲了兼容 Koa1,咱們須要安裝一個老一點的koa-router

npm i koa-router@5.x

koa-logger

用於打印請求日誌和耗時

npm i koa-logger@1 // 1.x 支持 Koa1

session

// 爲了兼容 Koa1
npm i koa-session@3.x

該版本的 koa-session 只須要執行其導出函數並傳入一個 app 對象便可使用

const session = require('koa-session')
app.use(session(app))
app.use(function * (next) {
    console.log(this.session.xxx)
})

koa-compress

默認的Koa應用,咱們觀察下瀏覽器的 Response 響應的話,會發現雖然請求時瀏覽器攜帶了 Accept-Encoding: gzip, 但實際上響應裏面並無 Content-Encoding: gzip, 也就是說並無壓縮。

安裝 koa-compress@1.x 以後,就可讓 Koa 默認開啓對響應內容的壓縮了:

app.use(require('koa-compress')())

大一點的文件纔有效果哦,過小的話還比不上 'Content-Encoding' 頭所佔的字節的話,就有點得不償失了。

koa-csrf

# 爲了兼容 Koa1 請安裝 2.x 版本
npm i koa-csrf@2.x

該模塊的導出對象是一個函數,函數會建立一箇中間件,你須要將他註冊到 Koa 的app裏面。使用方式以下:

app.use(session(app)) // koa-csrf 的機制要依賴session能力
app.use(csrf()) // 這是koa1的用法

koa-csrf原理:

解決跨站請求僞造攻擊,須要在客戶端請求時攜帶一個祕密的token,這個token要確保只有服務器端知道,並且用後即焚. 其思路是,一個用戶在訪問頁面時,服務端先把這個 csrf-token 放置到頁面中,而後頁面再次發起 POST 請求時,頁面須要帶上這個token,由服務端來校驗是否是服務器頒發的token.

回到koa-csrf這個模塊,在每次請求週期中, koa-csrf 都會在它的中間件內生成一個祕鑰secret, 而後基於secret生成一個 csrf-token; 並把這個 csrf-token 掛在 ctx 上,把 secret 掛在session上(由於secret做爲一個祕鑰基於session能夠針對一個獨立用戶,不必每次都變). 咱們把koa-csrf 中生成token過程的源碼撿出來以下:

// 建立一個祕鑰並放在session裏,每次生成和校驗csrf時都用這個token
var secret = this.session.secret
        || (this.session.secret = tokens.secretSync())
// 摘錄tokens.secretSync的實現以下:
Tokens.prototype.secretSync = function secretSync () {
  return uid.sync(this.secretLength) // 其實就是使用 uid-safe 模塊生成一個固定長度的隨機uniqueId
}

有了secret祕鑰了,再來看下 csrf-token 咋生成的:

// 基於上一步的祕鑰secret來生成csrf-token 放在ctx對象上
this._csrf = tokens.create(secret)
// csrf-token的生成過程以下:
Tokens.prototype.create = function create (secret) {
  if (!secret || typeof secret !== 'string') {
    throw new TypeError('argument secret is required')
  }
  // 重點在這裏。其中rndm模塊僅僅就是用來生成n位數的隨機字符串;而_tokenize函數就是用來生成csrf-token的,其實現我摘錄在下面
  return this._tokenize(secret, rndm(this.saltLength))
}

// tokenize實現
Tokens.prototype._tokenize = function tokenize (secret, salt) {
    // csrf-token 的格式爲: salt隨機字符串 + hash(salt+secret)
  return salt + '-' + hash(salt + '-' + secret)
}

至此,csrf-token就生成了。接下來,你須要在 GET 請求的頁面上,把 this.csrf 渲染到頁面中。而後前端再次請求後端的 POST 接口時,須要帶上那個token。這樣,POST請求到達服務器時 koa-csrf 中間件就會在請求到來時優先進行校驗。

校驗規則已經顯而易見了:

  1. 從前端的query或body或cookie中取出 _csrf 這個變量(csrf-token)
  2. 按照csrf-token的規則,取出 橫線 前面的字符串做爲 salt隨機串,取後面的做爲 待校驗的哈希[fehash]
  3. 從服務端的session中(this.session.secret)拿出祕鑰 secret
  4. 使用與當初如出一轍的tokenize函數算一下這個哈希:
var result = hash('前端傳來的salt' + '服務端祕鑰secret')
  1. 比對本次算出來的 result 與 前端傳來的 待校驗fehash值 是否一致。不一致則說明是僞造的請求。koa-csrf 中間件會直接跑錯

那麼,會不會存在黑客在中間網絡竊取到某次請求的token後,再利用這個token來實施 CSRF 呢? 這個其實是沒法避免的,既然黑客能竊取到http報文(說明請求被中間人劫持或站點被XSS注入),那黑客徹底能夠竊取到 cookie 等信息,至關於徹底模擬了用戶,這種狀況下任何防範都沒有做用了;只能說若是發現IP變了那就要求用戶從新登陸且切換 secret。

更多中間件

幾乎全部的網絡應用所需的功能都有中間件提供。能夠在官方 wiki 中看到中間件列表

中間件編寫最佳實踐

帶參數的中間件

對於編寫公共中間件的場景來講,更多的須要用戶能自定義中間件中一些配置。此時須要支持用戶對中間件進行配置。要實現可配置的中間件也簡單,只須要寫一個包裝函數,返回一個 generator 的函數便可。例如咱們的日誌中間件,能夠容許用戶自定義日誌格式,則能夠這樣:

// 可自定義日誌格式的中間件
const mylogger = function (format) {
    format = format || '{{method}} {{url}} - {{time}}'
    return function *(next) {
        const start = Date.now()
        yield next
        const ms = Date.now() - start
        console.log(format
            .replace('{{method}}', this.method)
            .replace('{{url}}', this.url)
            .replace('{{time}}', ms)
        )
    }
}
// 使用該中間件
app.use(mylogger('{{time}} - {{method}} : {{url}}'))

合併多箇中間件

有時可能須要將多箇中間件合併爲一個。對於 Generator來講,可使用 .call(this. next) 的方式將他們合併。

const Koa = require('koa')
const app = new Koa()

function *a(next) {
    console.log('come a')
    yield next;
    console.log('end a')
}
  
function *b(next) {
    console.log('come b')
    yield next
    console.log('end b')
}

function *all(next) {
    console.log('come all')
    yield a.call(this, b.call(this, next));
    console.log('end all')
}

app.use(all)
app.listen(3000)

執行上述代碼,控制檯會輸出:

come all
come a
come b
end b
end a
end all

你必定比較疑惑爲何多個 generator 函數經過 call(this, next) 是怎麼作到如此合併執行的? 其實本質上 Koa 的運做也是基於合併 middlware 來執行的。這裏大概是這樣的:

  1. 首先咱們須要知道 a、b、all 都是 generator 函數;而 next 是 generator 對象
  2. a.call(this, b.call(this, next)) 至關於先執行 b.call(this, next) 建立 b 的 generator 對象。而後該對象會做爲 next 參數去執行 a 函數,進而建立出 a 的 generator 對象
  3. 所以 a.call(this, b.call(this, next)) 的返回值是 a 函數的 generator 對象;且 a 函數中的 next 表示的是 b 函數的 generator 對象。 這樣執行這個表達式的返回值,便至關於執行:
function *a(next) {
    console.log('come a')
    yield (b中間件 的 generator 對象);
    console.log('end a')
}
  1. 如此,all 這個 generator 函數就把各個中間件聯合起來執行了。

上述過程相似於 koa-compse 模塊的合併能力, 這裏貼一個 compose 模塊的實現(引用自阮一峯的 Koa 教程):

function compose(middleware){
  return function *(next){
    if (!next) next = noop();

    var i = middleware.length;

    while (i--) {
      next = middleware[i].call(this, next);
    }

    yield *next;
  }
}

function *noop(){}

以上就是中間件合併的原理了,合併後會返回一個新的 generator 函數。而 Koa 是如何使用 co 庫把合併後的 generator 中間件函數運行起來的呢? 這個就有點複雜了,更詳細的 middleware合併 和 Koa 原理能夠參考: qianlongo github

總結

本章節介紹了幾個經常使用中間件,如koa-router和 koa-view,並對中間件的合併和傳參進行了簡單介紹。基本上 Koa 的全部使用方式都已經介紹完畢,後面就是赤裸裸的實踐了

相關文章
相關標籤/搜索