OAuth2 for en Spring REST API - Håndter Oppdater Token i vinkel

1. Oversikt

I denne veiledningen vil vi fortsette å utforske OAuth2 autorisasjonskodestrømmen som vi begynte å sette sammen i vår forrige artikkel og Vi vil fokusere på hvordan du håndterer Refresh Token i en Angular-app. Vi bruker også Zuul-proxyen.

Vi bruker OAuth-stakken i Spring Security 5. Hvis du vil bruke vår sikkerhet OAuth arv stack, kan du ta en titt på denne forrige artikkelen: OAuth2 for en Spring REST API - Håndter Refresh Token i AngularJS (eldre OAuth stack)

2. Utløp for tilgangstoken

Husk først at klienten skaffet seg et tilgangstoken ved hjelp av en godkjenningskode i to trinn. I det første trinnet får vi autorisasjonskoden. Og i det andre trinnet oppnår vi faktisk Access Token.

Vårt tilgangstoken lagres i en informasjonskapsel som utløper basert på når tegnet selv utløper:

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

Det som er viktig å forstå er det selve informasjonskapselen brukes kun til lagring og det driver ikke noe annet i OAuth2-strømmen. For eksempel vil nettleseren aldri automatisk sende ut informasjonskapselen til serveren med forespørsler, så vi er sikret her.

Men legg merke til hvordan vi faktisk definerer dette retrieveToken () funksjon for å få Access Token:

retrieveToken (kode) {let params = new URLSearchParams (); params.append ('grant_type', 'autorisasjonskode'); params.append ('client_id', this.clientId); params.append ('client_secret', 'newClientSecret'); params.append ('redirect_uri', this.redirectUri); params.append ('kode', kode); la 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 => alert ('Invalid Credentials')); }

Vi sender klientens hemmelighet i params, som egentlig ikke er en sikker måte å håndtere dette på. La oss se hvordan vi kan unngå å gjøre dette.

3. Fullmakten

Så, vi skal nå ha en Zuul-proxy som kjører i front-end-applikasjonen og i utgangspunktet sitter mellom front-end-klienten og Authorization Server. All sensitiv informasjon vil bli håndtert på dette laget.

Front-end-klienten blir nå vert som en Boot-applikasjon, slik at vi kan koble sømløst til den innebygde Zuul-proxyen vår ved hjelp av Spring Cloud Zuul-starter.

Hvis du vil gå gjennom det grunnleggende om Zuul, kan du lese raskt om Zuul-hovedartikkelen.

la oss konfigurere rutene til proxyen:

zuul: ruter: auth / kode: bane: / 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 /

Vi har satt opp ruter for å håndtere følgende:

  • autentiseringskode - få autorisasjonskoden og lagre den i en informasjonskapsel
  • godkjenne / omdirigere - håndter omdirigeringen til autorisasjonsserverens påloggingsside
  • godkjenning / ressurser - kart til autorisasjonsserverens tilsvarende bane for innloggingssidens ressurser (css og js)
  • auth / token - få tilgangstoken, fjern refresh_token fra nyttelasten og lagre den i en informasjonskapsel
  • godkjenne / oppdatere - få Refresh Token, fjern den fra nyttelasten og lagre den i en informasjonskapsel

Det som er interessant her er at vi bare proxyer trafikk til autorisasjonsserveren og ikke noe annet. Vi trenger bare fullmakten til å komme inn når klienten skaffer nye tokens.

Deretter, la oss se på alle disse en etter en.

4. Få koden ved hjelp av Zuul Pre Filter

Den første bruken av proxyen er enkel - vi setter opp en forespørsel om å få autorisasjonskoden:

@Component public class CustomPreZuulFilter utvider ZuulFilter {@Override public Object run () {RequestContext ctx = RequestContext.getCurrentContext (); HttpServletRequest req = ctx.getRequest (); StrengforespørselURI = 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); } returner 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 () {retur 6; } @ Override public String filterType () {return "pre"; }}

Vi bruker en filtertype pre å behandle forespørselen før du sender den videre.

