重拾后端之Spring Boot(五) -- 跨域伸眶、自定義查詢及分頁

重拾后端之Spring Boot(一):REST API的搭建可以這樣簡單
重拾后端之Spring Boot(二):MongoDb的無縫集成
重拾后端之Spring Boot(三):找回熟悉的Controller,Service
重拾后端之Spring Boot(四):使用 JWT 和 Spring Security 保護(hù) REST API
重拾后端之Spring Boot(五):跨域雾棺、自定義查詢及分頁
重拾后端之Spring Boot(六):熱加載白对、容器和多項(xiàng)目

跨域

前面我們初步做出了一個(gè)可以實(shí)現(xiàn)受保護(hù)的 REST API怔匣,但是我們沒有涉及一個(gè)前端領(lǐng)域很重要的問題击胜,那就是跨域請求( cross-origin HTTP request )亏狰。先來回顧一些背景知識:

跨域請求

定義:當(dāng)我們從本身站點(diǎn)請求不同域名或端口的服務(wù)所提供的資源時(shí),就會發(fā)起跨域請求偶摔。

例如最常見的我們很多的 css 樣式文件是會鏈接到某個(gè)公共 CDN 服務(wù)器上暇唾,而不是在本身的服務(wù)器上,這其實(shí)就是典型的一個(gè)跨域請求辰斋。但瀏覽器由于安全原因限制了在腳本( script )中發(fā)起的跨域 HTTP 請求策州。也就是說 XMLHttpRequestFetch 等是遵循“同源規(guī)則”的,即只能訪問自己服務(wù)器的指定端口的資源(同一服務(wù)器不同端口也會視為跨域)宫仗。但這種限制在今天够挂,我們的應(yīng)用需要訪問多種外部 API 或 資源的時(shí)候就不能滿足開發(fā)者的需求了,因此就產(chǎn)生了若干對于跨域的解決方案藕夫,JSONP 是其中一種孽糖,但在今天來看主流的更徹底的解決方案是 CORS ( Cross-Origin Resource Sharing )。

跨域資源共享 ( CORS )

這種機(jī)制將跨域的訪問控制權(quán)交給服務(wù)器汁胆,這樣可以保證安全的跨域數(shù)據(jù)傳輸∷笮眨現(xiàn)代瀏覽器一般會將 CORS 的支持封裝在 HTTP API 之中( 比如 XMLHttpRequestFetch )霜幼,這樣可以有效控制使用跨域請求的風(fēng)險(xiǎn)嫩码,因?yàn)槟憷@不過去,總得要使用 API 吧罪既。

概括來說铸题,這個(gè)機(jī)制是增加一系列的 HTTP 頭來讓服務(wù)器可以描述哪些源是允許使用瀏覽器來訪問資源的。而且對于簡單的請求和復(fù)雜請求琢感,處理機(jī)制是不一樣的丢间。

簡單請求僅允許三個(gè) HTTP 方法:GET,POST 以及 HEAD驹针,另外只能支持若干 header 參數(shù):Accept 烘挫, Accept-Language , Content-Language , Content-Type (值只能是 application/x-www-form-urlencoded饮六、multipart/form-datatext/plain)其垄, DPR , Downlink 卤橄, Save-Data 绿满, Viewport-Width 和 Width。

對于簡單請求來說窟扑,比如下面這樣一個(gè)簡單的GET請求:從 http://me.domain 發(fā)起到 http://another.domain/data/blablabla 的資源請求

GET /data/blablabla/ HTTP/1.1
// 請求的域名
Host: another.domain
...//省略其它部分喇颁,重點(diǎn)是下面這句,說明了發(fā)起請求者的來源
Origin: http://me.domain

應(yīng)用了 CORS 的對方服務(wù)器返回的響應(yīng)應(yīng)該像下面這個(gè)樣子嚎货,當(dāng)然這里 Access-Control-Allow-Origin: * 中的 * 表示任何網(wǎng)站都可以訪問該資源橘霎,如果要限制只能從 me.domain 訪問,那么需要改成 Access-Control-Allow-Origin: http://me.domain

HTTP/1.1 200 OK
...//省略其它部分
Access-Control-Allow-Origin: *
...//省略其它部分
Content-Type: application/json

那么對于復(fù)雜請求怎么辦呢殖属?這需要一次預(yù)檢請求和一次實(shí)際的請求茎毁,也就是說需要兩次和對方服務(wù)器的請求/響應(yīng)。預(yù)檢請求是以 OPTION 方法進(jìn)行的忱辅,因?yàn)?OPTION 方法不會改變?nèi)魏钨Y源七蜘,所以這個(gè)預(yù)檢請求是安全的,它的職責(zé)在于發(fā)送實(shí)際請求將會使用的 HTTP 方法以及將要發(fā)送的 HEADER 中將攜帶哪些內(nèi)容墙懂,這樣對方服務(wù)器可以根據(jù)預(yù)檢請求的信息決定是否接受橡卤。

// 預(yù)檢請求
OPTIONS /resources/post/ HTTP/1.1
Host: another.domain
...// 省略其它部分
Origin: http://me.domain
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

服務(wù)器對預(yù)檢請求的響應(yīng)如下:

HTTP/1.1 200 OK
// 省略其它部分
Access-Control-Allow-Origin: http://me.domain
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
// 省略其它部分
Content-Type: text/plain

接下來的正式請求就和上面的簡單請求差不多了,就不贅述了损搬。

在 Spring Boot 中如何啟用 CORS

啰嗦了這么多碧库,終于進(jìn)入正題,但我一直覺得不能光知其然而不知其所以然巧勤,所以各位就忍了吧嵌灰。加入 CORS 的支持在 Spring Boot 中簡單到不忍直視,添加一個(gè)配置類即可:

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // 設(shè)置你要允許的網(wǎng)站域名颅悉,如果全允許則設(shè)為 *
        config.addAllowedOrigin("http://localhost:4200");
        // 如果要限制 HEADER 或 METHOD 請自行更改
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        // 這個(gè)順序很重要哦沽瞭,為避免麻煩請?jiān)O(shè)置在最前
        bean.setOrder(0);
        return bean;
    }
}

如果我們使用 POSTMAN 訪問一下 API,會發(fā)現(xiàn)得到一個(gè) Invalid CORS request 的響應(yīng)剩瓶,因?yàn)槲覀兊?API 只授權(quán)給了 localhost:4200

用 POSTMAN 無法得到請求結(jié)果
用 POSTMAN 無法得到請求結(jié)果

當(dāng)然驹溃,如果我們使用 CURL 的話是可以訪問的,這是因?yàn)?CURL 不是瀏覽器延曙。

嗯嗯豌鹤,這樣就結(jié)束了,這節(jié)好水枝缔,但就是這么簡單啊布疙。

自定義查詢

我們回過頭來再來看看數(shù)據(jù)查詢,大部分情況下 Spring Data 提供的按方法名進(jìn)行查詢的方式足夠簡單也足夠強(qiáng)大,但總歸還是有很多局限灵临。為了說明這個(gè)問題拣挪,也順便為我的新 Angular 項(xiàng)目打造一個(gè) API,我們把 API 的需求改一下【阒睿現(xiàn)在我們要做的不是一個(gè)簡單的 Todo 了菠劝,而是類似 Teambition 或 Worktile 那樣的企業(yè)協(xié)作平臺,當(dāng)然我們不會做的那么復(fù)雜睁搭,是個(gè)簡化版本赶诊。那么這時(shí)我們的對象模型是這樣的:

主要的領(lǐng)域?qū)ο?><div   id=主要的領(lǐng)域?qū)ο?/div>

我們首先看一下 Project 這個(gè)對象,我們來構(gòu)建它的 API园骆,增刪改沒啥可講舔痪。但是查詢上會有點(diǎn)不一樣,首先我們并不希望把所有的 Project 都查出來锌唾,而是該用戶參與的項(xiàng)目要提供一個(gè) API 給客戶端锄码。

Project 和 User 按關(guān)系型數(shù)據(jù)庫的看法是個(gè)多對多的關(guān)系,在MongoDB中這其實(shí)有多種做法晌涕,可以在 User 對象中設(shè)置一個(gè) Project 的集合屬性滋捶,也可以在 Project 中設(shè)置 User 的集合屬性 (在我們的例子里是 memebers ),還可以兩者結(jié)合余黎,就是在 User 和 Projet 中互相有對方的集合屬性重窟。具體采用哪種需要看業(yè)務(wù)場景和性能需求,比如如果任何一個(gè)項(xiàng)目的成員數(shù)如果不會很大惧财,那么在 Project 中嵌入 User 集合就比較劃算巡扇;如果項(xiàng)目的成員較多,但一個(gè)成員歸屬的項(xiàng)目不會很多的情況下垮衷,就可以把 Project 的集合嵌入到 User 中厅翔。我們這里采用了第一種做法。

那么接下來我們來寫該用戶參與的項(xiàng)目的查詢搀突。當(dāng)然我們可以按照 Spring Data 強(qiáng)大的按方法名稱來生成對應(yīng)查詢的方式來做:尋找 members 屬性中包含該用戶的集合

Set<Project> findByMembersContaining(User user)

看起來還可以刀闷,挺簡單,但是如果我們說再加兩個(gè)條件要篩選 project.enabled == true (我們不會物理刪除項(xiàng)目描姚,而是設(shè)置其標(biāo)志位 enabled涩赢,所以這就是篩選未刪除的項(xiàng)目) 和 project.archived == false (項(xiàng)目完結(jié)后需要?dú)w檔戈次,這就是篩選未歸檔的)轩勘。這兩個(gè)條件一加上,好家伙怯邪,我們的方法名變成了下面這個(gè)樣子绊寻,不忍直視啊:

Set<Project> findByMembersContainingAndEnabledAndArchived(User user, boolean enabled, boolean archived)

當(dāng)然好用還是好用了,但是這個(gè)方法名也太長了澄步,好在 Spring Data 中提供很多種方式自定義查詢冰蘑,我們介紹一種相對簡單的:利用 @Query 注解來進(jìn)行查詢,方法名字就沒有那么雷人了:

@Query("{'owner.$id': ?#{[0]}, 'enabled': ?#{[1]}, 'archived': ?#{[2]}}")
Set<Project> findRelated(User user, boolean enabled, boolean archived)

這個(gè)注解中的內(nèi)容是一個(gè) JSON 對象村缸,就和我們在 MongoDB 的控制臺查詢的find()中的內(nèi)容是一樣的祠肥,只不過將雙引號換成單引號,將需要變量用 [0]梯皿、[1][2] 的形式表示第一仇箱、第二和第三個(gè)參數(shù)。那么 ?#{} 是表示里面的內(nèi)容是個(gè) SpEL ( Spring 的表達(dá)式語言) 表達(dá)式东羹。

所以實(shí)踐中剂桥,我們可以在 MongoDB 的控制臺去實(shí)驗(yàn)語句是否好用,然后在 Spring 中編寫表達(dá)式属提。

db.project.find(
    {
        "owner.$id": ObjectId("58f5a904edc76ab0e033cfc3"),        
        "enabled": true, 
        "archived": false
    })
在MongoDB的console查詢
在MongoDB的console查詢

數(shù)據(jù)的分頁

很多時(shí)候我們希望 API 返回的數(shù)據(jù)是可以分頁的权逗,這個(gè)分頁問題在 Spring Boot 有怎樣便捷的方法呢?我們是否需要再定義一堆什么 pageSize冤议,pageCount斟薇,start, off 的參數(shù)呢恕酸?答案是完全沒必要奔垦,分頁這個(gè)事情對于 Spring Boot 來說很簡單,只需改變各層級原有方法中返回的 List 或 Set 對象為 Page 對象尸疆,傳入?yún)?shù)多一個(gè) Pageable 類型的參數(shù)即可椿猎。

// Controller
@RequestMapping(method = RequestMethod.GET)
public Page<Project> findRelated(
        @RequestHeader(value = "userId") String userId,
        @RequestParam(value = "enabled", defaultValue = "true", required = false) boolean enabled,
        @RequestParam(value = "archived", defaultValue = "false", required = false) boolean archived,
        Pageable pageable) {
    return service.findRelated(userId, enabled, archived, pageable);
}

