Spring REST API + OAuth2 + Angular (met behulp van de Spring Security OAuth legacy-stack)

1. Overzicht

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

De applicatie die we gaan bouwen zal bestaan ​​uit vier afzonderlijke modules:

  • Autorisatieserver
  • Bronserver
  • UI impliciet - een front-end-app die de impliciete stroom gebruikt
  • UI-wachtwoord - een front-end-app die de wachtwoordstroom gebruikt

Opmerking: dit artikel maakt gebruik van het verouderde Spring OAuth-project. Voor de versie van dit artikel met de nieuwe Spring Security 5-stack, bekijk ons ​​artikel Spring REST API + OAuth2 + Angular.

Oké, laten we meteen beginnen.

2. De autorisatieserver

Laten we eerst beginnen met het opzetten van een autorisatieserver als een eenvoudige Spring Boot-applicatie.

2.1. Maven-configuratie

We zullen de volgende reeks afhankelijkheden instellen:

 org.springframework.boot spring-boot-starter-web org.springframework spring-jdbc mysql mysql-connector-java runtime org.springframework.security.oauth spring-security-oauth2 

Merk op dat we spring-jdbc en MySQL gebruiken omdat we een door JDBC ondersteunde implementatie van de token store gaan gebruiken.

2.2. @EnableAuthorizationServer

Laten we nu beginnen met het configureren van de autorisatieserver die verantwoordelijk is voor het beheren van toegangstokens:

@Configuration @EnableAuthorizationServer openbare klasse AuthServerOAuth2Config breidt AuthorizationServerConfigurerAdapter {@Autowired @Qualifier ("authenticationManagerBean") privé AuthenticationManager authenticationManager uit; @Override public void configure (AuthorizationServerSecurityConfigurer oauthServer) genereert Uitzondering {oauthServer .tokenKeyAccess ("allowAll ()") .checkTokenAccess ("isAuthenticated ()"); } @Override public void configure (ClientDetailsServiceConfigurer clients) genereert Uitzondering {clients.jdbc (dataSource ()) .withClient ("sampleClientId") .authorizedGrantTypes ("impliciet") .scopes ("read") .autoApprove (true) .en ( ) .withClient ("clientIdPassword") .secret ("geheim") .authorizedGrantTypes ("wachtwoord", "autorisatiecode", "refresh_token") .scopes ("lezen"); } @Override public void configure (AuthorizationServerEndpointsConfigurer-eindpunten) genereert Uitzondering {eindpunten .tokenStore (tokenStore ()) .authenticationManager (authenticationManager); } @Bean public TokenStore tokenStore () {retourneer nieuwe JdbcTokenStore (dataSource ()); }}

Let daar op:

  • Om de tokens te behouden, hebben we een JdbcTokenStore
  • We hebben een klant geregistreerd voor de “impliciet”Toekenningstype
  • We hebben een andere klant geregistreerd en de "wachtwoord“, “Authorisatie Code"En"refresh_token”Soorten subsidie
  • Om de “wachtwoord”Toekenningstype dat we nodig hebben om de AuthenticationManager Boon

2.3. Gegevensbronconfiguratie

Laten we vervolgens onze gegevensbron configureren voor gebruik door de JdbcTokenStore:

@Value ("classpath: schema.sql") private Resource schemaScript; @Bean openbare DataSourceInitializer dataSourceInitializer (DataSource dataSource) {DataSourceInitializer initializer = nieuwe DataSourceInitializer (); initializer.setDataSource (dataSource); initializer.setDatabasePopulator (databasePopulator ()); terugkeer initializer; } private DatabasePopulator databasePopulator () {ResourceDatabasePopulator populator = nieuwe ResourceDatabasePopulator (); populator.addScript (schemaScript); terugkeer populator; } @Bean openbare DataSource dataSource () {DriverManagerDataSource dataSource = nieuwe DriverManagerDataSource (); dataSource.setDriverClassName (env.getProperty ("jdbc.driverClassName")); dataSource.setUrl (env.getProperty ("jdbc.url")); dataSource.setUsername (env.getProperty ("jdbc.user")); dataSource.setPassword (env.getProperty ("jdbc.pass")); retourneer dataSource; }

Merk op dat, zoals we gebruiken JdbcTokenStore we moeten het databaseschema initialiseren, dus gebruikten we DataSourceInitializer - en het volgende SQL-schema:

