Spring REST API + OAuth2 + Angular (ved hjelp av Spring Security OAuth legacy stack)

1. Oversikt

I denne opplæringen vil vi sikre oss en REST API med OAuth og konsumere den fra en enkel Angular-klient.

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

  • Autorisasjonsserver
  • Ressursserver
  • UI implicit - en frontend-app som bruker Implicit Flow
  • UI passord - en frontend-app som bruker Password Flow

Merk: denne artikkelen bruker Spring OAuth arvprosjekt. For versjonen av denne artikkelen som bruker den nye Spring Security 5-stakken, kan du se på artikkelen vår REST API + OAuth2 + Angular.

Ok, la oss hoppe rett inn.

2. Autorisasjonsserveren

Først, la oss begynne å konfigurere en autorisasjonsserver som en enkel Spring Boot-applikasjon.

2.1. Maven-konfigurasjon

Vi setter opp følgende sett av avhengigheter:

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

Merk at vi bruker spring-jdbc og MySQL fordi vi skal bruke en JDBC-støttet implementering av tokenbutikken.

2.2. @EnableAuthorizationServer

La oss begynne å konfigurere autorisasjonsserveren som er ansvarlig for å administrere tilgangstokener:

@Configuration @EnableAuthorizationServer offentlig klasse AuthServerOAuth2Config utvider AuthorizationServerConfigurerAdapter {@Autowired @Qualifier ("authenticationManagerBean") privat AuthenticationManager authenticationManager; @Override public void configure (AuthorizationServerSecurityConfigurer oauthServer) kaster unntak {oauthServer .tokenKeyAccess ("permitAll ()") .checkTokenAccess ("isAuthenticated ()"); } @Override public void configure (ClientDetailsServiceConfigurer clients) kaster unntak {clients.jdbc (dataSource ()) .withClient ("sampleClientId") .authorizedGrantTypes ("implicit") .scopes ("read") .autoApprove (true) .and ( ) .withClient ("clientIdPassword") .secret ("secret") .authorizedGrantTypes ("password", "authorisation_code", "refresh_token") .scopes ("read"); } @ Override public void configure (AuthorizationServerEndpointsConfigurer endpoints) kaster Unntak {endpoints .tokenStore (tokenStore ()) .authenticationManager (authenticationManager); } @Bean offentlig TokenStore tokenStore () {returner ny JdbcTokenStore (dataSource ()); }}

Noter det:

  • For å vedvare tokens brukte vi a JdbcTokenStore
  • Vi registrerte en klient for “implisitt”Tilskuddstype
  • Vi registrerte en annen klient og autorisertepassord“, “Godkjennelseskoden”Og”refresh_token”Tilskuddstyper
  • For å bruke “passord”Tilskuddstype vi trenger for å koble inn og bruke AuthenticationManager bønne

2.3. Datakildekonfigurasjon

Deretter la oss konfigurere datakilden vår som skal brukes av JdbcTokenStore:

@Value ("classpath: schema.sql") private Ressurs schemaScript; @Bean public DataSourceInitializer dataSourceInitializer (DataSource dataSource) {DataSourceInitializer initializer = new DataSourceInitializer (); initializer.setDataSource (dataSource); initializer.setDatabasePopulator (databasePopulator ()); initialisering for retur; } privat DatabasePopulator databasePopulator () {ResourceDatabasePopulator populator = ny ResourceDatabasePopulator (); populator.addScript (schemaScript); returpopulator; } @Bean public DataSource dataSource () {DriverManagerDataSource dataSource = new 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")); returner datakilde; }

Merk at, som vi bruker JdbcTokenStore vi trenger å initialisere databaseskjema, så vi brukte DataSourceInitializer - og følgende SQL-skjema:

slipp tabellen hvis det finnes oauth_client_details; opprett tabell oauth_client_details (client_id VARCHAR (255) PRIMARY KEY, resource_ids VARCHAR (255), client_secret VARCHAR (255), scope VARCHAR (255), autoriserte_grant_types VARCHAR (255), web_server_redirect_uri VARCHAR (25), 25 VARCHAR (25)) , refresh_token_validity INTEGER, tilleggsinformasjon VARCHAR (4096), autoapprove VARCHAR (255)); slipp tabellen hvis det eksisterer oauth_client_token; opprett tabell oauth_client_token (token_id VARCHAR (255), token LONG VARBINARY, authentication_id VARCHAR (255) PRIMARY KEY, user_name VARCHAR (255), client_id VARCHAR (255)); slipp tabellen hvis det eksisterer oauth_access_token; opprett tabell oauth_access_token (token_id VARCHAR (255), token LONG VARBINARY, authentication_id VARCHAR (255) PRIMARY KEY, user_name VARCHAR (255), client_id VARCHAR (255), authentication LONG VARBINARY, refresh_token VARCHAR) slipp tabell hvis det eksisterer oauth_refresh_token; opprett tabell oauth_refresh_token (token_id VARCHAR (255), token LANG VARBINÆR, autentisering LANG VARBINÆR); slipp tabell hvis det eksisterer oauth_code; lage tabell oauth_code (kode VARCHAR (255), autentisering LANG VARBINÆR); slipp tabellen hvis det finnes oauth_approvals; lage tabell oauth_approvals (userId VARCHAR (255), clientId VARCHAR (255), scope VARCHAR (255), status VARCHAR (10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP); slipp tabell hvis det finnes ClientDetails; opprett tabell ClientDetails (appId VARCHAR (255) PRIMARY KEY, resourceIds VARCHAR (255), appSecret VARCHAR (255), scope VARCHAR (255), grantTypes VARCHAR (255), redirectUrl VARCHAR (255), myndigheter VARCHAR (255), access , refresh_token_validity INTEGER, tilleggsinformasjon VARCHAR (4096), autoApproveScopes VARCHAR (255));

Merk at vi ikke nødvendigvis trenger det eksplisitte DatabasePopulator bønne - vi kunne bare bruke en schema.sql - som Spring Boot bruker som standard.

2.4. Sikkerhetskonfigurasjon

Til slutt, la oss sikre autorisasjonsserveren.

Når klientapplikasjonen trenger å skaffe seg et Access Token, vil det gjøre det etter en enkel skjemainnloggingsdrevet autentiseringsprosess:

@Configuration offentlig klasse ServerSecurityConfig utvider WebSecurityConfigurerAdapter {@Override beskyttet tomkonfigurasjon (AuthenticationManagerBuilder auth) kaster Unntak {auth.inMemoryAuthentication () .withUser ("john"). Passord ("123"). Roller ("USER"); } @Override @Bean offentlig AuthenticationManager authenticationManagerBean () kaster Unntak {return super.authenticationManagerBean (); } @ Override beskyttet ugyldig konfigurasjon (HttpSecurity http) kaster Unntak {http.authorizeRequests () .antMatchers ("/ login"). PermitAll () .anyRequest (). Autentisert () .and () .formLogin (). PermitAll () ; }}

En rask merknad her er at konfigurering av skjemainnlogging er ikke nødvendig for passordflyten - bare for den implisitte strømmen - så du kan kanskje hoppe over den, avhengig av hvilken OAuth2-strøm du bruker.

3. Ressursserveren

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

3.1. Maven-konfigurasjon

Vår ressursserverkonfigurasjon er den samme som den forrige konfigurasjon av programmet for autorisasjonsserver.

3.2. Token Store-konfigurasjon

Deretter konfigurerer vi vår TokenStore for å få tilgang til den samme databasen som autorisasjonsserveren bruker til å lagre tilgangstokener:

@Autowired private Environment env; @Bean public DataSource dataSource () {DriverManagerDataSource dataSource = new 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")); returner datakilde; } @Bean offentlig TokenStore tokenStore () {returner ny JdbcTokenStore (dataSource ()); }

Merk at for denne enkle implementeringen, vi deler SQL-støttet tokenbutikk selv om autorisasjons- og ressursserverne er separate applikasjoner.

Årsaken er selvfølgelig at ressursserveren må kunne sjekk gyldigheten til tilgangstokenene utstedt av autorisasjonsserveren.

3.3. Remote Token Service

I stedet for å bruke en TokenStore i vår ressursserver kan vi bruke RemoteTokeServices:

@Primary @Bean offentlig RemoteTokenServices tokenService () {RemoteTokenServices tokenService = ny RemoteTokenServices (); tokenService.setCheckTokenEndpointUrl ("// localhost: 8080 / spring-security-oauth-server / oauth / check_token"); tokenService.setClientId ("fooClientIdPassword"); tokenService.setClientSecret ("hemmelig"); retur tokenService; }

Noter det:

  • Dette RemoteTokenService vil bruke CheckTokenEndPoint på Authorization Server for å validere AccessToken og få Godkjenning objekt fra den.
  • Du finner dem på AuthorizationServerBaseURL + ”/ oauth / sjekk_token
  • Autorisasjonsserveren kan bruke hvilken som helst TokenStore-type [JdbcTokenStore, JwtTokenStore, ...] - dette vil ikke påvirke RemoteTokenService eller Ressursserver.

3.4. En prøvekontroller

La oss deretter implementere en enkel kontroller som avslører en Foo ressurs:

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

Legg merke til hvordan klienten trenger "lese" muligheten til å få tilgang til denne ressursen.

Vi må også aktivere global metodesikkerhet og konfigurere MethodSecurityExpressionHandler:

@Configuration @EnableResourceServer @EnableGlobalMethodSecurity (prePostEnabled = true) offentlig klasse OAuth2ResourceServerConfig utvider GlobalMethodSecurityConfiguration {@Override-beskyttet MethodSecurityExpressionHandler createExpressionHandler () {returner ny OAuth2M; }}

Og her er vår grunnleggende Foo Ressurs:

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

3.5. Nettkonfigurasjon

Til slutt, la oss sette opp en veldig grunnleggende nettkonfigurasjon for API:

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

4. Front End - Oppsett

Vi skal nå se på en enkel front-end Angular implementering for klienten.

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

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

Deretter 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

Merk at vi har to frontend-moduler - en for passordflyt og den andre for implisitt flyt.

I de følgende avsnittene vil vi diskutere Angular-applogikken for hver modul.

5. Passordflyt ved bruk av vinkel

Vi kommer til å bruke OAuth2-passordflyten her - det er derfor dette er bare et bevis på konseptet, ikke en produksjonsklar applikasjon. Du vil legge merke til at klientopplysningene blir utsatt for frontend - noe som vi vil ta opp i en fremtidig artikkel.

Brukssaken vår er enkel: Når en bruker oppgir legitimasjonen sin, bruker front-end-klienten dem til å skaffe seg et Access Token fra autorisasjonsserveren.

5.1. App-tjeneste

La oss starte med vår AppService - ligger ved app.service.ts - som inneholder logikken for serverinteraksjoner:

  • obtainAccessToken (): for å skaffe Access token gitt brukerlegitimasjon
  • 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
eksportklasse Foo {constructor (offentlig id: nummer, offentlig navn: streng) {}} @Injectable () eksportklasse AppService {constructor (privat _router: Ruter, privat _http: Http) {} fåAccessToken (loginData) {la params = ny URLSearchParams (); params.append ('brukernavn', loginData.username); params.append ('passord', loginData.password); params.append ('grant_type', 'password'); params.append ('client_id', 'fooClientIdPassword'); la overskrifter = nye overskrifter ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Basic' + btoa ("fooClientIdPassword: secret")}); la alternativer = nye RequestOptions ({headers: headers}); this._http.post ('// localhost: 8081 / spring-security-oauth-server / oauth / token', params.toString (), options) .map (res => res.json ()). 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); this._router.navigate (['/']); } getResource (resourceUrl): Observerbar {var headers = new Headers ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + Cookie.get ('tilgangstoken')}); var options = new RequestOptions ({headers: headers}); returner dette._http.get (resourceUrl, options) .map ((res: Response) => res.json ()) .catch ((error: any) => Observable.throw (error.json (). error || 'Serverfeil')); } checkCredentials () {if (! Cookie.check ('access_token')) {this._router.navigate (['/ login']); }} avlogging () {Cookie.delete ('access_token'); this._router.navigate (['/ login']); }}

Noter det:

  • For å få et Access Token sender vi en POST til "/ oauth / token”Endepunkt
  • Vi bruker klientlegitimasjonen og Basic Auth for å nå dette endepunktet
  • Vi sender deretter brukerlegitimasjonen sammen med klient-ID og tilskuddstypeparametere URL-kodet
  • Etter at vi har fått tilgangstoken - 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 angrep og sårbarheter av type forfalskning på tvers av nettsteder (CSRF).

5.2. Påloggingskomponent

La oss ta en titt på vår Logg inn Komponent som er ansvarlig for påloggingsskjemaet:

@Component ({selector: 'login-form', providers: [AppService], mal: `Login`}) eksportklasse LoginComponent {public loginData = {brukernavn:" ", passord:" "}; constructor (private _service: AppService) {} login () {this._service.obtainAccessToken (this.loginData); }

5.3. Hjemmekomponent

Neste, vår Hjemmekomponent som er ansvarlig for å vise og manipulere hjemmesiden vår:

@Component ({selector: 'home-header', providers: [AppService], template: `Welcome !! Logout`}) eksportklasse HomeComponent {constructor (private _service: AppService) {} ngOnInit () {this._service.checkCredentials (); } logg ut () {this._service.logout (); }}

5.4. 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: 8082 / spring-security-oauth-resource / foos /'; constructor (private _service: AppService) {} getFoo () {this._service.getResource (this.foosUrl + this.foo.id). abonner (data => this.foo = data, error => this.foo.name = 'Feil'); }}

5.5. App-komponent

Vår enkle AppComponent å fungere som rotkomponent:

@Component ({selector: 'app-root', mal: ``}) eksportklasse AppComponent {}

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

@NgModule ({erklæringer: [AppComponent, HomeComponent, LoginComponent, FooComponent], importerer: [BrowserModule, FormsModule, HttpModule, RouterModule.forRoot ([{path: '', component: HomeComponent}, {path: 'login', component: LoginComponent}])], leverandører: [], bootstrap: [AppComponent]}) eksportklasse AppModule {}

6. Implisitt flyt

Deretter vil vi fokusere på Implicit Flow-modulen.

6.1. App-tjeneste

På samme måte begynner vi med tjenesten vår, men denne gangen bruker vi biblioteket angular-oauth2-oidc i stedet for å få tilgangstoken selv:

@Injectable () eksportklasse AppService {konstruktør (privat _ruter: Ruter, privat _http: Http, privat oauthService: OAuthService) {this.oauthService.loginUrl = '// localhost: 8081 / spring-security-oauth-server / oauth / authorize '; this.oauthService.redirectUri = '// localhost: 8086 /'; this.oauthService.clientId = "sampleClientId"; this.oauthService.scope = "les skriv foo bar"; this.oauthService.setStorage (sessionStorage); this.oauthService.tryLogin ({}); } fåAccessToken () {this.oauthService.initImplicitFlow (); } getResource (resourceUrl): Observerbar {var headers = new Headers ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + this.oauthService .getAccessToken ()}); var options = new RequestOptions ({headers: headers}); returner dette._http.get (resourceUrl, options) .map ((res: Response) => res.json ()) .catch ((error: any) => Observable.throw (error.json (). error || 'Serverfeil')); } isLoggedIn () {if (this.oauthService.getAccessToken () === null) {return false; } returner sant; } avlogging () {this.oauthService.logOut (); location.reload (); }}

Legg merke til hvordan vi, etter å ha mottatt Access Token, bruker det via Autorisasjon header når vi bruker beskyttede ressurser fra ressursserveren.

6.2. Hjemmekomponent

Våre Hjemmekomponent for å håndtere den enkle hjemmesiden vår:

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

`}) eksportklasse HomeComponent {public isLoggedIn = false; konstruktør (privat _tjeneste: AppService) {} ngOnInit () {this.isLoggedIn = this._service.isLoggedIn (); } pålogging () {this._service.obtainAccessToken (); } logg ut () {this._service.logout (); }}

6.3. Foo-komponent

Våre FooComponent er nøyaktig det samme som i passordflytmodulen.

6.4. App-modul

Til slutt, vår AppModule:

@NgModule ({erklæringer: [AppComponent, HomeComponent, FooComponent], importer: [BrowserModule, FormsModule, HttpModule, OAuthModule.forRoot (), RouterModule.forRoot ([{path: '', komponent: HomeComponent}])], 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 8086 for eksempel:

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

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.


$config[zx-auto] not found$config[zx-overlay] not found