Last year, Micah Silverman wrote about integrating Spring Boot, Spring Security, and Stormpath. Today, I’m going to take you on a similar journey, except this time you’ll be using AngularJS and Stormpath’s AngularJS SDK for the UI. Along the way, you’ll learn how to create REST endpoints with Spring Data REST, configure Spring Boot to handle cross-domain requests, and use Stormpath to make authentication a breeze.
Get Started with AngularJS
To get started with AngularJS, I used to recommend cloning Angular’s angular-seed project. I wrote about developing a simple Angular app with angular-seed early last year. I followed that up with an article on testing your Angular app.
As part of a recent update to the JHipster mini-book, I created my own my angular-seed fork that uses UI-Router, Gulp, and Browsersync. This project will be the starting point for this tutorial. This tutorial assumes you have Node.js, Git and Gulp installed.
To begin, clone my fork of the angular-seed project and install all its dependencies:
1 2 3 4 |
git clone https://github.com/mraible/angular-seed.git angularjs-spring-boot-stormpath cd angularjs-spring-boot-stormpath npm i gulp |
TIP: You can also use Facebook’s yarn as an alternative to npm. It caches downloads and can be up to 11 times faster with a warm cache. I tried it on this project and found it took 20.63 seconds the first time I ran “yarn install”. The second time, it took 0.14 seconds! You can install yarn with npm using npm -g install yarn
.
After you’ve performed these steps, your browser should launch and you should see a screen like this:
You can modify one of its CSS files (e.g. app/css/app2.css
) to add some body padding:
1 2 3 |
body { padding: 10px; } |
When you save the file, Browsersync will auto-reload your changes and things look a bit better.
Angular has a $resource
service that allows you to easily make HTTP calls to a REST endpoint. In the following section, you’ll use $resource
, and a JSON file to create an application that allows searching and editing. After that, you’ll change to use Spring Boot for that endpoint. Finally, we’ll integrate Stormpath to provide the following:
- Registration
- Login
- Logout
- Forgot Password
- Only allow admins to search
Add a Search Feature
The AngularJS code below might look a bit different than you’re used to. This code uses John Papa’s Angular 1 Style Guide. This guide promotes an opinionated way to write AngularJS 1.x applications; allowing you to worry less about syntax and naming conventions and more about your code.
To create a new feature, you’ll need a few different files:
search.service.js
to interact with your datasearch.controller.js
to contain your controller logicsearch.html
to display your rendered datasearch.state.js
to contain routing information
Create app/search/search.service.js
and populate it with the following JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
(function () { 'use strict'; angular .module('myApp') .factory('SearchService', SearchService); SearchService.$inject = ['$resource']; function SearchService($resource) { return $resource('/api/search/people.json'); } })(); |
The $resource
service is not included by default in angular-seed. To add it to your project, add "angular-resource": "~1.5.0"
to your dependencies in bower.json
, or run the following command:
1 |
bower install angular-resource --save |
To activate this service in your application, add a link to angular-resource.js
in app/index.html
.
1 |
<script src="bower_components/angular-resource/angular-resource.js"></script> |
And reference it as a dependency in app/app.js
.
1 2 3 4 5 |
angular.module('myApp', [ 'ui.router', 'ngResource', 'myApp.view1', ... |
In addition to using $resource
, SearchService
reads from a people.json
file. Create app/api/search/people.json
with the following contents:
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 |
[ { "id": 1, "name": "Peyton Manning", "phone": "(303) 567-8910", "address": { "street": "1234 Main Street", "city": "Greenwood Village", "state": "CO", "zip": "80111" } }, { "id": 2, "name": "Demaryius Thomas", "phone": "(720) 213-9876", "address": { "street": "5555 Marion Street", "city": "Denver", "state": "CO", "zip": "80202" } }, { "id": 3, "name": "Von Miller", "phone": "(917) 323-2333", "address": { "street": "14 Mountain Way", "city": "Vail", "state": "CO", "zip": "81657" } } ] |
Now that the data and service are in place create app/search/search.html
to search and display data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<form ng-submit="vm.search()"> <input type="search" name="search" ng-model="vm.term"> <button>Search</button> </form> <table ng-show="vm.searchResults.length"> <thead> <tr> <th>Name</th> <th>Phone</th> <th>Address</th> </tr> </thead> <tbody> <tr ng-repeat="person in vm.searchResults"> <td>{{person.name}}</td> <td>{{person.phone}}</td> <td>{{person.address.street}}<br/> {{person.address.city}}, {{person.address.state}} {{person.address.zip}} </td> </tr> </tbody> </table> |
In this file, there are two “vm” (a.k.a. view-model) variables that the template expects in its controller: vm.term
and vm.searchResults
. Create app/search/search.controller.js
to expose these variables:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
(function () { 'use strict'; angular .module('myApp') .controller('SearchController', SearchController); SearchController.$inject = ['SearchService']; function SearchController(SearchService) { var vm = this; vm.search = function () { SearchService.query(vm.term, function (response) { var results = response.filter(function (item) { return JSON.stringify(item).toLowerCase().includes(vm.term.toLowerCase()); }); vm.searchResults = results; }); }; } })(); |
To make things look better, you can add some new CSS rules to app/css/app2.css
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
table { margin-top: 10px; border-collapse: collapse; width: 100%; } th { text-align: left; border-bottom: 2px solid #ddd; padding: 8px; } td { border-top: 1px solid #ddd; padding: 8px; } |
Add a link to the search feature in app/index.html
by adding a “search” menu item:
1 2 3 4 5 |
<ul class="menu"> <li><a ui-sref="view1">view1</a></li> <li><a ui-sref="view2">view2</a></li> <li><a ui-sref="search">search</a></li> </ul> |
To make this link work, you need to create a search.state.js
file in the search directory that configures the “search” state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
(function () { 'use strict'; angular.module('myApp') .config(stateConfig); stateConfig.$inject = ['$stateProvider']; function stateConfig($stateProvider) { $stateProvider .state('search', { url: '/search', templateUrl: 'search/search.html', controller: 'SearchController', controllerAs: 'vm' }); } })(); |
The final step to enabling search is making sure all the JavaScript files are referenced in app/index.html
:
1 2 3 |
<script src="search/search.state.js"></script> <script src="search/search.controller.js"></script> <script src="search/search.service.js"></script> |
At this point, you should be able to run “gulp”, click on the search link and perform a search. For example, below is a screenshot after searching for “Von”.
If you’ve made it this far, congratulations! If you encountered issues along the way, see the add search feature commit to see what’s changed since cloning the original angular-seed project.
Add an Edit Feature
For the edit feature, you’ll create similar files to what you did for the search feature.
edit.state.js
to contain routing informationedit.controller.js
to contain your controller logicedit.html
to display your rendered data
You’ll reuse the search.service.js
and add a new fetch
function for retrieving a single record.
1 2 3 4 5 6 7 8 |
Search.fetch = function (id, callback) { Search.query(function (response) { var results = response.filter(function (item) { return item.id === parseInt(id); }); return callback(results[0]); }); }; |
Create app/edit/edit.state.js
to route “/edit/{id}” URL to the EditController
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
(function () { 'use strict'; angular.module('myApp') .config(stateConfig); stateConfig.$inject = ['$stateProvider']; function stateConfig($stateProvider) { $stateProvider .state('edit', { url: '/edit/:id', templateUrl: 'edit/edit.html', controller: 'EditController', controllerAs: 'vm' }); } })(); |
Create app/edit/edit.controller.js
and create a simple controller that fetches a person’s record by the passed in identifier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
(function () { 'use strict'; angular .module('myApp') .controller('EditController', EditController); EditController.$inject = ['SearchService', '$stateParams']; function EditController(SearchService, $stateParams) { var vm = this; SearchService.fetch($stateParams.id, function (response) { vm.person = response; }); } })(); |
Add app/edit/edit.html
and populate it with the following HTML to display a person’s information.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<form> <div> <label for="name">Name:</label> <input type="text" ng-model="vm.person.name" id="name"> </div> <div> <label for="phone">Phone:</label> <input type="text" ng-model="vm.person.phone" id="phone"> </div> <fieldset> <legend>Address:</legend> <address style="margin-left: 50px"> <input type="text" ng-model="vm.person.address.street"><br/> <input type="text" ng-model="vm.person.address.city">, <input type="text" ng-model="vm.person.address.state" size="2"> <input type="text" ng-model="vm.person.address.zip" size="5"> </address> </fieldset> </form> |
Add a link to the edit state from app/search/search.html
:
1 2 3 |
<tr ng-repeat="person in vm.searchResults"> <td><a ui-sref="edit({id: person.id})">{{person.name}}</a></td> <td>{{person.phone}}</td> |
And add a link to the new JavaScript files you added in app/index.html
:
1 2 |
<script src="edit/edit.state.js"></script> <script src="edit/edit.controller.js"></script> |
TIP: If you get tired of adding <script>
tags in your index.html
, you can use gulp-inject to add new files automatically.
Finally, add some CSS in app/css/app2.css
to make the form look a bit better.
1 2 3 4 5 6 7 |
form { line-height: 2; } address { font-style: normal; } |
After making all these changes, you should be able to search for a person, click on their name and view their information.
Get Started with Spring Boot and Stormpath
Spring Initializr is a project that makes it super easy to get started with Spring Boot. It’s deployed at https://start.spring.io by default and Stormpath has an instance deployed at http://start.stormpath.io. Our instance has Stormpath Spring Boot starters available, and they should be available soon on the default instance.
To create an application with Spring Boot and Stormpath, go to http://start.stormpath.io and select the following dependencies: Web, JPA, H2, Stormpath Default, and DevTools. DevTools is a handy plugin for Spring Boot that allows you to hot-reload the application when you recompile any Java files.
Click “Generate Project” and download the resulting demo.zip file. Expand the file and copy its contents into the Angular project you created (e.g. angularjs-spring-boot-stormpath).
Because you’ve integrated Stormpath in this project, you’ll need a Stormpath account and API Keys to start the application. If you don’t have an account, go to https://api.stormpath.com/register and sign up. A developer account is free, with up to 10K API calls per month.
You’ll receive an email to activate your account. Click on the activation link and login to your account.
Click on the “Create API Key” button and copy the resulting file to ~/.stormpath/apiKey.properties
.
At this point, you should be able to start the Spring Boot app by running mvn spring-boot:run
. When you open http://localhost:8080 in your browser, you’ll be prompted to login using basic authentication. You can turn off basic authentication if you want (using security.basic.enabled = false
in src/main/resources/application.properties
), but you can also integrate Stormpath with Spring Security instead.
To add Stormpath support to Spring Security, create src/main/java/com/example/SecurityConfiguration.java
with the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.example; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import static com.stormpath.spring.config.StormpathWebSecurityConfigurer.stormpath; @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.apply(stormpath()); } } |
After recompiling this class and waiting for your application to reload, go to http://localhost:8080. You’ll be prompted to log in with Stormpath’s login form.
You won’t be able to log in because you haven’t created any accounts in your default application. Click on “Create Account” to create a new account. You can use the same email address as you did when you registered.
After completing registration, you should be able to log in using the email and password you entered.
NOTE: The above steps are covered in Stormpath’s Spring Boot Quickstart Guide.
You’ll likely see a 404 page from Spring Boot after you’ve successfully logged in.
This is because Stormpath redirects to / by default. To make it so you don’t see an error, create a homepage at src/main/resources/static/index.html
with the following HTML:
1 |
<h1>Hello World</h1> |
The auto-discovery of HTML files in this directory is covered in Spring Boot’s static content documentation. If you want to make your homepage dynamic, you have to create a Controller that serves up a Thymleaf template. Thymeleaf is enabled by default when using the Stormpath Spring Boot starter.
To see what this looks like, create src/main/java/com/example/HomeController.java
with the following code.
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 |
package com.example; import com.stormpath.sdk.account.Account; import com.stormpath.sdk.servlet.account.AccountResolver; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; @Controller public class HomeController { @RequestMapping("/") public String home(HttpServletRequest request, Model model) { String name = "World"; Account account = AccountResolver.INSTANCE.getAccount(request); if (account != null) { name = account.getGivenName(); model.addAttribute(account); } model.addAttribute("name", name); return "index"; } } |
Then create src/main/templates/index.html
to say hello to the user with their name.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!doctype html> <html xmlns:th="http://www.thymeleaf.org"> <body> <h1 th:text="'Hello, ' + ${name} + '!'"/> <div th:unless="${account}"> <a th:href="@{/login}" class="btn btn-primary">Login</a> </div> <div th:if="${account}"> <h4 th:text="'Account Store: ' + ${account.Directory.Name}"></h4> <h4 th:text="'Provider: ' + ${account.ProviderData.ProviderId}"></h4> <form id="logoutForm" th:action="@{/logout}" method="post"> <input type="submit" class="btn btn-danger" value="Logout"/> </form> </div> </body> </html> |
If you compile HelloController
and DevTools reloads your application, you’ll likely see the following error:
1 2 |
org.thymeleaf.exceptions.TemplateInputException: Error resolving template "index", template might not exist or might not be accessible by any of the configured Template Resolvers |
Restarting your application manually will solve this issue, but a better solution is to configure the Spring Boot Maven plugin to add resources.
1 2 3 4 5 6 7 |
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <addResources>true</addResources> </configuration> </plugin> |
This setting will allow you to modify templates and see the changes immediately without restarting your server.
If you’re still logged in, you should be able to refresh your browser and see a page like the one below.
You might notice that clicking the “Logout” button will prompt you to log in again. If you’d prefer to show the homepage and allow users to click a link to log in, you can allow this in your Spring Security configuration. Simply update SecurityConfiguration.java
to have the following. While you’re at it, add protection for the API endpoints you’re about to create.
1 2 3 4 5 6 7 |
@Override protected void configure(HttpSecurity http) throws Exception { http.apply(stormpath()).and() .authorizeRequests() .antMatchers("/api/**").fullyAuthenticated() .antMatchers("/**").permitAll(); } |
For more information on the features that Stormpath provides for Spring Boot and Spring Security, see A Simple Web App with Spring Boot, Spring Security, and Stormpath – in 15 Minutes.
Add an API Endpoint
To add an API for /people, you’re going to need some data first. Spring Boot’s Spring Data JPA provides an easy way to do this. You’ll need a JPA entity to represent your data, so create src/main/java/com/example/Person.java
.
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; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Person { private Long id; private String name; private String phone; private Address address; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } // getters and setters removed for brevity @Override public String toString() { return "Person{" + "id=" + id + ", name='" + name + '\'' + ", phone='" + phone + '\'' + ", address=" + address + '}'; } } |
Create an Address.java
class in the same directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package com.example; import javax.persistence.Embeddable; @Embeddable public class Address { private String street; private String city; private String state; private String zip; // getters and setters removed for brevity @Override public String toString() { return "Address{" + "street='" + street + '\'' + ", city='" + city + '\'' + ", state='" + state + '\'' + ", zip='" + zip + '\'' + '}'; } } |
Then create src/main/resources/data.sql
to create sample data on startup.
1 2 3 4 5 6 |
insert into person (name, phone, street, city, state, zip) values ('Peyton Manning', '(303) 567-8910', '1234 Main Street', 'Greenwood Village', 'CO', '80111'); insert into person (name, phone, street, city, state, zip) values ('Damaryius Thomas', '(720) 213-9876', '5555 Marion Street', 'Denver', 'CO', '80202'); insert into person (name, phone, street, city, state, zip) values ('Von Miller', '(917) 323-2333', '14 Mountain Way', 'Vail', 'CO', '81657'); |
Spring Data JPA provides JPA repositories that make it easy to CRUD an entity. The Spring Data REST project provides support for creating JPA repositories and exposing them as REST endpoints.
Add the Spring Boot starter for Spring Data REST to your pom.xml
.
1 2 3 4 |
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> |
Create a PersonRepository.java
file in src/main/java/com/example
that utilizes Spring Data REST.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.example; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import java.util.List; @RepositoryRestResource(collectionResourceRel = "people", path = "people") public interface PersonRepository extends PagingAndSortingRepository<Person, Long> { List<Person> findByName(@Param("name") String name); } |
To make all Spring Data REST endpoints have an /api prefix, add the following to src/main/resources/application.properties
.
1 |
spring.data.rest.basePath=/api |
Using HTTPie, you should be able to login via the command line using:
1 |
http -f POST localhost:8080/login login=mraible+stormpath@gmail.com password=<password> |
To see the data at /api/people, copy the access token from the result of the command above and use it in an Authorization header.
1 |
http localhost:8080/api/people Authorization:'Bearer <access_token>' |
You can also login with your browser and navigate to http://localhost:8080/api/people.
In this example, there’s a PersonRespository#findByName
method that allows you to search by a person’s full name. Below is an example call to this REST endpoint.
http://localhost:8080/api/people/search/findByName?name=Von%20Miller
You’ll notice this doesn’t provide full-text searching. Spring Data Elasticsearch is a good place to start if you’re looking for full-text searching.
Integrate AngularJS with Spring Boot
To integrate the AngularJS with Spring Boot, let’s first turn off Stormpath so you can access api/people without logging in. Modify SecurityConfiguration.java
to remove the stormpath() hook and allow all requests.
1 2 3 |
protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/**").permitAll(); } |
Next, modify app/search/search.service.js
to talk to http://localhost:8080/api/people, configure the ‘query’ function to not expect an array, and change the search function to handle the new data structure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function SearchService($resource) { var Search = $resource('http://localhost:8080/api/people', {}, { 'query': {isArray: false} }); Search.search = function (term, callback) { if (term == undefined) { // handle empty search term term = ''; } Search.query(function (response) { var people = response._embedded.people; var results = people.filter(function (item) { return JSON.stringify(item).toLowerCase().includes(term.toLowerCase()); }); return callback(results); }); }; // Search.fetch function return Search; } |
Fire up your Angular app using gulp
, then try its search feature. You’ll see an error in your console when you try to search. This happens because of your browser’s same-origin policy.
Spring Boot supports Cross-Origin Resource Sharing (CORS) to help solve this issue. However, it only works for Spring MVC, not Spring Data REST (see DATAREST-573 for more information). The good news is the Spring Framework provides a CorsFilter you can use for filter-based frameworks. Add the following to SpringConfiguration.java
.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("http://localhost:3000"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(0); return bean; } |
After making these changes and waiting for your application to reload, you should be able to search people, just like you did before.
Enable Stormpath
To make everything work with Stormpath enabled, there’re several things that need to happen:
- Re-enable Stormpath in SecurityConfiguration.java
- Add Stormpath’s AngularJS SDK to the Angular app
- Create pages and controllers for Login, Logout, Registration, and Forgot Password
Revert the changes you made in SecurityConfiguration.java
to allow all requests.
1 2 3 4 5 6 7 8 9 10 |
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.apply(stormpath()).and() .authorizeRequests() .antMatchers("/api/**").fullyAuthenticated() .antMatchers("/**").permitAll(); } } |
To add Stormpath’s AngularJS SDK to your project, you can use Bower.
1 |
bower install stormpath-sdk-angularjs --save |
Add these new files to app/index.html
:
1 2 |
<script src="bower_components/stormpath-sdk-angularjs/dist/stormpath-sdk-angularjs.min.js"></script> <script src="bower_components/stormpath-sdk-angularjs/dist/stormpath-sdk-angularjs.tpls.min.js"></script> |
Open app/app.js
and add these two Stormpath dependencies at the bottom of the list.
1 2 3 4 5 6 7 8 9 |
angular.module('myApp', [ 'ui.router', 'ngResource', 'myApp.view1', 'myApp.view2', 'myApp.version', 'stormpath', 'stormpath.templates' ]). |
Modify the config block in this file to configure Stormpath to point to Spring Boot on http://localhost:8080.
1 2 3 4 5 6 7 |
config(['$stateProvider', '$urlRouterProvider', 'STORMPATH_CONFIG', function($stateProvider, $urlRouterProvider, STORMPATH_CONFIG) { // For any unmatched url, redirect to /view1 $urlRouterProvider.otherwise('/view1'); STORMPATH_CONFIG.ENDPOINT_PREFIX = 'http://localhost:8080'; }]). |
In the same app.js
file, add a run block, place it below the config block (make sure you move the semicolon from the config block to the run block):
1 2 3 4 5 6 |
run(['$stormpath', function($stormpath){ $stormpath.uiRouter({ loginState: 'login', defaultPostLoginState: 'view1' }]); }); |
This configures Stormpath to do the following:
- Redirect users to the login view if they try to access a restricted view. After logging in, they are sent back to the view that they originally requested.
- Send users to the view1 view after login if they have visited the login page directly (they did not try to access a restricted view first).
Modify app/index.html
to adjust the menu so only logged in users can access view2 and the search feature. This code makes use of the if-user
and if-not-user
directives, which are documented in the AngularJS SDK docs.
1 2 3 4 5 6 7 |
<ul class="menu"> <li><a ui-sref="view1">view1</a></li> <li if-user><a ui-sref="view2">view2</a></li> <li if-user><a ui-sref="search">search</a></li> <li if-not-user><a ui-sref="login">Login</a></li> <li if-user><a href="" sp-logout>Logout</a> </ul> |
NOTE: There is an issue in the Stormpath’s Java SDK where if-user-in-group=”ADMIN” doesn’t work. A fix will be available in a future release. In the meantime, there’s a workaround in Stormpath’s AngularJS SDK.
Since no “login” state exists yet, you’ll need to create one. Create app/login/login.state.js
and populate it with the following.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(function () { 'use strict'; angular.module('myApp') .config(stateConfig); stateConfig.$inject = ['$stateProvider']; function stateConfig($stateProvider) { $stateProvider .state('login', { url: '/login', templateUrl: 'login/login.html' }); } })(); |
No controller is needed because Stormpath provides the Controller for you. Create app/login/login.html
and use the spLoginForm directive to render the login form.
1 |
<div sp-login-form></div> |
Add login.state.js
to index.html
.
1 |
<script src="login/login.state.js"></script> |
At this point, I noticed my console said I was not allowed to access http://localhost:8080/me because my origin (http://localhost:3000/me) was not supported. After debugging, I figured out this was because the main StormpathFilter
comes before the CorsFilter
. To fix this, add the following to application.properties
.
1 |
stormpath.web.stormpathFilter.order=1 |
After making this change, you should be able to hit http://localhost:3000 and click the Login link.
It doesn’t look great by default, but that’s because the default template expects Bootstrap to be one of the CSS files. Add Bootstrap with Bower (bower install bootstrap --save
), add a <link>
in app/index.html
, and the form becomes a lot prettier.
1 |
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"> |
You should be able to login with the username and password combination you registered with earlier.
NOTE: There’s an issue when trying to logout cross-domain with the Stormpath’s Spring Boot integration. We hope to have a fix soon. In the meantime, refreshing your browser after clicking the Logout link will show you have successfully logged out.
Unlike the Spring Boot Thymeleaf templates, the AngularJS spLoginForm
directive does not include a link to register. You can add this above the form by modifying app/login/login.html
and adding the following HTML at the top.
1 2 3 |
<h2 class="col-sm-offset-2" style="margin-bottom: 30px"> Login or <a href="" ui-sref="register">Create Account</a> </h2> |
After making this change, the login form looks more like the Thymeleaf version.
However, the “Create Account” link won’t work since there is no “register” state yet. To create it, add app/register/register.state.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(function () { 'use strict'; angular.module('myApp') .config(stateConfig); stateConfig.$inject = ['$stateProvider']; function stateConfig($stateProvider) { $stateProvider .state('register', { url: '/register', templateUrl: 'register/register.html' }); } })(); |
Create a register.html
file in the same directory.
1 2 3 4 5 6 |
<div class="container-fluid"> <h2 class="col-sm-offset-2" style="margin-bottom: 30px"> Create Account </h2> <div sp-registration-form></div> </div> |
TIP: Wrapping the template in a .container-fluid
class removes the horizontal scrolling you can see in the previous screenshot.
Add a reference to register.state.js
in app/index.html
.
1 |
<script src="register/register.state.js"></script> |
Now clicking on the “Create Account” link should render a registration form.
When I first tried to register using this form, I received a strange error.
1 2 3 |
Unable to invoke Stormpath controller: Unable to read JSON value from request body: No content to map due to end-of-input at [Source: org.apache.catalina.connector. CoyoteInputStream@19b6af88; line: 1, column: 0] |
To fix this, I had to modify app/app.js
and set the form content type to application/json
.
1 2 |
STORMPATH_CONFIG.ENDPOINT_PREFIX = 'http://localhost:8080'; STORMPATH_CONFIG.FORM_CONTENT_TYPE = 'application/json'; |
There is an open issue to remove this extra step in Stormpath’s AngularJS SDK.
The last step is to configure the state for the Forgot Password feature. Create app/forgot/forgot.state.js
and populate it with the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(function () { 'use strict'; angular.module('myApp') .config(stateConfig); stateConfig.$inject = ['$stateProvider']; function stateConfig($stateProvider) { $stateProvider .state('forgot', { url: '/forgot', templateUrl: 'forgot/forgot.html' }); } })(); |
Create forgot.html
in the same directory and use the spPasswordResetRequestForm directive in it.
1 2 3 4 5 6 |
<div class="container-fluid"> <h2 class="col-sm-offset-2" style="margin-bottom: 30px"> Forgot your password? </h2> <div sp-password-reset-request-form></div> </div> |
Don’t forget to add a reference to this new state in app/index.html
.
1 |
<script src="forgot/forgot.state.js"></script> |
Now, you should be able to click the Forgot Password link in the Login form. However, the current release of Stormpath’s AngularJS SDK doesn’t support Angular’s hashbang mode and expects HTML5 mode. The Forgot Password link doesn’t work in hashbang mode.
The easy way to workaround this is to pass in a $locationProvider
to the config block in app/app.js
and set HTML5 mode to true.
1 |
$locationProvider.html5Mode(true); |
You’ll also need to add <base href="/">
to the <head>
of app/index.html
to make HTML5 mode work. This does cause issues with Browsersync reloading; I haven’t figured out a solution for that yet.
If you don’t want to use HTML5 mode in your AngularJS application, you can override the templates to provide relative links. For example, you can change login.html
to point to your customized template.
1 |
<div sp-login-form template-url="app/stormpath/login.tpl.html"></div> |
Then you can create app/stormpath/login.tpl
and tweak the default template so the registration link uses UI Router’s ui-sref
directive.
1 |
<a ui-sref="forgot" class="pull-right">Forgot Password</a> |
Both methods will allow you to navigate to the “forgot” state when clicking on the “Forgot Password” link.
Summary
I hope you’ve enjoyed this tour of AngularJS, Spring Boot, and Stormpath. I showed you how to do a number of things in this article:
- Create a simple AngularJS application with a search feature
- Create a Spring Boot application with Stormpath integrated
- Communicate between the AngularJS and Spring Boot apps cross-domain
- Add login, registration, and forgot password features to the AngularJS application
The source code for the completed application referenced here is available at https://github.com/stormpath/stormpath-angularjs-spring-boot-example.
In future posts, I’ll talk about how the Stormpath’s AngularJS SDK can be used to prevent access to certain states and hide links to different groups. I also plan to show you how to integrate Stormpath into JHipster.
As far as Stormpath’s Angular 2 support—we’re working on it! We hope to have something for you to experiment with by Thanksgiving (November 24). Update: We released a beta version of our Angular 2 support on November 14!
If you have any questions, don’t hesitate to leave a comment or hit me up on Twitter at @mraible.
References
The good Dr. Dave Syer wrote an excellent blog series in 2015 on integrating AngularJS with Spring Boot and Spring Security.