Heads up… this post is old!
For an updated version of this post, see Bootiful Development with Spring Boot and Angular on the Okta developer blog.
Last fall, I wrote about how to get started with AngularJS, Spring Boot, and Stormpath. Since then, AngularJS has become a legacy technology, verified by ThoughWorks Radar and their “hold” recommendation in November 2016. This article shows how to get started with the latest release of Angular, and how to add authentication to it, a necessary feature in most applications.
In this tutorial, you’ll be using Angular and Stormpath’s Angular SDK for the UI. Along the way, you’ll learn how to create REST endpoints with Spring Data REST, use Stormpath to make authentication easy, and configure Stormpath’s Spring Boot support to allow CORS.
Spring Boot has greatly simplified how to develop applications with Spring. Its auto-configuration and many starters has fostered a Spring renaissance that makes developing Spring apps fun again! Stormpath’s Spring Boot starter is one of the most sophisticated in the ecosystem. It works with and without Spring Security, providing standard authentication flows as well as sophisticated standards compliant authorization flows (e.g. OAuth2 and OpenID Connect).
This article will show you how to build an application that serves up a REST API and an application that consumes that API. It’ll have a Spring Boot backend and an Angular frontend. It’ll use Stormpath to secure everything with an OAuth workflow and JWTs for tokens. This app will display a list of beers from the API, then fetch a GIF from http://giphy.com/ that matches the beer’s name. In a future article, I’ll show you how to turn this application into an PWA (Progressive Web Application) that works offline.
Build an API with Spring Boot
To build an API with Spring Boot, the easiest way to begin is to navigate to start.spring.io. In the “Search for dependencies” box, select the following:
- JPA
- H2
- Rest Repositories
- Web
- Security
- Stormpath
- DevTools
If you like the command-line better, you can use the following command to download a demo.zip
file with HTTPie.
1 2 |
http https://start.spring.io/starter.zip \ dependencies==data-jpa,data-rest,h2,web,devtools,security,stormpath -d |
The app you’ll create with have a client and a server. The client can be deployed anywhere that supports static sites, (like AWS), while the server will need to live somewhere that supports Java.
Create a directory called stormpath-spring-boot-angular-pwa
, with a server
directory in it. Expand the contents of demo.zip
into the server
directory. If you don’t have a Stormpath account, you’ll need to create one.
The Stormpath Spring Boot Quickstart shows how to create an API key; here’s the abridged version:
From the Home tab of the Admin Console select Manage API Keys under the Developer Tools heading. Click the Create API Key button to trigger a download of a apiKey-{API_KEY}.properties file. Move the file to ~/.stormpath/apiKey.properties
.
Open the “server” project in your favorite IDE and run DemoApplication
or start it from the command line using ./mvnw spring-boot:run
.
Create a com.example.beer
package and a Beer.java
file in it. This will be the entity that holds your data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
package com.example.beer; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Beer { @Id @GeneratedValue private Long id; private String name; public Beer() {} public Beer(String name) { this.name = name; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Beer{" + "id=" + id + ", name='" + name + '\'' + '}'; } } |
Add a BeerRepository
class that leverages Spring Data to do CRUD on this entity.
1 2 3 4 5 6 |
package com.example.beer; import org.springframework.data.jpa.repository.JpaRepository; interface BeerRepository extends JpaRepository<Beer, Long> { } |
Add a BeerCommandLineRunner
that uses this repository and creates a default set of data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package com.example.beer; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import java.util.stream.Stream; @Component public class BeerCommandLineRunner implements CommandLineRunner { private final BeerRepository repository; public BeerCommandLineRunner(BeerRepository repository) { this.repository = repository; } @Override public void run(String... strings) throws Exception { // top 5 beers from https://www.beeradvocate.com/lists/top/ Stream.of("Good Morning", "Kentucky Brunch Brand Stout", "ManBearPig", "King Julius", "Very Hazy", "Budweiser", "Coors Light", "PBR").forEach(name -> repository.save(new Beer(name)) ); System.out.println(repository.findAll()); } } |
Rebuild your project and you should see a list of beers printed in your terminal.
Add a @RepositoryRestResource
annotation to BeerRepository
to expose all its CRUD operations as REST endpoints.
1 2 3 4 5 |
import org.springframework.data.rest.core.annotation.RepositoryRestResource; @RepositoryRestResource interface BeerRepository extends JpaRepository<Beer, Long> { } |
Add a BeerController
class to create an endpoint that filters out less-than-great beers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package com.example.beer; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @RestController public class BeerController { private BeerRepository repository; public BeerController(BeerRepository repository) { this.repository = repository; } @GetMapping("/good-beers") public Collection<Map<String, String>> goodBeers() { return repository.findAll().stream() .filter(this::isGreat) .map(b -> { Map<String, String> m = new HashMap<>(); m.put("name", b.getName()); return m; }).collect(Collectors.toList()); } private boolean isGreat(Beer beer) { return !beer.getName().equals("Budweiser") && !beer.getName().equals("Coors Light") && !beer.getName().equals("PBR"); } } |
Re-build your application and navigate to http://localhost:8080/good-beers. You should be prompted to login.
After entering valid credentials, you should see the list of good beers in your browser.
You should also see this same result in your terminal window when using basic authentication and HTTPie.
1 |
http localhost:8080/good-beers --auth yourusername:yourpassword |
Create a project with Angular CLI
It’s cool that you created an API to display a list of beers, but APIs aren’t that cool without a UI. In this section, you’ll create a new Angular app, integration Stormpath for authentication, build services to fetch beers/images, and create components to display this data.
To create an Angular project, make sure you have Node.js and the Angular CLI installed.
1 2 3 |
npm uninstall -g angular-cli @angular/cli npm cache clean npm install -g @angular/cli@latest |
Run ng --version
to confirm you’re using version 1.0.0-beta.32.3 (or later). From a terminal window, cd into the root of the stormpath-spring-boot-angular-pwa
directory and run the following command.
1 |
ng new client |
This will create a new client
directory and run npm install
to install all the necessary dependencies. To verify everything works, run ng e2e
in a terminal window. If everything works, you should see output like the following in your terminal.
1 2 3 4 5 6 7 8 9 10 |
[09:02:35] I/direct - Using ChromeDriver directly... [09:02:35] I/launcher - Running 1 instances of WebDriver Spec started client App ✓ should display message saying app works Executed 1 of 1 spec SUCCESS in 0.77 sec. [09:02:38] I/launcher - 0 instance(s) of WebDriver still running [09:02:38] I/launcher - chrome #01 passed |
TIP: If you’re just getting started with Angular, you might want to watch a video of my resent Getting Started with Angular webinar.
If you’d rather not use the command line and have IntelliJ IDEA (or WebStorm) installed, you can create a new Static Web Project and select Angular CLI.
Open the client
project in your favorite editor.
Secure your Angular Application with Stormpath
Install Stormpath’s Angular SDK to make it possible to communicate with the secured server.
1 |
npm install --save angular-stormpath |
Modify app.module.ts
to import StormpathConfiguration
and StormpathModule
. Then create a function to configure the endpointPrefix
to point to http://localhost:8080
.
1 2 3 4 5 6 7 |
import { StormpathConfiguration, StormpathModule } from 'angular-stormpath'; export function stormpathConfig(): StormpathConfiguration { let spConfig: StormpathConfiguration = new StormpathConfiguration(); spConfig.endpointPrefix = 'http://localhost:8080'; return spConfig; } |
Add StormpathModule
to the imports in @NgModule
and use the stormpathConfig
function to override the default StormpathConfiguration
in the providers
list.
1 2 3 4 5 6 7 8 9 10 11 |
@NgModule({ … imports: [ ... StormpathModule ], providers: [{ provide: StormpathConfiguration, useFactory: stormpathConfig }], bootstrap: [AppComponent] }) |
Modify app.component.html
to add the Stormpath <sp-authport></sp-authport>
component and a section to show the user’s name and a logout link.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<sp-authport></sp-authport> <div *ngIf="(user$ | async)" class="row text-center"> <h2> Welcome, {{ ( user$ | async ).givenName }}! </h2> <ul class="nav nav-pills nav-stacked text-centered"> <li role="presentation" (click)="logout(); false"><a href="">Logout</a></li> </ul> </div> <div [hidden]="!(user$ | async)"> <!-- secure main component or <router-outlet></router-outlet> --> </div> |
You’ll notice the user$
variable in the HTML. In order to resolve this, you need to change your AppComponent
so it extends AuthPortComponent
.
1 2 3 |
import { AuthPortComponent } from 'angular-stormpath'; ... export class AppComponent extends AuthPortComponent { |
You can also inject the Stormpath
service into your component, subscribe to stormpath.user$
and implement logout()
yourself.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { Account, Stormpath } from 'angular-stormpath'; ... export class AppComponent { user$: Observable<Account | boolean>; constructor(private stormpath: Stormpath) { this.user$ = this.stormpath.user$; } logout(): void { this.stormpath.logout(); } } |
Make sure your server is started (with mvn spring-boot:run
in the server directory, and ng serve
in the client directory) and navigate to http://localhost:4200. You should see an error in your console that you means you have to configure cross-origin resource sharing (CORS) on the server.
1 2 3 4 |
XMLHttpRequest cannot load http://localhost:8080/me. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access. The response had HTTP status code 401. |
To fix this issue, you’ll need to configure Spring Boot to allow cross-domain access from http://localhost:4200
.
Configure CORS for Spring Boot
In the server project, add a property to application.properties
that allows cross-origin resource sharing (CORS) from the client (http://localhost:4200).
1 |
stormpath.web.cors.allowed.originUris = http://localhost:4200 |
After making these changes, you should be able to start the client and server apps and see a login screen on the Angular app.
After logging in with valid credentials, you should see the user’s first name and a logout link.
Add Bootstrap
The Stormpath Angular SDK’s default HTML templates useBootstrap CSS classes. If you add Bootstrap to your project, the login form will look much better. You can easily integrate Bootstrap by adding the following HTML to the <head>
of index.html
.
1 |
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> |
If you’d rather integrate Bootstrap using the Angular CLI way, install it using npm.
1 |
npm install --save bootstrap@3.3.7 |
Then modify .angular-cli.json
to add it to the styles
array:
1 2 3 4 5 6 |
... "styles": [ "../node_modules/bootstrap/dist/css/bootstrap.min.css", "styles.css" ], ... |
Both techniques will result in a more stylish login form.
Create a BeerListComponent and BeerService
Thus far, you’ve integrated Stormpath’s Angular SDK for authentication, but you haven’t created the UI to display the list of beers from your API. To do this, create a <beer-list>
component by running Angular CLI’s generate component
command.
1 2 3 4 5 6 7 |
$ ng generate component beer-list installing component create src/app/beer-list/beer-list.component.css create src/app/beer-list/beer-list.component.html create src/app/beer-list/beer-list.component.spec.ts create src/app/beer-list/beer-list.component.ts update src/app/app.module.ts |
TIP: There is a g
alias for generate
and a c
alias for component
, so you can type ng g c beer-list
too.
Create a beer
service:
1 2 3 4 5 |
$ ng g s beer installing service create src/app/beer.service.spec.ts create src/app/beer.service.ts WARNING Service is generated but not provided, it must be provided to be used |
Create a src/app/shared/beer
directory and move beer.service.*
into it.
1 2 |
mkdir -p src/app/shared/beer mv src/app/beer.service.* src/app/shared/beer/. |
Create a src/app/shared/index.ts
file and export the BeerService
.
1 |
export * from './beer/beer.service'; |
Modify beer.service.ts
to call the “good-beers” API service.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { Injectable } from '@angular/core'; import { Http, Response, RequestOptions } from '@angular/http'; import { Observable } from 'rxjs'; import { StormpathConfiguration } from 'angular-stormpath'; @Injectable() export class BeerService { constructor(public http: Http, public config: StormpathConfiguration) {} getAll(): Observable<any> { let options = new RequestOptions({ withCredentials: true }); return this.http.get(this.config.endpointPrefix + '/good-beers', options) .map((response: Response) => response.json()); } } |
TIP: If you don’t want to pass in withCredentials: true
, you can add the API URI as an autoAuthorizeUri
in StormpathConfiguration
.
1 2 3 4 5 6 |
export function stormpathConfig(): StormpathConfiguration { let spConfig: StormpathConfiguration = new StormpathConfiguration(); spConfig.endpointPrefix = 'http://localhost:8080'; spConfig.autoAuthorizedUris.push(new RegExp('http://localhost:8080/good-beers')); return spConfig; } |
Modify beer-list.component.ts
to use the BeerService
and store the results in a local variable. Notice that you need to add the service as a provider in the @Component
definition or you will see an error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { Component, OnInit } from '@angular/core'; import { BeerService } from '../shared/index'; @Component({ selector: 'beer-list', templateUrl: './beer-list.component.html', styleUrls: ['./beer-list.component.css'], providers: [BeerService] }) export class BeerListComponent implements OnInit { beers: Array<any>; constructor(private beerService: BeerService) { } ngOnInit() { this.beerService.getAll().subscribe( data => { this.beers = data; }, error => console.log(error) ) } } |
Modify beer-list.component.html
so it renders the list of beers.
1 2 3 4 5 |
<h2>Beer List</h2> <div *ngFor="let b of beers"> {{b.name}} </div> |
Update app.component.html
to have the BeerListComponent
rendered when you’re logged in. Note that I changed the generated selector from app-beer-list
to beer-list
.
1 2 3 |
<div [hidden]="!(user$ | async)"> <beer-list></beer-list> </div> |
Now when you login, you should see a list of beers from your Spring Boot API.
To make it look a little better, add a Giphy service to fetch images based on the beer’s name.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs'; @Injectable() // http://tutorials.pluralsight.com/front-end-javascript/getting-started-with-angular-2-by-building-a-giphy-search-application export class GiphyService { giphyApi = '//api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&q='; constructor(public http: Http) {} get(searchTerm): Observable<any> { let apiLink = this.giphyApi + searchTerm; return this.http.request(apiLink).map((res: Response) => { let giphies = res.json().data; return giphies[0].images.original.url; }); } } |
Add an export for this class in src/app/shared/index.ts
.
1 2 |
export * from './beer/beer.service'; export * from './giphy/giphy.service'; |
Then add it to BeerListComponent
to set a giphyUrl
on each beer
object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import { Component, OnInit } from '@angular/core'; import { BeerService, GiphyService } from '../shared/index'; @Component({ selector: 'beer-list', templateUrl: './beer-list.component.html', styleUrls: ['./beer-list.component.css'], providers: [BeerService, GiphyService] }) export class BeerListComponent implements OnInit { beers: Array<any>; constructor(private beerService: BeerService, private giphyService: GiphyService) { } ngOnInit() { this.beerService.getAll().subscribe( data => { this.beers = data; for (let beer of this.beers) { this.giphyService.get(beer.name).subscribe(url => beer.giphyUrl = url); } }, error => console.log(error) ) } } |
Then update beer-list.component.html
to include a reference to this image.
1 2 3 4 |
<div *ngFor="let b of beers"> {{b.name}}<br> <img width="100" src="{{b.giphyUrl}}" alt="{{b.name}}"> </div> |
The result should look something like the following list of beer names with images.
You’ve created an Angular app that talks to a Spring Boot API that is secured with Stormpath for authentication. Congratulations! Protecting both apps with Stormpath didn’t require much code and it was much easier than building the authentication yourself. If you’d like to learn more, see our Build vs. Buy whitepaper.
Source Code
You can find the source code associated with this article on GitHub. If you find any bugs, please file an issue, or leave a comment on this post. Of course, you can always ping me on Twitter too.
What’s Next?
This article showed you how to develop a Spring Boot backend, and lock it down with Stormpath. You learned how to develop an Angular front end and use Stormpath’s Angular SDK to communicate with the secure backend. In a future article, I’ll show you how to create a progressive web application and deploy it to the cloud.
To learn more about Angular, our Java SDK, or Stormpath, check out the following resources: