前面章節(jié)寫(xiě)到,我們通過(guò) Router 將用戶的請(qǐng)求基于 method 和 URL 分發(fā)到了對(duì)應(yīng)的 Controller 上,那 Controller 負(fù)責(zé)做什么?
簡(jiǎn)單的說(shuō) Controller 負(fù)責(zé)解析用戶的輸入,處理后返回相應(yīng)的結(jié)果,例如
框架推薦 Controller 層主要對(duì)用戶的請(qǐng)求參數(shù)進(jìn)行處理(校驗(yàn)、轉(zhuǎn)換),然后調(diào)用對(duì)應(yīng)的 service 方法處理業(yè)務(wù),得到業(yè)務(wù)結(jié)果后封裝并返回:
所有的 Controller 文件都必須放在 app/controller 目錄下,可以支持多級(jí)目錄,訪問(wèn)的時(shí)候可以通過(guò)目錄名級(jí)聯(lián)訪問(wèn)。Controller 支持多種形式進(jìn)行編寫(xiě),可以根據(jù)不同的項(xiàng)目場(chǎng)景和開(kāi)發(fā)習(xí)慣來(lái)選擇。
我們可以通過(guò)定義 Controller 類(lèi)的方式來(lái)編寫(xiě)代碼:
// app/controller/post.js |
我們通過(guò)上面的代碼定義了一個(gè) PostController 的類(lèi),類(lèi)里面的每一個(gè)方法都可以作為一個(gè) Controller 在 Router 中引用到,我們可以從 app.controller 根據(jù)文件名和方法名定位到它。
// app/router.js |
Controller 支持多級(jí)目錄,例如如果我們將上面的 Controller 代碼放到 app/controller/sub/post.js 中,則可以在 router 中這樣使用:
// app/router.js |
定義的 Controller 類(lèi),會(huì)在每一個(gè)請(qǐng)求訪問(wèn)到 server 時(shí)實(shí)例化一個(gè)全新的對(duì)象,而項(xiàng)目中的 Controller 類(lèi)繼承于 egg.Controller,會(huì)有下面幾個(gè)屬性掛在 this 上。
按照類(lèi)的方式編寫(xiě) Controller,不僅可以讓我們更好的對(duì) Controller 層代碼進(jìn)行抽象(例如將一些統(tǒng)一的處理抽象成一些私有方法),還可以通過(guò)自定義 Controller 基類(lèi)的方式封裝應(yīng)用中常用的方法。
// app/core/base_controller.js |
此時(shí)在編寫(xiě)應(yīng)用的 Controller 時(shí),可以繼承 BaseController,直接使用基類(lèi)上的方法:
//app/controller/post.js |
每一個(gè) Controller 都是一個(gè) async function,它的入?yún)檎?qǐng)求的上下文 Context 對(duì)象的實(shí)例,通過(guò)它我們可以拿到框架封裝好的各種便捷屬性和方法。
例如我們寫(xiě)一個(gè)對(duì)應(yīng)到 POST /api/posts 接口的 Controller,我們會(huì)在 app/controller 目錄下創(chuàng)建一個(gè) post.js 文件
// app/controller/post.js |
在上面的例子中我們引入了許多新的概念,但還是比較直觀,容易理解的,我們會(huì)在下面對(duì)它們進(jìn)行更詳細(xì)的介紹。
由于 Controller 基本上是業(yè)務(wù)開(kāi)發(fā)中唯一和 HTTP 協(xié)議打交道的地方,在繼續(xù)往下了解之前,我們首先簡(jiǎn)單的看一下 HTTP 協(xié)議是怎樣的。
如果我們發(fā)起一個(gè) HTTP 請(qǐng)求來(lái)訪問(wèn)前面例子中提到的 Controller:
curl -X POST http://localhost:3000/api/posts --data '{"title":"controller", "content": "what is controller"}' --header 'Content-Type:application/json; charset=UTF-8' |
通過(guò) curl 發(fā)出的 HTTP 請(qǐng)求的內(nèi)容就會(huì)是下面這樣的:
POST /api/posts HTTP/1.1 |
請(qǐng)求的第一行包含了三個(gè)信息,我們比較常用的是前面兩個(gè):
從第二行開(kāi)始直到遇到的第一個(gè)空行位置,都是請(qǐng)求的 Headers 部分,這一部分中有許多常用的屬性,包括這里看到的 Host,Content-Type,還有 Cookie,User-Agent 等等。在這個(gè)請(qǐng)求中有兩個(gè)頭:
之后的內(nèi)容全部都是請(qǐng)求的 body,當(dāng)請(qǐng)求是 POST, PUT, DELETE 等方法的時(shí)候,可以帶上請(qǐng)求體,服務(wù)端會(huì)根據(jù) Content-Type 來(lái)解析請(qǐng)求體。
在服務(wù)端處理完這個(gè)請(qǐng)求后,會(huì)發(fā)送一個(gè) HTTP 響應(yīng)給客戶端
HTTP/1.1 201 Created |
第一行中也包含了三段,其中我們常用的主要是響應(yīng)狀態(tài)碼,這個(gè)例子中它的值是 201,它的含義是在服務(wù)端成功創(chuàng)建了一條資源。
和請(qǐng)求一樣,從第二行開(kāi)始到下一個(gè)空行之間都是響應(yīng)頭,這里的 Content-Type, Content-Length 表示這個(gè)響應(yīng)的格式是 JSON,長(zhǎng)度為 8 個(gè)字節(jié)。
最后剩下的部分就是這次響應(yīng)真正的內(nèi)容。
從上面的 HTTP 請(qǐng)求示例中可以看到,有好多地方可以放用戶的請(qǐng)求數(shù)據(jù),框架通過(guò)在 Controller 上綁定的 Context 實(shí)例,提供了許多便捷方法和屬性獲取用戶通過(guò) HTTP 請(qǐng)求發(fā)送過(guò)來(lái)的參數(shù)。
在 URL 中 ? 后面的部分是一個(gè) Query String,這一部分經(jīng)常用于 GET 類(lèi)型的請(qǐng)求中傳遞參數(shù)。例如 GET /posts?category=egg&language=node 中 category=egg&language=node 就是用戶傳遞過(guò)來(lái)的參數(shù)。我們可以通過(guò) ctx.query 拿到解析過(guò)后的這個(gè)參數(shù)體
class PostController extends Controller { |
當(dāng) Query String 中的 key 重復(fù)時(shí),ctx.query 只取 key 第一次出現(xiàn)時(shí)的值,后面再出現(xiàn)的都會(huì)被忽略。GET /posts?category=egg&category=koa 通過(guò) ctx.query 拿到的值是 { category: 'egg' }。
這樣處理的原因是為了保持統(tǒng)一性,由于通常情況下我們都不會(huì)設(shè)計(jì)讓用戶傳遞 key 相同的 Query String,所以我們經(jīng)常會(huì)寫(xiě)類(lèi)似下面的代碼:
const key = ctx.query.key || ''; |
而如果有人故意發(fā)起請(qǐng)求在 Query String 中帶上重復(fù)的 key 來(lái)請(qǐng)求時(shí)就會(huì)引發(fā)系統(tǒng)異常。因此框架保證了從 ctx.query 上獲取的參數(shù)一旦存在,一定是字符串類(lèi)型。
有時(shí)候我們的系統(tǒng)會(huì)設(shè)計(jì)成讓用戶傳遞相同的 key,例如 GET /posts?category=egg&id=1&id=2&id=3。針對(duì)此類(lèi)情況,框架提供了 ctx.queries 對(duì)象,這個(gè)對(duì)象也解析了 Query String,但是它不會(huì)丟棄任何一個(gè)重復(fù)的數(shù)據(jù),而是將他們都放到一個(gè)數(shù)組中:
// GET /posts?category=egg&id=1&id=2&id=3 |
ctx.queries 上所有的 key 如果有值,也一定會(huì)是數(shù)組類(lèi)型。
在 Router 中,我們介紹了 Router 上也可以申明參數(shù),這些參數(shù)都可以通過(guò) ctx.params 獲取到。
// app.get('/projects/:projectId/app/:appId', 'app.listApp'); |
雖然我們可以通過(guò) URL 傳遞參數(shù),但是還是有諸多限制:
在前面的 HTTP 請(qǐng)求報(bào)文示例中,我們看到在 header 之后還有一個(gè) body 部分,我們通常會(huì)在這個(gè)部分傳遞 POST、PUT 和 DELETE 等方法的參數(shù)。一般請(qǐng)求中有 body 的時(shí)候,客戶端(瀏覽器)會(huì)同時(shí)發(fā)送 Content-Type 告訴服務(wù)端這次請(qǐng)求的 body 是什么格式的。Web 開(kāi)發(fā)中數(shù)據(jù)傳遞最常用的兩類(lèi)格式分別是 JSON 和 Form。
框架內(nèi)置了 bodyParser 中間件來(lái)對(duì)這兩類(lèi)格式的請(qǐng)求 body 解析成 object 掛載到 ctx.request.body 上。HTTP 協(xié)議中并不建議在通過(guò) GET、HEAD 方法訪問(wèn)時(shí)傳遞 body,所以我們無(wú)法在 GET、HEAD 方法中按照此方法獲取到內(nèi)容。
// POST /api/posts HTTP/1.1 |
框架對(duì) bodyParser 設(shè)置了一些默認(rèn)參數(shù),配置好之后擁有以下特性:
一般來(lái)說(shuō)我們最經(jīng)常調(diào)整的配置項(xiàng)就是變更解析時(shí)允許的最大長(zhǎng)度,可以在 config/config.default.js 中覆蓋框架的默認(rèn)值。
module.exports = { |
如果用戶的請(qǐng)求 body 超過(guò)了我們配置的解析最大長(zhǎng)度,會(huì)拋出一個(gè)狀態(tài)碼為 413 的異常,如果用戶請(qǐng)求的 body 解析失?。ㄥe(cuò)誤的 JSON),會(huì)拋出一個(gè)狀態(tài)碼為 400 的異常。
注意:在調(diào)整 bodyParser 支持的 body 長(zhǎng)度時(shí),如果我們應(yīng)用前面還有一層反向代理(Nginx),可能也需要調(diào)整它的配置,確保反向代理也支持同樣長(zhǎng)度的請(qǐng)求 body。
一個(gè)常見(jiàn)的錯(cuò)誤是把 ctx.request.body 和 ctx.body 混淆,后者其實(shí)是 ctx.response.body 的簡(jiǎn)寫(xiě)。
請(qǐng)求 body 除了可以帶參數(shù)之外,還可以發(fā)送文件,一般來(lái)說(shuō),瀏覽器上都是通過(guò) Multipart/form-data 格式發(fā)送文件的,框架通過(guò)內(nèi)置 Multipart 插件來(lái)支持獲取用戶上傳的文件,我們?yōu)槟闾峁┝藘煞N方式:
如果你完全不知道 Nodejs 中的 Stream 用法,那么 File 模式非常合適你:
1)在 config 文件中啟用 file 模式:
// config/config.default.js |
2)上傳 / 接收文件:
你的前端靜態(tài)頁(yè)面代碼應(yīng)該看上去如下樣子:
<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data"> |
對(duì)應(yīng)的后端代碼如下:
// app/controller/upload.js |
對(duì)于多個(gè)文件,我們借助 ctx.request.files 屬性進(jìn)行遍歷,然后分別進(jìn)行處理:
你的前端靜態(tài)頁(yè)面代碼應(yīng)該看上去如下樣子:
<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data"> |
對(duì)應(yīng)的后端代碼:
// app/controller/upload.js |
如果你對(duì)于 Node 中的 Stream 模式非常熟悉,那么你可以選擇此模式。在 Controller 中,我們可以通過(guò) ctx.getFileStream() 接口能獲取到上傳的文件流。
<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data"> |
const path = require('path'); |
要通過(guò) ctx.getFileStream 便捷的獲取到用戶上傳的文件,需要滿足兩個(gè)條件:
如果要獲取同時(shí)上傳的多個(gè)文件,不能通過(guò) ctx.getFileStream() 來(lái)獲取,只能通過(guò)下面這種方式:
const sendToWormhole = require('stream-wormhole'); |
為了保證文件上傳的安全,框架限制了支持的的文件格式,框架默認(rèn)支持白名單如下:
// images |
用戶可以通過(guò)在 config/config.default.js 中配置來(lái)新增支持的文件擴(kuò)展名,或者重寫(xiě)整個(gè)白名單
module.exports = { |
module.exports = { |
注意:當(dāng)重寫(xiě)了 whitelist 時(shí),fileExtensions 不生效。
欲了解更多相關(guān)此技術(shù)細(xì)節(jié)和詳情,請(qǐng)參閱 Egg-Multipart。
除了從 URL 和請(qǐng)求 body 上獲取參數(shù)之外,還有許多參數(shù)是通過(guò)請(qǐng)求 header 傳遞的??蚣芴峁┝艘恍┹o助屬性和方法來(lái)獲取。
由于 header 比較特殊,有一些是 HTTP 協(xié)議規(guī)定了具體含義的(例如 Content-Type,Accept),有些是反向代理設(shè)置的,已經(jīng)約定俗成(X-Forwarded-For),框架也會(huì)對(duì)他們?cè)黾右恍┍憬莸?getter,詳細(xì)的 getter 可以查看 API 文檔。
特別是如果我們通過(guò) config.proxy = true 設(shè)置了應(yīng)用部署在反向代理(Nginx)之后,有一些 Getter 的內(nèi)部處理會(huì)發(fā)生改變。
優(yōu)先讀通過(guò) config.hostHeaders 中配置的 header 的值,讀不到時(shí)再?lài)L試獲取 host 這個(gè) header 的值,如果都獲取不到,返回空字符串。
config.hostHeaders 默認(rèn)配置為 x-forwarded-host。
通過(guò)這個(gè) Getter 獲取 protocol 時(shí),首先會(huì)判斷當(dāng)前連接是否是加密連接,如果是加密連接,返回 https。
如果處于非加密連接時(shí),優(yōu)先讀通過(guò) config.protocolHeaders 中配置的 header 的值來(lái)判斷是 HTTP 還是 https,如果讀取不到,我們可以在配置中通過(guò) config.protocol 來(lái)設(shè)置兜底值,默認(rèn)為 HTTP。
config.protocolHeaders 默認(rèn)配置為 x-forwarded-proto。
通過(guò) ctx.ips 獲取請(qǐng)求經(jīng)過(guò)所有的中間設(shè)備 IP 地址列表,只有在 config.proxy = true 時(shí),才會(huì)通過(guò)讀取 config.ipHeaders 中配置的 header 的值來(lái)獲取,獲取不到時(shí)為空數(shù)組。
config.ipHeaders 默認(rèn)配置為 x-forwarded-for。
通過(guò) ctx.ip 獲取請(qǐng)求發(fā)起方的 IP 地址,優(yōu)先從 ctx.ips 中獲取,ctx.ips 為空時(shí)使用連接上發(fā)起方的 IP 地址。
注意:ip 和 ips 不同,ip 當(dāng) config.proxy = false 時(shí)會(huì)返回當(dāng)前連接發(fā)起者的 ip 地址,ips 此時(shí)會(huì)為空數(shù)組。
HTTP 請(qǐng)求都是無(wú)狀態(tài)的,但是我們的 Web 應(yīng)用通常都需要知道發(fā)起請(qǐng)求的人是誰(shuí)。為了解決這個(gè)問(wèn)題,HTTP 協(xié)議設(shè)計(jì)了一個(gè)特殊的請(qǐng)求頭:Cookie。服務(wù)端可以通過(guò)響應(yīng)頭(set-cookie)將少量數(shù)據(jù)響應(yīng)給客戶端,瀏覽器會(huì)遵循協(xié)議將數(shù)據(jù)保存,并在下次請(qǐng)求同一個(gè)服務(wù)的時(shí)候帶上(瀏覽器也會(huì)遵循協(xié)議,只在訪問(wèn)符合 Cookie 指定規(guī)則的網(wǎng)站時(shí)帶上對(duì)應(yīng)的 Cookie 來(lái)保證安全性)。
通過(guò) ctx.cookies,我們可以在 Controller 中便捷、安全的設(shè)置和讀取 Cookie。
class CookieController extends Controller { |
Cookie 雖然在 HTTP 中只是一個(gè)頭,但是通過(guò) foo=bar;foo1=bar1; 的格式可以設(shè)置多個(gè)鍵值對(duì)。
Cookie 在 Web 應(yīng)用中經(jīng)常承擔(dān)了傳遞客戶端身份信息的作用,因此有許多安全相關(guān)的配置,不可忽視,Cookie 文檔中詳細(xì)介紹了 Cookie 的用法和安全相關(guān)的配置項(xiàng),可以深入閱讀了解。
對(duì)于 Cookie 來(lái)說(shuō),主要有下面幾個(gè)屬性可以在 config.default.js 中進(jìn)行配置:
module.exports = { |
舉例: 配置應(yīng)用級(jí)別的 Cookie SameSite 屬性等于 Lax。
module.exports = { |
通過(guò) Cookie,我們可以給每一個(gè)用戶設(shè)置一個(gè) Session,用來(lái)存儲(chǔ)用戶身份相關(guān)的信息,這份信息會(huì)加密后存儲(chǔ)在 Cookie 中,實(shí)現(xiàn)跨請(qǐng)求的用戶身份保持。
框架內(nèi)置了 Session 插件,給我們提供了 ctx.session 來(lái)訪問(wèn)或者修改當(dāng)前用戶 Session 。
class PostController extends Controller { |
Session 的使用方法非常直觀,直接讀取它或者修改它就可以了,如果要?jiǎng)h除它,直接將它賦值為 null:
class SessionController extends Controller { |
和 Cookie 一樣,Session 也有許多安全等選項(xiàng)和功能,在使用之前也最好閱讀 Session 文檔深入了解。
對(duì)于 Session 來(lái)說(shuō),主要有下面幾個(gè)屬性可以在 config.default.js 中進(jìn)行配置:
module.exports = { |
在獲取到用戶請(qǐng)求的參數(shù)后,不可避免的要對(duì)參數(shù)進(jìn)行一些校驗(yàn)。
借助 Validate 插件提供便捷的參數(shù)校驗(yàn)機(jī)制,幫助我們完成各種復(fù)雜的參數(shù)校驗(yàn)。
// config/plugin.js |
通過(guò) ctx.validate(rule, [body]) 直接對(duì)參數(shù)進(jìn)行校驗(yàn):
class PostController extends Controller { |
當(dāng)校驗(yàn)異常時(shí),會(huì)直接拋出一個(gè)異常,異常的狀態(tài)碼為 422,errors 字段包含了詳細(xì)的驗(yàn)證不通過(guò)信息。如果想要自己處理檢查的異常,可以通過(guò) try catch 來(lái)自行捕獲。
class PostController extends Controller { |
參數(shù)校驗(yàn)通過(guò) Parameter 完成,支持的校驗(yàn)規(guī)則可以在該模塊的文檔中查閱到。
除了上一節(jié)介紹的內(nèi)置檢驗(yàn)類(lèi)型外,有時(shí)候我們希望自定義一些校驗(yàn)規(guī)則,讓開(kāi)發(fā)時(shí)更便捷,此時(shí)可以通過(guò) app.validator.addRule(type, check) 的方式新增自定義規(guī)則。
// app.js |
添加完自定義規(guī)則之后,就可以在 Controller 中直接使用這條規(guī)則來(lái)進(jìn)行參數(shù)校驗(yàn)了
class PostController extends Controller { |
我們并不想在 Controller 中實(shí)現(xiàn)太多業(yè)務(wù)邏輯,所以提供了一個(gè) Service 層進(jìn)行業(yè)務(wù)邏輯的封裝,這不僅能提高代碼的復(fù)用性,同時(shí)可以讓我們的業(yè)務(wù)邏輯更好測(cè)試。
在 Controller 中可以調(diào)用任何一個(gè) Service 上的任何方法,同時(shí) Service 是懶加載的,只有當(dāng)訪問(wèn)到它的時(shí)候框架才會(huì)去實(shí)例化它。
class PostController extends Controller { |
Service 的具體寫(xiě)法,請(qǐng)查看 Service 章節(jié)。
當(dāng)業(yè)務(wù)邏輯完成之后,Controller 的最后一個(gè)職責(zé)就是將業(yè)務(wù)邏輯的處理結(jié)果通過(guò) HTTP 響應(yīng)發(fā)送給用戶。
HTTP 設(shè)計(jì)了非常多的狀態(tài)碼,每一個(gè)狀態(tài)碼都代表了一個(gè)特定的含義,通過(guò)設(shè)置正確的狀態(tài)碼,可以讓響應(yīng)更符合語(yǔ)義。
框架提供了一個(gè)便捷的 Setter 來(lái)進(jìn)行狀態(tài)碼的設(shè)置
class PostController extends Controller { |
具體什么場(chǎng)景設(shè)置什么樣的狀態(tài)碼,可以參考 List of HTTP status codes 中各個(gè)狀態(tài)碼的含義。
絕大多數(shù)的數(shù)據(jù)都是通過(guò) body 發(fā)送給請(qǐng)求方的,和請(qǐng)求中的 body 一樣,在響應(yīng)中發(fā)送的 body,也需要有配套的 Content-Type 告知客戶端如何對(duì)數(shù)據(jù)進(jìn)行解析。
注意:ctx.body 是 ctx.response.body 的簡(jiǎn)寫(xiě),不要和 ctx.request.body 混淆了。
class ViewController extends Controller { |
由于 Node.js 的流式特性,我們還有很多場(chǎng)景需要通過(guò) Stream 返回響應(yīng),例如返回一個(gè)大文件,代理服務(wù)器直接返回上游的內(nèi)容,框架也支持直接將 body 設(shè)置成一個(gè) Stream,并會(huì)同時(shí)處理好這個(gè) Stream 上的錯(cuò)誤事件。
class ProxyController extends Controller { |
通常來(lái)說(shuō),我們不會(huì)手寫(xiě) HTML 頁(yè)面,而是會(huì)通過(guò)模板引擎進(jìn)行生成。 框架自身沒(méi)有集成任何一個(gè)模板引擎,但是約定了 View 插件的規(guī)范,通過(guò)接入的模板引擎,可以直接使用 ctx.render(template) 來(lái)渲染模板生成 html。
class HomeController extends Controller { |
具體示例可以查看模板渲染。
有時(shí)我們需要給非本域的頁(yè)面提供接口服務(wù),又由于一些歷史原因無(wú)法通過(guò) CORS 實(shí)現(xiàn),可以通過(guò) JSONP 來(lái)進(jìn)行響應(yīng)。
由于 JSONP 如果使用不當(dāng)會(huì)導(dǎo)致非常多的安全問(wèn)題,所以框架中提供了便捷的響應(yīng) JSONP 格式數(shù)據(jù)的方法,封裝了 JSONP XSS 相關(guān)的安全防范,并支持進(jìn)行 CSRF 校驗(yàn)和 referrer 校驗(yàn)。
// app/router.js |
// app/controller/posts.js |
用戶請(qǐng)求對(duì)應(yīng)的 URL 訪問(wèn)到這個(gè) controller 的時(shí)候,如果 query 中有 _callback=fn 參數(shù),將會(huì)返回 JSONP 格式的數(shù)據(jù),否則返回 JSON 格式的數(shù)據(jù)。
框架默認(rèn)通過(guò) query 中的 _callback 參數(shù)作為識(shí)別是否返回 JSONP 格式數(shù)據(jù)的依據(jù),并且 _callback 中設(shè)置的方法名長(zhǎng)度最多只允許 50 個(gè)字符。應(yīng)用可以在 config/config.default.js 全局覆蓋默認(rèn)的配置:
// config/config.default.js |
通過(guò)上面的方式配置之后,如果用戶請(qǐng)求 /api/posts/1?callback=fn,響應(yīng)為 JSONP 格式,如果用戶請(qǐng)求 /api/posts/1,響應(yīng)格式為 JSON。
我們同樣可以在 app.jsonp() 創(chuàng)建中間件時(shí)覆蓋默認(rèn)的配置,以達(dá)到不同路由使用不同配置的目的:
// app/router.js |
默認(rèn)配置下,響應(yīng) JSONP 時(shí)不會(huì)進(jìn)行任何跨站攻擊的防范,在某些情況下,這是很危險(xiǎn)的。我們初略將 JSONP 接口分為三種類(lèi)型:
如果我們的 JSONP 接口提供下面兩類(lèi)服務(wù),在不做任何跨站防御的情況下,可能泄露用戶敏感數(shù)據(jù)甚至導(dǎo)致用戶被釣魚(yú)。因此框架給 JSONP 默認(rèn)提供了 CSRF 校驗(yàn)支持和 referrer 校驗(yàn)支持。
在 JSONP 配置中,我們只需要打開(kāi) csrf: true,即可對(duì) JSONP 接口開(kāi)啟 CSRF 校驗(yàn)。
// config/config.default.js |
注意,CSRF 校驗(yàn)依賴(lài)于 security 插件提供的基于 Cookie 的 CSRF 校驗(yàn)。
在開(kāi)啟 CSRF 校驗(yàn)時(shí),客戶端在發(fā)起 JSONP 請(qǐng)求時(shí),也要帶上 CSRF token,如果發(fā)起 JSONP 的請(qǐng)求方所在的頁(yè)面和我們的服務(wù)在同一個(gè)主域名之下的話,可以讀取到 Cookie 中的 CSRF token(在 CSRF token 缺失時(shí)也可以自行設(shè)置 CSRF token 到 Cookie 中),并在請(qǐng)求時(shí)帶上該 token。
如果在同一個(gè)主域之下,可以通過(guò)開(kāi)啟 CSRF 的方式來(lái)校驗(yàn) JSONP 請(qǐng)求的來(lái)源,而如果想對(duì)其他域名的網(wǎng)頁(yè)提供 JSONP 服務(wù),我們可以通過(guò)配置 referrer 白名單的方式來(lái)限制 JSONP 的請(qǐng)求方在可控范圍之內(nèi)。
//config/config.default.js |
whiteList 可以配置為正則表達(dá)式、字符串或者數(shù)組:
exports.jsonp = { |
exports.jsonp = { |
exports.jsonp = { |
當(dāng) CSRF 和 referrer 校驗(yàn)同時(shí)開(kāi)啟時(shí),請(qǐng)求發(fā)起方只需要滿足任意一個(gè)條件即可通過(guò) JSONP 的安全校驗(yàn)。
我們通過(guò)狀態(tài)碼標(biāo)識(shí)請(qǐng)求成功與否、狀態(tài)如何,在 body 中設(shè)置響應(yīng)的內(nèi)容。而通過(guò)響應(yīng)的 Header,還可以設(shè)置一些擴(kuò)展信息。
通過(guò) ctx.set(key, value) 方法可以設(shè)置一個(gè)響應(yīng)頭,ctx.set(headers) 設(shè)置多個(gè) Header。
// app/controller/api.js |
框架通過(guò) security 插件覆蓋了 koa 原生的 ctx.redirect 實(shí)現(xiàn),以提供更加安全的重定向。
用戶如果使用ctx.redirect方法,需要在應(yīng)用的配置文件中做如下配置:
// config/config.default.js |
若用戶沒(méi)有配置 domainWhiteList 或者 domainWhiteList數(shù)組內(nèi)為空,則默認(rèn)會(huì)對(duì)所有跳轉(zhuǎn)請(qǐng)求放行,即等同于ctx.unsafeRedirect(url)
更多建議: