Authentisierung ist ein Thema, das so gut wie jede größere Webanwendung betrifft. In diesem Artikel und anhand einer Beispielanwendung erleben Sie, wie Spring Security, Spring Boot und AngularJS zusammengespielt können um ein Token-basierte Authentifizierung umzusetzen.

 

 Der Server-Side (REST Service) der Anwendung ist mit Spring Framework implementiert und für den Client-Side (Single Page Application SPA) ist AngularJS eingesetzt. Am Ende des Tutorials haben Sie eine Vorlage mir der folgenden Features, die Sie in Ihrer eigenen Arbeit zu Rate ziehen können:

 

  • Basiskonfiguration von Spring Security und Token Basierte Authentifizierung

  • Basiskonfiguration von Spring Boot um einen embedded Tomcat zu verwenden und den REST Service, mit dem booRepackage-Task von Gradle, als .jar-Datei zu exportieren

  • Einsetzung von Gradle als Build-Management-Tool

  • Basiskonfiguration AngularJS

  • Einfache Authorisierung

Spring Security

Bevor ich nun zur Sache komme, will ich mich zunächst einen allgemeinen Überblick zu Spring Security Komponente, die bei der Absicherung von Anwendungen eine Rolle spielen, verschaffen.

  • Der AuthenticationManager: ist für die Authentifizierung mit Benutzername und Passwort zuständig. Als Rückgabewert dient ein Authentication-Objekt, welches einen Authentifizierungs-Token und die Rollen des Benutzers beinhaltet.

  • AuthenticationProvider: der ProviderManager von Spring delegiert die Authentifizierung an AuthenticationProvider der für einen bestimmten Authentifizierungs-Mechanismus zuständig ist.

  • Filter: ist für sicherungsrelevante Aspekte der Applikation zuständig.

  • WebSecurityConfigurerAdapter: es bietet eine praktische Basisklasse für die Erstellung eines WebSecurityConfigurer-Instanz.

  • SecurityContextHolder: das ist das Objekt wo die Details des aktuellen Sicherheitskontexts der Anwendung gespeichert sind.

Der Authentifizierungsprozess

Im ersten Schritt ist es nötig die Spring Security Konfiguration zu erstellen. Hierfür erstelle ich die Klasse SecurityConfiguration (Listing 1) als Subklasse von WebSecurityConfigurerAdapter. Diese registriert, anhand der addFilterAfter()-Methode, den Filter AuthenticationProcessingFilter (Listing 2) (abgeleitet vom GenericFilterBean) der für alle sicherungsrelevanten Aspekte der Applikation (Absicherung der Urls, etc ...) zuständig ist.

 

 

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(new AuthenticationProcessingFilter(tokenService(), appUserStorage), BasicAuthenticationFilter.class)
            .and()
            .authorizeRequests()
            .antMatchers("/login").permitAll()
            .antMatchers("/task/update/**").hasRole("ADMIN")
            .anyRequest().hasRole("USER");
	}

 Die Login URL steht anonymen Benutzer zur Verfügung. Das Update eines Tasks erfordert aber dass der Benutzer angemeldet ist und die Rolle „ADMIN“ hat.

1.2 AuthenticationProcessingFilter

