Spring REST API + OAuth2 + Angular

1. Übersicht

In diesem Tutorial sichern wir eine REST-API mit OAuth2 und verwenden sie von einem einfachen Angular-Client.

Die Anwendung, die wir erstellen werden, besteht aus drei separaten Modulen:

  • Autorisierungsserver
  • Ressourcenserver
  • UI-Autorisierungscode: Eine Front-End-Anwendung, die den Autorisierungscode-Fluss verwendet

Wir werden den OAuth-Stack in Spring Security 5 verwenden. Wenn Sie den OAuth-Legacy-Stack von Spring Security verwenden möchten, lesen Sie den vorherigen Artikel: Spring REST API + OAuth2 + Angular (Verwenden des Spring Security OAuth Legacy Stack).

Lass uns gleich hineinspringen.

2. Der OAuth2 Authorization Server (AS)

Einfach ausgedrückt ist ein Autorisierungsserver eine Anwendung, die Token für die Autorisierung ausstellt.

Zuvor bot der Spring Security OAuth-Stack die Möglichkeit, einen Autorisierungsserver als Spring-Anwendung einzurichten. Das Projekt wurde jedoch abgelehnt, vor allem, weil OAuth ein offener Standard für viele etablierte Anbieter wie Okta, Keycloak und ForgeRock ist, um nur einige zu nennen.

Von diesen werden wir Keycloak verwenden. Es handelt sich um einen Open-Source-Identitäts- und Zugriffsverwaltungsserver, der von Red Hat verwaltet und von JBoss in Java entwickelt wurde. Es unterstützt nicht nur OAuth2, sondern auch andere Standardprotokolle wie OpenID Connect und SAML.

In diesem Tutorial richten wir einen eingebetteten Keycloak-Server in einer Spring Boot-App ein.

3. Der Ressourcenserver (RS)

Lassen Sie uns nun den Ressourcenserver diskutieren. Dies ist im Wesentlichen die REST-API, die wir letztendlich nutzen möchten.

3.1. Maven-Konfiguration

Der POM unseres Ressourcenservers entspricht weitgehend dem des vorherigen POM des Autorisierungsservers, ohne den Keycloak-Teil und mit einer zusätzlichen Spring-Boot-Starter-Oauth2-Resource-Server- Abhängigkeit :

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

3.2. Sicherheitskonfiguration

Da wir Spring Boot verwenden, können wir die minimal erforderliche Konfiguration mithilfe der Boot-Eigenschaften definieren.

Wir machen das in einer application.yml- Datei:

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

Hier haben wir angegeben, dass wir JWT-Token für die Autorisierung verwenden.

Die Eigenschaft jwk-set-uri verweist auf den URI, der den öffentlichen Schlüssel enthält, damit unser Ressourcenserver die Integrität der Token überprüfen kann.

Die Eigenschaft issuer-uri stellt eine zusätzliche Sicherheitsmaßnahme dar, um den Aussteller der Token (den Autorisierungsserver) zu validieren. Das Hinzufügen dieser Eigenschaft erfordert jedoch auch, dass der Autorisierungsserver ausgeführt wird, bevor die Resource Server-Anwendung gestartet werden kann.

Als Nächstes richten wir eine Sicherheitskonfiguration für die API ein, um Endpunkte zu sichern :

@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { 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(); } }

Wie wir sehen können, erlauben wir für unsere GET-Methoden nur Anforderungen mit Lesebereich . Für die POST-Methode muss der Anforderer zusätzlich zum Lesen über eine Schreibberechtigung verfügen . Für jeden anderen Endpunkt sollte die Anforderung jedoch nur bei einem beliebigen Benutzer authentifiziert werden.

Auch die oauth2ResourceServer () Methode gibt , dass dies ein Ressourcen - Server, mit jwt () - formatierte Token.

Ein weiterer Punkt, der hier zu beachten ist, ist die Verwendung der Methode cors () , um Access-Control-Header für die Anforderungen zuzulassen. Dies ist besonders wichtig, da es sich um einen Angular-Client handelt und unsere Anfragen von einer anderen Ursprungs-URL stammen.

3.4. Das Modell und das Repository

Als nächstes definieren wir eine javax.persistence.Entity für unser Modell Foo :

@Entity public class Foo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // constructor, getters and setters }

Dann brauchen wir ein Repository von Foo s. Wir werden das PagingAndSortingRepository von Spring verwenden :

public interface IFooRepository extends PagingAndSortingRepository { } 

3.4. Der Service und die Implementierung

Danach definieren und implementieren wir einen einfachen Service für unsere API:

public interface IFooService { Optional findById(Long id); Foo save(Foo foo); Iterable findAll(); } @Service public class FooServiceImpl implements IFooService { private IFooRepository fooRepository; public FooServiceImpl(IFooRepository fooRepository) { this.fooRepository = fooRepository; } @Override public Optional 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. Ein Beispiel-Controller

Implementieren wir nun einen einfachen Controller, der unsere Foo- Ressource über ein DTO verfügbar macht:

@RestController @RequestMapping(value = "/api/foos") public class FooController { private IFooService fooService; public FooController(IFooService fooService) { this.fooService = fooService; } @CrossOrigin(origins = "//localhost:8089") @GetMapping(value = "/{id}") public FooDto findOne(@PathVariable Long id) { Foo entity = fooService.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); return convertToDto(entity); } @GetMapping public Collection findAll() { Iterable foos = this.fooService.findAll(); List fooDtos = new ArrayList(); foos.forEach(p -> fooDtos.add(convertToDto(p))); return fooDtos; } protected FooDto convertToDto(Foo entity) { FooDto dto = new FooDto(entity.getId(), entity.getName()); return dto; } }

Beachten Sie die Verwendung von @CrossOrigin oben; Dies ist die Konfiguration auf Controller-Ebene, die wir benötigen, damit CORS von unserer Angular App unter der angegebenen URL ausgeführt werden kann.

Hier ist unser FooDto :

public class FooDto { private long id; private String name; }

4. Frontend - Setup

Wir werden uns nun eine einfache Front-End-Angular-Implementierung für den Client ansehen, die auf unsere REST-API zugreifen wird.

Wir werden zuerst Angular CLI verwenden, um unsere Front-End-Module zu generieren und zu verwalten.

Zuerst installieren wir Node und npm , da Angular CLI ein npm-Tool ist.

Dann müssen wir das Frontend-Maven-Plugin verwenden , um unser Angular-Projekt mit Maven zu erstellen:

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

Und schließlich generieren Sie ein neues Modul mit Angular CLI:

ng new oauthApp

Im folgenden Abschnitt werden wir die Angular-App-Logik diskutieren.

5. Autorisierungscode-Fluss mit Angular

Wir werden hier den OAuth2-Autorisierungscode-Fluss verwenden.

Unser Anwendungsfall: Die Client-App fordert einen Code vom Autorisierungsserver an und erhält eine Anmeldeseite. Sobald ein Benutzer seine gültigen Anmeldeinformationen angegeben und übermittelt hat, gibt uns der Autorisierungsserver den Code. Der Front-End-Client verwendet es dann, um ein Zugriffstoken zu erhalten.

5.1. Hauptkomponente

Beginnen wir mit unserer Hauptkomponente, der HomeComponent , wo alle Aktionen beginnen:

@Component({ selector: 'home-header', providers: [AppService], template: ` Login Welcome !! Logout

` }) export class HomeComponent { public isLoggedIn = false; constructor(private _service: AppService) { } ngOnInit() { this.isLoggedIn = this._service.checkCredentials(); let 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&scope=openid%20write%20read&client_id=' + this._service.clientId + '&redirect_uri='+ this._service.redirectUri; } logout() { this._service.logout(); } }

In the beginning, when the user is not logged in, only the login button appears. Upon clicking this button, the user is navigated to the AS's authorization URL where they key in username and password. After a successful login, the user is redirected back with the authorization code, and then we retrieve the access token using this code.

5.2. App Service

Now let's look at AppService — located at app.service.ts — which contains the logic for server interactions:

  • retrieveToken(): to obtain access token using authorization code
  • saveToken(): to save our access token in a cookie using ng2-cookies library
  • getResource(): to get a Foo object from server using its ID
  • checkCredentials () : um zu überprüfen, ob der Benutzer angemeldet ist oder nicht
  • logout () : Zum Löschen des Zugriffstoken-Cookies und zum Abmelden des Benutzers
export class Foo { constructor(public id: number, public name: string) { } } @Injectable() export class AppService { public clientId = 'newClient'; public redirectUri = '//localhost:8089/'; constructor(private _http: HttpClient) { } retrieveToken(code) { let params = new URLSearchParams(); params.append('grant_type','authorization_code'); 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('Invalid Credentials')); } saveToken(token) { var expireDate = new Date().getTime() + (1000 * token.expires_in); Cookie.set("access_token", token.access_token, expireDate); console.log('Obtained Access token'); 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('access_token')}); return this._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(); } }

Bei der RetrieveToken- Methode verwenden wir unsere Client-Anmeldeinformationen und Basic Auth, um einen POST an den Endpunkt / openid-connect / token zu senden , um das Zugriffstoken abzurufen. Die Parameter werden in einem URL-codierten Format gesendet. Nachdem wir das Zugriffstoken erhalten haben, speichern wir es in einem Cookie.

Der Cookie-Speicher ist hier besonders wichtig, da wir den Cookie nur zu Speicherzwecken verwenden und den Authentifizierungsprozess nicht direkt steuern. Dies schützt vor CSRF-Angriffen (Cross-Site Request Forgery) und Sicherheitslücken.

5.3. Foo-Komponente

Schließlich unsere FooComponent , um unsere Foo-Details anzuzeigen:

@Component({ selector: 'foo-details', providers: [AppService], template: `  ID {{foo.id}} Name {{foo.name}} New Foo ` }) export class FooComponent { public foo = new Foo(1,'sample 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 = 'Error'); } }

5.5. App-Komponente

Unsere einfache AppComponent als Root-Komponente:

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

Und das AppModule, in dem wir alle unsere Komponenten, Services und Routen verpacken:

@NgModule({ declarations: [ AppComponent, HomeComponent, FooComponent ], imports: [ BrowserModule, HttpClientModule, RouterModule.forRoot([ { path: '', component: HomeComponent, pathMatch: 'full' }], {onSameUrlNavigation: 'reload'}) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } 

7. Führen Sie das Frontend aus

1. Um eines unserer Front-End-Module auszuführen, müssen wir zuerst die App erstellen:

mvn clean install

2. Dann müssen wir zu unserem Angular-App-Verzeichnis navigieren:

cd src/main/resources

3. Zum Schluss starten wir unsere App:

npm start

Der Server wird standardmäßig an Port 4200 gestartet. Um den Port eines Moduls zu ändern, ändern Sie:

"start": "ng serve"

in package.json; Fügen Sie beispielsweise Folgendes hinzu, damit es auf Port 8089 ausgeführt wird:

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

8. Fazit

In diesem Artikel haben wir gelernt, wie Sie unsere Anwendung mit OAuth2 autorisieren.

Die vollständige Implementierung dieses Tutorials finden Sie im GitHub-Projekt.