drop table indien aanwezig oauth_client_details; tabel maken oauth_client_details (client_id VARCHAR (255) PRIMARY KEY, resource_ids VARCHAR (255), client_secret VARCHAR (255), bereik VARCHAR (255), geautoriseerde_grant_types VARCHAR (255), web_server_redirect_EGuri VARCHAR (255), autoriteiten VARCHAR (255), autoriteiten VARCHAR (255) , refresh_token_validity INTEGER, aanvullende_informatie VARCHAR (4096), autoapprove VARCHAR (255)); laat de tabel vallen als deze bestaat oauth_client_token; tabel maken oauth_client_token (token_id VARCHAR (255), token LONG VARBINARY, authentication_id VARCHAR (255) PRIMAIRE SLEUTEL, gebruikersnaam VARCHAR (255), client_id VARCHAR (255)); laat de tabel vallen als deze bestaat oauth_access_token; tabel maken oauth_access_token (token_id VARCHAR (255), token LONG VARBINARY, authentication_id VARCHAR (255) PRIMAIRE SLEUTEL, gebruikersnaam VARCHAR (255), client_id VARCHAR (255), authenticatie LONG VARBINARY, refresh_token VARCHAR (255)); laat de tabel vallen als deze bestaat oauth_refresh_token; maak een tabel oauth_refresh_token (token_id VARCHAR (255), token LONG VARBINARY, authentication LONG VARBINARY); drop table indien aanwezig oauth_code; maak een tabel oauth_code (code VARCHAR (255), authenticatie LONG VARBINARY); drop-table als deze bestaat oauth_approvals; tabel maken oauth_approvals (userId VARCHAR (255), clientId VARCHAR (255), scope VARCHAR (255), status VARCHAR (10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP); drop table indien aanwezig ClientDetails; maak tabel ClientDetails (appId VARCHAR (255) PRIMARY KEY, resourceIds VARCHAR (255), appSecret VARCHAR (255), bereik VARCHAR (255), grantTypes VARCHAR (255), redirectUrl VARCHAR (255), autoriteiten VARCHEGAR (255), access_token_validity , refresh_token_validity INTEGER, aanvullende informatie VARCHAR (4096), autoApproveScopes VARCHAR (255));

Merk op dat we de expliciete DatabasePopulator Boon - we zouden gewoon een schema.sql - waar Spring Boot standaard gebruik van maakt.

2.4. Beveiligingsconfiguratie

Laten we tot slot de autorisatieserver beveiligen.

Wanneer de clienttoepassing een toegangstoken moet aanschaffen, gebeurt dit na een eenvoudig aanmeldingsproces op basis van een formulier:

@Configuration openbare klasse ServerSecurityConfig breidt WebSecurityConfigurerAdapter uit {@Override beschermde ongeldige configuratie (AuthenticationManagerBuilder auth) genereert Uitzondering {auth.inMemoryAuthentication () .withUser ("john"). Wachtwoord ("123"). Rollen ("USER"); } @Override @Bean openbare AuthenticationManager authenticationManagerBean () genereert Uitzondering {return super.authenticationManagerBean (); } @Override protected void configure (HttpSecurity http) genereert uitzondering {http.authorizeRequests () .antMatchers ("/ login"). AllowAll () .anyRequest (). Authenticated (). En () .formLogin (). AllowAll () ; }}

Een korte opmerking hier is dat de aanmeldingsconfiguratie van het formulier is niet nodig voor de wachtwoordstroom - alleen voor de impliciete stroom - dus u kunt deze mogelijk overslaan, afhankelijk van de OAuth2-stroom die u gebruikt.

3. De bronserver

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

3.1. Maven-configuratie

Onze Resource Server-configuratie is hetzelfde als de vorige configuratie van de Authorization Server-applicatie.

3.2. Token Store-configuratie

Vervolgens zullen we onze TokenStore om toegang te krijgen tot dezelfde database die de autorisatieserver gebruikt om toegangstokens op te slaan:

@Autowired privéomgeving env; @Bean openbare DataSource dataSource () {DriverManagerDataSource dataSource = nieuwe DriverManagerDataSource (); dataSource.setDriverClassName (env.getProperty ("jdbc.driverClassName")); dataSource.setUrl (env.getProperty ("jdbc.url")); dataSource.setUsername (env.getProperty ("jdbc.user")); dataSource.setPassword (env.getProperty ("jdbc.pass")); retourneer dataSource; } @Bean public TokenStore tokenStore () {retourneer nieuwe JdbcTokenStore (dataSource ()); }

Merk op dat voor deze eenvoudige implementatie, we delen de door SQL ondersteunde token store ook al zijn de autorisatie- en bronservers afzonderlijke applicaties.

De reden is natuurlijk dat de Resource Server dat moet kunnen controleer de geldigheid van de toegangstokens uitgegeven door de autorisatieserver.

3.3. Remote Token Service

In plaats van een TokenStore in onze Resource Server kunnen we RemoteTokeServices:

@Primary @Bean openbare RemoteTokenServices tokenService () {RemoteTokenServices tokenService = nieuwe RemoteTokenServices (); tokenService.setCheckTokenEndpointUrl ("// localhost: 8080 / spring-security-oauth-server / oauth / check_token"); tokenService.setClientId ("fooClientIdPassword"); tokenService.setClientSecret ("geheim"); retourneer tokenService; }

Let daar op:

  • Dit RemoteTokenService zal gebruiken CheckTokenEndPoint op autorisatieserver om AccessToken te valideren en te verkrijgen Authenticatie bezwaar ervan.
  • Het is te vinden op AuthorizationServerBaseURL + "/ oauth / check_token
  • De autorisatieserver kan elk TokenStore-type [JdbcTokenStore, JwtTokenStore,…] - dit heeft geen invloed op de RemoteTokenService of Bronserver.

3.4. Een voorbeeldcontroller

Laten we vervolgens een eenvoudige controller implementeren die een Foo bron:

@Controller openbare klasse FooController {@PreAuthorize ("# oauth2.hasScope ('read')") @RequestMapping (method = RequestMethod.GET, value = "/ foos / {id}") @ResponseBody openbare Foo findById (@PathVariable lang id) {retourneer nieuwe Foo (Long.parseLong (randomNumeric (2)), randomAlphabetic (4)); }}

Merk op hoe de cliënt het "lezen" bereik om toegang te krijgen tot deze bron.

We moeten ook globale methodebeveiliging inschakelen en configureren MethodSecurityExpressionHandler:

@Configuration @EnableResourceServer @EnableGlobalMethodSecurity (prePostEnabled = true) openbare klasse OAuth2ResourceServerConfig breidt GlobalMethodSecurityConfiguration uit {@Override beschermde MethodSecurityExpressionHandler createExpressionHandler () {return new OAuth2Methler () {return new OAuth (); }}

En hier is onze basis Foo Bron:

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

3.5. Webconfiguratie

Laten we tot slot een zeer eenvoudige webconfiguratie voor de API opzetten:

@Configuration @EnableWebMvc @ComponentScan ({"org.baeldung.web.controller"}) openbare klasse ResourceWebConfig implementeert WebMvcConfigurer {}

4. Front End - Instellingen

We gaan nu kijken naar een eenvoudige front-end Angular-implementatie voor de klant.

Ten eerste gebruiken we Angular CLI om onze front-end-modules te genereren en te beheren.

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

Vervolgens moeten we de frontend-maven-plugin om ons Angular-project te bouwen met behulp van 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

Merk op dat we twee front-end modules hebben: een voor wachtwoordstroom en de andere voor impliciete stroom.

In de volgende secties bespreken we de Angular-app-logica voor elke module.

5. Wachtwoordstroom met Angular

We gaan hier de OAuth2-wachtwoordstroom gebruiken - en daarom dit is slechts een proof of concept, geen applicatie die klaar is voor productie. U zult merken dat de inloggegevens van de klant zichtbaar zijn voor de front-end - iets dat we in een toekomstig artikel zullen bespreken.

Onze use case is eenvoudig: zodra een gebruiker zijn inloggegevens heeft opgegeven, gebruikt de front-endclient deze om een ​​toegangstoken van de autorisatieserver te verkrijgen.

5.1. App Service

Laten we beginnen met onze AppService - gevestigd in app.service.ts - die de logica bevat voor serverinteracties:

  • gainAccessToken (): om toegangstoken te verkrijgen op basis van gebruikersreferenties
  • 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
export class Foo {constructor (public id: number, public name: string) {}} @Injectable () export class AppService {constructor (private _router: Router, private _http: Http) {} getAccessToken (loginData) {let params = new URLSearchParams (); params.append ('gebruikersnaam', loginData.username); params.append ('wachtwoord', loginData.password); params.append ('grant_type', 'wachtwoord'); params.append ('client_id', 'fooClientIdPassword'); let headers = new Headers ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Basic' + btoa ("fooClientIdPassword: secret")}); let options = new RequestOptions ({headers: headers}); this._http.post ('// localhost: 8081 / spring-security-oauth-server / oauth / token', params.toString (), opties) .map (res => res.json ()) .subscribe (data => this.saveToken (data), err => alert ('Ongeldige referenties')); } saveToken (token) {var expireDate = nieuwe datum (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate); this._router.navigate (['/']); } getResource (resourceUrl): Observable {var headers = new Headers ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + Cookie.get ('toegangstoken')}); var options = nieuwe RequestOptions ({headers: headers}); retourneer this._http.get (resourceUrl, options) .map ((res: Response) => res.json ()) .catch ((error: any) => Observable.throw (error.json (). error || 'Serverfout')); } checkCredentials () {if (! Cookie.check ('access_token')) {this._router.navigate (['/ login']); }} logout () {Cookie.delete ('access_token'); this._router.navigate (['/ login']); }}

Let daar op:

  • Om een ​​toegangstoken te krijgen, sturen we een POST naar de "/ oauth / token”Eindpunt
  • We gebruiken de clientreferenties en basisverificatie om dit eindpunt te bereiken
  • We sturen vervolgens de gebruikersreferenties samen met de client-ID en de URL van de URL-gecodeerde parameters voor toekenningstype
  • 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 aanvallen en kwetsbaarheden van het type cross-site request forgery (CSRF).

5.2. Aanmeldingscomponent

Laten we vervolgens eens kijken naar onze LoginComponent die verantwoordelijk is voor het inlogformulier:

@Component ({selector: 'login-formulier', providers: [AppService], sjabloon: `Login`}) exportklasse LoginComponent {openbare loginData = {gebruikersnaam:" ", wachtwoord:" "}; constructor (private _service: AppService) {} login () {this._service.obtainAccessToken (this.loginData); }

5.3. Home Component

Vervolgens onze HomeComponent die verantwoordelijk is voor het weergeven en manipuleren van onze startpagina:

@Component ({selector: 'home-header', providers: [AppService], sjabloon: `Welkom !! Uitloggen`}) exportklasse HomeComponent {constructor (private _service: AppService) {} ngOnInit () {this._service.checkCredentials (); } logout () {this._service.logout (); }}

5.4. 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: 8082 / spring-security-oauth-resource / 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: ``}) exportklasse AppComponent {}

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

@NgModule ({declaraties: [AppComponent, HomeComponent, LoginComponent, FooComponent], importeert: [BrowserModule, FormsModule, HttpModule, RouterModule.forRoot ([{pad: '', component: HomeComponent}, {pad: 'login', component: LoginComponent}])], providers: [], bootstrap: [AppComponent]}) exportklasse AppModule {}

6. Impliciete stroom

Vervolgens zullen we ons concentreren op de Impliciete Flow-module.

6.1. App Service

Evenzo zullen we beginnen met onze service, maar deze keer zullen we bibliotheek angular-oauth2-oidc gebruiken in plaats van zelf een toegangstoken te verkrijgen:

@Injectable () exportklasse AppService {constructor (private _router: Router, private _http: Http, private oauthService: OAuthService) {this.oauthService.loginUrl = '// localhost: 8081 / spring-security-oauth-server / oauth / autoriseren '; this.oauthService.redirectUri = '// localhost: 8086 /'; this.oauthService.clientId = "sampleClientId"; this.oauthService.scope = "lees schrijf foo bar"; this.oauthService.setStorage (sessionStorage); this.oauthService.tryLogin ({}); } getAccessToken () {this.oauthService.initImplicitFlow (); } getResource (resourceUrl): Observable {var headers = new Headers ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + this.oauthService .getAccessToken ()}); var options = nieuwe RequestOptions ({headers: headers}); retourneer this._http.get (resourceUrl, options) .map ((res: Response) => res.json ()) .catch ((error: any) => Observable.throw (error.json (). error || 'Serverfout')); } isLoggedIn () {if (this.oauthService.getAccessToken () === null) {return false; } retourneren waar; } logout () {this.oauthService.logOut (); location.reload (); }}

Merk op hoe we, na het verkrijgen van het toegangstoken, het gebruiken via het Autorisatie header telkens we beschermde bronnen gebruiken vanuit de Resource Server.

6.2. Home Component

Onze HomeComponent om onze eenvoudige startpagina te beheren:

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

`}) exportklasse HomeComponent {public isLoggedIn = false; constructor (private _service: AppService) {} ngOnInit () {this.isLoggedIn = this._service.isLoggedIn (); } login () {this._service.obtainAccessToken (); } logout () {this._service.logout (); }}

6.3. Foo-component

Onze FooComponent is precies hetzelfde als in de wachtwoordstroommodule.

6.4. App-module

Eindelijk, onze AppModule:

@NgModule ({declaraties: [AppComponent, HomeComponent, FooComponent], importeert: [BrowserModule, FormsModule, HttpModule, OAuthModule.forRoot (), RouterModule.forRoot ([{pad: '', component: HomeComponent}])], 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 willekeurige module te wijzigen, wijzigt u de

"start": "ng serveren"

in package.json om het bijvoorbeeld op poort 8086 te laten draaien:

"start": "ng serve --port 8086"

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.