OAuth2 za proljetni REST API - rukovanje žetonom za osvježavanje u kutu

1. Pregled

U ovom uputstvu nastavit ćemo istraživati ​​tijek OAuth2 autorizacijskog koda koji smo započeli sastavljati u našem prethodnom članku i usredotočit ćemo se na rukovanje žetonom za osvježavanje u kutnoj aplikaciji. Također ćemo koristiti Zuul proxy.

Koristit ćemo OAuth stog u Spring Security 5. Ako želite koristiti naslijeđeni stek Spring Security OAuth, pogledajte ovaj prethodni članak: OAuth2 za Spring REST API - rukovanje tokenom osvježavanja u AngularJS (naslijeđeni OAuth stog)

2. Pristup isteku tokena

Prvo, imajte na umu da je klijent u dva koraka dobivao pristupni žeton koristeći vrstu odobrenja autorizacijskog koda. U prvom koraku dobivamo autorizacijski kod. I u drugom koraku zapravo dobivamo pristupni žeton.

Naš pristupni žeton pohranjen je u kolačiću koji istječe na temelju isteka tokena:

var expireDate = novi datum (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate);

Ono što je važno razumjeti je to sam kolačić koristi se samo za pohranu i ne pokreće ništa drugo u protoku OAuth2. Na primjer, preglednik nikada neće automatski poslati kolačić na poslužitelj sa zahtjevima, tako da smo ovdje zaštićeni.

Ali imajte na umu kako to zapravo definiramo retrieveToken () funkcija za dobivanje tokena pristupa:

retrieveToken (kod) {let params = novi URLSearchParams (); params.append ('vrsta_dodate', 'autorizacijski kod'); params.append ('client_id', this.clientId); params.append ('client_secret', 'newClientSecret'); params.append ('redirect_uri', this.redirectUri); params.append ('kod', kod); neka zaglavlja = novi HttpHeaders ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8'}); this._http.post ('// localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / token', params.toString (), {headers: headers}) .subscribe (data => this.saveToken ( podaci), pogreška => upozorenje ('Nevaljane vjerodajnice')); }

Šaljemo tajnu klijenta u parametarima, što zapravo nije siguran način da se to riješi. Pogledajmo kako to možemo izbjeći.

3. Proxy

Tako, sada ćemo imati Zuul proxy pokrenut u front-end aplikaciji i u osnovi sjediti između front-end klijenta i Autorizacijskog poslužitelja. Sve osjetljive informacije obrađivat će se na ovom sloju.

Prednji klijent sada će biti hostiran kao Boot aplikacija, tako da se možemo bez problema povezati s našim ugrađenim Zuul proxyjem pomoću pokretača Spring Cloud Zuul.

Ako želite proučiti osnove Zuula, pročitajte na brzinu glavni članak o Zuulu.

Sada konfigurirajmo rute proxyja:

zuul: routes: auth / code: path: / auth / code / ** sensitiveHeaders: url: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / auth auth / token: path: / auth / token / ** sensitiveHeaders: url: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / token auth / refresh: path: / auth / refresh / ** sensitiveHeaders: url: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / token auth / redirect: path: / auth / redirect / ** sensitiveHeaders: url: // localhost: 8089 / auth / resources: path: / auth / resources / ** sensitiveHeaders: url: // localhost: 8083 / auth / resources /

Postavili smo rute za obradu sljedećeg:

  • autorizacija / kod - nabavite autorizacijski kod i spremite ga u kolačić
  • autorizacija / preusmjeravanje - obraditi preusmjeravanje na stranicu za prijavu poslužitelja za autorizaciju
  • auth / resursi - mapa na odgovarajući put poslužitelja za autorizaciju za resurse njegove stranice za prijavu (css i js)
  • auth / token - uzmi Access Token, ukloni refresh_token iz korisnog tereta i spremite ga u kolačić
  • auth / refresh - uzmite Refresh Token, uklonite ga iz korisnog tereta i spremite u kolačić

Ono što je ovdje zanimljivo jest da samo proksiramo promet na Autorizacijskom poslužitelju, a ne bilo što drugo. Proxy nam stvarno treba samo kad klijent dobiva nove tokene.

Dalje, pogledajmo sve ovo jedno po jedno.

4. Nabavite kod pomoću Zuul Pre filtra

Prva upotreba proxyja je jednostavna - postavili smo zahtjev za dobivanje autorizacijskog koda:

@Component javna klasa CustomPreZuulFilter proširuje ZuulFilter {@Override public Object run () {RequestContext ctx = RequestContext.getCurrentContext (); HttpServletRequest req = ctx.getRequest (); Niz zahtjevaURI = req.getRequestURI (); if (requestURI.contens ("auth / code")) {Map params = ctx.getRequestQueryParams (); if (params == null) {params = Maps.newHashMap (); } params.put ("type_type", Lists.newArrayList (novi String [] {"code"})); params.put ("opseg", Lists.newArrayList (novi niz [] {"čitanje"})); params.put ("client_id", Lists.newArrayList (novi niz [] {CLIENT_ID})); params.put ("redirect_uri", Lists.newArrayList (novi niz [] {REDIRECT_URL})); ctx.setRequestQueryParams (params); } return null; } @Override javni boolean shouldFilter () {boolean shouldfilter = false; RequestContext ctx = RequestContext.getCurrentContext (); URI niza = ctx.getRequest (). GetRequestURI (); if (URI.contens ("auth / code") || URI.contens ("auth / token") || URI.contens ("auth / refresh")) {shouldfilter = true; } return shouldfilter; } @Override public int filterOrder () {return 6; } @Override public String filterType () {return "pre"; }}

Koristimo tip filtra prije za obradu zahtjeva prije prosljeđivanja.

U filtrima trčanje() metodu dodajemo parametre upita za vrsta_odgovora, opseg, client_id i preusmjeriti_uri- sve što je potrebno našem Autorizacijskom poslužitelju da nas odvede na svoju stranicu za prijavu i pošalje natrag kôd.

Također imajte na umu shouldFilter () metoda. Zahtjeve filtriramo samo s spomenuta 3 URI-ja, drugi ne idu do trčanje metoda.

5. Stavite kod u kolačić Koristeći Zuul filtar posta

Ono što ovdje planiramo je spremiti Kôd kao kolačić kako bismo ga mogli poslati na Autorizacijski poslužitelj kako bismo dobili pristupni žeton. Kôd je prisutan kao parametar upita u URL-u zahtjeva na koji nas Autorizacijski poslužitelj preusmjerava nakon prijave.

Postavit ćemo Zuul post-filter za izdvajanje ovog koda i postavljanje u kolačić. Ovo nije samo uobičajeni kolačić, već a zaštićen, samo HTTP kolačić s vrlo ograničenim putem (/ auth / token):

@Component javna klasa CustomPostZuulFilter proširuje ZuulFilter {private ObjectMapper mapper = new ObjectMapper (); @Override public Object run () {RequestContext ctx = RequestContext.getCurrentContext (); isprobajte {Map params = ctx.getRequestQueryParams (); if (requestURI.contens ("auth / redirect")) {Cookie cookie = novi Cookie ("code", params.get ("code"). get (0)); cookie.setHttpOnly (true); cookie.setPath (ctx.getRequest (). getContextPath () + "/ auth / token"); ctx.getResponse (). addCookie (kolačić); }} catch (Iznimka e) {logger.error ("Došlo je do pogreške u filtru zuul posta", e); } return null; } @Override javni boolean shouldFilter () {boolean shouldfilter = false; RequestContext ctx = RequestContext.getCurrentContext (); URI niza = ctx.getRequest (). GetRequestURI (); if (URI.contens ("auth / redirect") || URI.contens ("auth / token") || URI.contens ("auth / refresh")) {shouldfilter = true; } return shouldfilter; } @Override public int filterOrder () {return 10; } @Override public String filterType () {return "post"; }}

Kako bismo dodali dodatni sloj zaštite od CSRF napada, svim ćemo našim kolačićima dodati zaglavlje kolačića iste web lokacije.

Za to ćemo stvoriti klasu konfiguracije:

@Configuration javna klasa SameSiteConfig implementira WebMvcConfigurer {@Bean public TomcatContextCustomizer sameSiteCookiesConfig () {return context -> {final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor (); cookieProcessor.setSameSiteCookies (SameSiteCookies.STRICT.getValue ()); context.setCookieProcessor (cookieProcessor); }; }}

Ovdje postavljamo atribut na strog, tako da se svaki kolačić s više web lokacija strogo zadržava.

6. Dohvatite i upotrijebite kôd iz kolačića

Sad kad imamo kod u kolačiću, kada front-end Angular aplikacija pokuša pokrenuti zahtjev za žetonom, poslat će zahtjev na / auth / token i tako će preglednik, naravno, poslati taj kolačić.

Dakle, sada ćemo imati još jedan uvjet u našem prije filtrirati u proxyju koji izvući će kôd iz kolačića i poslati ga zajedno s ostalim parametrima obrasca kako bi dobio žeton:

javni Object run () {RequestContext ctx = RequestContext.getCurrentContext (); ... inače if (requestURI.contens ("auth / token"))) {try {String code = extractCookie (req, "code"); String formParams = String.format ("grant_type =% s & client_id =% s & client_secret =% s & redirect_uri =% s & code =% s", "auth_code", CLIENT_ID, CLIENT_SECRET, REDIRECT_URL, code); bajt [] bajtova = formParams.getBytes ("UTF-8"); ctx.setRequest (novi CustomHttpServletRequest (zahtjev, bajtovi)); } catch (IOException e) {e.printStackTrace (); }} ...} privatni niz extractCookie (HttpServletRequest zahtjev, naziv niza) {Cookie [] kolačići = req.getCookies (); if (cookies! = null) {for (int i = 0; i <cookies.length; i ++) {if (cookies [i] .getName (). equalsIgnoreCase (name)) {return cookies [i] .getValue () ; }}} return null; }

I ovdje je našCustomHttpServletRequest - koristi se za slanje našeg tijela zahtjeva s potrebnim parametrima obrasca pretvorenim u bajtove:

javna klasa CustomHttpServletRequest proširuje HttpServletRequestWrapper {privatni bajt [] bajtova; javni CustomHttpServletRequest (zahtjev HttpServletRequest, bajt [] bajtova) {super (zahtjev); this.bytes = bajtova; } @Override public ServletInputStream getInputStream () baca IOException {return new ServletInputStreamWrapper (bajtovi); } @Override public int getContentLength () {return bytes.length; } @Override public long getContentLengthLong () {return bytes.length; } @Override public String getMethod () {return "POST"; }}

U odgovoru ćemo dobiti pristupni žeton s poslužitelja za autorizaciju. Zatim ćemo vidjeti kako transformiramo odgovor.

7. Stavite žeton za osvježavanje u kolačić

Na zabavne stvari.

Ono što ovdje planiramo je da klijent dobije Refresh Token kao kolačić.

Dodati ćemo u naš Zuul filtar za izvlačenje žetona za osvježavanje iz JSON tijela odgovora i postaviti ga u kolačić. Ovo je opet zaštićeni, samo HTTP kolačić s vrlo ograničenim putem (/ auth / refresh):

