Spring REST API + OAuth2 + Angular

1. Overzicht

In deze tutorial we beveiligen een REST API met OAuth2 en gebruiken deze van een eenvoudige Angular-client.

De applicatie die we gaan bouwen, zal uit drie afzonderlijke modules bestaan:

  • Autorisatieserver
  • Bronserver
  • UI-autorisatiecode: een front-end-applicatie die gebruikmaakt van de autorisatiecodestroom

We gebruiken de OAuth-stack in Spring Security 5. Als je de Spring Security OAuth legacy-stack wilt gebruiken, bekijk dan dit vorige artikel: Spring REST API + OAuth2 + Angular (met behulp van de Spring Security OAuth Legacy Stack).

Laten we er meteen in springen.

2. De OAuth2-autorisatieserver (AS)

Simpel gezegd, een autorisatieserver is een applicatie die tokens uitgeeft voor autorisatie.

Eerder bood de Spring Security OAuth-stack de mogelijkheid om een ​​Authorization Server in te richten als Spring Application. Maar het project is verouderd, vooral omdat OAuth een open standaard is met veel gevestigde providers zoals Okta, Keycloak en ForgeRock, om er maar een paar te noemen.

Hiervan zullen we Keycloak gebruiken. Het is een open-source Identity and Access Management-server beheerd door Red Hat, ontwikkeld in Java, door JBoss. Het ondersteunt niet alleen OAuth2, maar ook andere standaardprotocollen zoals OpenID Connect en SAML.

Voor deze tutorial, we gaan een embedded Keycloak-server opzetten in een Spring Boot-app.

3. De Resource Server (RS)

Laten we nu de Resource Server bespreken; dit is in wezen de REST API, die we uiteindelijk willen kunnen consumeren.

3.1. Maven-configuratie

De pom van onze Resource Server is vrijwel hetzelfde als de vorige autorisatieserver pom, zonder het Keycloak-gedeelte en met een extra spring-boot-starter-oauth2-resource-server afhankelijkheid:

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

3.2. Beveiligingsconfiguratie

Omdat we Spring Boot gebruiken, we kunnen de minimaal vereiste configuratie definiëren met behulp van Boot-eigenschappen.

We doen dit in een application.yml het dossier:

server: poort: 8081 servlet: contextpad: / 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

Hier hebben we gespecificeerd dat we JWT-tokens zullen gebruiken voor autorisatie.

De jwk-set-uri eigenschap verwijst naar de URI die de openbare sleutel bevat, zodat onze Resource Server de integriteit van de tokens kan verifiëren.

De uitgever-uri eigenschap vertegenwoordigt een aanvullende beveiligingsmaatregel om de uitgever van de tokens te valideren (dit is de autorisatieserver). Het toevoegen van deze eigenschap vereist echter ook dat de autorisatieserver actief moet zijn voordat we de Resource Server-toepassing kunnen starten.

Laten we vervolgens een beveiligingsconfiguratie voor de API om eindpunten te beveiligen:

@Configuration public class SecurityConfig breidt WebSecurityConfigurerAdapter uit {@Override protected void configure (HttpSecurity http) genereert uitzondering {http.cors () .and () .authorizeRequests () .antMatchers (HttpMethod.GET, "/ user / info", "/ api / foos / ** ") .hasAuthority (" SCOPE_read ") .antMatchers (HttpMethod.POST," / api / foos ") .hasAuthority (" SCOPE_write ") .anyRequest () .authenticated () .en () .oauth2ResourceServer ( ) .jwt (); }}

Zoals we kunnen zien, staan ​​we voor onze GET-methoden alleen verzoeken toe die lezen reikwijdte. Voor de POST-methode moet de aanvrager een schrijven autoriteit naast lezen. Voor elk ander eindpunt moet het verzoek echter alleen worden geverifieerd bij een willekeurige gebruiker.

Ook, de oauth2ResourceServer () methode geeft aan dat dit een bronserver is, met jwt () -geformatteerde tokens.

Een ander punt om op te merken is het gebruik van de methode cors () om Access-Control-headers op de verzoeken toe te staan. Dit is vooral belangrijk omdat we te maken hebben met een Angular-client en onze verzoeken zullen afkomstig zijn van een andere oorspronkelijke URL.

3.4. Het model en de opslagplaats

Laten we vervolgens een javax.persistence.Entity voor ons model, Foo:

@Entity openbare klasse Foo {@Id @GeneratedValue (strategy = GenerationType.IDENTITY) privé Lange id; private String naam; // constructeur, getters en setters}

Dan hebben we een repository nodig van Foos. We gebruiken Spring's PagingAndSortingRepository:

openbare interface IFooRepository breidt PagingAndSortingRepository {} uit 

3.4. De service en implementatie

Daarna zullen we een eenvoudige service voor onze API definiëren en implementeren:

openbare interface IFooService {Optioneel findById (lange id); Foo save (Foo foo); Herhaalbare findAll (); } @Service openbare klasse FooServiceImpl implementeert IFooService {privé IFooRepository fooRepository; openbare FooServiceImpl (IFooRepository fooRepository) {this.fooRepository = fooRepository; } @Override public Optioneel findById (lange id) {return fooRepository.findById (id); } @Override public Foo save (Foo foo) {return fooRepository.save (foo); } @Override public Iterable findAll () {return fooRepository.findAll (); }} 

3.5. Een voorbeeldcontroller

Laten we nu een eenvoudige controller implementeren die onze Foo resource via een DTO:

@RestController @RequestMapping (value = "/ api / foos") openbare klasse FooController {privé IFooService fooService; openbare FooController (IFooService fooService) {this.fooService = fooService; } @CrossOrigin (origins = "// localhost: 8089") @GetMapping (value = "/ {id}") openbare FooDto findOne (@PathVariable Long id) {Foo entity = fooService.findById (id) .orElseThrow (() -> nieuwe ResponseStatusException (HttpStatus.NOT_FOUND)); return convertToDto (entiteit); } @GetMapping openbare verzameling findAll () {Herhaalbare foos = this.fooService.findAll (); List fooDtos = nieuwe ArrayList (); foos.forEach (p -> fooDtos.add (convertToDto (p))); fooDtos teruggeven; } beschermde FooDto convertToDto (Foo entiteit) {FooDto dto = nieuwe FooDto (entity.getId (), entity.getName ()); terugkeer dto; }}

Let op het gebruik van @CrossOrigin bovenstaande; dit is de configuratie op controllerniveau die we nodig hebben om CORS van onze Angular App op de opgegeven URL toe te staan.

Hier is onze FooDto:

openbare klasse FooDto {privé lange id; private String naam; }

4. Front End - Instellingen

We gaan nu kijken naar een eenvoudige front-end Angular-implementatie voor de klant, die toegang heeft tot onze REST API.

We zullen eerst Angular CLI gebruiken om onze front-end-modules te genereren en te beheren.

Eerst installeren we node en npm, aangezien Angular CLI een npm-tool is.

Dan moeten we de frontend-maven-plugin om ons Angular-project te bouwen met Maven:

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

En tenslotte, genereer een nieuwe module met Angular CLI:

ng nieuwe oauthApp

In de volgende sectie bespreken we de logica van de Angular-app.

5. Autorisatiecodestroom met Angular

We gaan hier de OAuth2-autorisatiecodestroom gebruiken.

Onze use case: de client-app vraagt ​​een code aan bij de autorisatieserver en krijgt een inlogpagina te zien. Zodra een gebruiker zijn geldige inloggegevens opgeeft en verzendt, geeft de autorisatieserver ons de code. Vervolgens gebruikt de front-end-client het om een ​​toegangstoken te verkrijgen.

5.1. Home Component

Laten we beginnen met ons hoofdbestanddeel, het HomeComponent, waar alle actie begint:

@Component ({selector: 'home-header', providers: [AppService], sjabloon: `Inloggen Welkom !!

`}) exportklasse HomeComponent {public isLoggedIn = false; constructor (private _service: AppService) {} ngOnInit () {this.isLoggedIn = this._service.checkCredentials (); laat i = window.location.href.indexOf ('code'); if (! 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 = code & bereik = openid% 20write% 20read & client_id = '+ this._service.clientId +' & redirect_uri = '+ this._service.redirectUri; } logout () {this._service.logout (); }}

In het begin, wanneer de gebruiker niet is ingelogd, verschijnt alleen de login-knop. Door op deze knop te klikken, wordt de gebruiker naar de autorisatie-URL van het AS geleid, waar hij de gebruikersnaam en het wachtwoord intoetst. Na een succesvolle login wordt de gebruiker teruggestuurd met de autorisatiecode, en vervolgens halen we het toegangstoken op met behulp van deze code.

5.2. App Service

Laten we nu eens kijken AppService - gevestigd in app.service.ts - die de logica bevat voor serverinteracties:

  • retrieveToken (): om toegangstoken te verkrijgen met autorisatiecode
  • saveToken (): om ons toegangstoken op te slaan in een cookie met behulp van de ng2-cookies-bibliotheek
  • getResource (): om een ​​Foo-object van de server te krijgen met behulp van zijn ID
  • checkCredentials (): om te controleren of de gebruiker is aangemeld of niet
  • uitloggen(): om de toegangstoken-cookie te verwijderen en de gebruiker uit te loggen
exportklasse Foo {constructor (publieke id: nummer, publieke naam: string) {}} @Injectable () exportklasse AppService {publieke clientId = 'newClient'; openbare redirectUri = '// localhost: 8089 /'; constructor (privé _http: HttpClient) {} retrieveToken (code) {let params = nieuwe URLSearchParams (); params.append ('grant_type', 'autorisatiecode'); params.append ('client_id', this.clientId); params.append ('client_secret', 'newClientSecret'); params.append ('redirect_uri', this.redirectUri); params.append ('code', code); let headers = new 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 ( data), err => alert ('Ongeldige inloggegevens')); } saveToken (token) {var expireDate = nieuwe datum (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate); console.log ('Toegangstoken verkregen'); window.location.href = '// localhost: 8089'; } getResource (resourceUrl): Observable {var headers = new HttpHeaders ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + Cookie.get ('toegangstoken')}); retourneer this._http.get (resourceUrl, {headers: headers}) .catch ((error: any) => Observable.throw (error.json (). error || 'Serverfout')); } checkCredentials () {return Cookie.check ('access_token'); } logout () {Cookie.delete ('access_token'); window.location.reload (); }}

In de retrieveToken methode gebruiken we onze clientreferenties en Basic Auth om een POST naar de / openid-connect / token eindpunt om het toegangstoken op te halen. De parameters worden verzonden in een URL-gecodeerde indeling. Nadat we het toegangstoken hebben verkregen, we slaan het op in een cookie.

De cookie-opslag is hier vooral belangrijk omdat we de cookie alleen gebruiken voor opslagdoeleinden en niet om het authenticatieproces rechtstreeks aan te sturen. Dit helpt beschermen tegen Cross-Site Request Forgery (CSRF) -aanvallen en kwetsbaarheden.

5.3. Foo-component

Eindelijk, onze FooComponent om onze Foo-details weer te geven:

@Component ({selector: 'foo-details', providers: [AppService], sjabloon: `ID {{foo.id}} Naam {{foo.name}} New Foo`}) exportklasse FooComponent {public foo = nieuw Foo (1, 'voorbeeld foo'); private foosUrl = '// localhost: 8081 / resource-server / api / foos /'; constructor (private _service: AppService) {} getFoo () {this._service.getResource (this.foosUrl + this.foo.id) .subscribe (data => this.foo = data, error => this.foo.name = 'Fout'); }}

5.5. App-component

Onze simpele AppComponent om als de root-component te fungeren:

@Component ({selector: 'app-root', template: `Spring Security Oauth - Authorization Code`}) exportklasse AppComponent {} 

En de AppModule waar we al onze componenten, diensten en routes inpakken:

@NgModule ({declaraties: [AppComponent, HomeComponent, FooComponent], importeert: [BrowserModule, HttpClientModule, RouterModule.forRoot ([{pad: '', component: HomeComponent, pathMatch: 'full'}], {onSameUrlNavigation: 'reload' })], providers: [], bootstrap: [AppComponent]}) exportklasse AppModule {} 

7. Voer de front-end uit

1. Om een ​​van onze front-end-modules uit te voeren, moeten we eerst de app bouwen:

mvn schone installatie

2. Vervolgens moeten we naar onze Angular-app-directory navigeren:

cd src / main / resources

3. Ten slotte starten we onze app:

npm start

De server start standaard op poort 4200; om de poort van een module te wijzigen, wijzigt u:

"start": "ng serveren"

in pakket.json; om het bijvoorbeeld op poort 8089 te laten draaien, voegt u toe:

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

8. Conclusie

In dit artikel hebben we geleerd hoe we onze applicatie kunnen autoriseren met OAuth2.

De volledige implementatie van deze tutorial is te vinden in het GitHub-project.