Spring REST API + OAuth2 + Angular (unter Verwendung des Spring Security OAuth-Legacy-Stacks)

1. Übersicht

In diesem Tutorial sichern wir eine REST-API mit OAuth und verwenden sie von einem einfachen Angular-Client.

Die Anwendung, die wir erstellen werden, besteht aus vier separaten Modulen:

  • Autorisierungsserver
  • Ressourcenserver
  • UI implicit - eine Front-End-App, die den impliziten Flow verwendet
  • UI-Passwort - eine Front-End-App, die den Passwortfluss verwendet

Hinweis : In diesem Artikel wird das Spring OAuth-Legacy-Projekt verwendet. Die Version dieses Artikels mit dem neuen Spring Security 5-Stack finden Sie in unserem Artikel Spring REST API + OAuth2 + Angular.

Okay, lass uns gleich hineinspringen.

2. Der Autorisierungsserver

Beginnen wir zunächst mit der Einrichtung eines Autorisierungsservers als einfache Spring Boot-Anwendung.

2.1. Maven-Konfiguration

Wir werden die folgenden Abhängigkeiten einrichten:

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

Beachten Sie, dass wir spring-jdbc und MySQL verwenden, da wir eine JDBC-gestützte Implementierung des Token-Speichers verwenden werden.

2.2. @EnableAuthorizationServer

Beginnen wir nun mit der Konfiguration des Autorisierungsservers, der für die Verwaltung der Zugriffstoken verantwortlich ist:

@Configuration @EnableAuthorizationServer public class AuthServerOAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired @Qualifier("authenticationManagerBean") private AuthenticationManager authenticationManager; @Override public void configure( AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource()) .withClient("sampleClientId") .authorizedGrantTypes("implicit") .scopes("read") .autoApprove(true) .and() .withClient("clientIdPassword") .secret("secret") .authorizedGrantTypes( "password","authorization_code", "refresh_token") .scopes("read"); } @Override public void configure( AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(tokenStore()) .authenticationManager(authenticationManager); } @Bean public TokenStore tokenStore() { return new JdbcTokenStore(dataSource()); } }

Beachten Sie, dass:

  • Um die Token beizubehalten , haben wir einen JdbcTokenStore verwendet
  • Wir haben einen Kunden für den impliziten Grant-Typ registriert
  • Wir haben einen anderen Kunden registriert und die Grant-Typen " password ", " authorisation_code " und " refresh_token " autorisiert
  • Um den Grant-Typ " password " verwenden zu können, müssen wir die AuthenticationManager- Bean anschließen und verwenden

2.3. Datenquellenkonfiguration

Als nächstes konfigurieren wir unsere Datenquelle für die Verwendung durch den JdbcTokenStore :

@Value("classpath:schema.sql") private Resource schemaScript; @Bean public DataSourceInitializer dataSourceInitializer(DataSource dataSource) { DataSourceInitializer initializer = new DataSourceInitializer(); initializer.setDataSource(dataSource); initializer.setDatabasePopulator(databasePopulator()); return initializer; } private DatabasePopulator databasePopulator() { ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.addScript(schemaScript); return populator; } @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")); return dataSource; }

Beachten Sie, dass wir bei der Verwendung von JdbcTokenStore das Datenbankschema initialisieren müssen. Daher haben wir DataSourceInitializer und das folgende SQL-Schema verwendet:

drop table if exists oauth_client_details; create table 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(255), authorities VARCHAR(255), access_token_validity INTEGER, refresh_token_validity INTEGER, additional_information VARCHAR(4096), autoapprove VARCHAR(255) ); drop table if exists oauth_client_token; create table oauth_client_token ( token_id VARCHAR(255), token LONG VARBINARY, authentication_id VARCHAR(255) PRIMARY KEY, user_name VARCHAR(255), client_id VARCHAR(255) ); drop table if exists oauth_access_token; create table 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 if exists oauth_refresh_token; create table oauth_refresh_token ( token_id VARCHAR(255), token LONG VARBINARY, authentication LONG VARBINARY ); drop table if exists oauth_code; create table oauth_code ( code VARCHAR(255), authentication LONG VARBINARY ); drop table if exists oauth_approvals; create table oauth_approvals ( userId VARCHAR(255), clientId VARCHAR(255), scope VARCHAR(255), status VARCHAR(10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP ); drop table if exists ClientDetails; create table ClientDetails ( appId VARCHAR(255) PRIMARY KEY, resourceIds VARCHAR(255), appSecret VARCHAR(255), scope VARCHAR(255), grantTypes VARCHAR(255), redirectUrl VARCHAR(255), authorities VARCHAR(255), access_token_validity INTEGER, refresh_token_validity INTEGER, additionalInformation VARCHAR(4096), autoApproveScopes VARCHAR(255) );

Beachten Sie, dass wir nicht unbedingt die explizite DatabasePopulator- Bean benötigen - wir könnten einfach eine schema.sql verwenden - die Spring Boot standardmäßig verwendet .

2.4. Sicherheitskonfiguration

Zum Schluss sichern wir den Authorization Server.

Wenn die Clientanwendung ein Zugriffstoken erwerben muss, erfolgt dies nach einem einfachen Authentifizierungsprozess mit Formularanmeldung:

@Configuration public class ServerSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("john").password("123").roles("USER"); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .formLogin().permitAll(); } }

