Spring REST API + OAuth2 + Angular (pomoću naslijeđenog naslaga Spring Security OAuth)

1. Pregled

U ovom uputstvu osigurat ćemo REST API pomoću OAuth-a i potrošiti ga iz jednostavnog Angular klijenta.

Aplikacija koju ćemo izraditi sastojat će se od četiri zasebna modula:

  • Autorizacijski poslužitelj
  • Resursni poslužitelj
  • UI implicitno - prednja aplikacija koja koristi Implicitni tok
  • UI lozinka - prednja aplikacija koja koristi protok lozinke

Bilješka: ovaj članak koristi nasljeđeni projekt Spring OAuth. Za verziju ovog članka koja koristi novi stog Spring Security 5, pogledajte naš članak Spring REST API + OAuth2 + Angular.

Dobro, uskočimo odmah.

2. Poslužitelj za autorizaciju

Prvo, krenimo postavljati Autorizacijski poslužitelj kao jednostavnu aplikaciju Spring Boot.

2.1. Maven konfiguracija

Postavit ćemo sljedeći skup ovisnosti:

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

Imajte na umu da koristimo spring-jdbc i MySQL jer ćemo koristiti JDBC-ovu implementaciju spremišta tokena podržanih JDBC-om.

2.2. @EnableAuthorizationServer

Počnimo sada konfigurirati autorizacijski poslužitelj odgovoran za upravljanje pristupnim tokenima:

@Configuration @EnableAuthorizationServer javna klasa AuthServerOAuth2Config proširuje AuthorizationServerConfigurerAdapter {@Autowired @Qualifier ("authenticationManagerBean") private AuthenticationManager authenticationManager; @Override javna void konfiguracija (AuthorizationServerSecurityConfigurer oauthServer) baca izuzetak {oauthServer .tokenKeyAccess ("dozvolaAll ()") .checkTokenAccess ("isAuthenticated ()"); } @Override javna void konfiguracija (ClientDetailsServiceConfigurer klijenti) baca izuzetak {clients.jdbc (dataSource ()) .withClient ("sampleClientId") .authorizedGrantTypes ("implicitno") .scopes ("read") .autoApprove (true). ) .withClient ("clientIdPassword") .secret ("secret") .authorizedGrantTypes ("lozinka", "autorizacijski kod", "refresh_token") .scopes ("read"); } @Override javna void konfiguracija (AuthorizationServerEndpointsConfigurer krajnje točke) baca izuzetak {endpoints .tokenStore (tokenStore ()) .authenticationManager (authenticationManager); } @Bean public TokenStore tokenStore () {return new JdbcTokenStore (dataSource ()); }}

Imajte na umu da:

  • Kako bismo zadržali žetone, koristili smo a JdbcTokenStore
  • Registrirali smo klijenta za „implicitno”Vrsta odobrenja
  • Registrirali smo drugog klijenta i odobrili „zaporka“, “autorizacijski_kod"I"refresh_token”Vrste bespovratnih sredstava
  • Da biste koristilizaporka”Vrsta odobrenja koju moramo povezati i koristiti AuthenticationManager grah

2.3. Konfiguracija izvora podataka

Dalje, konfigurirajmo naš izvor podataka da ga koristi JdbcTokenStore:

@Value ("classpath: schema.sql") private Resource schemaScript; @Bean javni DataSourceInitializer dataSourceInitializer (DataSource dataSource) {DataSourceInitializer initializer = novi DataSourceInitializer (); inicijalizator.setDataSource (dataSource); Initializer.setDatabasePopulator (databasePopulator ()); povrat inicijalizatora; } privatni DatabasePopulator databasePopulator () {ResourceDatabasePopulator populator = novi ResourceDatabasePopulator (); populator.addScript (schemaScript); povratni populator; } @Bean public DataSource dataSource () {DriverManagerDataSource dataSource = novi 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")); vratiti dataSource; }

Imajte na umu da, kao što koristimo JdbcTokenStore moramo inicijalizirati shemu baze podataka, pa smo koristili DataSourceInitializer - i sljedeću SQL shemu:

ispustite tablicu ako postoji oauth_client_details; izraditi tablicu oauth_client_details (client_id VARCHAR (255) PRIMARNI KLJUČ, resource_ids VARCHAR (255), client_secret VARCHAR (255), opseg VARCHAR (255), odobreni_grant_tipovi VARCHAR (255), web_server_redirect_uri VARCHAR (255) INTEGER_CHAR, VARCHAR (255), VARCHAR, 255AR, VARCHAR (255) , refresh_token_validity INTEGER, dodatne_informacije VARCHAR (4096), automatski odobri VARCHAR (255)); ispustiti tablicu ako postoji oauth_client_token; stvoriti tablicu oauth_client_token (token_id VARCHAR (255), token LONG VARBINARY, authentication_id VARCHAR (255) PRIMARNI KLJUČ, korisničko ime VARCHAR (255), client_id VARCHAR (255)); ispustiti tablicu ako postoji oauth_access_token; stvoriti tablicu oauth_access_token (token_id VARCHAR (255), token LONG VARBINARY, authentication_id VARCHAR (255) PRIMARNI KLJUČ, korisničko ime VARCHAR (255), client_id VARCHAR (255), autentifikacija LONG VARBINARY, refresh_token VARCHAR (255); ispustiti tablicu ako postoji oauth_refresh_token; stvoriti tablicu oauth_refresh_token (token_id VARCHAR (255), token LONG VARBINARY, provjera autentičnosti LONG VARBINARY); ispustite tablicu ako postoji oauth_code; stvoriti tablicu oauth_code (kod VARCHAR (255), provjera autentičnosti LONG VARBINARY); ispustite tablicu ako postoji oauth_approvals; stvoriti tablicu oauth_ Approvals (userId VARCHAR (255), clientId VARCHAR (255), opseg VARCHAR (255), status VARCHAR (10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP); ispustite tablicu ako postoji ClientDetails; izraditi tablicu ClientDetails (appId VARCHAR (255) PRIMARNI KLJUČ, id-ovi VARCHAR (255), appSecret VARCHAR (255), opseg VARCHAR (255), grantTypes VARCHAR (255), redirectUrl VARCHAR (255), pooblastila VARCHAR (255), access_EG INTER , refresh_token_validity INTEGER, dodatne informacije VARCHAR (4096), autoApproveScopes VARCHAR (255));

Imajte na umu da nam nije nužno potrebno eksplicitno Baza podatakaPopulator grah - jednostavno bismo mogli koristiti a shema.sql - koje Spring Boot koristi prema zadanim postavkama.

2.4. Konfiguracija sigurnosti

Konačno, osigurajmo poslužitelj za autorizaciju.

Kada klijentska aplikacija treba nabaviti Access Token, to će učiniti nakon jednostavnog postupka autorizacije koji se vodi putem obrasca:

@Configuration javna klasa ServerSecurityConfig proširuje WebSecurityConfigurerAdapter {@Override zaštićena void konfiguracija (AuthenticationManagerBuilder auth) baca izuzetak {auth.inMemoryAuthentication () .withUser ("john"). Password ("US")). Uloge ("USER"). } @Override @Bean public AuthenticationManager authenticationManagerBean () baca iznimku {return super.authenticationManagerBean (); } @Override zaštićena void konfiguracija (HttpSecurity http) baca izuzetak {http.authorizeRequests () .antMatchers ("/ login"). AllowAll () .anyRequest (). Authenticated () .and () .formLogin (). AllowAll () ; }}

Ovdje je brza napomena konfiguracija prijave za obrazac nije potrebna za tijek lozinke - samo za implicitni protok - pa ćete ga možda moći preskočiti, ovisno o tome koji OAuth2 tok koristite.

3. Resursni poslužitelj

Sada, razgovarajmo o poslužitelju resursa; ovo je u osnovi REST API koji u konačnici želimo biti u mogućnosti konzumirati.

3.1. Maven konfiguracija

Naša konfiguracija poslužitelja resursa je ista kao i prethodna konfiguracija aplikacije Autorizacijski poslužitelj.

3.2. Konfiguracija trgovine tokena

Dalje, konfigurirat ćemo svoj TokenStore za pristup istoj bazi podataka koju autorizacijski poslužitelj koristi za pohranu tokena pristupa:

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

Imajte na umu da za ovu jednostavnu implementaciju, dijelimo SQL skladište tokena s potporom iako su poslužitelji za autorizaciju i resurse zasebne aplikacije.

Razlog je, naravno, taj što Resource Server mora biti u mogućnosti provjeriti valjanost pristupnih tokena izdan od strane Autorizacijskog poslužitelja.

3.3. Udaljena usluga tokena

Umjesto da koristite TokenStore na našem poslužitelju resursa možemo koristiti RemoteTokeServices:

@ Primarni @Bean javni RemoteTokenServices tokenService () {RemoteTokenServices tokenService = novi RemoteTokenServices (); tokenService.setCheckTokenEndpointUrl ("// localhost: 8080 / spring-security-oauth-server / oauth / check_token"); tokenService.setClientId ("fooClientIdPassword"); tokenService.setClientSecret ("tajno"); povratak tokenService; }

Imajte na umu da:

  • Ovaj RemoteTokenService koristit ću CheckTokenEndPoint na Autorizacijskom poslužitelju za provjeru valjanosti AccessTokena i dobivanje Ovjera objekt iz njega.
  • Mogu se pronaći na AuthorizationServerBaseURL + ”/ oauth / check_token
  • Poslužitelj za autorizaciju može koristiti bilo koju vrstu TokenStore [JdbcTokenStore, JwtTokenStore, ...] - ovo neće utjecati na RemoteTokenService ili poslužitelju resursa.

3.4. Uzorak kontrolera

Dalje, provedimo jednostavan kontroler koji izlaže a Foo resurs:

@Controller javna klasa 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)); }}

