Spring Security Abstract

Die Verwendung von Spring Security

Das initiale Projekt

Da Spring Security Teil von Spring Boot ist, legen wir zunächst ein neues Spring Boot Projekt an. Intellij bietet uns durch den `Spring Initializr` dabei Unterstützung an. Nach dem Ausfüllen diverser Dialogfenster besteht das initiale Projekt aus einer Startklasse, einer Testklasse, der `pom.xml` und der `application.properties`. Da ich in diesem Beitrag weder auf Tests eingehe, noch die Default-Einstellungen von Spring verändern werde, ignorieren wir die Testklasse und `application.properties` und widmen uns den zwei verbleibenden Dateien. Die Startklasse, die einen beliebigen Namen haben kann, besitzt die Annotation `@SpringBootApplication` und startet unsere Anwendung in einer main-Methode:
package de.micromata.spring.security.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringSecurityApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringSecurityApplication.class, args);
	}
}
Die `pom.xml` referenziert die Spring Boot pom als `parent` und besitzt initial zwei Abhängikeiten. Da eine dieser Abhängikeiten zum Entwickeln von Tests benötigt wird, habe ich diese entfernt. Das Ergebnis ist nachfolgend zu sehen:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>de.micromata.spring.security.example</groupId>
	<artifactId>spring-security</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>spring-security</name>
	<description>Spring Security Talk</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.4.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>
Wenn wir versuchen diese intitale Anwendung zu starten, dann erhalten wir die Meldung `Unregistering JMX-exposed beans on shutdown`. Dies liegt daran, dass wir noch keinen Webcontainer definiert haben. Hierzu reicht es folgende Abhängigkeit einzubinden:
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Per Standard wird nun ein integrierter Tomcat verwendet, um die Applikation bereitzustellen, weshalb diese gestartet und unter [localhost:8080](localhost:8080) erreicht werden kann. Aktuell erhalten wir jedoch nur eine 404, da wir noch keinen Controller beitzen. Dies holen wir nun nach:
package de.micromata.spring.security.example.controlller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DefaultController {

    @RequestMapping("/")
    public String index() {
        return "You can only see this, if you are logged in!";
    }

    @RequestMapping("/noSecurity")
    public String noSecurity() {
        return "Everybody can see this!";
    }

}
Der Controller bietet uns zwei Endpunkte mit jewils unterschiedlichen Texten an. Die Annotation `@RestController` sorgt dafür, dass diese Texte direkt in den Browser gerendert werden.

Einführung von Spring Security

In modernen Anwendungen ist Security ein wichtiger Faktor. Aus diesem Grund führen wir nun Spring Security in unsere Anwendung ein. Dazu binden wir zunächst folgende Abhängigkeit ein:
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Außerdem legen wir eine Klasse an, die von `WebSecurityConfigurerAdapter` erbt:
package de.micromata.spring.security.example.securitiy.conf;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/noSecurity").permitAll()
            .anyRequest().authenticated()
            .and().formLogin().permitAll();
    }

    @Autowired
    public void globalSecurityConfiguration(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
        auth.inMemoryAuthentication().withUser("admin").password("password").roles("USER","ADMIN");
    }

}
Wichtig an dieser Stelle ist, dass wir die Methode `configure` überschreiben. Hier defieren wir `/noSecurity` als Endpunkt, der von allen erreicht werden soll. Für alle anderen wird ein Form-Login vorrausgesetzt. In der Methode `globalSecurityConfiguration` konfigurieren wir eine In-Memory-Authentication für die Benutzer `user` und `admin`. Der Name der Methode ist dabei irrelevant. Sie hätte genausogut `test` genannt werden können. Wichtig ist, dass durch sie der `AuthenticationManagerBuilder` konfiguriert wird. Wird die Anwendung durchgestert, dann stellen wir fest, dass nun ein Login notwendig ist, um die Indexseite zu öffnen.

Thymeleaf und Spring Security

Viele moderne Anwendungen stellen eigene Ansprüche an die Loginseite. Aus diesem Grund führen wir nun Thymleaf ein, um mit dessen Hilfe eine eigene Login Seite zu erstellen. Dazu binden wir zunächst folgende Abhängigkeit ein:
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Außerdem passen wir die `configure` Methode der Klasse `WebSecurityConfig` leicht an:
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/noSecurity").permitAll()
        .anyRequest().authenticated()
        .and().formLogin().loginPage("/login").permitAll()
        .and().logout().permitAll();
}
Hier haben wir nun definiert, dass unsere Loginseite unter dem Endpunkt `/login` aufgerufen wird und dass der Logout von allen ausgeführt werden kann. Die letzten Änderungen haben wir an unserem Controller vorgenommen:
package de.micromata.spring.security.example.controlller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class DefaultController {

    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @RequestMapping("/login")
    public String login() {
        return "login";
    }

    @RequestMapping("/noSecurity")
    public String noSecurity() {
        return "noSecurity";
    }

}
Hier sehen wir nun den Endpunkt `login`. Außerdem besitzt unser Controller nun anstelle von `@RestController` die Annotation `@Controller`. Dies führt dazu, dass der String nicht mehr in den Browser gerendert wird, sondern unter `/resources/templates` nach einem Template mit dem entsprechenden Namen gesucht wird. Nachfolgend sind die Templates für unsere drei Endpunkte aufgelistet.
`index.html`:

<html xmlns:th="http://www.thymeleaf.org">
    <head th:replace="fragments/headerAndNav :: header"/>
    <body>
        <div th:replace="fragments/headerAndNav :: navbar"/>
        <div class="container">
            You can only see this, if you are logged in!
        </div>
    </body>
</html>


`noSecurity.html`:

<html xmlns:th="http://www.thymeleaf.org">
    <head th:replace="fragments/headerAndNav :: header"/>
    <body>
        <div th:replace="fragments/headerAndNav :: navbar"/>
        <div class="container">
            Everybody can see this!
        </div>
    </body>
</html>


`login.html`:

<html xmlns:th="http://www.thymeleaf.org">
    <head th:replace="fragments/headerAndNav :: header"/>
    <body>
        <div th:replace="fragments/headerAndNav :: navbar"/>
        <div class="container">
            <form name="f" th:action="@{/login}" method="post">
                <fieldset>
                    <legend>Please Login</legend>
                    <div th:if="${param.error}" class="alert alert-error">
                        Invalid username and password.
                    </div>
                    <div th:if="${param.logout}" class="alert alert-success">
                        You have been logged out.
                    </div>
                    <div class="form-group">
                        <label for="username">Username</label>
                        <input type="text" id="username" name="username"/>
                    </div>
                    <div class="form-group">
                        <label for="password">Password</label>
                        <input type="password" id="password" name="password"/>
                    </div>
                    <button type="submit" class="btn btn-primary">Log in</button>

                </fieldset>
            </form>
        </div>
    </body>
</html>
Sowie dem Fragment `headerAndNav`, welches alle drei Templates einbinden und das unter `/resources/templates/fragments` zu finden ist:
<html xmlns:th="http://www.thymeleaf.org" xmlns:tiles="http://www.thymeleaf.org">
    <head th:fragment="header">
        <title tiles:fragment="title">Spring Security Example</title>
        <link rel="stylesheet" 
        href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
        integrity=
        "sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7"
        crossorigin="anonymous"/>
    </head>
    <body>
        <nav class="navbar navbar-inverse" th:fragment="navbar">
            <div class="container-fluid" >
                <div class="navbar-inner" 
                th:with="currentUser=${#httpServletRequest.userPrincipal?.name}">
                    <a class="navbar-brand" th:href="@{/}">Home</a>
                    <a class="navbar-brand" th:href="@{/noSecurity}">No Security</a>
                    <div th:if="${currentUser != null}">
                        <form class="navbar-form navbar-right" 
                        th:action="@{/logout}" method="post">
                            <input type="submit" class="btn btn-primary" 
                            value="Log out" />
                        </form>
                        <p class="navbar-text navbar-right" th:text="${currentUser}">
                            example_user
                        </p>
                    </div>
                </div>
            </div>
        </nav>
    </body>
