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.