Primijetite kako klijent treba "čitati" opseg za pristup ovom Resursu.

Također moramo omogućiti globalnu sigurnost i konfiguraciju metoda MethodSecurityExpressionHandler:

@Configuration @EnableResourceServer @EnableGlobalMethodSecurity (prePostEnabled = true) javna klasa OAuth2ResourceServerConfig proširuje GlobalMethodSecurityConfiguration {@Override protected MethodSecurityExpressionHandler createExpressionHandler () {Return to: }}

I evo našeg osnovnog Foo Resurs:

javna klasa Foo {private long id; privatni naziv niza; }

3.5. Web konfiguracija

Na kraju, postavimo vrlo osnovnu web konfiguraciju za API:

@Configuration @EnableWebMvc @ComponentScan ({"org.baeldung.web.controller"}) javna klasa ResourceWebConfig implementira WebMvcConfigurer {}

4. Prednji kraj - Postavljanje

Sada ćemo pogledati jednostavnu front-end Angular implementaciju za klijenta.

Prvo ćemo koristiti Angular CLI za generiranje i upravljanje našim front-end modulima.

Prvo ćemo instalirati node i npm - jer je Angular CLI npm alat.

Zatim, trebamo koristiti frontend-maven-plugin za izgradnju našeg Angular projekta koristeći maven:

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

I konačno, generirati novi modul koristeći Angular CLI:

novi oauthApp

Imajte na umu da ćemo imati dva front-end modula - jedan za protok lozinke i drugi za implicitni tok.

U sljedećim odjeljcima razgovarat ćemo o logici aplikacije Angular za svaki modul.

5. Tok lozinke pomoću kutnog

Ovdje ćemo koristiti protok OAuth2 lozinke - zato ovo je samo dokaz koncepta, a ne aplikacija spremna za proizvodnju. Primijetit ćete da su vjerodajnice klijenta izložene prednjem dijelu - o čemu ćemo se pozabaviti u budućem članku.

Naš je slučaj upotrebe jednostavan: nakon što korisnik pruži svoje vjerodajnice, front-end klijent ih koristi za dobivanje pristupnog tokena s autorizacijskog poslužitelja.

5.1. Usluga aplikacija