Der Codeausschnitt der doFilter() Methode der Klasse AuthenticationProcessingFilter (Listing 2) liest den Token vom X-Auth-Token HTTP-Header (dazu komme ich später im „AngularJS Authentication“ Abschnitt) und authentifiziert den Benutzer falls der Token nicht null und gültig ist.

 

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
  String token = request.getHeader("X-Auth-Token");
  if (token != null) {
      String jsonUserDetails = tokenService.extractJsonUserDetails(token);
      AppUser appUser = appUserStorage.getAppUser(jsonMapper.readValue(jsonUserDetails, AppUser.class));
      Date expirationDate=tokenService.extractExpirationDate(token);
      if (expirationDate != null && expirationDate.getTime() > System.currentTimeMillis()) {
          addUpdatedAuthTokenToResponse(jsonUserDetails, httpResponse);
          appUserStorage.storeAppUser(appUser);
      } else {
          throw new RestException("User athentication expired ", HttpStatus.UNAUTHORIZED);

1.3 Cross Site Request

Die Client- und Server-Teile unsere Anwendung laufen auf demselben Rechner auf unterschiedlichen Ports: REST Service (Spring Applikation) auf http://localhost:8080 und UI (Angular Applikation) auf http://localhost:8000.

Beim Senden einer http-Request (GET, POST, …) vom Front- zum Backend, wird eine Durchführung eines Cross-Domain-Requests gemäß der Cross-Origin Resource Sharing (CORS)-Spezifikation stattfinden.

Die Spezifikation sieht vor, jedem regulären Request einen OPTIONS-Request voranzustellen, mit dem erfragt wird, ob der eigentliche Request über die Domänengrenze hinweg erlaubt ist.

Das passiert mit der Prüfung der entsprechenden CORS-Header-Parameters (Listing 3), die die Request auch über Domänengrenzen hinweg erlauben.

 

httpResponse.setHeader("Access-Control-Allow-Origin", "*");
httpResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, HEAD");
httpResponse.setHeader("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,X-Auth-Token");
httpResponse.setHeader("Access-Control-Expose-Headers", "X-Auth-Token");

 Diese Parameter lassen sich, wie es in der AuthenticationProcessingFilter-Klasse schon gezeigt ist, über einem Servlet-Filter setzen.

Mit eingebundenem Filter schickt der Client dem OPTIONS-Request einen nachgestellten GET-Request hinterher, und die Daten werden als JSON-String an den Client geliefert:

OPTIONS http://localhost:8080/tasks/ [HTTP/1.1 200 OK 7ms]

GET http://localhost:8080/tasks/ [HTTP/1.1 200 OK 134ms]

 

1.4 AuthenticationController

 

Die authentication() und lougout() Methoden der AuthenticationController (Listing 4) erfassen die entsprechenden An- und Abmeldung-Requests der Anwendung.

 

 

public TokenResponse authenticate(User user, HttpServletRequest request) {
	UsernamePasswordAuthenticationToken usernamePasswordAuth = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
	Authentication authentication= authenticationManager.authenticate(usernamePasswordAuth);
	jsonUserDetails=jsonMapper.writeValueAsString(authentication.getPrincipal());
	return new TokenResponse(tokenService.createToken(jsonUserDetails));
	}
public MessageResponse logout(@PathVariable("username") String username, HttpServletRequest request, HttpServletResponse response) {
  	Authentication auth = SecurityContextHolder.getContext().getAuthentication();
	new SecurityContextLogoutHandler().logout(request, response, auth);
  	appUserStorage.removeAppUser(username);
return new MessageResponse("Successfull logout for User " + username);

Spring Boot

Der Codeausschnitt vom Listing 5 listet der Main Klasse unserer Spring Boot Applikation.

 

@EnableWebMvc
@SpringBootApplication
@ComponentScan
public class RestApplication {
    public static void main(String[] args) {
        System.setProperty("spring.profiles.default", System.getProperty("spring.profiles.default", "test"));
        SpringApplication.run(RestApplication.class, args);
    }
}

 Der größte Teil der Mächtigkeit von Spring Boot versteckt sich hinter der Annotation @SpringBootApplication. Wird eine Anwendung mit dieser und der @ComponentScan-Annotation gestartet, werden alle Klassen im Klassenpfad der Anwendung gescannt. Auf Basis der gefundenen Klassen wird eine Standard-Konfiguration erstellt und die notwendigen Spring Beans initialisiert.

Embedded Tomcat

Unsere Anwendung kommt ohne Application-Server aus, sie bringt einfach einen embedded Tomcat als Web Container mit. Mit dem Befehl gradle bootRepackage wird eine jar-Datei für das Deployment erzeugt. Diese Datei enthält den Web-Server und alle benötigten Bibliotheken und lässt sich mit dem Befehl java -jar <jar-file> starten.

Da die Applikation ihren eigenen Server mitbringt und eine zusätzliche Installation deshalb nicht nötig ist, kann man den Betrieb mit Spring Boot massiv vereinfachen. Konfigurationsprobleme beim Server oder Versionsinkompatibilitäten zwischen Application Server und Anwendung sind ausgeschlossen.

Gradle als Build-Management-Tool

Bevor ich mit der Vorstellung der Clientseite anfange, zeige ich im Listing 6 schließlich die Konfigurationszeilen (aus der build.gradle-Datei), die die Abhängigkeiten für die Spring Anwendung definieren.

 

apply plugin: 'java'
apply plugin: 'spring-boot'
dependencies {
    compile("org.springframework.boot:spring-boot-starter-security:1.2.5.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-data-jpa:1.2.5.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-web")
    compile('org.springframework:spring-context-support:4.1.7.RELEASE')
    compile('org.springframework.security:spring-security-config:3.2.8.RELEASE')
    compile('org.springframework.security:spring-security-web:3.2.8.RELEASE')
    compile('org.springframework.security:spring-security-jwt:1.0.3.RELEASE')
}
bootRepackage {
    mainClass = 'com.sambi.app.rest.RestApplication'
}

AngularJS Authentication

Da HTTP ein zustandsloses (stateless) Protokoll ist, und damit keine Sessions kennt, muss bei jeder Anfrage eine Authentisierungsinformation mitgeschickt werden. Damit sich ein Benutzer nur einmal pro Session anmelden muss, soll diese Information erhalten bleiben und automatisch bei jedem Request mitgeschickt werden.

Das heißt der Benutzer meldet sich bei der Anwendung an, und der Server ordnet dem authentifizierten Benutzer ein Token zu, das auf Serverseite gespeichert wird. Das Token wird dem Client übermittelt, der sich dieses merken muss, um es bei jeder weiteren Anfrage mitzuschicken. Der Server kann dann überprüfen, ob das Token gültig ist.

Unsere Single Page Application (Client-Side) schickt das vom Server erhaltene Token bei jedem Request mit. Dazu ist den HTTP-Header X-Auth-Token benutzt.

Damit sich der Benutzer bei einem Page-Reload (oder neuer Tab öffnen) nicht neu anmelden muss, habe ich das Token im Cookie abgelegt.

$cookies.putObject('token', token);

Möchte man den Header X-Auth-Token an alle Requests hängen, gibt es dafür das Objekt headers.

 

angular.module('myApp').factory('authenticationInterceptor', function ($q, authenticator) {
	...
	request: function (config) {
   		config.headers['X-Auth-Token'] = authenticator.getToken();
           return config;
   }
       response: function (response) {
           authenticator.setToken(response.headers('X-Auth-Token'));
           return response;

Die response() Methode (Listing 7) liest das vom Backend empfangenen Token und speichert es, anhand der setToken() Methode des authenticator Service, um es wieder im X-Auth-Token Header jeder AJAX-Request zu injizieren.

Der eingen definierten 'authenticationInterceptor' Service (Listing 7) ist ein HTTP-Interceptor der jede Response abfängt und entscheidet, ob die Response an die aufrufende Funktion weitergeleitet wird oder nicht. Die folgenden Zeilen zeigen wie dieser Service zum AngularJS $httpProvider.interceptors-Objekt hinzugefügt sein kann:

app.config(function ($httpProvider) {

$httpProvider.interceptors.push('authenticationInterceptor');

});

Auf abgelaufene Token reagieren

Was passiert wenn das Token abgelaufen oder verloren ist? In diesem Fall wird das 401 (nicht authentifizierend), 419 (Session Timeout) oder 440 (Session Timeout)HTTP-Statuscode vom REST Server auf die HTTP -Anfragen als Antwort geliefert. Die responseError() Methode (Listing 8) des authenticationInterceptors kümmert sich um der Verarbeitung dieser Antwort.

 

responseError: function(response) {
	if (response.status === 401 || response.status === 419 || response.status === 440) {
		$cookies.remove('token');
		$location.path('/login');
	}
	return $q.reject(response);

 Zusätzlich ist das Ereignis $stateChangeStart (Listing 9), im Konfiguration-Skript app.js, bei jeder Seite-Wechsel oder Aktualisierung gefeuert um die Authentifizierung und Authorisierung des Benutzers zu prüfen.

 

$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState) {
	var isAuthenticationRequired = !authenticator.getUser();
	if (isAuthenticationRequired) {
            $location.path('/login');
   }
 

Die im Listing 9 erwähnte getUser() Methode vom ‚authenticator‘ Service liefert das JSON Objekt zurück das die User-Daten enthält (Listing 10). Diese Daten sind im Objekt 'sub' im Token gespeichert.

getUser: function () {
	var token = this.getDecodedToken();
	if ((token !== null) && (token !== undefined)) {
   		return JSON.parse(token.sub);
   }
	return null;

 Der kompletten Quellcode der REST Service und GUI kann hier (https://github.com/rakia/spring-boot-security, https://github.com/rakia/angularjs-authentication) eingesehen werden.

Rakia Ben Sassi ist Software Engineer. Sie verfügt über langjährige Erfahrung im Bereich Webentwicklung mit Java, JavaScript und PHP und hat darüber hinaus Erfahrungen mit vielen Frameworks (z. B. Spring, Struts, Zend, AngularJS, jQuery und Extjs) gesammelt.

Wer bist du? - SAM Business Informatics