OAuth2 til en Spring REST API - Håndter opdateringstokenet i vinkel

1. Oversigt

I denne vejledning fortsætter vi med at udforske OAuth2 Authorization Code-strømmen, som vi begyndte at sammensætte i vores forrige artikel og vi fokuserer på, hvordan du håndterer Opdater Token i en vinkelapp. Vi bruger også Zuul-proxyen.

Vi bruger OAuth-stakken i Spring Security 5. Hvis du vil bruge Spring Security OAuth legacy stack, skal du kigge på denne tidligere artikel: OAuth2 for en Spring REST API - Håndter Refresh Token i AngularJS (legacy OAuth stack)

2. Adgangstoken udløb

Husk først, at klienten var ved at få et adgangstoken ved hjælp af en tildelingstype for autorisationskode i to trin. I det første trin opnår vi autorisationskoden. Og i det andet trin opnår vi faktisk Access Token.

Vores adgangstoken opbevares i en cookie, der udløber baseret på, når selve tokenet udløber:

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

Hvad der er vigtigt at forstå er det selve cookien bruges kun til opbevaring og det driver ikke noget andet i OAuth2-strømmen. For eksempel sender browseren aldrig automatisk cookien til serveren med anmodninger, så vi er sikret her.

Men bemærk hvordan vi faktisk definerer dette retrieveToken () funktion for at få adgangstoken:

retrieveToken (kode) {let params = ny URLSearchParams (); params.append ('grant_type', 'authorisation_code'); params.append ('client_id', this.clientId); params.append ('client_secret', 'newClientSecret'); params.append ('redirect_uri', this.redirectUri); params.append ('kode', kode); lad overskrifter = nye 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}). abonner (data => this.saveToken ( data), err => alarm ('Ugyldige legitimationsoplysninger')); }

Vi sender klientens hemmelighed i params, som ikke rigtig er en sikker måde at håndtere dette på. Lad os se, hvordan vi kan undgå at gøre dette.

3. Proxyen

Så, vi skal nu have en Zuul-proxy, der kører i frontend-applikationen og dybest set sidder mellem frontend-klienten og autorisationsserveren. Alle de følsomme oplysninger håndteres i dette lag.

Frontend-klienten hostes nu som en Boot-applikation, så vi kan oprette forbindelse problemfrit til vores integrerede Zuul-proxy ved hjælp af Spring Cloud Zuul-starter.

Hvis du vil gennemgå det grundlæggende i Zuul, skal du hurtigt læse den vigtigste Zuul-artikel.

Nu lad os konfigurere proxyens ruter:

zuul: ruter: auth / code: sti: / 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: sti: / auth / redirect / ** sensitiveHeaders: url: // localhost: 8089 / auth / resources: sti: / auth / resources / ** sensitiveHeaders: url: // localhost: 8083 / auth / resources /

Vi har oprettet ruter til at håndtere følgende:

  • godkendelse / kode - få autorisationskoden og gem den i en cookie
  • godkende / omdirigere - håndter omdirigering til autorisationsservers login-side
  • auth / ressourcer - kort til autorisationsservers tilsvarende sti for dets login-sideressourcer (css og js)
  • godkendelse / token - få adgangstoken, fjern opdater_token fra nyttelasten og gem den i en cookie
  • godkend / opdater - hent Refresh Token, fjern det fra nyttelasten og gem det i en cookie

Hvad der er interessant her er, at vi kun proxyer trafik til autorisationsserveren og ikke noget andet. Vi har kun virkelig brug for proxyen for at komme ind, når klienten får nye tokens.

Lad os derefter se på alle disse en efter en.

4. Hent koden ved hjælp af Zuul Pre Filter

Den første brug af proxyen er enkel - vi opretter en anmodning om at få autorisationskoden:

@Komponent offentlig klasse CustomPreZuulFilter udvider ZuulFilter {@Override public Object run () {RequestContext ctx = RequestContext.getCurrentContext (); HttpServletRequest req = ctx.getRequest (); StrengeanmodningURI = req.getRequestURI (); hvis (requestURI.contains ("auth / code")) {Map params = ctx.getRequestQueryParams (); hvis (params == null) {params = Maps.newHashMap (); } params.put ("respons_type", Lists.newArrayList (ny streng [] {"kode"})); params.put ("scope", Lists.newArrayList (new String [] {"read"})); params.put ("client_id", Lists.newArrayList (ny streng [] {CLIENT_ID})); params.put ("redirect_uri", Lists.newArrayList (ny streng [] {REDIRECT_URL})); ctx.setRequestQueryParams (params); } returnere null; } @ Override public boolean shouldFilter () {boolean shouldfilter = false; RequestContext ctx = RequestContext.getCurrentContext (); Streng URI = ctx.getRequest (). GetRequestURI (); hvis (URI.contains ("auth / code") || URI.contains ("auth / token") || URI.contains ("auth / refresh")) {shouldfilter = true; } returnere skalfilter; } @ Override public int filterOrder () {return 6; } @ Override public String filterType () {return "pre"; }}

