Projekti sa razdvojenim frontendom i backendom: kako rešiti problem cross-domain pristupa?
Projekti sa razdvojenim frontendom i backendom: kako rešiti problem cross-domain pristupa?
Problem cross-domain pristupa veoma je čest problem u projektima sa razdvojenim frontendom i backendom. Na primer, frontend servis sajta za učenje Codingmore (codingmore) radi pod portom 8080, a backend servis pod portom 9002, pa prilikom poziva backend interfejsa s frontenda nastaje cross-domain problem.

403 Forbidden jeste statusni kod (Status Code) u HTTP protokolu koji znači da je backend servis uspešno rasparsirao zahtev, ali frontend nema dozvolu da pristupi tom resursu.
Kako onda rešiti ovaj problem? Uglavnom postoje dva pristupa:
- Frontend koristi Nodejs proxy (u razvojnom okruženju; u produkcionom okruženju može ga zameniti Nginx)
- Ili backend uključuje deljenje cross-domain resursa (CORS)
1. O cross-domain pristupu
Cross-domain pristup je za frontend i backend programere kao nalepnica za rane — susreću se s njim često, bilo na intervjuu ili u razvoju.
Cross-domain problem nastaje zbog politikom istog izvora (same-origin policy) pregledača; da bi izolovao potencijalno zlonamerne fajlove i odbranio se od napada sa strane, pregledač ograničava interakciju dokumenata ili skripti učitanih iz jednog izvora sa resursima iz drugog izvora.
Ranije smo spomenuli da frontend radi pod portom 8080, a backend pod portom 9002. Ova situacija spada u različite izvore (različito ime domena, različit protokol, različit port), pa kada frontend zahtev sa porta 8080 direktno pristupa backend interfejsu na portu 9002, pristup ne uspeva.
Koji je onda ispravan pristup? Kao što smo već spomenuli, frontend koristi Nodejs proxy ili backend uključuje deljenje cross-domain resursa — hajde da oboje isprobamo.

2. Nodejs proxy
Pre pojave Nodejs-a, programi pisani u JavaScript-u obično su se izvršavali u pregledaču korisnika. Nakon pojave Node.js, JavaScript se mogao koristiti i za serversko programiranje. Niz ugrađenih modula Nodejs-a omogućava programu da radi kao samostalni server, nezavisno od Web servisa poput IIS ili Apache.
Naša zamisao za rešavanje cross-domain problema pomoću Nodejs-a jeste da lokalno kreiramo virtuelni server koji proxy-uje frontend zahteve sa porta 8080, a istovremeno prima serverske odgovore sa porta 9002. Tako razmena podataka između serverske i serverske strane neće izazvati cross-domain problem.
Prvi korak, konfigurišite Nodejs proxy servis.
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/api': {
target: 'http://localhost:9002', // interfejs treće strane kojem šaljete zahtev
changeOrigin: false, // lokalno će se kreirati virtuelni server koji šalje podatke zahteva i istovremeno prima podatke zahteva, pa razmena podataka između servera i servera neće imati cross-domain problem
pathRewrite: { // prepisivanje putanje,
'^/api': '' // zamenjuje adresu zahteva u targetu, odnosno kasnije kada tražite adresu http://api.codingmore.top/v2/XXXXX možete je direktno pisati kao /api
}
},
},
}Drugi korak, konfigurišite putanju frontend zahteva.
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
VUE_APP_BASE_API: '"/api"'
// VUE_APP_BASE_API: '"http://localhost:9002"'
})Treći korak, ponovo pokrenite frontend servis.

Ponovo kliknite na dugme „Prijavi se" i videćete da se URL zahteva promenio: umesto http://localhost:9002/users/login sada je http://localhost:8080/api/users/login. Uz to pojavio se još jedan Remote Address, takođe na portu 8080, što znači da posredstvom Nodejs proxy-ja interakcija frontenda i backenda sada teče iz istog izvora, pa cross-domain problem neće nastati.
Istovremeno se vidi da je statusni kod koji vraća server postao 200, što označava uspešan zahtev.