I filteret løpe() metode, legger vi til søkeparametere for respons_type, omfang, klient-ID og redirect_uri- alt som autorisasjonsserveren vår trenger for å føre oss til påloggingssiden og sende en kode tilbake.

Legg også merke til shouldFilter () metode. Vi filtrerer bare forespørsler med de tre nevnte URI-ene, andre går ikke til løpe metode.

5. Sett koden i en informasjonskapsel Ved hjelp av Zuul Post Filter

Det vi planlegger å gjøre her er å lagre koden som en informasjonskapsel, slik at vi kan sende den til autorisasjonsserveren for å få tilgangstokenet. Koden er til stede som en søkeparameter i forespørsels-URL-en som autorisasjonsserveren omdirigerer oss til etter innlogging.

Vi setter opp et Zuul-etterfilter for å trekke ut denne koden og sette den i informasjonskapselen. Dette er ikke bare en vanlig informasjonskapsel, men en sikret, kun HTTP-informasjonskapsel med en veldig begrenset sti (/ auth / token):

@Component public class CustomPostZuulFilter utvider ZuulFilter {private ObjectMapper mapper = new ObjectMapper (); @Override public Object run () {RequestContext ctx = RequestContext.getCurrentContext (); prøv {Map params = ctx.getRequestQueryParams (); hvis (requestURI.contains ("auth / redirect")) {Cookie cookie = new Cookie ("code", params.get ("code"). get (0)); cookie.setHttpOnly (true); cookie.setPath (ctx.getRequest (). getContextPath () + "/ auth / token"); ctx.getResponse (). addCookie (informasjonskapsel); }} fange (Unntak e) {logger.error ("Feil oppstod i zuul post filter", e); } returner 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 () {retur 10; } @ Override public String filterType () {return "post"; }}

For å legge til et ekstra beskyttelseslag mot CSRF-angrep, Vi legger til en cookiehode fra Same-Site i alle informasjonskapslene våre.

For det oppretter vi en konfigurasjonsklasse:

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

Her setter vi attributtet til streng, slik at overføring av informasjonskapsler på tvers av nettstedet strengt holdes tilbake.

6. Få og bruk koden fra informasjonskapslen

Nå som vi har koden i informasjonskapselen, når front-end Angular-applikasjonen prøver å utløse en tokenforespørsel, kommer den til å sende forespørselen kl. / auth / token og så vil nettleseren selvfølgelig sende den informasjonskapselen.

Så vi får nå en annen tilstand i vår pre filter i proxyen som vil trekke ut koden fra informasjonskapselen og sende den sammen med andre skjemaparametere for å skaffe token:

offentlig objektkjøring () {RequestContext ctx = RequestContext.getCurrentContext (); ... annet 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", "autorisasjonskode", CLIENT_ID, CLIENT_SECRET, REDIRECT_URL, kode); byte [] bytes = formParams.getBytes ("UTF-8"); ctx.setRequest (ny CustomHttpServletRequest (req, bytes)); } fange (IOException e) {e.printStackTrace (); }} ...} private String extractCookie (HttpServletRequest req, String name) {Cookie [] cookies = req.getCookies (); hvis (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 vårCustomHttpServletRequest - brukes til å sende forespørselsorganet vårt med de nødvendige skjemaparametrene konvertert til byte:

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

Dette vil gi oss et tilgangstoken fra autorisasjonsserveren i svaret. Deretter får vi se hvordan vi transformerer responsen.

7. Legg Refresh Token i en informasjonskapsel

Videre til de morsomme tingene.

Det vi planlegger å gjøre her er at klienten får Refresh Token som informasjonskapsel.

Vi legger til Zuul-postfilteret vårt for å trekke ut oppdateringstokenet fra JSON-kroppen i svaret og sette det i informasjonskapselen. Dette er igjen en sikret, kun HTTP-informasjonskapsel med en veldig begrenset sti (/ auth / refresh):

offentlig objektkjøring () {... else if (requestURI.contains ("auth / token") || requestURI.contains ("auth / refresh")) {InputStream is = ctx.getResponseDataStream (); String responseBody = IOUtils.toString (er "UTF-8"); if (responseBody.contains ("refresh_token")) {Map responseMap = mapper.readValue (responseBody, new TypeReference() {}); String refreshToken = responseMap.get ("refresh_token"). ToString (); responseMap.remove ("refresh_token"); responseBody = mapper.writeValueAsString (responsMap); Cookie cookie = ny Cookie ("refreshToken", refreshToken); cookie.setHttpOnly (true); cookie.setPath (ctx.getRequest (). getContextPath () + "/ auth / refresh"); cookie.setMaxAge (2592000); // 30 dager ctx.getResponse (). AddCookie (informasjonskapsel); } ctx.setResponseBody (responsBody); } ...}

Som vi kan se, la vi til her en betingelse i vårt Zuul-postfilter for å lese svaret og trekke ut Oppdater token for rutene auth / token og godkjenne / oppdatere. Vi gjør nøyaktig det samme for de to fordi autorisasjonsserveren i det vesentlige sender den samme nyttelasten mens du får tilgangstokenet og oppdateringstokenet.

Så fjernet vi refresh_token fra JSON-svaret for å sikre at den aldri er tilgjengelig for frontenden utenfor informasjonskapselen.

Et annet poeng å merke seg her er at vi setter maksimal alder på informasjonskapsel til 30 dager - da dette samsvarer med utløpstiden for tokenet.

8. Få og bruk Oppfriskningstoken fra informasjonskapselen

Nå som vi har Refresh Token i informasjonskapselen, når front-end Angular-applikasjonen prøver å utløse en tokenoppdatering, det kommer til å sende forespørselen kl / auth / refresh og så vil nettleseren selvfølgelig sende den informasjonskapselen.

Så vi får nå en annen tilstand i vår pre filtrer i proxyen som trekker ut oppdateringstokenet fra informasjonskapselen og sender den videre som en HTTP-parameter - slik at forespørselen er gyldig:

offentlig objektkjøring () {RequestContext ctx = RequestContext.getCurrentContext (); ... annet 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 ligner på det vi gjorde da vi først fikk tak i Access Token. Men legg merke til at formlegemet er annerledes. Nå sender vi en tilskuddstype av refresh_token i stedet for Godkjennelseskoden sammen med symbolet vi hadde lagret før i informasjonskapselen.

Etter å ha fått svaret, går det igjen gjennom den samme transformasjonen i pre som vi så tidligere i avsnitt 7.

9. Oppdater tilgangstoken fra vinkel

Til slutt, la oss endre vårt enkle frontend-program og faktisk bruke forfriskende token:

Her er vår funksjon 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 => alert ('Invalid Credentials')); }

Legg merke til hvordan vi bare bruker det eksisterende saveToken () funksjon - og bare sende forskjellige innganger til den.

Legg også merke til det vi legger ikke til noen skjemaparametere med refresh_token oss selv - da det blir tatt hånd om av Zuul-filteret.

10. Kjør frontenden

Siden vår front-end Angular-klient nå er vert som en Boot-applikasjon, vil den kjøre litt annerledes enn før.

Det første trinnet er det samme. Vi trenger å bygge appen:

mvn ren installasjon

Dette vil utløse frontend-maven-plugin definert i vår pom.xml å bygge Angular-koden og kopiere UI-gjenstandene til mål / klasser / statisk mappe. Denne prosessen overskriver alt annet vi har i src / main / resources katalog. Så vi må sørge for og inkludere nødvendige ressurser fra denne mappen, for eksempel application.yml, i kopiprosessen.

I det andre trinnet må vi kjøre vår SpringBootApplication klasse Ui-applikasjon. Klientappen vår vil være i gang på port 8089 som spesifisert i application.yml.

11. Konklusjon

I denne OAuth2-opplæringen lærte vi hvordan du lagrer Refresh Token i et Angular-klientprogram, hvordan du oppdaterer et utgått Access Token og hvordan du kan utnytte Zuul-proxyen for alt dette.

Den fulle implementeringen av denne veiledningen finner du på GitHub.


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