Spring REST API + OAuth2 + kantet

1. Oversigt

I denne vejledning vi sikrer en REST API med OAuth2 og forbruger den fra en simpel Angular-klient.

Den applikation, vi skal bygge ud, vil bestå af tre separate moduler:

  • Autorisationsserver
  • Ressource server
  • UI-godkendelseskode: en front-end-applikation, der bruger autorisationskodeflowet

Vi bruger OAuth-stakken i Spring Security 5. Hvis du vil bruge Spring Security OAuth legacy stack, skal du se på denne tidligere artikel: Spring REST API + OAuth2 + Angular (Brug af Spring Security OAuth Legacy Stack).

Lad os hoppe lige ind.

2. OAuth2 autorisationsserver (AS)

Kort fortalt, en autorisationsserver er et program, der udsteder tokens til godkendelse.

Tidligere tilbød Spring Security OAuth-stakken muligheden for at oprette en autorisationsserver som en Spring Application. Men projektet er forældet, hovedsageligt fordi OAuth er en åben standard med mange veletablerede udbydere som Okta, Keycloak og ForgeRock, for at nævne nogle få.

Af disse bruger vi Keycloak. Det er en open source-identitets- og adgangsstyringsserver administreret af Red Hat, udviklet i Java, af JBoss. Det understøtter ikke kun OAuth2, men også andre standardprotokoller som OpenID Connect og SAML.

Til denne vejledning, vi opretter en integreret Keycloak-server i en Spring Boot-app.

3. Resource Server (RS)

Lad os nu diskutere ressource-serveren; dette er i det væsentlige REST API, som vi i sidste ende vil være i stand til at forbruge.

3.1. Maven-konfiguration

Vores ressource server's pom er meget den samme som den forrige autorisationsserver pom, sans Keycloak delen og med en ekstra spring-boot-starter-oauth2-resource-server afhængighed:

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

3.2. Sikkerhedskonfiguration

Da vi bruger Spring Boot, Vi kan definere den minimale krævede konfiguration ved hjælp af Boot-egenskaber.

Vi gør dette i en ansøgning.yml fil:

server: port: 8081 servlet: kontekststi: / resource-server fjeder: sikkerhed: oauth2: resourceserver: jwt: udsteder-uri: // localhost: 8083 / auth / realms / baeldung jwk-set-uri: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / certs

Her specificerede vi, at vi bruger JWT-tokens til godkendelse.

Det jwk-set-uri egenskab peger på URI, der indeholder den offentlige nøgle, så vores ressource server kan verificere tokens integritet.

Det udsteder-uri egenskab repræsenterer en yderligere sikkerhedsforanstaltning for at validere udstederen af ​​tokens (som er autorisationsserveren). Tilføjelse af denne egenskab kræver imidlertid også, at autorisationsserveren skal køre, inden vi kan starte Resource Server-applikationen.

Lad os derefter oprette en sikkerhedskonfiguration for API til sikring af slutpunkter:

@Configuration offentlig klasse SecurityConfig udvider WebSecurityConfigurerAdapter {@Override beskyttet ugyldig konfiguration (HttpSecurity http) kaster undtagelse {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 vores GET-metoder tillader vi kun anmodninger, der har Læs rækkevidde. For POST-metoden skal anmoderen have et skrive autoritet ud over Læs. For ethvert andet slutpunkt skal anmodningen dog kun godkendes med enhver bruger.

Også, det oauth2ResourceServer () metoden angiver, at dette er en ressource server med jwt () -formaterede tokens.

Et andet punkt at bemærke her er brugen af ​​metode cors () for at tillade adgangskontroloverskrifter på anmodningerne. Dette er især vigtigt, da vi har at gøre med en kantet klient, og vores anmodninger kommer fra en anden oprindelses-URL.

3.4. Modellen og arkivet

Lad os derefter definere en javax.persistence.Entity til vores model, Foo:

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

Så har vi brug for et lager af Foos. Vi bruger Spring's PagingAndSortingRepository:

offentlig grænseflade IFooRepository udvider PagingAndSortingRepository {} 

3.4. Service og implementering

Derefter definerer og implementerer vi en simpel tjeneste til vores API:

offentlig grænseflade IFooService {Valgfri findById (lang id); Foo gemme (Foo foo); Iterabel findAll (); } @Service offentlig klasse FooServiceImpl implementerer IFooService {privat IFooRepository fooRepository; offentlig FooServiceImpl (IFooRepository fooRepository) {this.fooRepository = fooRepository; } @ Override public Valgfri 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

Lad os nu implementere en simpel controller, der udsætter vores Foo ressource via en DTO:

@RestController @RequestMapping (værdi = "/ 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 (() -> nyt ResponseStatusException (HttpStatus.NOT_FOUND)); returner convertToDto (enhed); } @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-enhed) {FooDto dto = ny FooDto (entity.getId (), entity.getName ()); returnere dto; }}

Bemærk brugen af @CrossOrigin over; dette er konfigurationsniveauet på controller-niveau, som vi har brug for for at tillade CORS fra vores Angular App, der kører på den angivne URL.

Her er vores FooDto:

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

4. Frontend - Opsætning

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

Vi bruger først Angular CLI til at generere og administrere vores front-end-moduler.

Først installerer vi node og npm, da Angular CLI er et npm-værktøj.

Så er vi nødt til at bruge frontend-maven-plugin at bygge vores Angular-projekt ved hjælp af 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 et nyt modul ved hjælp af Angular CLI:

ng ny oauthApp

I det følgende afsnit vil vi diskutere Angular app-logikken.

5. Autorisationskodestrøm ved hjælp af vinkel

Vi bruger OAuth2-godkendelseskodestrømmen her.

Vores brugssag: Klientappen anmoder om en kode fra autorisationsserveren og får en login-side. Når en bruger først har givet deres gyldige legitimationsoplysninger og indsender, giver autorisationsserveren os koden. Derefter bruger front-klienten det til at erhverve et adgangstoken.

5.1. Hjemmekomponent

Lad os begynde med vores hovedkomponent, HjemKomponent, hvor al handlingen starter:

@Component ({selector: 'home-header', udbydere: [AppService], skabelon: `Log ind Velkommen !! Log af

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

I begyndelsen, når brugeren ikke er logget ind, vises kun login-knappen. Ved at klikke på denne knap navigeres brugeren til AS's autorisations-URL, hvor de indtaster brugernavn og adgangskode. Efter et vellykket login omdirigeres brugeren tilbage med autorisationskoden, og derefter henter vi adgangstokenet ved hjælp af denne kode.

5.2. App-service

Lad os nu se på AppService - placeret på app.service.ts - som indeholder logikken for serverinteraktioner:

  • retrieveToken (): for at få adgangstoken ved hjælp af autorisationskode
  • saveToken (): for at gemme vores adgangstoken i en cookie ved hjælp af ng2-cookies-biblioteket
  • getResource (): for at hente et Foo-objekt fra serveren ved hjælp af dets ID
  • checkCredentials (): for at kontrollere, om brugeren er logget ind eller ej
  • Log ud(): for at slette adgangstoken-cookie og logge brugeren ud
eksport klasse Foo {konstruktør (offentligt id: nummer, offentligt navn: streng) {}} @Injectable () eksport klasse AppService {offentlig clientId = 'newClient'; offentlig redirectUri = '// localhost: 8089 /'; konstruktør (privat _http: HttpClient) {} 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')); } saveToken (token) {var expireDate = ny dato (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate); console.log ('Opnået adgangstoken'); 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 ('access_token')}); returner denne._http.get (resourceUrl, {headers: headers}) .catch ((error: any) => Observable.throw (error.json (). error || 'Server error')); } checkCredentials () {return Cookie.check ('access_token'); } logout () {Cookie.delete ('access_token'); window.location.reload (); }}

I retrieveToken metode bruger vi vores klientlegitimationsoplysninger og Basic Auth til at sende en STOLPE til / openid-forbindelse / token slutpunkt for at få adgangstokenet. Parametrene sendes i et URL-kodet format. Når vi har fået adgangstokenet, vi gemmer det i en cookie.

Cookieopbevaring er især vigtig her, fordi vi kun bruger cookien til opbevaringsformål og ikke til at drive autentificeringsprocessen direkte. Dette hjælper med at beskytte mod Cross-Site Request Forgery (CSRF) angreb og sårbarheder.

5.3. Foo-komponent

Endelig vores FooComponent for at få vist vores Foo-detaljer:

@Component ({selector: 'foo-details', udbydere: [AppService], skabelon: 'ID {{foo.id}} Navn {{foo.name}} Ny Foo'}) eksportklasse FooComponent {public foo = new Foo (1, 'prøve foo'); privat 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, fejl => this.foo.name = 'Fejl'); }}

5.5. Appkomponent

Vores enkle AppComponent at fungere som rodkomponent:

@Component ({selector: 'app-root', skabelon: 'Spring Security Oauth - autorisationskode'}) eksportklasse AppComponent {} 

Og AppModule hvor vi indpakker alle vores komponenter, tjenester og ruter:

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

7. Kør frontenden

1. For at køre et af vores front-end-moduler skal vi først oprette appen:

mvn ren installation

2. Så er vi nødt til at navigere til vores Angular-app-bibliotek:

cd src / main / ressourcer

3. Endelig starter vi vores app:

npm start

Serveren starter som standard på port 4200; for at ændre porten på et hvilket som helst modul skal du ændre:

"start": "ng serve"

i pakke.json; for at få det til at køre på port 8089 skal du tilføje:

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

8. Konklusion

I denne artikel lærte vi, hvordan vi godkendte vores ansøgning ved hjælp af OAuth2.

Den fulde implementering af denne vejledning kan findes i GitHub-projektet.