3. Uključivanje deljenja cross-domain resursa
Deljenje cross-domain resursa, odnosno Cross-Origin Resource Sharing, skraćeno CORS, mehanizam je zasnovan na HTTP zaglavljima koji omogućava serveru da identifikuje resurse osim svojih sopstvenih, čime se ostvaruje cross-domain pristup.
Prvi korak, uključite podršku za CORS.
U Spring Boot aplikaciji dodavanje podrške za CORS smešno je jednostavno — dovoljno je dodati jednu konfiguracionu klasu.
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// Postavite ime domena sajta koji želite da dozvolite
config.addAllowedOrigin("http://localhost:8080");
// Dozvoli slanje cookie-ja cross-domain
config.setAllowCredentials(true);
// Propusti sva originalna zaglavlja
config.addAllowedHeader("*");
// Dozvoli cross-domain pozive svim metodama zahteva
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}Drugi korak, ponovo pokrenite backend servis, kliknite ponovo na dugme za prijavljivanje i videćete da zahtev sada može normalno da pristupa.

U ovom primeru, backend vraća Access-Control-Allow-Origin: http://localhost:8080, što znači da backend interfejs na portu 9002 može biti dostupan frontend zahtevima sa porta 8080.
Ako želite da dozvolite svim domenima cross-domain pozive, dovoljno je izmeniti jednu liniju koda.
// Dozvoli svim domenima cross-domain pozive
config.addAllowedOriginPattern("*");
// Postavite ime domena sajta koji želite da dozvolite
// config.addAllowedOrigin("http://localhost:8080");Za jednostavne zahteve poput login, oni neće okinuti CORS preflight (pred-proveru), pa nije potrebno dodavati drugu konfiguraciju na serveru. Šta je onda jednostavan zahtev?
- Metoda zahteva je jedna od sledeće tri metode:
- HEAD
- GET
- POST
- HTTP zaglavlja ne prelaze sledeća polja:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type: ograničeno samo na tri vrednosti
application/x-www-form-urlencoded, multipart/form-data, text/plain
A šta je sa ne-jednostavnim zahtevima koji okidaju CORS preflight (na primer metoda zahteva je PUT ili DELETE, ili je tip polja Content-Type application/json, ili zaglavlje zahteva sadrži neka prilagođena polja)?
Ne-jednostavni zahtevi pre zvanične komunikacije dodaju jedan HTTP zahtev za proveru, koji se naziva „preflight" zahtev. Tek nakon što preflight zahtev prođe, vraća se normalan sadržaj odgovora.

Uzmimo za primer stranicu za upravljanje člancima u Codingmore: ta stranica šalje backendu paginirani upit posts/queryPageable. Taj zahtev sadrži prilagođeno zaglavlje Authorization, pa pregledač zaključuje da je to ne-jednostavan zahtev i automatski šalje jedan OPTIONS zahtev. Ali pošto je naš Spring Boot projekat integrisao sigurnosni okvir SpringSecurity, a nismo oslobodili OPTIONS zahtev od autentifikacije pri prijavljivanju, provera ne uspeva, pa se podaci odgovora paginiranog upita članaka ne vraćaju.

Treći korak, propustite OPTIONS zahtev sledećim kodom.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
// Dozvoli OPTIONS zahteve cross-domain zahteva
registry.antMatchers(HttpMethod.OPTIONS)
.permitAll();
}
}Ponovo pokrenite backend servis, ponovo pristupite interfejsu liste članaka i videćete da odgovor sadrži podatke.

Ne-jednostavan zahtev prvo mora metodom OPTIONS da pošalje preflight zahtev serveru, kako bi saznao da li server dozvoljava stvarni zahtev. Korišćenje „preflight zahteva" izbegava neočekivane uticaje cross-domain zahteva na korisničke podatke na serveru.
Hajde da se preko dve slike kratko rezimiramo čitav proces preflight zahteva. Prva slika: slanje OPTIONS preflight zahteva:

Druga slika: slanje zvaničnog zahteva:

4. Putanja izvornog koda
Izvorni kod backend-a Codingmore:
https://github.com/itwanger/coding-more
Izvorni kod frontend-a za administraciju Codingmore:
https://github.com/itwanger/codingmore-admin-web
Referentni linkovi:
Cross-domain: https://segmentfault.com/a/1190000015597029 CORS: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS Ruan Yifeng: https://www.ruanyifeng.com/blog/2016/04/cors.html Jednostavan zahtev + preflight zahtev: https://github.com/amandakelake/blog/issues/62

Ništa me ne zadržava — osim cilja; ma koliko na obali bilo ruža, zelene senke i mirnih luka, ja sam nevezani čamac.