Vi bruger en filtertype af præ at behandle anmodningen, inden den videresendes.

I filteret løb() metode, tilføjer vi forespørgselsparametre for respons_type, rækkevidde, klient_id og redirect_uri- alt hvad vores autorisationsserver har brug for for at føre os til sin login-side og sende en kode tilbage.

Bemærk også shouldFilter () metode. Vi filtrerer kun anmodninger med de nævnte 3 URI'er, andre går ikke videre til løb metode.

5. Sæt koden i en cookie Ved brug af Zuul Post Filter

Hvad vi planlægger at gøre her, er at gemme koden som en cookie, så vi kan sende den over til autorisationsserveren for at få adgangstokenet. Koden findes som en forespørgselsparameter i anmodnings-URL'en, som autorisationsserveren omdirigerer os til efter at have logget ind.

Vi opretter et Zuul-postfilter for at udtrække denne kode og sætte den i cookien. Dette er ikke bare en normal cookie, men en sikret, kun HTTP-cookie med en meget begrænset sti (/ auth / token):

@Komponent offentlig klasse CustomPostZuulFilter udvider ZuulFilter {private ObjectMapper mapper = ny ObjectMapper (); @Override public Object run () {RequestContext ctx = RequestContext.getCurrentContext (); prøv {Map params = ctx.getRequestQueryParams (); hvis (requestURI.contains ("auth / redirect")) {Cookie cookie = ny cookie ("kode", params.get ("kode"). get (0)); cookie.setHttpOnly (sand); cookie.setPath (ctx.getRequest (). getContextPath () + "/ auth / token"); ctx.getResponse (). addCookie (cookie); }} fange (Undtagelse e) {logger.error ("Der opstod en fejl i zuul post filter", e); } returnere null; } @ Override public boolean shouldFilter () {boolean shouldfilter = false; RequestContext ctx = RequestContext.getCurrentContext (); Streng URI = ctx.getRequest (). GetRequestURI (); hvis (URI.contains ("auth / redirect") || URI.contains ("auth / token") || URI.contains ("auth / refresh")) {shouldfilter = true; } returnere skalfilter; } @ Override public int filterOrder () {return 10; } @ Override public String filterType () {return "post"; }}

For at tilføje et ekstra lag af beskyttelse mod CSRF-angreb, vi tilføjer en Same-Site-cookieoverskrift til alle vores cookies.

Til det opretter vi en konfigurationsklasse:

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

Her sætter vi attributten til streng, så enhver overførsel af cookies på tværs af webstedet strengt tilbageholdes.

6. Hent og brug koden fra cookien

Nu hvor vi har koden i cookien, når front-end Angular-applikationen forsøger at udløse en token-anmodning, sender den anmodningen kl. / auth / token og så sender browseren selvfølgelig den cookie.

Så vi får nu en anden tilstand i vores præ filter i proxyen udtrækker koden fra cookien og sender den sammen med andre formularparametre for at opnå tokenet:

offentlig Objektkørsel () {RequestContext ctx = RequestContext.getCurrentContext (); ... ellers hvis (requestURI.contains ("auth / token"))) {prøv {String code = extractCookie (req, "code"); String formParams = String.format ("grant_type =% s & client_id =% s & client_secret =% s & redirect_uri =% s & code =% s", "autorisationskode", CLIENT_ID, CLIENT_SECRET, REDIRECT_URL, kode); byte [] bytes = formParams.getBytes ("UTF-8"); ctx.setRequest (ny CustomHttpServletRequest (req, bytes)); } fange (IOException e) {e.printStackTrace (); }} ...} privat String extractCookie (HttpServletRequest req, String name) {Cookie [] cookies = req.getCookies (); if (cookies! = null) {for (int i = 0; i <cookies.length; i ++) {if (cookies [i] .getName (). equalsIgnoreCase (name)) {return cookies [i] .getValue () ; }}} returner null; }

Og her er voresCustomHttpServletRequest - bruges til at sende vores anmodningsorgan med de krævede formularparametre konverteret til bytes:

offentlig klasse CustomHttpServletRequest udvider HttpServletRequestWrapper {private byte [] bytes; offentlig CustomHttpServletRequest (HttpServletRequest anmodning, byte [] bytes) {super (anmodning); this.bytes = bytes; } @ Override offentlige ServletInputStream getInputStream () kaster IOException {returner nye ServletInputStreamWrapper (bytes); } @ Override public int getContentLength () {return bytes.length; } @ Override offentlig lang getContentLengthLong () {return bytes.length; } @ Override public String getMethod () {return "POST"; }}

