Vår REST API + OAuth2 + kantet

1. Oversikt

I denne veiledningen, vi vil sikre oss en REST API med OAuth2 og konsumere den fra en enkel Angular-klient.

Applikasjonen vi skal bygge ut vil bestå av tre separate moduler:

  • Autorisasjonsserver
  • Ressursserver
  • UI-autorisasjonskode: et front-end-program som bruker autorisasjonskodestrømmen

Vi bruker OAuth-stakken i Spring Security 5. Hvis du vil bruke Spring Security OAuth arvstakken, kan du ta en titt på denne forrige artikkelen: Spring REST API + OAuth2 + Angular (ved hjelp av Spring Security OAuth Legacy Stack).

La oss hoppe rett inn.

2. OAuth2 autorisasjonsserver (AS)

For å si det enkelt, en autorisasjonsserver er et program som utsteder tokens for autorisasjon.

Tidligere tilbød Spring Security OAuth-stakken muligheten for å sette opp en autorisasjonsserver som en vårapplikasjon. Men prosjektet er avviklet, hovedsakelig fordi OAuth er en åpen standard med mange veletablerte leverandører som Okta, Keycloak og ForgeRock, for å nevne noen.

Av disse bruker vi Keycloak. Det er en åpen kildekode Identity and Access Management-server administrert av Red Hat, utviklet i Java, av JBoss. Den støtter ikke bare OAuth2, men også andre standardprotokoller som OpenID Connect og SAML.

For denne opplæringen, vi setter opp en innebygd Keycloak-server i en Spring Boot-app.

3. Ressursserveren (RS)

La oss nå diskutere ressursserveren; dette er egentlig REST API, som vi til slutt vil kunne konsumere.

3.1. Maven-konfigurasjon

Vår ressursserverpom er omtrent den samme som den forrige autorisasjonsserverpom, sans Keycloak-delen og med en ekstra spring-boot-starter-oauth2-resource-server avhengighet:

 org.springframework.boot spring-boot-starter-oauth2-resource-server 

3.2. Sikkerhetskonfigurasjon

Siden vi bruker Spring Boot, vi kan definere den minimale nødvendige konfigurasjonen ved hjelp av Boot-egenskaper.

Vi gjør dette på en application.yml fil:

server: port: 8081 servlet: context-path: / resource-server spring: security: oauth2: resourceserver: jwt: issuer-uri: // localhost: 8083 / auth / realms / baeldung jwk-set-uri: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / certs

Her spesifiserte vi at vi vil bruke JWT-tokens for autorisasjon.

De jwk-set-uri egenskap peker til URI som inneholder den offentlige nøkkelen, slik at ressursserveren vår kan verifisere tokens integritet.

De utsteder-uri egenskap representerer et ekstra sikkerhetstiltak for å validere utstederen av tokens (som er autorisasjonsserveren). Å legge til denne egenskapen krever imidlertid at autorisasjonsserveren skal kjøre før vi kan starte Resource Server-applikasjonen.

La oss deretter sette opp en sikkerhetskonfigurasjon for API for å sikre sluttpunkter:

@Configuration offentlig klasse SecurityConfig utvider WebSecurityConfigurerAdapter {@Override-beskyttet tomkonfigurasjon (HttpSecurity http) kaster unntak {http.cors () .and () .authorizeRequests () .antMatchers (HttpMethod.GET, "/ user / info", "/ api) / foos / ** ") .hasAuthority (" SCOPE_read ") .antMatchers (HttpMethod.POST," / api / foos ") .hasAuthority (" SCOPE_write ") .anyRequest () .authenticated () .and () .oauth2ResourceServer ( ) .jwt (); }}

Som vi kan se, for våre GET-metoder tillater vi bare forespørsler som har lese omfang. For POST-metoden må rekvirenten ha et skrive autoritet i tillegg til lese. For ethvert annet endepunkt bør forespørselen imidlertid bare autentiseres med alle brukere.

Også, de oauth2ResourceServer () metoden angir at dette er en ressursserver, med jwt () -formaterte tokens.

Et annet poeng å merke seg her er bruken av metode cors () for å tillate tilgangskontrolloverskrifter på forespørslene. Dette er spesielt viktig siden vi har å gjøre med en Angular-klient, og våre forespørsler kommer fra en annen opprinnelses-URL.

3.4. Modellen og depotet

Deretter la oss definere en javax.persistence.Entity for vår modell, Foo:

@Entity offentlig klasse Foo {@Id @GeneratedValue (strategi = GenerationType.IDENTITY) privat Lang id; privat strengnavn; // constructor, getters and setters}

Da trenger vi et lager av Foos. Vi bruker Spring's PagingAndSortingRepository:

offentlig grensesnitt IFooRepository utvider PagingAndSortingRepository {} 

3.4. Tjenesten og implementeringen

Etter det vil vi definere og implementere en enkel tjeneste for API: et:

offentlig grensesnitt IFooService {Valgfri findById (lang id); Foo save (Foo foo); Iterable findAll (); } @Service offentlig klasse FooServiceImpl implementerer IFooService {privat IFooRepository fooRepository; offentlig FooServiceImpl (IFooRepository fooRepository) {this.fooRepository = fooRepository; } @ Override public Valgfritt findById (Long id) {return fooRepository.findById (id); } @ Override public Foo save (Foo foo) {return fooRepository.save (foo); } @ Override public Iterable findAll () {return fooRepository.findAll (); }} 

3.5. En prøvekontroller

La oss nå implementere en enkel kontroller som avslører vår Foo ressurs via en DTO:

@RestController @RequestMapping (verdi = "/ api / foos") offentlig klasse FooController {privat IFooService fooService; offentlig FooController (IFooService fooService) {this.fooService = fooService; } @CrossOrigin (origins = "// localhost: 8089") @GetMapping (value = "/ {id}") offentlig FooDto findOne (@PathVariable Long id) {Foo entity = fooService.findById (id) .orElseThrow (() -> nytt ResponseStatusException (HttpStatus.NOT_FOUND)); return convertToDto (enhet); } @GetMapping offentlig samling findAll () {Iterable foos = this.fooService.findAll (); Liste fooDtos = ny ArrayList (); foos.forEach (p -> fooDtos.add (convertToDto (p))); returner fooDtos; } beskyttet FooDto convertToDto (Foo-enhet) {FooDto dto = ny FooDto (entity.getId (), entity.getName ()); returnere dto; }}

Legg merke til bruken av @CrossOrigin ovenfor; dette er konfigurasjonsnivået på kontrollernivået vi trenger for å tillate CORS fra Angular-appen vår som kjører på den angitte URL-en.

Her er vår FooDto:

offentlig klasse FooDto {privat lang id; privat strengnavn; }

4. Front End - Oppsett

Vi skal nå se på en enkel front-end Angular-implementering for klienten, som får tilgang til REST API.

Vi bruker først Angular CLI til å generere og administrere frontend-modulene våre.

Først installerer vi node og npm, ettersom Angular CLI er et npm-verktøy.

Da må vi bruke frontend-maven-plugin å bygge vårt Angular-prosjekt ved hjelp av Maven:

   com.github.eirslett frontend-maven-plugin 1.3 v6.10.2 3.10.10 src / main / resources install node og npm install-node-and-npm npm install npm npm run build npm run build 

Og endelig, generere en ny modul ved bruk av Angular CLI:

ng ny oauthApp

I det neste avsnittet vil vi diskutere Angular-applogikken.

5. Autorisasjonskodestrøm ved bruk av vinkel

Vi skal bruke OAuth2 autorisasjonskodestrømmen her.

Vår brukstilfelle: Klientappen ber om en kode fra autorisasjonsserveren og får en påloggingsside. Når en bruker oppgir gyldig legitimasjon og sender inn, gir autorisasjonsserveren oss koden. Deretter bruker front-end-klienten den til å skaffe seg et tilgangstoken.

5.1. Hjemmekomponent

La oss begynne med hovedkomponenten vår Hjemmekomponent, der all handlingen starter:

@Component ({selector: 'home-header', providers: [AppService], mal: `Logg inn Velkommen !! Logg ut

`}) eksportklasse HomeComponent {public isLoggedIn = false; konstruktør (privat _service: AppService) {} ngOnInit () {this.isLoggedIn = this._service.checkCredentials (); la i = window.location.href.indexOf ('kode'); if (! this.isLoggedIn && i! = -1) {this._service.retrieveToken (window.location.href.substring (i + 5)); }} logg inn () {window.location.href = '// localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / auth? response_type = code & scope = openid% 20write% 20read & client_id = '+ this._service.clientId +' & redirect_uri = '+ this._service.redirectUri; } logg ut () {this._service.logout (); }}

I begynnelsen, når brukeren ikke er logget inn, vises bare påloggingsknappen. Når du klikker på denne knappen, navigeres brukeren til ASens autorisasjons-URL der de taster inn brukernavn og passord. Etter en vellykket pålogging blir brukeren omdirigert tilbake med autorisasjonskoden, og deretter henter vi tilgangstokenet ved hjelp av denne koden.

5.2. App-tjeneste

La oss nå se på AppService - ligger ved app.service.ts - som inneholder logikken for serverinteraksjoner:

  • retrieveToken (): for å få tilgangstoken ved hjelp av autorisasjonskode
  • saveToken (): for å lagre tilgangstokenet vårt i en informasjonskapsel ved hjelp av ng2-cookies-biblioteket
  • getResource (): for å hente et Foo-objekt fra serveren ved hjelp av ID-en
  • checkCredentials (): for å sjekke om brukeren er pålogget eller ikke
  • Logg ut(): for å slette tilgangstoken-informasjonskapsel og logge ut brukeren
eksportere klasse Foo {konstruktør (offentlig id: nummer, offentlig navn: streng) {}} @Injectable () eksportere klasse AppService {offentlig clientId = 'newClient'; offentlig redirectUri = '// localhost: 8089 /'; konstruktør (privat _http: HttpClient) {} retrieveToken (kode) {la params = ny 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')); } saveToken (token) {var expireDate = new Date (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate); console.log ('Oppnådd tilgangstoken'); window.location.href = '// localhost: 8089'; } getResource (resourceUrl): Observerbar {var headers = new HttpHeaders ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + Cookie.get ('tilgangstoken')}); returner dette._http.get (resourceUrl, {headers: headers}) .catch ((error: any) => Observable.throw (error.json (). error || 'Server error')); } checkCredentials () {return Cookie.check ('access_token'); } avlogging () {Cookie.delete ('access_token'); window.location.reload (); }}

I retrieveToken metode bruker vi klientlegitimasjonen vår og Basic Auth for å sende en POST til / openid-connect / token sluttpunkt for å få tilgangstokenet. Parametrene sendes i et URL-kodet format. Etter at vi har fått tilgangstokenet, vi lagrer den i en informasjonskapsel.

Oppbevaring av informasjonskapsler er spesielt viktig her fordi vi bare bruker informasjonskapslen til lagringsformål og ikke for å drive autentiseringsprosessen direkte. Dette bidrar til å beskytte mot Cross-Site Request Forgery (CSRF) angrep og sårbarheter.

5.3. Foo-komponent

Til slutt, vår FooComponent for å vise Foo-detaljene våre:

@Component ({selector: 'foo-details', providers: [AppService], template: 'ID {{foo.id}} Name {{foo.name}} New Foo `}) eksportklasse FooComponent {public foo = new Foo (1, 'prøve foo'); private foosUrl = '// localhost: 8081 / resource-server / api / foos /'; konstruktør (privat _service: AppService) {} getFoo () {this._service.getResource (this.foosUrl + this.foo.id). abonner (data => this.foo = data, feil => this.foo.name = 'Feil'); }}

5.5. App-komponent

Vår enkle AppComponent å fungere som rotkomponent:

@Component ({selector: 'app-root', mal: 'Spring Security Oauth - Authorization Code'}) eksportklasse AppComponent {} 

Og AppModule der vi pakker inn alle våre komponenter, tjenester og ruter:

@NgModule ({erklæringer: [AppComponent, HomeComponent, FooComponent], importerer: [BrowserModule, HttpClientModule, RouterModule.forRoot ([{path: '', component: HomeComponent, pathMatch: 'full'}], {onSameUrlNavigation: ' })], leverandører: [], bootstrap: [AppComponent]}) eksportklasse AppModule {} 

7. Kjør frontenden

1. For å kjøre noen av frontendemodulene, må vi først bygge appen:

mvn ren installasjon

2. Da må vi navigere til vår Angular-appkatalog:

cd src / main / resources

3. Til slutt vil vi starte appen vår:

npm start

Serveren starter som standard på port 4200; for å endre porten til en hvilken som helst modul, endre:

"start": "ng serve"

i pakke.json; for å få den til å kjøre på port 8089, legg til:

"start": "ng serve --port 8089"

8. Konklusjon

I denne artikkelen lærte vi hvordan vi autoriserer søknaden vår ved hjelp av OAuth2.

Den fulle implementeringen av denne veiledningen finner du i GitHub-prosjektet.