Ein kurzer Hinweis hier ist, dass die Formularanmeldekonfiguration für den Kennwortfluss nicht erforderlich ist - nur für den impliziten Fluss -, sodass Sie sie möglicherweise überspringen können, je nachdem, welchen OAuth2-Fluss Sie verwenden.

3. Der Ressourcenserver

Lassen Sie uns nun den Ressourcenserver diskutieren. Dies ist im Wesentlichen die REST-API, die wir letztendlich nutzen möchten.

3.1. Maven-Konfiguration

Unsere Resource Server-Konfiguration entspricht der vorherigen Authorization Server-Anwendungskonfiguration.

3.2. Token Store Konfiguration

Als Nächstes konfigurieren wir unseren TokenStore so , dass er auf dieselbe Datenbank zugreift , die der Autorisierungsserver zum Speichern von Zugriffstoken verwendet:

@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")); return dataSource; } @Bean public TokenStore tokenStore() { return new JdbcTokenStore(dataSource()); }

Beachten Sie, dass wir für diese einfache Implementierung den von SQL unterstützten Token-Speicher gemeinsam nutzen , obwohl die Autorisierungs- und Ressourcenserver separate Anwendungen sind.

Der Grund ist natürlich, dass der Ressourcenserver in der Lage sein muss, die Gültigkeit der vom Autorisierungsserver ausgestellten Zugriffstoken zu überprüfen .

3.3. Remote Token Service

Anstatt einen TokenStore in unserem Ressourcenserver zu verwenden, können wir RemoteTokeServices verwenden :

@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("secret"); return tokenService; }

Beachten Sie, dass:

  • Dieser RemoteTokenService verwendet CheckTokenEndPoint auf dem Autorisierungsserver, um AccessToken zu validieren und ein Authentifizierungsobjekt von ihm abzurufen .
  • The can be found at AuthorizationServerBaseURL +”/oauth/check_token
  • The Authorization Server can use any TokenStore type [JdbcTokenStore, JwtTokenStore, …] – this won't affect the RemoteTokenService or Resource server.

3.4. A Sample Controller

Next, let's implement a simple controller exposing a Foo resource:

@Controller public class 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)); } }

Note how the client needs the “read” scope to access this Resource.

We also need to enable global method security and configure MethodSecurityExpressionHandler:

@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class OAuth2ResourceServerConfig extends GlobalMethodSecurityConfiguration { @Override protected MethodSecurityExpressionHandler createExpressionHandler() { return new OAuth2MethodSecurityExpressionHandler(); } }

And here's our basic Foo Resource:

public class Foo { private long id; private String name; }

3.5. Web Configuration

Finally, let's set up a very basic web configuration for the API:

@Configuration @EnableWebMvc @ComponentScan({ "org.baeldung.web.controller" }) public class ResourceWebConfig implements WebMvcConfigurer {}

4. Front End – Setup

We're now going to look at a simple front-end Angular implementation for the client.

First, we'll use Angular CLI to generate and manage our front-end modules.

First, we'll install node and npm – as Angular CLI is an npm tool.

Then, we need to use the frontend-maven-plugin to build our Angular project using maven:

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

And finally, generate a new Module using Angular CLI:

ng new oauthApp

Note that we'll have two front-end modules – one for password flow and the other for implicit flow.

In the following sections, we will discuss the Angular app logic for each module.

5. Password Flow Using Angular

We're going to be using the OAuth2 Password flow here – which is why this is just a proof of concept, not a production-ready application. You'll notice that the client credentials are exposed to the front end – which is something we'll address in a future article.

Our use case is simple: once a user provides their credentials, the front-end client uses them to acquire an Access Token from the Authorization Server.

5.1. App Service

Let's start with our AppService – located at app.service.ts – which contains the logic for server interactions:

  • obtainAccessToken(): to obtain Access token given user credentials
  • saveToken(): to save our access token in a cookie using ng2-cookies library
  • getResource(): to get a Foo object from server using its ID
  • checkCredentials(): to check if user is logged in or not
  • logout(): to delete access token cookie and log the user out
