一天下午,正認(rèn)真的上(摸)班(魚)呢,一個(gè)前端開發(fā)同事找到運(yùn)維團(tuán)隊(duì)“后端服務(wù)是不是有什么異常啊,為什么我的訪問不通呢?”“接口地址拿來~”運(yùn)維工程師使用本地的postman進(jìn)行調(diào)用。結(jié)果是正常返回。“我這調(diào)用沒問題啊,你寫的code的問題吧......”一場大戰(zhàn)一觸即發(fā).......

這天可以記為兩位工程師的歷史性時(shí)刻——發(fā)現(xiàn)了CORS!
跨源資源共享(Cross-Origin Resource Sharing,或通俗地譯為跨域資源共享)是一種基于 HTTP 頭的機(jī)制,該機(jī)制通過允許服務(wù)器標(biāo)示除了它自己以外的其他源(域、協(xié)議或端口),使得瀏覽器允許這些源訪問加載自己的資源。跨源資源共享還通過一種機(jī)制來檢查服務(wù)器是否會(huì)允許要發(fā)送的真實(shí)請求,該機(jī)制通過瀏覽器發(fā)起一個(gè)到服務(wù)器托管的跨源資源的“預(yù)檢”請求。在預(yù)檢中,瀏覽器發(fā)送的頭中標(biāo)示有 HTTP 方法和真實(shí)請求中會(huì)用到的頭。
看的有點(diǎn)懵,現(xiàn)在舉個(gè)現(xiàn)實(shí)中的例子:有一位公司的老板,他有一個(gè)秘書,秘書負(fù)責(zé)在辦公室接通各個(gè)客戶的電話后,會(huì)詢問是誰從什么地方打來的電話,然后通知老板是否愿意與他們通話。老板比較忙的時(shí)候會(huì)告訴秘書:“我今天只接受A公司XX人的電話同步的信息”。那么秘書就會(huì)按照老板的要求進(jìn)行同步。但是也有特殊情況:比如B公司老板直接知道老板的電話。也會(huì)直接聯(lián)系老板
從現(xiàn)實(shí)生活到軟件工程訪問,我們做一個(gè)對應(yīng):
訪問的逐步順序如下:
我們啟動(dòng)一個(gè)后端和前端來模擬問題:
package main import ( "encoding/json" "errors" "fmt" "github.com/go-chi/chi/v5" "net/http" ) var books = [] string { "指環(huán)王" , "霍比特人" , "精靈寶鉆" } type Book struct {標(biāo)題字符串 `json:"title"` } func main () { err := runServer() if err != nil { iferrors.Is( err , http.ErrServerClosed ) { fmt.Println( "服務(wù)器關(guān)閉" ) } else { fmt.Println( "服務(wù)器失敗" , err) } } } func runServer () error { httpRouter := chi.NewRouter() httpRouter.Route( "/api/ v1" , func (r chi.Router) { r.Get( "/books" , getAllBooks) r.Post( "/books" , addBook) r.Delete( "/books" , deleteAllBooks) }) server := &http .Server{Addr: "localhost:8888" , Handler: httpRouter} return server.ListenAndServe() } func getAllBooks (w http.ResponseWriter, req *http.Request) { respBody, err := json.Marshal(books) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set( "Content-Type" , "application/json" ) w.WriteHeader(http.StatusOK) w.Write(respBody) } func addBook (w http.ResponseWriter, req *http.Request) { var book Book err := json.NewDecoder(req.Body).Decode(&book) if err != nil { w.WriteHeader(http.StatusBadRequest) return } books = append (books, book.Title) w.WriteHeader(http.StatusCreated) } func deleteAllBooks (w http.ResponseWriter, req *http.Request) { books = [] string {} w.WriteHeader(http.StatusNoContent) }運(yùn)行這段代碼,服務(wù)器將運(yùn)行為http://localhost:8888
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Books</title> <link rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous"> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script></head><body><div class="container p-3"> <button type="button" class="btn btn-primary" id="getBooks">Get books</button> <button type="button" class="btn btn-danger" id="deleteAllBooks">Delete all books</button> <br> <br> <form> <div class="mb-3"> <label for="inputBookTitle" class="form-label">Book title</label> <input type="text" class="form-control" id="inputBookTitle" aria-describedby="emailHelp"> </div> <button type="submit" class="btn btn-primary">Add</button> </form></div><script> function getBooks () { fetch('http://localhost:8888/api/v1/books') .then(response => response.json()) .then(data => { const booksList = document.querySelector('.books-list') if (booksList) { booksList.remove() } const ul = document.createElement('ul') ul.classList.add('books-list') data.forEach(book => { const li = document.createElement('li') li.innerText = book ul.appendChild(li) }) document.body.appendChild(ul) }) } function deleteAllBooks () { fetch('http://localhost:8888/api/v1/books', { method: 'DELETE' }) .then(response => { if (response.status === 204) { getBooks() } else { const div = document.createElement('div') div.innerText = 'Something went wrong' document.body.appendChild(div) } }) } const getBooksButton = document.getElementById('getBooks') const deleteAllBooksButton = document.getElementById('deleteAllBooks') const input = document.querySelector('input') const form = document.querySelector('form') getBooksButton.addEventListener('click', () => getBooks()) deleteAllBooksButton.addEventListener('click', () => deleteAllBooks()) form.addEventListener('submit', (event) => { event.preventDefault() const title = input.value fetch('http://localhost:8888/api/v1/books', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) }) .then(response => { if (response.status === 201) { input.value = '' getBooks() } else { const div = document.createElement('div') div.innerText = 'Something wend wrong' document.body.appendChild(div) } }) })</script></body></html>一個(gè)Go 服務(wù)(與index.html放在一個(gè)文件夾下):
package mainimport ( "errors" "fmt" "github.com/go-chi/chi/v5" "net/http")func main() { err := runServer() if err != nil { if errors.Is(err, http.ErrServerClosed) { fmt.Println("client server shutdown") } else { fmt.Println("client server failed", err) } }}func runServer() error { httpRouter := chi.NewRouter() httpRouter.Get("/", serveIndex) server := &http.Server{Addr: "localhost:3333", Handler: httpRouter} return server.ListenAndServe()}func serveIndex(w http.ResponseWriter, req *http.Request) { http.ServeFile(w, req, "./index.html")}運(yùn)行這段代碼,前端html將運(yùn)行為http://localhost:3333
使用瀏覽器訪問,得到如下頁面,打開F12調(diào)試,在文本框中輸入書名,點(diǎn)擊Add:

得到了與文章開始時(shí)類似的報(bào)錯(cuò)。
您可能已經(jīng)發(fā)現(xiàn),我們的后端代碼根本沒有提及 CORS。確實(shí)如此,到目前為止我們還沒有實(shí)現(xiàn)任何 CORS 配置。但這對于瀏覽器來說并不重要:它無論如何都會(huì)嘗試發(fā)出預(yù)檢請求。(就像秘書一定要征求老板的意見,不會(huì)擅自決定)
如果我們單擊405這個(gè)報(bào)錯(cuò),會(huì)展開一些詳細(xì)信息,我們可以看到瀏覽器嘗試向與添加圖書端點(diǎn)相同的路徑發(fā)出 OPTIONS 請求,并收到響應(yīng)405 Method Not Allowed,這是有道理的,因?yàn)槲覀冞€沒有定義我們后端的 OPTIONS 端點(diǎn)。

前端應(yīng)用程序保持不變,但對于后端,我們需要進(jìn)行一些更改:
引入一個(gè)新函數(shù)來啟用 CORS:
func enableCors (w http.ResponseWriter) { // 指定允許哪些域訪問此 API w.Header().Set( "Access-Control-Allow-Origin" , "http://localhost:3333" ) //指定允許哪些方法訪問此 API w.Header().Set( "Access-Control-Allow-Methods" , "GET, POST, DELETE" ) // 指定允許哪些標(biāo)頭訪問此 API w.Header( ).Set( "Access-Control-Allow-Headers" , "Accept, Content-Type" ) // 指定瀏覽器可以緩存預(yù)檢請求結(jié)果的時(shí)間(以秒為單位) w.Header().Set( “訪問控制最大時(shí)間”,strconv.Itoa( 60 * 60 * 2))}在現(xiàn)有端點(diǎn)旁邊引入一個(gè) OPTIONS 端點(diǎn)以及一個(gè)處理它的函數(shù):
... httpRouter.Route( "/api/v1" , func (r chi.Router) { r.Options( "/books" , corsOptions) r.Get( "/books" , getAllBooks) r.Post( "/ books" , addBook) r.Delete( "/books" , deleteAllBooks) }) ... func corsOptions (w http.ResponseWriter, req *http.Request) { enableCors(w) w.WriteHeader(http.StatusOK) }添加enableCors對其他端點(diǎn)現(xiàn)有函數(shù)的調(diào)用,例如:
func getAllBooks (w http.ResponseWriter, req *http.Request) {respBody, err := json.Marshal(books) if err != nil { w.WriteHeader(http.StatusInternalServerError) return }enableCors(w)w.Header( ).Set( "Content-Type" , "application/json" )w.WriteHeader(http.StatusOK)w.Write(respBody)}最后的后端代碼如下:
package mainimport ( "encoding/json" "errors" "fmt" "github.com/go-chi/chi/v5" "net/http" "strconv")var books = []string{"The Lord of the Rings", "The Hobbit", "The Silmarillion"}type Book struct { Title string `json:"title"`}func main() { err := runServer() if err != nil { if errors.Is(err, http.ErrServerClosed) { fmt.Println("server shutdown") } else { fmt.Println("server failed", err) } }}func runServer() error { httpRouter := chi.NewRouter() httpRouter.Route("/api/v1", func(r chi.Router) { r.Options("/books", corsOptions) r.Get("/books", getAllBooks) r.Post("/books", addBook) r.Delete("/books", deleteAllBooks) }) server := &http.Server{Addr: "localhost:8888", Handler: httpRouter} return server.ListenAndServe()}func corsOptions(w http.ResponseWriter, req *http.Request) { enableCors(w) w.WriteHeader(http.StatusOK)}func getAllBooks(w http.ResponseWriter, req *http.Request) { respBody, err := json.Marshal(books) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } enableCors(w) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(respBody)}func addBook(w http.ResponseWriter, req *http.Request) { var book Book err := json.NewDecoder(req.Body).Decode(&book) if err != nil { w.WriteHeader(http.StatusBadRequest) return } books = append(books, book.Title) enableCors(w) w.WriteHeader(http.StatusCreated)}func deleteAllBooks(w http.ResponseWriter, req *http.Request) { books = []string{} enableCors(w) w.WriteHeader(http.StatusNoContent)}func enableCors(w http.ResponseWriter) { // specifies which domains are allowed to access this API w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3333") // specifies which methods are allowed to access this API (GET is allowed by default) w.Header().Set("Access-Control-Allow-Methods", "POST, DELETE") // specifies which headers are allowed to access this API w.Header().Set("Access-Control-Allow-Headers", "Content-Type") // specifies for how long the browser can cache the results of a preflight request (in seconds) w.Header().Set("Access-Control-Max-Age", strconv.Itoa(60*60*2))}重新啟動(dòng)前端和后端,重新嘗試訪問會(huì)發(fā)現(xiàn)問題解決了~

其中重要的部分是Response headers
如果嘗試改變后端配置。允許訪問的地址改為http://localhost:33333:

此時(shí)再去訪問則發(fā)現(xiàn):

此時(shí)就是后端的配置導(dǎo)致的。當(dāng)人你也可以更改其他的配置做一些嘗試。
我們到這就理解了CORS是一種允許當(dāng)前域(domain)的資源(比如http://localhost:8888)被其他域(http://localhost:3333)的腳本請求訪問的機(jī)制,通常由于同域安全策略(the same-origin security policy)瀏覽器會(huì)禁止這種跨域請求。當(dāng)瀏覽器發(fā)出PUT請求,OPTION(預(yù)檢)請求返回Access-Control-Allow-Origin:http://localhost:3333,Access-Control-Allow-Methods:’PUT’,服務(wù)器同意指定域的PUT請求,瀏覽器收到并繼續(xù)發(fā)出真正的PUT請求,服務(wù)器響應(yīng)并再次返回Access-Control-Allow-Origin:http://localhost:3333,允許瀏覽器的腳本執(zhí)行服務(wù)器返回的數(shù)據(jù)。
希望能對您有幫助!
參考:https://itnext.io/understanding-cors-4157bf640e11
本文鏈接:http://m.www897cc.com/showinfo-26-70437-0.html老板與秘書的故事理解CORS(跨域),真的超級簡單
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com