DE EN

Spring-Security-5-Benutzer unter Verwendung von MySQL und JDBC authentifizieren

von

Alle Artikel dieser Serie

Spring Security 5

Dies ist eine Fortsetzung unseres früheren Artikels Einführung in Spring Security 5. Im vorherigen Artikel haben wir eine einfache Formularanmeldung mit In-Memory-Authentifizierung unter Verwendung der grundlegenden Mechanismen von Spring Security konfiguriert.

Das Problem: Verwendung von JDBC und MySQL mit Spring Security

Es ist klar, dass jede Anwendung die Registrierung neuer Benutzer und die Deaktivierung älterer Benutzer unterstützen muss. Es ist nicht praktikabel, jedes Mal eine neue Version der Anwendung zu veröffentlichen, wenn man eine solche Änderung vornehmen möchten.

Wie gehen wir also vor? Eine der besten Lösungen besteht darin, eine Datenbank zu erstellen und die Benutzerdaten in einer Tabelle zu verwalten. Im nächsten Abschnitt sehen wir, wie das gemacht wird.

MySQL: Erstellen der Schematabellen mit SQL

Wir müssen zwei verschiedene Dinge pflegen: die Benutzer und die Rollen. Es gibt grundsätzlich einen kleinen Unterschied zwischen den beiden, aber in diesem Beitrag werden sie fast gleich behandelt.

Die erste Tabelle, die wir brauchen, ist die Benutzertabelle. Per Definition muss diese den Benutzernamen und das Passwort speichern. Laut Dokumentation wird außerdem verlangt, dass ein Flag gespeichert werden soll, dass angibt ob ein Benutzer aktiv ist oder deaktiviert wurde.

Hier ist der SQL-Code (läuft unter MySQL) für unsere Tabelle:

create table users
(
    `id`       bigint(11) unsigned NOT NULL AUTO_INCREMENT,
    `username` varchar(100) not null,
    `password` varchar(100) not null,
    `enabled`  boolean     not null,
    PRIMARY KEY (`id`),
    UNIQUE KEY `username_unique` (`username`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

Die Tabelle kann natürlich nach eigenen Wünschen angepasst und es können auch weitere Felder hinzufügt werden. Außerdem habe ich den Benutzernamen eindeutig (UNIQUE) gemacht, so dass man die ID-Spalte wahrscheinlich entfernen könnte, ich bevorzuge aber normalerweise ein solches Feld.

In der zweiten Tabelle werden die Berechtigungen bzw. Rollen gespeichert, die jedem Benutzer zugewiesenen werden.

create table user_authorities
(
    `id`        bigint(11) unsigned NOT NULL AUTO_INCREMENT,
    `user_id`   bigint(11) unsigned NOT NULL,
    `authority` varchar(50) not null,
    PRIMARY KEY (`id`),
    UNIQUE KEY `username_authorities_unique` (`user_id`, `authority`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

Die obige Tabelle enthält nur eine Referenz auf die Benutzer-ID der Benutzertabelle und eine Berechtigung. Auch hier ist es meine Vorliebe, eine Extraspalte für die ID zu erstellen. Außerdem habe ich user_id und authority eindeutig gemacht, um doppelte Datensätze zu vermeiden.

Nun müssen diese beiden Tabellen miteinander verbunden werden. Dies erfolgt durch einen Fremdschlüssel für user_authorities, der auf die user-Tabelle verweist.

ALTER TABLE `user_authorities`
    ADD CONSTRAINT `fk_authorities`
        FOREIGN KEY (`user_id`) REFERENCES `users` (`id`);

Das wars. Unsere Benutzertabellen sind fertiggestellt. Erstell sie nach einem Schema deiner Wahl und dann kehren wir zu unserer Anwendung zurück.

Verbindung zu einer Datenbank herstellen

Es ist manchmal unwirklich, wie einfach Spring Boot es einem macht, bestimmte Aufgaben auszuführen. Durch Hinzufügen dieser Konfigurationszeilen zu application.properties weiß Spring Boot bereits, dass ein DataSource-Objekt erstellt werden muss.

spring.datasource.url=jdbc:mysql://localhost/db
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=dbuser
spring.datasource.password=dbpassword

Für den Fall, dass du nicht Spring Boot verwendest, kannst du der Konfigurationsklasse folgenden Code hinzufügen:

@Bean
public DataSource createDataSource() {
    SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
    dataSource.setDriverClassName(driver);
    dataSource.setUrl(url);
    dataSource.setUsername(username);
    dataSource.setPassword(password);
    return dataSource;
}

PS: Normalerweise verwendet man einen Datenbankverbindungspool wie Hikari.

Sobald du diese Konfiguration vorgenommen hast, steht automatisch ein DataSource-Objekt für die Verbindung zur Verfügung.

Erstellen der Sicherheitskonfiguration, die JDBC verwendet

Auch hier ist es normalerweise eine gute Idee von WebSecurityConfigurer zu erben.

Genau wie im vorhergehenden Post habe ich meiner Konfiguration einige Autorisierungsregeln hinzugefügt.

@EnableWebSecurity
public class JdbcSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin();

        http.authorizeRequests()
                .antMatchers("/devs/*").hasAnyRole("boss", "dev")
                .antMatchers("/boss/*").hasRole("boss")
                .antMatchers("/").permitAll();
    }
}

Jetzt muss Spring darüber informiert werden, dass wir die Art und Weise ändern möchten, wie wir Benutzer authentifizieren. Zuvor wurde dies “im Speicher” durchgeführt.

Zunächst benötigen wir Zugriff auf unsere Datenquelle. Es ist also ratsam, die Datenquellen-Bean @Autowire zu verwenden. Zusätzlich verwenden wir außerdem noch BCryptEncoding. Da wir möchten, dass sich unsere Benutzer registrieren können, ist es eine gute Idee, den Passwort-Encoder auch zu einer Spring Bean zu machen. So können wir denselben Algorithmus auch für den Registrierungscode unserer Anwendung verwenden.

@EnableWebSecurity
public class JdbcSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception { ... }
}

Jetzt ist es Zeit, den Authentifizierungsprozess zu reimplementieren. Erst der offensichtliche Teil: Wir greifen zuerst auf den Builder zu, dann verwenden wir anstelle von “inMemoryAuthentication” die “jdbcAuthentication”. Als nächstes legen wir die Datenquelle fest und rufen schlussendlich den passwordEncoder auf, damit der richtige Passwortalgorithmus verwendet wird.

@EnableWebSecurity
public class JdbcSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .jdbcAuthentication()
                .dataSource(dataSource)
                .passwordEncoder(passwordEncoder());
    }


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception { ... }
}

Aber das ist leider noch nicht alles. Da jeder ein wenig anders Tabellen erstellt, sollten wir Spring Security mitteilen, wie Benutzer- und Rolleninformationen ermittelt werden können. Es müssen zwei weitere Methoden aufgerufen werden, die die entsprechenden SQL-Anweisungen enthalten.

@EnableWebSecurity
public class JdbcSecurityConfiguration extends WebSecurityConfigurerAdapter {
    ...

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .jdbcAuthentication()
            .dataSource(dataSource)
            .passwordEncoder(passwordEncoder())
            .usersByUsernameQuery(
                "SELECT username, password, enabled from users where username = ?")
            .authoritiesByUsernameQuery(
                "SELECT u.username, a.authority " +
                "FROM user_authorities a, users u " +
                "WHERE u.username = ? " +
                "AND u.id = a.user_id"
            );
    }
    ...
}

Die erste Anweisung “usersByUsernameQuery” wählt lediglich den Benutzer aus unserer Benutzerdatenbank aus. Das ? ist ein Platzhalter, der die Stelle markiert, an die Spring Security später den Benutzernamen setzt.

Die zweite Anweisung “authorityByUsernameQuery” wählt den Benutzernamen und die jeweilige Berechtigung aus.

Es wird empfohlen, nur die Felder “username” und “authority” bzw. “username”, “password”, “enabled” aus der Datenbank zurückzugeben, egal aus welchen Tabellen man sie bekommt. Es wäre also theoretisch auch möglich, eine komplexere Abfrage an die Datenbank zu stellen, die viel Joins beinhaltet - hauptsache die erwarteten Felder werden als Ergebnis geliefert.

Das war’s - Spring Security ist mit der Datenbank verbunden.

So registriert man einen Benutzer

Das Registrieren eines Benutzers sollte nun eine halbwegs einfache Aufgabe sein. Grundsätzlich muss man nur das entsprechende Webformular auslesen und einen neuen Benutzer in die Benutzerdatenbank einfügen - fertig.

Ein Beispiel, wie das aussehen könnte, findest du unten. Zu beachten ist, dass man zum Speichern und Lesen denselben Passwort-Encoder verwenden muss. Wir stellen dies sicher, indem wir unseren PasswordEncoder zu einer Bean machen.

Im folgenden Beispiel werden JPA-Entities verwendet, da sie gut lesbar sind. Aber nichts hindert einen Entwickler daran, auch hier einfaches JDBC zu verwenden.

@Controller
public class RegisterController {
    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    private UserRepository userRepository;

    @PostMapping("/register")
    public String doRegister(@ModelAttribute UserDto userDto) {
        String encodedPassword  = passwordEncoder.encode(userDto.getPassword1());

        User user = new User();
        user.setEnabled(Boolean.TRUE);
        user.setPassword(encodedPassword);
        user.setUsername(userDto.getUsername());

        UserAuthority boardAuthority = new UserAuthority();
        boardAuthority.setAuthority("BOARD");
        boardAuthority.setUser(user);
        user.getAuthorities().add(boardAuthority);
        userRepository.save(user);

        return "register-success";
    }
}

Was sollte ich als nächstes lesen?

Wir haben jetzt unser Login-Formular mit der Datenbank verbunden. Bei der Authentifizierung gibt es noch fortgeschrittenere Szenarien. Wie wäre es zum Beispiel mit Basic Auth oder JWT? Im folgenden Artikel erfährst du mehr über Spring Security mit Basic Auth und JWT.

Image Credits

Tags: #Java #Spring #Spring Security #Login #MySQL #JDBC

Newsletter

ABMELDEN