export class Foo { constructor( public id: number, public name: string) { } } @Injectable() export class AppService { constructor( private _router: Router, private _http: Http){} obtainAccessToken(loginData){ let params = new URLSearchParams(); params.append('username',loginData.username); params.append('password',loginData.password); params.append('grant_type','password'); params.append('client_id','fooClientIdPassword'); let headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8', 'Authorization': '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( 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) : Observable{ var headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8', 'Authorization': 'Bearer '+Cookie.get('access_token')}); var options = new RequestOptions({ headers: headers }); return this._http.get(resourceUrl, options) .map((res:Response) => res.json()) .catch((error:any) => Observable.throw(error.json().error || 'Server error')); } checkCredentials(){ if (!Cookie.check('access_token')){ this._router.navigate(['/login']); } } logout() { Cookie.delete('access_token'); this._router.navigate(['/login']); } }

Note that:

  • To get an Access Token we send a POST to the “/oauth/token” endpoint
  • We're using the client credentials and Basic Auth to hit this endpoint
  • We're then sending the user credentials along with the client id and grant type parameters URL encoded
  • After we obtain the Access Token – we store it in a cookie

The cookie storage is especially important here, because we're only using the cookie for storage purposes and not to drive the authentication process directly. This helps protect against cross-site request forgery (CSRF) type of attacks and vulnerabilities.

5.2. Login Component

Next, let's take a look at our LoginComponent which is responsible for the login form:

@Component({ selector: 'login-form', providers: [AppService], template: `   Login` }) export class LoginComponent { public loginData = {username: "", password: ""}; constructor(private _service:AppService) {} login() { this._service.obtainAccessToken(this.loginData); }

5.3. Home Component

Next, our HomeComponent which is responsible for displaying and manipulating our Home Page:

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

5.4. Foo Component

Finally, our FooComponent to display our Foo details:

@Component({ selector: 'foo-details', providers: [AppService], template: ` ID {{foo.id}} Name {{foo.name}} New Foo` }) export class FooComponent { public foo = new Foo(1,'sample foo'); private foosUrl = '//localhost:8082/spring-security-oauth-resource/foos/'; constructor(private _service:AppService) {} getFoo(){ this._service.getResource(this.foosUrl+this.foo.id) .subscribe( data => this.foo = data, error => this.foo.name = 'Error'); } }

5.5. App Component

Our simple AppComponent to act as the root component:

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

And the AppModule where we wrap all our components, services and routes:

@NgModule({ declarations: [ AppComponent, HomeComponent, LoginComponent, FooComponent ], imports: [ BrowserModule, FormsModule, HttpModule, RouterModule.forRoot([ { path: '', component: HomeComponent }, { path: 'login', component: LoginComponent }]) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

6. Implicit Flow

Next, we'll focus on the Implicit Flow module.

6.1. App Service

Similarly, we will start with our service, but this time we will use library angular-oauth2-oidc instead of obtaining access token ourselves:

@Injectable() export class AppService { constructor( 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 write foo bar"; this.oauthService.setStorage(sessionStorage); this.oauthService.tryLogin({}); } obtainAccessToken(){ this.oauthService.initImplicitFlow(); } getResource(resourceUrl) : Observable{ 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 }); return this._http.get(resourceUrl, options) .map((res:Response) => res.json()) .catch((error:any) => Observable.throw(error.json().error || 'Server error')); } isLoggedIn(){ if (this.oauthService.getAccessToken() === null){ return false; } return true; } logout() { this.oauthService.logOut(); location.reload(); } }

Note how, after obtaining the Access Token, we're using it via the Authorization header whenever we consume protected resources from within the Resource Server.

6.2. Home Component

Our HomeComponent to handle our simple Home Page:

@Component({ selector: 'home-header', providers: [AppService], template: ` Login Welcome !! Logout

` }) export class HomeComponent { public isLoggedIn = false; constructor( private _service:AppService){} ngOnInit(){ this.isLoggedIn = this._service.isLoggedIn(); } login() { this._service.obtainAccessToken(); } logout() { this._service.logout(); } }

6.3. Foo Component

Our FooComponent is exactly the same as in the password flow module.

6.4. App Module

Finally, our AppModule:

@NgModule({ declarations: [ AppComponent, HomeComponent, FooComponent ], imports: [ BrowserModule, FormsModule, HttpModule, OAuthModule.forRoot(), RouterModule.forRoot([ { path: '', component: HomeComponent }]) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

7. Run the Front End

1. Um eines unserer Front-End-Module auszuführen, müssen wir zuerst die App erstellen:

mvn clean install

2. Dann müssen wir zu unserem Angular-App-Verzeichnis navigieren:

cd src/main/resources

3. Zum Schluss starten wir unsere App:

npm start

Der Server wird standardmäßig an Port 4200 gestartet, um den Port eines Moduls zu ändern

"start": "ng serve"

in package.json , damit es auf Port 8086 ausgeführt wird, zum Beispiel:

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

8. Fazit

In diesem Artikel haben wir gelernt, wie Sie unsere Anwendung mit OAuth2 autorisieren.

Die vollständige Implementierung dieses Tutorials finden Sie im GitHub-Projekt.