Spring REST API + OAuth2 + Angular (ved hjælp af Spring Security OAuth legacy stack)

1. Oversigt

I denne vejledning sikrer vi en REST API med OAuth og forbruger den fra en simpel Angular-klient.

Den applikation, vi skal bygge ud, vil bestå af fire separate moduler:

  • Autorisationsserver
  • Ressource server
  • UI implicit - en frontend-app, der bruger Implicit Flow
  • UI-adgangskode - en frontend-app, der bruger Password Flow

Bemærk: denne artikel bruger Spring OAuth-arvsprojektet. For den version af denne artikel, der bruger den nye Spring Security 5-stak, skal du se på vores artikel Spring REST API + OAuth2 + Angular.

Okay, lad os hoppe lige ind.

2. Autorisationsserveren

Lad os først oprette en autorisationsserver som en simpel Spring Boot-applikation.

2.1. Maven-konfiguration

Vi opretter følgende sæt afhængigheder:

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

Bemærk, at vi bruger spring-jdbc og MySQL, fordi vi skal bruge en JDBC-understøttet implementering af tokenbutikken.

2.2. @EnableAuthorizationServer

Lad os nu begynde at konfigurere autorisationsserveren, der er ansvarlig for administration af adgangstokener:

@Configuration @EnableAuthorizationServer offentlig klasse AuthServerOAuth2Config udvider AuthorizationServerConfigurerAdapter {@Autowired @Qualifier ("authenticationManagerBean") privat AuthenticationManager authenticationManager; @Override offentlig ugyldig konfiguration (AuthorizationServerSecurityConfigurer oauthServer) kaster undtagelse {oauthServer .tokenKeyAccess ("permitAll ()") .checkTokenAccess ("isAuthenticated ()"); } @ Override offentlig ugyldig konfiguration (ClientDetailsServiceConfigurer-klienter) kaster undtagelse {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 offentlig ugyldig konfiguration (AuthorizationServerEndpointsConfigurer slutpunkter) kaster Undtagelse {endepunkter .tokenStore (tokenStore ()) .authenticationManager (authenticationManager); } @Bean offentlig TokenStore tokenStore () {returner ny JdbcTokenStore (dataSource ()); }}

Noter det:

  • For at opretholde tokens brugte vi en JdbcTokenStore
  • Vi registrerede en klient til “implicit”Tilskudstype
  • Vi registrerede en anden klient og godkendte “adgangskode“, “autorisationskode”Og“opdater_token”Tilskudstyper
  • For at bruge “adgangskode”Tilskudstype, vi har brug for til at tilslutte og bruge AuthenticationManager bønne

2.3. Datakildekonfiguration

Lad os derefter konfigurere vores datakilde, der skal bruges af JdbcTokenStore:

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

Bemærk, at som vi bruger JdbcTokenStore vi er nødt til at initialisere databaseskemaet, så vi brugte det DataSourceInitializer - og følgende SQL-skema:

slip tabel, hvis der findes oauth_client_details; Opret tabel oauth_client_details (client_id VARCHAR (255) PRIMARY KEY, resource_ids VARCHAR (255), client_secret VARCHAR (255), scope VARCHAR (255), authorized_grant_types VARCHAR (255), web_server_redirect_uri VARCHAR (25)) 25 , refresh_token_validity INTEGER, yderligere_information VARCHAR (4096), autoapprove VARCHAR (255)); slip tabel, hvis der findes oauth_client_token; Opret tabel oauth_client_token (token_id VARCHAR (255), token LONG VARBINARY, authentication_id VARCHAR (255) PRIMARY KEY, user_name VARCHAR (255), client_id VARCHAR (255)); slip tabel, hvis der findes oauth_access_token; Opret tabel oauth_access_token (token_id VARCHAR (255), token LANG VARBINÆR, authentication_id VARCHAR (255) PRIMÆR NØGLE, brugernavn VARCHAR (255), client_id VARCHAR (255), godkendelse LANG VARBINÆR, refresh_token VARCHAR) drop tabel, hvis der findes oauth_refresh_token; Opret tabel oauth_refresh_token (token_id VARCHAR (255), token LANG VARBINÆR, godkendelse LANG VARBINÆR); slip tabel, hvis der findes oauth_code; Opret tabel oauth_code (kode VARCHAR (255), godkendelse LANG VARBINÆR); slip tabel, hvis der findes oauth_approvals; Opret tabel oauth_approvals (userId VARCHAR (255), clientId VARCHAR (255), scope VARCHAR (255), status VARCHAR (10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP); slip tabel, hvis der findes ClientDetails; Opret tabel ClientDetails (appId VARCHAR (255) PRIMARY KEY, resourceIds VARCHAR (255), appSecret VARCHAR (255), scope VARCHAR (255), grantTypes VARCHAR (255), redirectUrl VARCHAR (255) ,eautoriteter VARCHAR (255), adgang , refresh_token_validity INTEGER, additionalInformation VARCHAR (4096), autoApproveScopes VARCHAR (255));

Bemærk, at vi ikke nødvendigvis har brug for det eksplicitte DatabasePopulator bønne - vi kunne simpelthen bruge en skema.sql - som Spring Boot bruger som standard.

2.4. Sikkerhedskonfiguration

Lad os endelig sikre autorisationsserveren.

Når klientapplikationen skal erhverve et Access Token, gør det det efter en simpel form-login-styret godkendelsesproces:

@Configuration offentlig klasse ServerSecurityConfig udvider WebSecurityConfigurerAdapter {@Override beskyttet ugyldig konfiguration (AuthenticationManagerBuilder auth) kaster Undtagelse {auth.inMemoryAuthentication () .withUser ("john"). Adgangskode ("123"). Roller ("USER"); } @Override @Bean offentlig AuthenticationManager authenticationManagerBean () kaster Undtagelse {returner super.authenticationManagerBean (); } @ Override beskyttet ugyldig konfiguration (HttpSecurity http) kaster undtagelse {http.authorizeRequests () .antMatchers ("/ login"). PermitAll () .anyRequest (). Godkendt () .and () .formLogin (). PermitAll () ; }}

En hurtig bemærkning her er, at konfiguration af formularlogin er ikke nødvendig for adgangskodeflowet - kun for den implicitte strøm - så du muligvis kan springe den over afhængigt af, hvilken OAuth2-strøm du bruger.

3. Ressource-serveren

Lad os nu diskutere ressource-serveren; dette er i det væsentlige REST API, som vi i sidste ende vil være i stand til at forbruge.

3.1. Maven-konfiguration

Vores Resource Server-konfiguration er den samme som den tidligere konfiguration af Application Server-applikationen.

3.2. Token Store-konfiguration

Dernæst konfigurerer vi vores TokenStore for at få adgang til den samme database, som autorisationsserveren bruger til at gemme adgangstokener:

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

Bemærk, at til denne enkle implementering, vi deler SQL-understøttet tokenbutik selvom autorisations- og ressource-serverne er separate applikationer.

Årsagen er selvfølgelig, at ressource-serveren skal være i stand til det kontroller gyldigheden af ​​adgangstokenerne udstedt af autorisationsserveren.

3.3. Remote Token Service

I stedet for at bruge en TokenStore i vores ressource server, kan vi bruge 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"); returnere tokenService; }

Noter det:

  • Det her RemoteTokenService vil bruge CheckTokenEndPoint på Authorization Server for at validere AccessToken og få Godkendelse objekt fra det.
  • De kan findes på AuthorizationServerBaseURL + ”/ oauth / check_token
  • Autorisationsserveren kan bruge enhver TokenStore-type [JdbcTokenStore, JwtTokenStore, ...] - dette påvirker ikke RemoteTokenService eller Ressource server.

3.4. En prøvekontroller

Lad os derefter implementere en simpel controller, der udsætter en Foo ressource:

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

Bemærk hvordan klienten har brug for "Læs" mulighed for at få adgang til denne ressource.

Vi skal også aktivere global metodesikkerhed og konfigurere MethodSecurityExpressionHandler:

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

Og her er vores grundlæggende Foo Ressource:

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

3.5. Webkonfiguration

Lad os endelig oprette en meget grundlæggende webkonfiguration til API:

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

4. Frontend - Opsætning

Vi skal nu se på en enkel front-end vinkelimplementering for klienten.

Først bruger vi Angular CLI til at generere og administrere vores frontend-moduler.

Først installerer vi node og npm - da Angular CLI er et npm-værktøj.

Derefter skal vi bruge frontend-maven-plugin at bygge vores Angular-projekt ved hjælp af 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 et nyt modul ved hjælp af Angular CLI:

ng ny oauthApp

Bemærk, at vi har to frontend-moduler - en til adgangskodeflow og den anden til implicit flow.

I de følgende afsnit vil vi diskutere Angular-applogikken for hvert modul.

5. Passwordflow ved hjælp af vinkel

Vi bruger OAuth2-adgangskodeflowet her - det er derfor dette er kun et bevis på konceptet, ikke en produktionsklar applikation. Du vil bemærke, at kundens legitimationsoplysninger udsættes for frontend - hvilket er noget, vi vil behandle i en fremtidig artikel.

Vores brugssag er enkel: Når en bruger først har givet deres legitimationsoplysninger, bruger frontend-klienten dem til at erhverve et adgangstoken fra autorisationsserveren.

5.1. App-service

Lad os starte med vores AppService - placeret på app.service.ts - som indeholder logikken for serverinteraktioner:

  • fåAccessToken (): for at få adgangs-token givet brugeroplysninger
  • saveToken (): for at gemme vores adgangstoken i en cookie ved hjælp af ng2-cookies-biblioteket
  • getResource (): for at hente et Foo-objekt fra serveren ved hjælp af dets ID
  • checkCredentials (): for at kontrollere, om brugeren er logget ind eller ej
  • Log ud(): for at slette adgangstoken-cookie og logge brugeren ud
eksportklasse Foo {konstruktør (offentligt id: nummer, offentligt navn: streng) {}} @Injectable () eksportklasse AppService {konstruktør (privat _router: Router, privat _http: Http) {} fåAccessToken (loginData) {lad params = ny URLSearchParams (); params.append ('brugernavn', loginData.username); params.append ('password', loginData.password); params.append ('grant_type', 'password'); params.append ('client_id', 'fooClientIdPassword'); lad overskrifter = nye overskrifter ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8', 'Authorization': 'Basic' + btoa ("fooClientIdPassword: secret")}); lad optioner = 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 => alarm ('Invalid Credentials')); } saveToken (token) {var expireDate = ny dato (). 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 ('adgangstoken')}); var optioner = nye RequestOptions ({headers: headers}); returner this._http.get (resourceUrl, optioner) .map ((res: Response) => res.json ()) .catch ((error: any) => Observable.throw (error.json (). error || 'Server Fejl')); } checkCredentials () {if (! Cookie.check ('access_token')) {this._router.navigate (['/ login']); }} logout () {Cookie.delete ('access_token'); this._router.navigate (['/ login']); }}

Noter det:

  • For at få en adgangstoken sender vi en STOLPE til "/ oauth / token”Slutpunkt
  • Vi bruger klientoplysningerne og Basic Auth for at nå dette slutpunkt
  • Vi sender derefter brugerlegitimationsoplysningerne sammen med klient-id'et og tildelingstypeparametre URL kodet
  • Når vi har fået adgangstokenet - vi gemmer det i en cookie

Cookieopbevaring er især vigtig her, fordi vi kun bruger cookien til opbevaringsformål og ikke til at drive autentificeringsprocessen direkte. Dette hjælper med at beskytte mod CSRF-type angreb og sårbarheder mod forfalskning på tværs af websteder.

5.2. Login-komponent

Lad os derefter se på vores LoginKomponent som er ansvarlig for loginformularen:

@Component ({selector: 'login-form', udbydere: [AppService], skabelon: `Login`}) eksportklasse LoginComponent {public loginData = {brugernavn:" ", adgangskode:" "}; konstruktør (privat _service: AppService) {} login () {this._service.obtainAccessToken (this.loginData); }

5.3. Hjemmekomponent

Dernæst vores HjemKomponent som er ansvarlig for at vise og manipulere vores hjemmeside:

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

5.4. Foo-komponent

Endelig vores FooComponent for at få vist vores Foo-detaljer:

@Component ({selector: 'foo-details', udbydere: [AppService], skabelon: 'ID {{foo.id}} Navn {{foo.name}} Ny Foo'}) eksportklasse FooComponent {public foo = new Foo (1, 'prøve foo'); private foosUrl = '// localhost: 8082 / spring-security-oauth-resource / foos /'; konstruktør (privat _service: AppService) {} getFoo () {this._service.getResource (this.foosUrl + this.foo.id). abonner (data => this.foo = data, fejl => this.foo.name = 'Fejl'); }}

5.5. Appkomponent

Vores enkle AppComponent at fungere som rodkomponent:

@Component ({selector: 'app-root', skabelon: '}} eksportklasse AppComponent {}

Og AppModule hvor vi indpakker alle vores komponenter, tjenester og ruter:

@NgModule ({erklæringer: [AppComponent, HomeComponent, LoginComponent, FooComponent], importerer: [BrowserModule, FormsModule, HttpModule, RouterModule.forRoot ([{sti: '', komponent: HomeComponent}, {sti: 'login', komponent: LoginComponent}])], udbydere: [], bootstrap: [AppComponent]}) eksportklasse AppModule {}

6. Implicit flow

Dernæst fokuserer vi på Implicit Flow-modulet.

6.1. App-service

Tilsvarende starter vi med vores service, men denne gang bruger vi biblioteket angular-oauth2-oidc i stedet for selv at få adgangstoken:

@Injectable () eksportklasse AppService {konstruktør (privat _router: Router, 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 = "læs 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 optioner = nye RequestOptions ({headers: headers}); returner this._http.get (resourceUrl, optioner) .map ((res: Response) => res.json ()) .catch ((error: any) => Observable.throw (error.json (). error || 'Server Fejl')); } isLoggedIn () {if (this.oauthService.getAccessToken () === null) {returner false; } returner sandt } logout () {this.oauthService.logOut (); location.reload (); }}

Bemærk, hvordan vi efter at have fået adgangstoken bruger det via Bemyndigelse header, når vi bruger beskyttede ressourcer fra ressource-serveren.

6.2. Hjemmekomponent

Vores HjemKomponent til at håndtere vores enkle startside:

@Component ({selector: 'home-header', udbydere: [AppService], skabelon: `Log ind Velkommen !! Log af

`}) eksportklasse HomeComponent {public isLoggedIn = false; konstruktør (privat _service: AppService) {} ngOnInit () {this.isLoggedIn = this._service.isLoggedIn (); } login () {this._service.obtainAccessToken (); } logout () {this._service.logout (); }}

6.3. Foo-komponent

Vores FooComponent er nøjagtigt den samme som i adgangskodeflowmodulet.

6.4. App-modul

Endelig vores AppModule:

@NgModule ({erklæringer: [AppComponent, HomeComponent, FooComponent], importerer: [BrowserModule, FormsModule, HttpModule, OAuthModule.forRoot (), RouterModule.forRoot ([{sti: '', komponent: HomeComponent}])] udbydere: [], bootstrap: [AppComponent]}) eksportklasse AppModule {}

7. Kør frontenden

1. For at køre et af vores front-end-moduler skal vi først oprette appen:

mvn ren installation

2. Så er vi nødt til at navigere til vores Angular-app-bibliotek:

cd src / main / ressourcer

3. Endelig starter vi vores app:

npm start

Serveren starter som standard på port 4200 for at ændre porten på ethvert modul ved at ændre

"start": "ng serve"

i pakke.json for at få det til at køre på port 8086 for eksempel:

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

8. Konklusion

I denne artikel lærte vi, hvordan vi godkendte vores ansøgning ved hjælp af OAuth2.

Den fulde implementering af denne vejledning kan findes i GitHub-projektet.


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