</html>
Wenn wir nun die Anwendung durchstarten und [localhost:8080](localhost:8080) aufrufen, sehen wir eine etwas hübschere Loginseite. Außerdem haben wir nun nach erfolgreichem Login einen Logout-Button zur Verfügung, über den wir uns wieder ausloggen können. Abschließend zu Thymleaf wäre zu erwähnen, dass die Spring-Starter Abhängigkeit ein altes Thymeleaf in der Version 2.1.5 anzieht. Es kann jedoch ohne großen Aufwand auf ein aktuelles Thymeleaf gewechselt werden. Dazu wird die Starter Abhängikeit einfach wieder entfernt und folgende Abhängigkeiten hinzugefügt:
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf</artifactId>
    <version>3.0.7.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring4</artifactId>
    <version>3.0.7.RELEASE</version>
</dependency>
JPA

Neben einer eigenen Loginseite besitzen heutige Webanwendungen für gewöhnlich auch eigene User. Aus diesem Grund führen wir nun JPA ein. Dazu binden wir folgende Abhängigkeiten ein:
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
</dependency>
Außerdem legen wir unsere User-Entity an:
package de.micromata.spring.security.example.data;

import org.hibernate.validator.constraints.NotEmpty;

import javax.persistence.*;

@Entity
public class User {

    @Id
    @GeneratedValue()
    private long id;

    @NotEmpty(message = "username is required")
    @Column(unique = true)
    private String username;

    @NotEmpty(message = "password is required")
    private String password;

    protected User() {}

    public User(String userName, String password) {
        this.username = userName;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
Sowie ein Repository für den Zugriff darauf:
package de.micromata.spring.security.example.data;

import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User, Long> {
    User findByUsername(String username);
}
Um die Implementation von `findByUsername` brauchen wir uns Dank der Vererbung von `CrudRepository` nicht zu kümmenrn. Dies macht Spring für uns. Schauen wir uns nun die Änderungen an der Methode `globalSecurityConfiguration` der Klasse `WebSecurityConfig` an:
@Autowired
private UserDetailsService userDetailsService;

@Autowired
public void globalSecurityConfiguration(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);
}
Wie wir sehen wurde die In-Memory-Authentication auf den `UserDetailsService` umgestellt. Dies stellt eine Schnittstelle für einen eigenen Authentifizierungsprozess dar. Das Interface `UserDetailsService` sieht dabei wie folgt aus:
package org.springframework.security.core.userdetails;

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Wie wir sehen muss lediglich die Methode `loadUserByUsername` implementiert werden, welche `UserDetails` zurückgibt, was ebenfalls ein Interface ist:
package org.springframework.security.core.userdetails;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import java.io.Serializable;
import java.util.Collection;

public interface UserDetails extends Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();

	String getPassword();

	String getUsername();

	boolean isAccountNonExpired();

	boolean isAccountNonLocked();

	boolean isCredentialsNonExpired();
	
	boolean isEnabled();
}
Unsere Implementation von UserDetailsService sieht dabei wie folgt aus:
package de.micromata.spring.security.example.securitiy.user;

import de.micromata.spring.security.example.data.User;
import de.micromata.spring.security.example.data.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class AuthenticatedUserService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("The user " + username + " does not exist");
        }
        return new AuthenticatedUser(user);
    }
}
Hier sehen wir nun auch den Aufruf der Methode `findByUsername` unseres `UserRepository`. `UserDetails` implementieren wir mit `AuthenticatedUser`, welches von unserer User-Entity erbt:
package de.micromata.spring.security.example.securitiy.user;