// Repository
@Query("{'owner.$id': ?#{[0]}, 'enabled': ?#{[1]}, 'archived': ?#{[2]}}")
Page<Project> findRelated(ObjectId userId, boolean enabled, boolean archived, Pageable pageable);

現(xiàn)在呢,我們就可以這樣使用了 GET http://localhost:8090/projects/?page=0&size=3 表示取每頁三個(gè)數(shù)據(jù)取第一頁寿弱。

本章代碼:https://github.com/wpcfan/spring-boot-tut/tree/chap05

重拾后端之Spring Boot(一):REST API的搭建可以這樣簡單
重拾后端之Spring Boot(二):MongoDb的無縫集成
重拾后端之Spring Boot(三):找回熟悉的Controller犯眠,Service
重拾后端之Spring Boot(四):使用 JWT 和 Spring Security 保護(hù) REST API
重拾后端之Spring Boot(五):跨域、自定義查詢及分頁

有問題的童鞋可以加入我的小密圈討論: http://t.xiaomiquan.com/jayRnaQ (該鏈接7天內(nèi)(5月14日前)有效)

另外症革,我的 《Angular 從零到一》出版了筐咧,下面是書籍的內(nèi)容簡介:

本書系統(tǒng)介紹Angular的基礎(chǔ)知識與開發(fā)技巧,可幫助前端開發(fā)者快速入門噪矛。共有9章量蕊,第1章介紹Angular的基本概念,第2~7章從零開始搭建一個(gè)待辦事項(xiàng)應(yīng)用艇挨,然后逐步增加功能残炮,如增加登錄驗(yàn)證、將應(yīng)用模塊化缩滨、多用戶版本的實(shí)現(xiàn)势就、使用第三方樣式庫泉瞻、動態(tài)效果制作等。第8章介紹響應(yīng)式編程的概念和Rx在Angular中的應(yīng)用苞冯。第9章介紹在React中非常流行的Redux狀態(tài)管理機(jī)制袖牙,這種機(jī)制的引入可以讓代碼和邏輯隔離得更好,在團(tuán)隊(duì)工作中強(qiáng)烈建議采用這種方案舅锄。本書不僅講解Angular的基本概念和最佳實(shí)踐鞭达,而且分享了作者解決問題的過程和邏輯,講解細(xì)膩皇忿,風(fēng)趣幽默碉怔,適合有面向?qū)ο缶幊袒A(chǔ)的讀者閱讀。

歡迎大家圍觀禁添、訂購撮胧、提出寶貴意見。

京東鏈接:https://item.m.jd.com/product/12059091.html?from=singlemessage&isappinstalled=0

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末老翘,一起剝皮案震驚了整個(gè)濱河市芹啥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铺峭,老刑警劉巖墓怀,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卫键,居然都是意外死亡傀履,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門莉炉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钓账,“玉大人,你說我怎么就攤上這事絮宁“鹉海” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵绍昂,是天一觀的道長啦粹。 經(jīng)常有香客問我,道長窘游,這世上最難降的妖魔是什么唠椭? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮忍饰,結(jié)果婚禮上贪嫂,老公的妹妹穿的比我還像新娘。我一直安慰自己喘批,他們只是感情好撩荣,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布铣揉。 她就那樣靜靜地躺著饶深,像睡著了一般餐曹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上敌厘,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天台猴,我揣著相機(jī)與錄音,去河邊找鬼俱两。 笑死饱狂,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的宪彩。 我是一名探鬼主播休讳,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尿孔!你這毒婦竟也來了俊柔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤活合,失蹤者是張志新(化名)和其女友劉穎雏婶,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體白指,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡留晚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了告嘲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片错维。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖橄唬,靈堂內(nèi)的尸體忽然破棺而出需五,到底是詐尸還是另有隱情,我是刑警寧澤轧坎,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布宏邮,位于F島的核電站,受9級特大地震影響缸血,放射性物質(zhì)發(fā)生泄漏蜜氨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一捎泻、第九天 我趴在偏房一處隱蔽的房頂上張望飒炎。 院中可真熱鬧,春花似錦笆豁、人聲如沸郎汪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽煞赢。三九已至抛计,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間照筑,已是汗流浹背吹截。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凝危,地道東北人波俄。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像蛾默,于是被迫代替她去往敵國和親懦铺。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容