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.