Dette giver os et adgangstoken fra autorisationsserveren i svaret. Derefter ser vi, hvordan vi transformerer svaret.

7. Læg opfriskningstokenet i en cookie

På de sjove ting.

Hvad vi planlægger at gøre her er at få klienten til at få Opdater Token som en cookie.

Vi tilføjer vores Zuul-postfilter for at udtrække Refresh Token fra svaret JSON og sætte det i cookien. Dette er igen en sikret, kun HTTP-cookie med en meget begrænset sti (/ autent / opdater):

public Object run () {... else if (requestURI.contains ("auth / token") || requestURI.contains ("auth / refresh")) {InputStream er = ctx.getResponseDataStream (); String responseBody = IOUtils.toString (er "UTF-8"); if (responseBody.contains ("refresh_token")) {Map responseMap = mapper.readValue (responsBody, ny TypeReference() {}); String refreshToken = responseMap.get ("refresh_token"). ToString (); responseMap.remove ("opdater_token"); responsBody = mapper.writeValueAsString (responsMap); Cookie cookie = ny cookie ("refreshToken", refreshToken); cookie.setHttpOnly (sand); cookie.setPath (ctx.getRequest (). getContextPath () + "/ auth / refresh"); cookie.setMaxAge (2592000); // 30 dage ctx.getResponse (). AddCookie (cookie); } ctx.setResponseBody (responsBody); } ...}

Som vi kan se, tilføjede vi her en betingelse i vores Zuul efterfilter for at læse svaret og udtrække Refresh Token til ruterne godkendelse / token og godkend / opdater. Vi laver nøjagtigt den samme ting for de to, fordi autorisationsserveren i det væsentlige sender den samme nyttelast, mens vi får adgangstokenet og opdateringstokenet.

Så fjernede vi opdater_token fra JSON-svaret for at sikre, at det aldrig er tilgængeligt for frontenden uden for cookien.

Et andet punkt at bemærke her er, at vi indstiller den maksimale alder for cookien til 30 dage - da dette svarer til udløbstiden for tokenet.

8. Hent og brug opfriskningstokenet fra cookien

Nu hvor vi har opdateringstokenet i cookien, når front-end Angular-applikationen forsøger at udløse en token-opdatering, den sender anmodningen kl / autent / opdater og så sender browseren selvfølgelig den cookie.

Så vi får nu en anden tilstand i vores præ filtrer i proxyen, der udtrækker opdateringstoken fra cookien og sender den frem som en HTTP-parameter - således at anmodningen er gyldig:

offentlig Objektkørsel () {RequestContext ctx = RequestContext.getCurrentContext (); ... ellers hvis (requestURI.contains ("auth / refresh"))) {prøv {String token = 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); byte [] bytes = formParams.getBytes ("UTF-8"); ctx.setRequest (ny CustomHttpServletRequest (req, bytes)); } fange (IOException e) {e.printStackTrace (); }} ...}

Dette svarer til det, vi gjorde, da vi først fik adgangstokenet. Men bemærk at formlegemet er anderledes. Nu sender vi en tilskudstype af opdater_token i stedet for autorisationskode sammen med det token, vi tidligere havde gemt i cookien.

Efter at have opnået svaret, går det igen gennem den samme transformation i præ som vi så tidligere i afsnit 7.

9. Opfriskning af adgangstoken fra kantet

Endelig lad os ændre vores enkle frontend-applikation og faktisk bruge opfriskning af tokenet:

Her er vores funktion refreshAccessToken ():

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

Bemærk, hvordan vi simpelthen bruger det eksisterende saveToken () funktion - og bare videregive forskellige input til den.

Bemærk også det vi tilføjer ingen formularparametre med opdater_token os selv - da det bliver taget hånd om af Zuul-filteret.

10. Kør frontenden

Da vores front-end Angular-klient nu er hostet som en Boot-applikation, vil den køre lidt anderledes end før.

Det første trin er det samme. Vi er nødt til at oprette appen:

mvn ren installation

Dette vil udløse frontend-maven-plugin defineret i vores pom.xml at bygge den vinklede kode og kopiere UI-artefakterne til mål / klasser / statisk folder. Denne proces overskriver alt andet, vi har i src / main / ressourcer vejviser. Så vi er nødt til at sikre os og medtage de nødvendige ressourcer fra denne mappe, f.eks ansøgning.yml, i kopiprocessen.

I det andet trin er vi nødt til at køre vores SpringBoot-applikation klasse Ui-applikation. Vores klientapp vil køre på port 8089 som specificeret i ansøgning.yml.

11. Konklusion

I denne OAuth2-tutorial lærte vi, hvordan du gemmer Refresh Token i en Angular-klientapplikation, hvordan du opdaterer et udløbet Access Token, og hvordan du udnytter Zuul-proxyen til alt dette.

Den fulde implementering af denne vejledning kan findes på GitHub.


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