import de.micromata.spring.security.example.data.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class AuthenticatedUser extends User implements UserDetails {
    protected AuthenticatedUser(User user) {
        super(user.getUsername(), user.getPassword());
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return AuthorityUtils.createAuthorityList("ROLE_USER");
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
Damit wir uns einloggen können, benötigen wir jedoch noch Daten in unserer H2-Datenbank. Dazu legen wir die Datei `resources/data.sql` an, welches von Spring automatisch angezogen und ausgeführt wird:
insert into user(id,username,password) values (0,'user','password');
insert into user(id,username,password) values (1,'admin','password');
Nun können wir uns wie gehabt über `user:password` in unsere Anwendung einloggen.

Rollen und Rechte

User allein reichen für moderne Webanwendungen jedoch meistens nicht aus. Aus diesem Grund führen wir nun Rollen und Rechte ein. Für die Rollen haben wir ein entsprechendes Feld in unsere User-Entity hinzugefügt:
package de.micromata.spring.security.example.data;

import org.hibernate.validator.constraints.NotEmpty;

import javax.persistence.*;

@Entity
public class User {

    @Id
    @GeneratedValue()
    private long id;

    @NotEmpty(message = "username is required")
    @Column(unique = true)
    private String username;

    @NotEmpty(message = "password is required")
    private String password;

    @NotEmpty(message = "role is required")
    private String role;

    protected User() {}

    public User(long id, String userName, String password, String role) {
        this.id = id;
        this.username = userName;
        this.password = password;
        this.role = role;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }
}
Sowie unsere Implementierung von `UserDetails` angepasst:
package de.micromata.spring.security.example.securitiy.user;

import de.micromata.spring.security.example.data.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class AuthenticatedUser extends User implements UserDetails {
    protected AuthenticatedUser(User user) {
        super(user.getId(), user.getUsername(), user.getPassword(), user.getRole());
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return AuthorityUtils.createAuthorityList(getRole());
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
Wie wir sehen wird nun nicht mehr fix `ROLE_USER` als Role zurückgegeben, sondern der Wert, der in der Datenbank hinterlegt ist. Um Rechte zu Demonstrieren führen wir nun Nachrichten in unsere Anwendung ein. Dazu legen wir eine weitere Entity an:
package de.micromata.spring.security.example.data;

import org.hibernate.validator.constraints.NotEmpty;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.validation.constraints.NotNull;

@Entity
public class Message {
    @Id
    @GeneratedValue()
    private long id;

    @OneToOne
    @NotNull
    private User user;

    @NotEmpty(message = "text is required")
    private String text;

    @NotEmpty(message = "title is required")
    private String title;

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }
}
Sowie das dazugehörige Repository:
package de.micromata.spring.security.example.data;

import org.springframework.data.repository.CrudRepository;
import org.springframework.security.access.prepost.PostAuthorize;

public interface MessageRepository extends CrudRepository<Message, Long> {

    @PostAuthorize("hasPermission(returnObject, 'message')")
    Message findById(Long id);

    Iterable<Message> findByUserId(Long id);

    @PostAuthorize("hasPermission(returnObject, 'privateMessage')")
    Message findOne(Long id);

}
Hier sehen wir bereits die Annotation `PostAuthorize`, die sich um die Zugriffsrechte kümmert. Die Methode `findOne` und `findById` werden von Spring identisch implementiert. Dies hilft uns dabei zu veranschaulichen, was passiert, wenn `message` oder `privateMessage` an `hasPermission` übergeben wird. Doch zunächst müssen wir die Annotaion aktivieren. Dazu müssen wir die Annotation `@EnableGlobalMethodSecurity(prePostEnabled = true)` zur Klasse `WebSecurityConfig` hinzufügen. Anschließend müssen wird das Interface `PermissionEvaluator` implementieren, dessen `hasPermission` Methode durch `PostAuthorize` aufgerufen wird:
package de.micromata.spring.security.example.securitiy.permission;

import de.micromata.spring.security.example.data.Message;
import de.micromata.spring.security.example.data.User;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.io.Serializable;

@Component
public class SimplePermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        if (authentication == null) {
            return false;
        }
        Message message = (Message) targetDomainObject;
        if (message == null) {
            return true;
        }
        User user = (User) authentication.getPrincipal();

        if (user.getId() == message.getUser().getId()) {
            return true;
        }

        if ("privateMessage".equals(permission)) {
            return false;
        } else if ("message".equals(permission) && "ROLE_ADMIN".equals(user.getRole())) {
            return true;
        } else {
            return false;
        }
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}
Hier sehen wir nun den Unterschied zwischen `privateMessage` und `message` als Parameter von `hasPermission`. Übergeben wir `privateMessage`, so ist nur dem Besitzer der Nachricht der Zugriff gestattet. Dies bedeutet, dass `findOne` in `MessageRepository` ausschließlich vom Beitzer selbst aufgerufen werden darf. Übergeben wir jedoch `message` als Parameter, dann ist auch einem User mit der Role `ROLE_ADMIN` der Zugriff auf eine fremde Nachricht gestattet. Somit kann ein Admin über `findById` in `MessageRepository` alle Nachrichten in der Datenbank abrufen. Um dies nun auch in unserer Applikation einzusetzen, führen wir drei neue Endpuukte in unseren Controler ein:
@RequestMapping("/messages")
public String listMessages(@AuthenticationPrincipal User user, Model model) {
    Iterable<Message> messages = messageRepository.findByUserId(user.getId());
    model.addAttribute("messages", messages);
    return "listMessages";
}

@RequestMapping(value = "/message/{id}")
public String viewMessage(@PathVariable Long id, Model model) {
    Message message = messageRepository.findById(id);
    model.addAttribute("message", message);
    return "viewMessage";
}

@RequestMapping("/privateMessage/{id}")
public String viewPrivateMessage(@PathVariable Long id, Model model) {
    Message message = messageRepository.findOne(id);
    model.addAttribute("message", message);
    return "viewMessage";
}
Außerdem fügen wir die Zeile `Messages` in unser Fragment `headerAndNav.html` ein, um die Navigation zu vereinfachen. Weiterhin fügen wir zwei weitere Templates hinzu:
`listMessages.html`:

<html xmlns:th="http://www.thymeleaf.org">
    <head th:replace="fragments/headerAndNav :: header"/>
    <body>
        <div th:replace="fragments/headerAndNav :: navbar"/>
        <div class="container">
            <ul th:each="message : ${messages}">
                <li><a href="viewMessage.html" th:href="@{'/message/' + ${message.id}}" th:text="${message.title}">The title</a></li>
            </ul>
        </div>
    </body>
</html>


`viewMessages.html`:

<html xmlns:th="http://www.thymeleaf.org">
    <head th:replace="fragments/headerAndNav :: header"/>
    <body>
        <div th:replace="fragments/headerAndNav :: navbar"/>
        <div class="container">
            <h1><span th:text="${message.title}">The message title</span></h1>
            <span th:text="${message.text}">The message text</span>
        </div>
    </body>
</html>
Abschließend sorgen wir mit der Anpassung von `data.sql` für initiale Daten:
insert into user(id,username,password,role) values (0,'max','password', 'ROLE_USER');
insert into user(id,username,password,role) values (1,'tom','password', 'ROLE_USER');
insert into user(id,username,password,role) values (2,'admin','password', 'ROLE_ADMIN');

insert into message(id,user_id,title,text) values (100,0,'Message for Max','This message is for Max. Under /message/100 only Max or an admin should see this message. Under /privateMessage/100 only Max should see this message.');
insert into message(id,user_id,title,text) values (110,1,'Message for Tom','This message is for Tom. Under /message/110 only Tom or an admin should see this message. Under /privateMessage/110 only Tom should see this message.');
Wenn wir nun die Applikation durchstarten, dann stellen wir fest, das Tom sowohl unter `/message` als auch unter `/privateMessage` nur seine eigene Nachricht mit der Id 110 aufrufen kann. Gleiches gilt für Max mit der Id 100. Der Admin kann hingegen unter `/message` beide Nachrichten abrufen und hat unter `/privateMessage` auf keine von beiden Zugriff.

Spring Security intern

Stellen wir uns Spring Security als Blackbox vor, durch die ein Request durch muss, bevor er den Controller erreicht:
Wenn wir uns unseren Projektstand als wir Thymleaf eingeführt haben anschauen, dann besteht diese Blackbox aus 12 Filtern. Ich möchte Nachfolgend nur auf 4 davon genauer eingehen und werde die anderen nur kurz beschreiben.

WebAsyncManagerIntegrationFilter (1. Filter)
Dieser Filter kann einen `SecurityContextCallableProcessingInterceptor` registrieren, der wiederum den `SecurityContextHolder` befüllt (`preProcess`) und wieder leert (`postProcess`). Der `SecurityContextHolder` hält dabei unser `Authentication` Objekt. Wenn der Aufruf von `SecurityContextHolder.getContext().getAuthentication()` einen Rückgabewert ungleich `null` hat, dann wissen wir, dass die Authentifizierung bereits stattgefunden hat. In unserem Beispiel ist kein `SecurityContextCallableProcessingInterceptor` registriert, wodurch der Filter nichts macht.

SecurityContextPersistenceFilter (2. Filter)
Befüllt den `SecurityContextHolder` mit dem Wert aus der Session.

HeaderWriterFilter (3. Filter)
Schreibt Header wie zum Beispiel X-Frame-Options in die Response.

CsrfFilter (4. Filter)
Kümmert sich um den Csrf-Token-Schutzmechanismus. Interessant ist an dieser Stelle, dass Spring Security für einen Endpunkt für den CSRF deaktiviert wurde, eine extra Filterchain mit fehlendem `CsrfFilter` anlegt. Diese besteht dann entsprechend nur aus 11 Filtern.

LogoutFilter (5. Filter)
Führt den Logout aus.

RequestCacheAwareFilter (7. Filter)
Ersetzt den Request, falls ein identischer im Cache vorhanden ist.

SecurityContextHolderAwareRequestFilter (8. Filter)
Dieser Filter wrapped das Request Objekt und enthält zusätzliche Security Informationen wie zum Beispiel das `rolePrefix`.

SessionManagementFilter (10. Filter)
Stellt eine Authentifizierung während Requests fest und führt eine SessionAuthenticationStrategy aus. Dies kann beispielsweise eine Prüfung auf Mehrfachlogin sein.

FilterSecurityInterceptor (12. Filter)
Dies ist der erste Filter auf den ich genauer eingehen möchte. Dieser Filter sorgt für einen Aufruf von `AffirmativeBased.decide()`. Im Erfolgsfall wird uns der Zugriff auf die Resource gestattet und der Request gelangt an den Controller. Andernfalls wird eine `AccessDeniedException` geworfen. Dies ist beispielsweise dann der Fall, wenn wir als anonymer User auf eine geschützte Resource zugreifen.

ExceptionTranslationFilter (11. Filter)
Dieser Filter ruft den `FilterSecurityInterceptor` in einem try-Block auf. Falls dieser eine `AccessDeniedException` geworfen hat, so wird nun ein Redirect auf die Loginseite ausgelöst.

AnonymousAuthenticationFilter (9. Filter)
Dies ist der letzte Filter, der sich um die Authentifizierung kümmert. Falls der `SecurityContextHolder` noch leer ist, so befüllt er ihn mit einem `AnonymousAuthenticationToken`, was uns als anonymen User darstellt.

UsernamePasswordAuthenticationFilter (6. Filter)
Dieser Filter prüft zunächst, ob es sich um einen Login Request handelt. Ist dies nicht der Fall, so wird mit dem nächsten Filter weiter gemacht. Andernfalls wird der Login durchgeführt. Ist dieser nicht erfolgreich, so wird ein Redirect zur Loginseite mit einer Fehlermeldung ausgelöst. Ist er jedoch erfolgreich, dann wird der `SecurityContextHolder` mit einem `UsernamePasswordAuthenticationToken` befüllt und ein Redirect zu der Seite, die vor dem Login versucht wurde aufzurufen, durchgeführt.


Autor

Jürgen Fast arbeitet seit 2014 bei der Micromata GmbH als Softwareentwickler in verschiedenen Projekten der Logistik- und Automobilbranche. Sein Schwerpunkt liegt in der Entwicklung von javabasierten Webapplikationen.

by Jürgen Fast

Go back