Spring REST API + OAuth2 + Angular (a Spring Security OAuth örökölt verem használatával)

1. Áttekintés

Ebben az oktatóanyagban egy REST API-t fogunk biztosítani az OAuth segítségével, és egy egyszerű Angular kliensből használjuk fel.

A kiépítendő alkalmazás négy különálló modulból áll:

  • Authorization Server
  • Erőforrás-kiszolgáló
  • UI implicit - kezelői alkalmazás az Implicit Flow használatával
  • Felhasználói felület jelszava - a Jelszófolyamatot használó kezelőfelület

jegyzet: ez a cikk a Spring OAuth örökölt projektet használja. A cikknek az új Spring Security 5 verem használatával készült verzióját tekintse meg a Spring REST API + OAuth2 + Angular cikkünket.

Rendben, ugorjunk be.

2. A hitelesítési kiszolgáló

Először kezdjük el beállítani az Authorization Server-t egyszerű Spring Boot alkalmazásként.

2.1. Maven konfiguráció

A következő függőségeket állítjuk be:

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

Vegye figyelembe, hogy a spring-jdbc és a MySQL-t használjuk, mert a token store JDBC által támogatott megvalósítását fogjuk használni.

2.2. @EnableAuthorizationServer

Most kezdjük el konfigurálni a hozzáférési tokenek kezeléséért felelős hitelesítési kiszolgálót:

@Configuration @EnableAuthorizationServer nyilvános osztály Az AuthServerOAuth2Config kiterjeszti az AuthorizationServerConfigurerAdapter {@Autowired @Qualifier ("authenticationManagerBean") privát AuthenticationManager authenticationManager; A @Orride public void configure (AuthorizationServerSecurityConfigurer oauthServer) dobja a {oauthServer .tokenKeyAccess ("engedélyAll ()") .checkTokenAccess ("isAuthenticated ()" kivételt; } A @Orride public void configure (ClientDetailsServiceConfigurer ügyfelek) a (z) {clients.jdbc (dataSource ()) .withClient ("sampleClientId") .authorizedGrantTypes ("implicit") .scopes ("read") .autoApprove (true). ) .withClient ("clientIdPassword") .secret ("titkos") .authorizedGrantTypes ("jelszó", "jogosultsági kód", "frissítő_beszélt") .scopes ("olvasás"); } @Orride public void configure (AuthorizationServerEndpointsConfigurer endpoints) dobja a Kivétel {végpontokat .tokenStore (tokenStore ()) .authenticationManager (authenticationManager); } @Bean public TokenStore tokenStore () {return new JdbcTokenStore (dataSource ()); }}

Vegye figyelembe, hogy:

  • A tokenek fennmaradása érdekében a JdbcTokenStore
  • Regisztráltunk egy klienst a “beleértett”Támogatás típusa
  • Regisztráltunk egy másik ügyfelet, és engedélyeztük aJelszó“, “megerősítő kód”És„refresh_token”Támogatási típusok
  • A „Jelszó”Támogatás típusát be kell vezetnünk és felhasználnunk kell a AuthenticationManager bab

2.3. Adatforrás beállítása

Ezután állítsuk be az adatforrásunkat a JdbcTokenStore:

@Value ("classpath: schema.sql") privát erőforrás schemaScript; @Bean public DataSourceInitializer dataSourceInitializer (DataSource dataSource) {DataSourceInitializer inicializáló = új DataSourceInitializer (); inicializáló.setDataSource (dataSource); Initializer.setDatabasePopulator (databasePopulator ()); visszatérő inicializáló; } privát DatabasePopulator databasePopulator () {ResourceDatabasePopulator populator = új ResourceDatabasePopulator (); populator.addScript (schemaScript); visszatérő populátor; } @Bean public DataSource dataSource () {DriverManagerDataSource dataSource = új 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")); return dataSource; }

Vegye figyelembe, hogy ahogy használjuk JdbcTokenStore inicializálnunk kell az adatbázis-sémát, ezért használtuk DataSourceInitializer - és a következő SQL séma:

drop table, ha létezik oauth_client_details; táblázat létrehozása oauth_client_details (ügyfél_azonosító VARCHAR (255) ELSŐ KULCS, erőforrás_idek VARCHAR (255), ügyfél_secret VARCHAR (255), hatókör VARCHAR (255), engedélyezett_grant_típusok VARCHAR (255), webszerver_redirect_uri VARCHAR_intervallum (255) , refresh_token_validity INTEGER, további_információk VARCHAR (4096), automatikus jóváhagyás VARCHAR (255)); drop table ha létezik oauth_client_token; tábla létrehozása oauth_client_token (token_id VARCHAR (255), token LONG VARBINARY, authentication_id VARCHAR (255) PRIMARY KEY, user_name VARCHAR (255), client_id VARCHAR (255)); dobótábla, ha létezik oauth_access_token; tábla létrehozása 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 (255)); drop table, ha létezik, oauth_refresh_token; tábla létrehozása oauth_refresh_token (token_id VARCHAR (255), token LONG VARBINARY, hitelesítés LONG VARBINARY); drop table, ha létezik oauth_code; Oauth_code tábla létrehozása (kód VARCHAR (255), hitelesítés LONG VARBINARY); drop table, ha létezik oauth_approvals; tábla létrehozása oauth_approvals (userId VARCHAR (255), clientId VARCHAR (255), hatókör VARCHAR (255), status VARCHAR (10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP); dobótábla, ha létezik ClientDetails; tábla létrehozása ClientDetails (appId VARCHAR (255) ELSŐ KULCS, resourceIds VARCHAR (255), appSecret VARCHAR (255), hatókör VARCHAR (255), grantTypes VARCHAR (255), redirectUrl VARCHAR (255), hatóságok VARCHAR (255), access_token_validity INT , refresh_token_validity INTEGER, további információ VARCHAR (4096), autoApproveScopes VARCHAR (255));

Ne feledje, hogy nem feltétlenül van szükségünk kifejezettre DatabasePopulator bab - egyszerűen használhatnánk a schema.sql - amelyet a Spring Boot alapértelmezés szerint használ.

2.4. Biztonsági konfiguráció

Végül védjük meg az Authorization Server-t.

Amikor az ügyfélalkalmazásnak hozzáférési tokent kell beszereznie, akkor ezt egy egyszerű űrlap-bejelentkezés által vezérelt hitelesítési folyamat után teszi meg:

A @Configuration public class ServerSecurityConfig kiterjeszti a WebSecurityConfigurerAdapter {@Orride védett érvénytelen konfigurációt (AuthenticationManagerBuilder auth) a {Excellation_MemoryAuthentication () .withUser ("john"). Jelszót ("123"). Szerepeket ("USER"); } @Override @Bean public AuthenticationManager authenticationManagerBean () dobja a Kivételt {return super.authenticationManagerBean (); } A @Orride védett void konfiguráció (HttpSecurity http) dobja a {http.authorizeRequests () .antMatchers ("/ login") kivételt. AllowAll () .anyRequest (). Hitelesített () .és () .formLogin (). AllowAll () ; }}

Itt egy rövid megjegyzés az űrlap bejelentkezési konfigurációja nem szükséges a Jelszó folyamathoz - csak az Implicit folyamathoz - így kihagyhatja azt attól függően, hogy milyen OAuth2 folyamatot használ.

3. Az erőforrás-kiszolgáló

Most beszéljük meg az erőforrás-kiszolgálót; ez lényegében a REST API, amelyet végül el akarunk használni.

3.1. Maven konfiguráció

Erőforrás-kiszolgálónk konfigurációja megegyezik az előző Authorization Server alkalmazás-konfigurációval.

3.2. Token Store konfiguráció

Ezután konfiguráljuk a TokenStore hozzáférés ugyanahhoz az adatbázishoz, amelyet az engedélyezési kiszolgáló használ a hozzáférési tokenek tárolásához:

@Autowired privát környezet env; @Bean public DataSource dataSource () {DriverManagerDataSource dataSource = új 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")); return dataSource; } @Bean public TokenStore tokenStore () {return new JdbcTokenStore (dataSource ()); }

Vegye figyelembe, hogy ehhez az egyszerű megvalósításhoz megosztjuk az SQL által támogatott token-tárolót annak ellenére, hogy az Authorization és az Resource kiszolgálók különálló alkalmazások.

Ennek oka természetesen az, hogy az erőforrás-kiszolgálónak képesnek kell lennie erre ellenőrizze a hozzáférési tokenek érvényességét az Authorization Server adta ki.

3.3. Távoli token szolgáltatás

Ahelyett, hogy a TokenStore az erőforrás-kiszolgálónkban használhatjuk RemoteTokeServices:

@Primary @Bean public RemoteTokenServices tokenService () {RemoteTokenServices tokenService = new RemoteTokenServices (); tokenService.setCheckTokenEndpointUrl ("// localhost: 8080 / spring-security-oauth-server / oauth / check_token"); tokenService.setClientId ("fooClientIdPassword"); tokenService.setClientSecret ("titok"); return tokenService; }

Vegye figyelembe, hogy:

  • Ez RemoteTokenService használni fogja CheckTokenEndPoint az Authorization Server-en az AccessToken érvényesítéséhez és megszerzéséhez Hitelesítés objektum tőle.
  • Ez megtalálható az AuthorizationServerBaseURL + címen. ”/ oauth / check_token
  • A Authorization Server bármilyen TokenStore típust használhat [JdbcTokenStore, JwtTokenStore,…] - ez nem érinti a RemoteTokenService vagy erőforrás-kiszolgáló.

3.4. Egy minta vezérlő

Ezután valósítsunk meg egy egyszerű vezérlőt, amely kiteszi a Foo forrás:

@Controller public class FooController {@PreAuthorize ("# oauth2.hasScope ('read')") @RequestMapping (method = RequestMethod.GET, value = "/ foos / {id}") @ResponseBody public Foo findById (@PathVariable long id) {visszatérés új Foo (Long.parseLong (randomNumeric (2)), randomAfabetic (4)); }}

Vegye figyelembe, hogy az ügyfélnek mire van szüksége "olvas" az erőforrás elérésének területe.

Engedélyeznünk kell a globális módszerbiztonságot és a konfigurálást is MethodSecurityExpressionHandler:

@Configuration @EnableResourceServer @EnableGlobalMethodSecurity (prePostEnabled = true) nyilvános osztály OAuth2ResourceServerConfig kiterjeszti a GlobalMethodSecurityConfiguration {@Override védett MethodSecurityExpressionHandler createExpressionHandsler (Return) (return) }}

És itt van az alapvető Foo Forrás:

nyilvános osztály Foo {private long id; privát karakterlánc neve; }

3.5. Webkonfiguráció

Végül állítsunk be egy nagyon egyszerű webkonfigurációt az API számára:

@Configuration @EnableWebMvc @ComponentScan ({"org.baeldung.web.controller"}) public class ResourceWebConfig implementálja a WebMvcConfigurer {}

4. Kezelőfelület - Beállítás

Most egy egyszerű front-end Angular megvalósítást fogunk megvizsgálni az ügyfél számára.

Először az Angular CLI-t fogjuk használni az elülső modulok előállításához és kezeléséhez.

Először telepítjük a csomópontot és az npm-et - mivel az Angular CLI egy npm eszköz.

Ezután használnunk kell a frontend-maven-plugin hogy szögletes projektünket maven segítségével építsük fel:

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

És végül, hozzon létre egy új modult az Angular CLI használatával:

új oauthApp

Ne feledje, hogy két front-end modulunk lesz - az egyik a jelszófolyamathoz, a másik pedig az implicit folyamathoz.

A következő szakaszokban az egyes modulok Angular alkalmazáslogikáját tárgyaljuk.

5. A jelszóáramlás szögletes használatával

Itt az OAuth2 jelszófolyamatot fogjuk használni - ezért ez csak a koncepció bizonyítéka, nem pedig a gyártásra kész alkalmazás. Észre fogja venni, hogy az ügyfél hitelesítő adatai a kezelőfelületnek vannak kitéve - erre egy későbbi cikkünkben kitérünk.

Felhasználási esetünk egyszerű: amint a felhasználó megadja a hitelesítő adatait, a kezelői ügyfél felhasználja őket az Access Token megszerzéséhez az Authorization Server-től.

5.1. App Service

Kezdjük a mi AppService - található app.service.ts - amely tartalmazza a szerver interakciók logikáját:

  • getAccessToken (): Hozzáférési token adott felhasználói hitelesítő adatok beszerzése
  • saveToken (): hozzáférési tokenünk mentése egy cookie-ba az ng2-cookies könyvtár segítségével
  • getResource (): egy Foo objektum megszerzése a szerverről az azonosítója használatával
  • checkCredentials (): annak ellenőrzése, hogy a felhasználó be van-e jelentkezve, vagy sem
  • Kijelentkezés(): a hozzáférési token cookie törléséhez és a felhasználó kijelentkezéséhez
exportáló osztály Foo {konstruktor (nyilvános azonosító: szám, nyilvános név: karakterlánc) {}} @Injectable () export osztály AppService {konstruktor (privát _router: Router, privát _http: Http) {} getAccessToken (loginData) {let params = new URLSearchParams (); params.append ('felhasználónév', loginData.username); params.append ('jelszó', loginData.password); params.append ('grant_type', 'jelszó'); params.append ('client_id', 'fooClientIdPassword'); let fejlécek = új Fejlécek ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Engedélyezés:' Basic '+ btoa ("fooClientIdPassword: titkos")}); let options = new RequestOptions ({fejlécek: fejlécek}); this._http.post ('// localhost: 8081 / spring-security-oauth-server / oauth / token', params.toString (), opciók) .map (res => res.json ()) .subscribe (adatok => this.saveToken (adatok), err => figyelmeztetés ('Érvénytelen hitelesítő adatok')); } saveToken (token) {var expireDate = new Date (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate); this._router.navigate (['/']); } getResource (resourceUrl): Megfigyelhető {var headers = new Headers ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + Cookie.get ('access_token')}); var options = new RequestOptions ({fejlécek: fejlécek}); adja vissza ezt :_http.get (resourceUrl, opciók) .map ((res: Response) => res.json ()) .catch ((hiba: bármely) => Megfigyelhető.throw (error.json (). hiba || 'Szerver hiba')); } checkCredentials () {if (! Cookie.check ('access_token')) {this._router.navigate (['/ login']); }} kijelentkezés () {Cookie.delete ('access_token'); this._router.navigate (['/ login']); }}

Vegye figyelembe, hogy:

  • Hozzáférési token megszerzéséhez a POST hoz "/ oauth / token”Végpont
  • Az ügyfél hitelesítő adatait és az Alap hitelesítést használjuk ennek a végpontnak a eléréséhez
  • Ezután elküldjük a felhasználói hitelesítő adatokat, a kódolt ügyfél-azonosítóval és a megadás típusának paramétereivel együtt
  • Miután megszereztük az Access Tokent - sütiben tároljuk

A sütitárolás itt különösen fontos, mert a sütit csak tárolási célokra használjuk, és nem közvetlenül a hitelesítési folyamatot hajtjuk végre. Ez segít megvédeni a webhelyek közötti kérelmek hamisítását (CSRF) típusú támadásokat és sebezhetőségeket.

5.2. Bejelentkezés komponens

Ezután vessünk egy pillantást a mi BejelentkezésKomponens amely felelős a bejelentkezési űrlapért:

@Komponens ({selector: 'login-form', szolgáltatók: [AppService], sablon: `Bejelentkezés`}) exportálási osztály LoginComponent {public loginData = {felhasználónév:" ", jelszó:" "}; konstruktor (private _service: AppService) {} login () {this._service.obtainAccessToken (this.loginData); }

5.3. Otthoni alkatrész

Ezután a mi HomeComponent amely felelős a kezdőlapunk megjelenítéséért és manipulálásáért:

@Component ({selector: 'home-header', szolgáltatók: [AppService], sablon: `Welcome !! Logout`}) exportálási osztály HomeComponent {constructor (private _service: AppService) {} ngOnInit () {this._service.checkCredentials (); } kijelentkezés () {this._service.logout (); }}

5.4. Foo komponens

Végül a mi FooComponent Foo adataink megjelenítéséhez:

@Component ({selector: 'foo-details', szolgáltatók: [AppService], sablon: `ID {{foo.id}} Név {{foo.name}} New Foo`}) exportálási osztály FooComponent {public foo = new Foo (1, 'minta foo'); private foosUrl = '// localhost: 8082 / spring-security-oauth-resource / foos /'; konstruktor (privát _szolgáltatás: AppService) {} getFoo () {this._service.getResource (this.foosUrl + this.foo.id) .subscribe (data => this.foo = data, error => this.foo.name = 'Hiba'); }}

5.5. App Component

Egyszerű AppComponent hogy gyökérkomponensként működjön:

@Component ({selector: 'app-root', template: ``}) exportálási osztály AppComponent {}

És a AppModule ahol az összes alkatrészünket, szolgáltatásunkat és útvonalunkat becsomagoljuk:

@NgModule ({deklarációk: [AppComponent, HomeComponent, LoginComponent, FooComponent], import: [BrowserModule, FormsModule, HttpModule, RouterModule.forRoot ([{útvonal: '', komponens: HomeComponent}, {elérési út: 'login', komponens: LoginComponent}])], szolgáltatók: [], bootstrap: [AppComponent]}) exportálási osztály AppModule {}

6. Implicit Flow

Ezután az Implicit Flow modulra koncentrálunk.

6.1. App Service

Ehhez hasonlóan a szolgáltatásunkkal is kezdünk, de ezúttal a angular-oauth2-oidc könyvtárat fogjuk használni, ahelyett, hogy saját magunk szereznénk hozzáférési tokent:

@Injectable () export osztály AppService {konstruktor (private _router: Router, private _http: Http, private 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 = "read foo bar olvasása"; this.oauthService.setStorage (sessionStorage); this.oauthService.tryLogin ({}); } getAccessToken () {this.oauthService.initImplicitFlow (); } getResource (resourceUrl): Megfigyelhető {var header = new Headers ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Bearer' + this.oauthService .getAccessToken ()}); var options = new RequestOptions ({fejlécek: fejlécek}); adja vissza ezt :_http.get (resourceUrl, opciók) .map ((res: Response) => res.json ()) .catch ((hiba: bármely) => Megfigyelhető.throw (error.json (). hiba || 'Szerver hiba')); } isLoggedIn () {if (this.oauthService.getAccessToken () === null) {return false; } return true; } kijelentkezés () {this.oauthService.logOut (); hely.reload (); }}

Vegye figyelembe, hogy az Access Token megszerzése után hogyan használjuk a Engedélyezés fejléc, amikor védett erőforrásokat fogyasztunk az erőforrás-kiszolgálón belül.

6.2. Otthoni alkatrész

A mi HomeComponent kezelni egyszerű kezdőlapunkat:

@Component ({selector: 'home-header', szolgáltatók: [AppService], sablon: `Bejelentkezés Üdvözöljük !! Kijelentkezés

`}) export osztály HomeComponent {public isLoggedIn = false; konstruktor (privát _szolgáltatás: AppService) {} ngOnInit () {this.isLoggedIn = this._service.isLoggedIn (); } login () {this._szolgáltatás.obtainAccessToken (); } kijelentkezés () {this._service.logout (); }}

6.3. Foo komponens

A mi FooComponent pontosan ugyanaz, mint a jelszófolyamat modulban.

6.4. App modul

Végül a mi AppModule:

@NgModule ({deklarációk: [AppComponent, HomeComponent, FooComponent], import: [BrowserModule, FormsModule, HttpModule, OAuthModule.forRoot (), RouterModule.forRoot ([{útvonal: '', komponens: HomeComponent}],] [], bootstrap: [AppComponent]}) exportálási osztály AppModule {}

7. Futtassa a kezelőfelületet

1. A front-end modulok futtatásához először fel kell építenünk az alkalmazást:

mvn tiszta telepítés

2. Ezután el kell navigálnunk az Angular alkalmazás könyvtárunkba:

cd src / main / resources

3. Végül elindítjuk az alkalmazást:

npm kezdés

A szerver alapértelmezés szerint elindul a 4200-as porton, hogy megváltoztassa bármelyik modul portját

"start": "ng tálalás"

ban ben csomag.json hogy például a 8086-os porton fusson:

"start": "ng serve - port 8086"

8. Következtetés

Ebben a cikkben megtudtuk, hogyan engedélyezhetjük alkalmazásunkat az OAuth2 használatával.

Az oktatóanyag teljes megvalósítása megtalálható a GitHub projektben.