Krenimo s našim AppService - nalazi se na app.service.ts - koji sadrži logiku za poslužiteljske interakcije:

  • получитьAccessToken (): za dobivanje tokena pristupa s dodijeljenim vjerodajnicama korisnika
  • saveToken (): za spremanje našeg pristupnog tokena u kolačić pomoću biblioteke ng2-cookies
  • getResource (): za dobivanje Foo objekta s poslužitelja koristeći njegov ID
  • checkCredentials (): za provjeru je li korisnik prijavljen ili nije
  • Odjavite se(): za brisanje kolačića s pristupnim tokenom i odjava korisnika
izvozna klasa Foo {konstruktor (javni id: broj, javno ime: niz) {}} @Injectable () izvozna klasa AppService {konstruktor (privatni _ruter: usmjerivač, privatni _http: Http) {} получитьAccessToken (loginData) {neka params = novo URLSearchParams (); params.append ('korisničko ime', loginData.username); params.append ('lozinka', loginData.password); params.append ('grant_type', 'lozinka'); params.append ('client_id', 'fooClientIdPassword'); neka zaglavlja = nova zaglavlja ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Autorizacija': 'Basic' + btoa ("fooClientIdPassword: secret")}); let options = new RequestOptions ({headers: headers}); this._http.post ('// localhost: 8081 / spring-security-oauth-server / oauth / token', params.toString (), options) .map (res => res.json ()) .subscribe (podaci => this.saveToken (podaci), pogreška => alert ('Nevažeće vjerodajnice')); } saveToken (token) {var expireDate = novi datum (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate); this._router.navigate (['/']); } getResource (resourceUrl): uočljivo {var headers = new Headers ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Autorizacija': 'Donositelj' + Cookie.get ('pristupni_token')}); var options = new RequestOptions ({headers: headers}); vrati this._http.get (resourceUrl, opcije) .map ((res: Response) => res.json ()) .catch ((error: any) => Observable.throw (error.json (). error || 'Pogreška servera')); } checkCredentials () {if (! Cookie.check ('access_token')) {this._router.navigate (['/ login']); }} odjava () {Cookie.delete ('access_token'); this._router.navigate (['/ login']); }}

Imajte na umu da:

  • Da bismo dobili pristupni token, šaljemo OBJAVI prema "/ oauth / token”Krajnja točka
  • Za postizanje ove krajnje točke koristimo vjerodajnice klijenta i Basic Auth
  • Zatim šaljemo korisničke vjerodajnice zajedno s kodiranim URL-om klijentskog ID-a i tipa odobrenja
  • Nakon što dobijemo pristupni žeton - pohranjujemo u kolačić

Pohrana kolačića ovdje je posebno važna jer kolačić koristimo samo za pohranu, a ne za izravno pokretanje postupka provjere autentičnosti. To pomaže u zaštiti od napada i ranjivosti od krivotvorenja zahtjeva za više web lokacija (CSRF).

5.2. Komponenta za prijavu

Dalje, pogledajmo našu Komponenta za prijavu koja je odgovorna za obrazac za prijavu:

@Component ({selector: 'login-form', dobavljači: [AppService], predložak: `Login`}) klasa izvoza LoginComponent {public loginData = {username:" ", password:" "}; konstruktor (private _service: AppService) {} login () {this._service.obtainAccessToken (this.loginData); }

5.3. Kućna komponenta

Dalje, naša HomeComponent koja je odgovorna za prikaz i manipulaciju našom početnom stranicom:

@Component ({selector: 'home-header', provideri: [AppService], predložak: `Dobrodošli !! Odjava`}) klasa izvoza HomeComponent {konstruktor (privatna _usluga: AppService) {} ngOnInit () {this._service.checkCredentials (); } odjava () {this._service.logout (); }}

5.4. Foo komponenta

Napokon, naša FooComponent za prikaz naših podataka o Foou:

@Component ({selector: 'foo-details', dobavljači: [AppService], predložak: `ID {{foo.id}} Ime {{foo.name}} New Foo`}) klasa izvoza FooComponent {public foo = new Foo (1, 'uzorak foo'); private foosUrl = '// localhost: 8082 / spring-security-oauth-resource / foos /'; konstruktor (private _service: AppService) {} getFoo () {this._service.getResource (this.foosUrl + this.foo.id) .subscribe (data => this.foo = data, error => this.foo.name = 'Pogreška'); }}

5.5. Komponenta aplikacije

Naše jednostavno AppComponent da djeluje kao korijenska komponenta:

@Component ({selector: 'app-root', template: ``}) klasa izvoza AppComponent {}

I AppModule gdje umotavamo sve svoje komponente, usluge i rute:

@NgModule ({deklaracije: [AppComponent, HomeComponent, LoginComponent, FooComponent], uvozi: [BrowserModule, FormsModule, HttpModule, RouterModule.forRoot ([{put: '', komponenta: HomeComponent}, {put: 'prijava', komponenta: LoginComponent}])], dobavljači: [], bootstrap: [AppComponent]}) klasa izvoza AppModule {}

6. Implicitni tok

Dalje ćemo se usredotočiti na modul Implicit Flow.

6.1. Usluga aplikacija

Slično tome, započet ćemo s našom uslugom, ali ovaj put koristit ćemo knjižnicu angular-oauth2-oidc umjesto da sami pribavimo token pristupa:

@Injectable () klasa izvoza AppService {konstruktor (private _router: Router, private _http: Http, private oauthService: OAuthService) {this.oauthService.loginUrl = '// localhost: 8081 / spring-security-oauth-server / oauth / autorizirajte '; this.oauthService.redirectUri = '// localhost: 8086 /'; this.oauthService.clientId = "sampleClientId"; this.oauthService.scope = "čitaj foo bar za pisanje"; this.oauthService.setStorage (sessionStorage); this.oauthService.tryLogin ({}); } dobitiAccessToken () {this.oauthService.initImplicitFlow (); } getResource (resourceUrl): Promatrano {var headers = new Headers ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Autorizacija': 'Donositelj' + this.oauthService .getAccessToken ()}); var options = new RequestOptions ({headers: headers}); vratite this._http.get (resourceUrl, options) .map ((res: Response) => res.json ()) .catch ((error: any) => Observable.throw (error.json (). error || 'Pogreška servera')); } isLoggedIn () {if (this.oauthService.getAccessToken () === null) {return false; } return true; } odjava () {this.oauthService.logOut (); location.reload (); }}

Imajte na umu kako ga nakon dobivanja pristupnog tokena koristimo putem Ovlaštenje zaglavlje kad god konzumiramo zaštićene resurse s Resursnog poslužitelja.

6.2. Kućna komponenta

Naše HomeComponent za rukovanje našom jednostavnom početnom stranicom:

@Component ({selector: 'home-header', dobavljači: [AppService], predložak: `Prijava Dobrodošli !! Odjava

`}) klasa izvoza HomeComponent {public isLoggedIn = false; konstruktor (privatna _service: AppService) {} ngOnInit () {this.isLoggedIn = this._service.isLoggedIn (); } login () {this._service.obtainAccessToken (); } odjava () {this._service.logout (); }}

6.3. Foo komponenta

Naše FooComponent je potpuno isti kao u modulu protoka lozinke.

6.4. Modul aplikacije

Napokon, naša AppModule:

@NgModule ({deklaracije: [AppComponent, HomeComponent, FooComponent], uvozi: [BrowserModule, FormsModule, HttpModule, OAuthModule.forRoot (), RouterModule.forRoot ([{put: '', komponenta: HomeComponent}])], dobavljači: [], bootstrap: [AppComponent]}) klasa izvoza AppModule {}

7. Pokrenite prednji kraj

1. Da bismo pokrenuli bilo koji od naših front-end modula, prvo moramo izraditi aplikaciju:

mvn čista instalacija

2. Zatim moramo prijeći na naš direktorij aplikacija Angular:

cd src / main / resources

3. Napokon ćemo pokrenuti našu aplikaciju:

npm start

Poslužitelj će se prema zadanim postavkama pokrenuti na priključku 4200, da bi promijenio priključak bilo kojeg modula, promijenio

"start": "posluživanje"

u paket.json da bi se pokrenuo na portu 8086, na primjer:

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

8. Zaključak

U ovom smo članku naučili kako autorizirati našu aplikaciju pomoću OAuth2.

Potpuna implementacija ovog vodiča može se naći u projektu GitHub.


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