javni Object run () {... else if (requestURI.contens ("auth / token") || requestURI.contains ("auth / refresh")) {InputStream je = ctx.getResponseDataStream (); String responseBody = IOUtils.toString (je, "UTF-8"); if (responseBody.contens ("refresh_token")) {Map responseMap = mapper.readValue (responseBody, novi TypeReference() {}); String refreshToken = responseMap.get ("refresh_token"). ToString (); responseMap.remove ("refresh_token"); responseBody = mapper.writeValueAsString (responseMap); Cookie cookie = novi Cookie ("refreshToken", refreshToken); cookie.setHttpOnly (true); cookie.setPath (ctx.getRequest (). getContextPath () + "/ auth / refresh"); cookie.setMaxAge (2592000); // 30 dana ctx.getResponse (). AddCookie (kolačić); } ctx.setResponseBody (responseBody); } ...}

Kao što vidimo, ovdje smo dodali uvjet u naš Zuul filtar za čitanje odgovora i izdvajanje toka osvježavanja za rute auth / token i auth / refresh. Za njih dvoje radimo potpuno istu stvar jer Autorizacijski poslužitelj u osnovi šalje isti korisni teret dok dobiva Access Token i Refresh Token.

Zatim smo uklonili refresh_token iz odgovora JSON-a kako biste bili sigurni da nikada neće biti dostupan prednjem kraju izvan kolačića.

Sljedeća točka koju treba napomenuti je da smo postavili maksimalnu dob kolačića na 30 dana - jer se to podudara s vremenom isteka Tokena.

8. Nabavite i upotrijebite žeton za osvježavanje iz kolačića

Sad kad u kolačiću imamo žeton za osvježavanje, kada front-end Angular aplikacija pokušava pokrenuti osvježavanje tokena, zahtjev će poslati na / auth / refresh i tako će preglednik, naravno, poslati taj kolačić.

Dakle, sada ćemo imati još jedan uvjet u našem prije filtrirajte u proxyju koji će iz kolačića izvući žeton za osvježavanje i poslati ga naprijed kao HTTP parametar - kako bi zahtjev bio valjan:

javni Object run () {RequestContext ctx = RequestContext.getCurrentContext (); ... inače if (requestURI.contains ("auth / refresh"))) {try {Tok niza = extractCookie (req, "token"); String formParams = String.format ("grant_type =% s & client_id =% s & client_secret =% s & refresh_token =% s", "refresh_token", CLIENT_ID, CLIENT_SECRET, token); bajt [] bajtova = formParams.getBytes ("UTF-8"); ctx.setRequest (novi CustomHttpServletRequest (zahtjev, bajtovi)); } catch (IOException e) {e.printStackTrace (); }} ...}

To je slično onome što smo radili kad smo prvi put dobili pristupni žeton. Ali primijetite da je oblik tijela različit. Sad šaljemo grant_type od refresh_token umjesto autorizacijski_kod zajedno s tokenom koji smo prije spremali u kolačić.

Nakon dobivanja odgovora, opet prolazi kroz istu transformaciju u prije filtar kao što smo vidjeli ranije u odjeljku 7.

9. Osvježavanje pristupnog tokena iz Angulala

Na kraju, izmijenimo našu jednostavnu front-end aplikaciju i zapravo iskoristimo osvježavanje tokena:

Ovdje je naša funkcija refreshAccessToken ():

refreshAccessToken () {let headers = novi HttpHeaders ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8'}); this._http.post ('auth / refresh', {}, {headers: headers}) .subscribe (data => this.saveToken (data), err => alert ('Invalid Credentials')); }

Imajte na umu kako jednostavno koristimo postojeće saveToken () funkcija - i samo prosljeđivanje različitih ulaza u nju.

Primijetite i to ne dodajemo nikakve parametre obrasca s refresh_token mi sami - jer će se za to pobrinuti Zuulov filter.

10. Pokrenite prednji kraj

Budući da je naš front-end Angular klijent sada hostiran kao Boot aplikacija, njegovo pokretanje bit će malo drugačije nego prije.

Prvi korak je isti. Moramo izgraditi aplikaciju:

mvn čista instalacija

To će pokrenuti frontend-maven-plugin definirana u našem pom.xml za izgradnju kutnog koda i kopiranje artefakata korisničkog sučelja u cilj / klase / statički mapu. Ovaj postupak prepisuje sve ostalo što imamo u src / glavni / resursi imenik. Stoga se moramo pobrinuti i uključiti sve potrebne resurse iz ove mape, poput primjena.iml, u postupku kopiranja.

U drugom koraku moramo pokrenuti svoj SpringBootApplication razred UiApplication. Naša klijentska aplikacija bit će pokrenuta i radi na priključku 8089 kako je navedeno u primjena.iml.

11. Zaključak

U ovom vodiču za OAuth2 naučili smo kako pohraniti žeton za osvježavanje u klijentsku aplikaciju Angular, kako osvježiti istekli pristupni žeton i kako iskoristiti Zuul proxy za sve to.

Potpuna implementacija ovog vodiča može se naći na GitHubu.


$config[zx-auto] not found$config[zx-overlay] not found