I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’ll be creating a login page using Spring Security with:

  • AngularJS
  • Angular 2, 4, 5, and 6

The example application which we’re going to discuss here consists of a client application that communicates with the REST service, secured with basic HTTP authentication.

2. Spring Security Configuration

First of all, let’s set up the REST API with Spring Security and Basic Auth:

Here is how it’s configured:

@Configuration
@EnableWebSecurity
public class BasicAuthConfiguration 
  extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth)
      throws Exception {
        auth
          .inMemoryAuthentication()
          .withUser("user")
          .password("password")
          .roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) 
      throws Exception {
        http.csrf().disable()
          .authorizeRequests()
          .antMatchers("/login").permitAll()
          .anyRequest()
          .authenticated()
          .and()
          .httpBasic();
    }
}

Now let’s create the endpoints. Our REST service will have two – one for login and the other for fetching the user data:

@RestController
@CrossOrigin
public class UserController {

    @RequestMapping("/login")
    public boolean login(@RequestBody User user) {
        return
          user.getUserName().equals("user") && user.getPassword().equals("password");
    }
	
    @RequestMapping("/user")
    public Principal user(HttpServletRequest request) {
        String authToken = request.getHeader("Authorization")
          .substring("Basic".length()).trim();
        return () ->  new String(Base64.getDecoder()
          .decode(authToken)).split(":")[0];
    }
}

Likewise, you can check out our other tutorial about Spring Security OAuth2 as well if you’re interested in implementing an OAuth2 server for authorization.

3. Setting Up the Angular Client

Now that we have created the REST service, let’s set up the login page with different versions of the Angular client.

The examples which we’re going to see here use npm for dependency management and nodejs for running the application.

Angular uses a single page architecture where all the child components (in our case these are login and home components) are injected into a common parent DOM.

Unlike AngularJS, which uses JavaScript, Angular version 2 onwards uses TypeScript as its main language. Hence the application also requires certain supporting files that are necessary for it to work correctly.

Due to the incremental enhancements of Angular, the files needed differs from version to version.

Let’s get familiar with each of these:

  • systemjs.config.js – system configurations (version 2)
  • package.json – node module dependencies (version 2 onwards)
  • tsconfig.json – root level Typescript configurations (version 2 onwards)
  • tsconfig.app.json – application level Typescript configurations (version 4 onwards)
  • .angular-cli.json – Angular CLI configurations (version 4 and 5)
  • angular.json – Angular CLI configurations (version 6 onwards)

4. Login page

4.1. Using AngularJS

Let’s create the index.html file and add the relevant dependencies to it:

<html ng-app="app">
<body>
    <div ng-view></div>

    <script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="//code.angularjs.org/1.6.0/angular.min.js"></script>
    <script src="//code.angularjs.org/1.6.0/angular-route.min.js"></script>
    <script src="app.js"></script>
    <script src="home/home.controller.js"></script>
    <script src="login/login.controller.js"></script>
</body>
</html>

Since this is a single page application, all the child components will be added to the div element with ng-view attribute based on the routing logic.

Now let’s create the app.js which defines the URL to component mapping:

(function () {
    'use strict';

    angular
        .module('app', ['ngRoute'])
        .config(config)
        .run(run);

    config.$inject = ['$routeProvider', '$locationProvider'];
    function config($routeProvider, $locationProvider) {
        $routeProvider.when('/', {
            controller: 'HomeController',
            templateUrl: 'home/home.view.html',
            controllerAs: 'vm'
        }).when('/login', {
            controller: 'LoginController',
            templateUrl: 'login/login.view.html',
            controllerAs: 'vm'
        }).otherwise({ redirectTo: '/login' });
    }

    run.$inject = ['$rootScope', '$location', '$http', '$window'];
    function run($rootScope, $location, $http, $window) {
        var userData = $window.sessionStorage.getItem('userData');
        if (userData) {
            $http.defaults.headers.common['Authorization']
              = 'Basic ' + JSON.parse(userData).authData;
        }

        $rootScope
        .$on('$locationChangeStart', function (event, next, current) {
            var restrictedPage
              = $.inArray($location.path(), ['/login']) === -1;
            var loggedIn
              = $window.sessionStorage.getItem('userData');
            if (restrictedPage && !loggedIn) {
                $location.path('/login');
            }
        });
    }
})();

The login component consists of two files, the login.controller.js, and the login.view.html.

Let’s look at the first one:

<h2>Login</h2>
<form name="form" ng-submit="vm.login()" role="form">
    <div>
        <label for="username">Username</label>
        <input type="text" name="username"
          id="username" ng-model="vm.username" required />
        <span ng-show="form.username.$dirty
          && form.username.$error.required">Username is required</span>
    </div>
    <div>
        <label for="password">Password</label>
        <input type="password"
          name="password" id="password" ng-model="vm.password" required />
        <span ng-show="form.password.$dirty
          && form.password.$error.required">Password is required</span>
    </div>
    <div class="form-actions">
        <button type="submit"
          ng-disabled="form.$invalid || vm.dataLoading">Login</button>
    </div>
</form>

and the second one:

(function () {
    'use strict';
    angular
        .module('app')
        .controller('LoginController', LoginController);

    LoginController.$inject = ['$location', '$window', '$http'];
    function LoginController($location, $window, $http) {
        var vm = this;
        vm.login = login;

        (function initController() {
            $window.localStorage.setItem('token', '');
        })();

        function login() {
            $http({
                url: 'http://localhost:8082/login',
                method: "POST",
                data: { 
                    'userName': vm.username,
                    'password': vm.password
                }
            }).then(function (response) {
                if (response.data) {
                    var token
                      = $window.btoa(vm.username + ':' + vm.password);
                    var userData = {
                        userName: vm.username,
                        authData: token
                    }
                    $window.sessionStorage.setItem(
                      'userData', JSON.stringify(userData)
                    );
                    $http.defaults.headers.common['Authorization']
                      = 'Basic ' + token;
                    $location.path('/');
                } else {
                    alert("Authentication failed.")
                }
            });
        };
    }
})();

The controller will invoke the REST service by passing the username and password. After the successful authentication, it’ll encode the username and password and store the encoded token in session storage for future use.

Similar to the login component, the home component also consists of two files, the home.view.html:

<h1>Hi {{vm.user}}!</h1>
<p>You're logged in!!</p>
<p><a href="#!/login" class="btn btn-primary" ng-click="logout()">Logout</a></p>

and the home.controller.js:

(function () {
    'use strict';
    angular
        .module('app')
        .controller('HomeController', HomeController);

    HomeController.$inject = ['$window', '$http', '$scope'];
    function HomeController($window, $http, $scope) {
        var vm = this;
        vm.user = null;

        initController();

        function initController() {
            $http({
                url: 'http://localhost:8082/user',
                method: "GET"
            }).then(function (response) {
                vm.user = response.data.name;
            }, function (error) {
                console.log(error);
            });
        };

        $scope.logout = function () {
            $window.sessionStorage.setItem('userData', '');
            $http.defaults.headers.common['Authorization'] = 'Basic';
        }
    }
})();

The home controller will request the user data by passing the Authorization header. Our REST service will return the user data only if the token is valid.

Now let’s install http-server for running the Angular application:

npm install http-server --save

Once this is installed, we can open the project root folder in command prompt and execute the command:

http-server -o

4.2. Using Angular Version 2, 4, 5

The index.html in version 2 differs slightly from the AngularJS version:

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <script src="node_modules/core-js/client/shim.min.js"></script>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
        System.import('app').catch(function (err) { console.error(err); });
    </script>
</head>
<body>
    <app>Loading...</app>
</body>
</html>

The main.ts is the main entry point of the application. It bootstraps the application module and as a result, the browser loads the login page:

platformBrowserDynamic().bootstrapModule(AppModule);

The app.routing.ts is responsible for the application routing:

const appRoutes: Routes = [
    { path: '', component: HomeComponent },
    { path: 'login', component: LoginComponent },
    { path: '**', redirectTo: '' }
];

export const routing = RouterModule.forRoot(appRoutes);

The app.module.ts declares the components and imports the relevant modules:

@NgModule({
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        routing
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        LoginComponent
    ],
    bootstrap: [AppComponent]
})

export class AppModule { }

Since we’re creating a single page application, let’s create a root component which adds all the child components to it:

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html'
})

export class AppComponent { }

The app.component.html will have only a <router-outlet> tag. The Angular uses this tag for its location routing mechanism.

Now let’s create the login component and its corresponding template in login.component.ts:

@Component({
    selector: 'login',
    templateUrl: './app/login/login.component.html'
})

export class LoginComponent implements OnInit {
    model: any = {};

    constructor(
        private route: ActivatedRoute,
        private router: Router,
        private http: Http
    ) { }

    ngOnInit() {
        sessionStorage.setItem('token', '');
    }

    login() {
        let url = 'http://localhost:8082/login';
        let result = this.http.post(url, {
            userName: this.model.username,
            password: this.model.password
        }).map(res => res.json()).subscribe(isValid => {
            if (isValid) {
                sessionStorage.setItem(
                  'token',
                  btoa(this.model.username + ':' + this.model.password)
                );
                this.router.navigate(['']);
            } else {
                alert("Authentication failed.");
            }
        });
    }
}

Finally, let’s have a look at the login.component.html:

<form name="form" (ngSubmit)="f.form.valid && login()" #f="ngForm" novalidate>
    <div [ngClass]="{ 'has-error': f.submitted && !username.valid }">
        <label for="username">Username</label>
        <input type="text"
          name="username" [(ngModel)]="model.username"
            #username="ngModel" required />
        <div *ngIf="f.submitted
          && !username.valid">Username is required</div>
    </div>
    <div [ngClass]="{ 'has-error': f.submitted && !password.valid }">
        <label for="password">Password</label>
        <input type="password"
          name="password" [(ngModel)]="model.password"
            #password="ngModel" required />
        <div *ngIf="f.submitted
          && !password.valid">Password is required</div>
    </div>
    <div>
        <button [disabled]="loading">Login</button>
    </div>
</form>

4.3. Using Angular 6

Angular team has made some enhancements in version 6. Due to these changes, our example will also be a little different compared to other versions. The only change we’ve in our example with respect to version 6 is in the service calling part.

Instead of HttpModule, the version 6 imports HttpClientModule  from @angular/common/http.

The service calling part will also be a little different from older versions:

this.http.post<Observable<boolean>>(url, {
    userName: this.model.username,
    password: this.model.password
}).subscribe(isValid => {
    if (isValid) {
        sessionStorage.setItem(
          'token', 
          btoa(this.model.username + ':' + this.model.password)
        );
	this.router.navigate(['']);
    } else {
        alert("Authentication failed.")
    }
});

5. Conclusion

We’ve learned how to implement a Spring Security login page with Angular. From version 4 onwards, we can make use of the Angular CLI project for easy development and testing.

As always all the example we’ve discussed here can be found over Github project.

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS