Maison  >  Article  >  Java  >  Connexion par formulaire avec Spring Boot et Thymeleaf

Connexion par formulaire avec Spring Boot et Thymeleaf

WBOY
WBOYoriginal
2024-07-25 19:39:43566parcourir

La connexion par formulaire est souvent le premier choix pour protéger l'interface Web d'une application Spring Boot. Il garantit que certaines zones de notre application ne sont accessibles qu'une fois qu'un utilisateur s'est authentifié avec un nom d'utilisateur et un mot de passe, et ce statut est stocké dans la session. Quelles sont les étapes requises pour ajouter une connexion par formulaire à notre application Spring Boot ?

Commencez par une application simple

Tout d'abord, nous utilisons le Bootify Builder pour créer une simple application Spring Boot dans la version actuelle 3.3.2 - pour ce faire, nous cliquons simplement sur ouvrir le projet. Là, nous sélectionnons Thymeleaf + Bootstrap comme pile frontale - Thymeleaf est le moteur de modèles le plus utilisé de Spring Boot et permet le rendu côté serveur. Bootstrap sera intégré à notre application en tant que WebJar. Sélectionnez n'importe quelle base de données à laquelle vous souhaitez vous connecter - une base de données intégrée fera également l'affaire pour le moment.

Dans l'Onglet Entités nous créons les tables User ainsi que TodoList, et les connectons avec une relation N:1. Pour la TodoList, nous activons l'option CRUD pour le frontend - ce sera ensuite la zone que nous sécuriserons avec Spring Security.

Form Login with Spring Boot and Thymeleaf
 Aperçu de notre schéma de base de données très simple

Maintenant, nous pouvons déjà télécharger l'application terminée et l'importer dans notre IDE préféré.

Form Login with Spring Boot and Thymeleaf
 La première version de notre application en IntelliJ

Configuration de la sécurité Spring

La connexion par formulaire est fournie avec l'aide de Spring Security. Nous avons donc d'abord besoin des dépendances pertinentes, que nous ajoutons respectivement à notre build.gradle ou pom.xml.

implementation('org.springframework.boot:spring-boot-starter-security')
implementation('org.thymeleaf.extras:thymeleaf-extras-springsecurity6')

Le module spring-boot-starter-security intègre Spring Security. Avec thymeleaf-extras-springsecurity6, un petit assistant est inclus qui fournit l'état d'authentification dans nos modèles Thymeleaf - nous en reparlerons plus tard.

Grâce à cela, nous pouvons déjà fournir la configuration centrale de sécurité - ici directement dans notre version finale.

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class HttpSecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(
            final AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain configure(final HttpSecurity http) throws Exception {
        return http.cors(withDefaults())
                .csrf(withDefaults())
                .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll())
                .formLogin(form -> form
                    .loginPage("/login")
                    .usernameParameter("email")
                    .failureUrl("/login?loginError=true"))
                .logout(logout -> logout
                    .logoutSuccessUrl("/login?logoutSuccess=true")
                    .deleteCookies("JSESSIONID"))
                .exceptionHandling(exception -> exception
                    .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login?loginRequired=true")))
                .build();
    }

}

 Notre configuration pour le formulaire de connexion

Théoriquement, Spring Security peut fournir le formulaire de connexion avec moins de configuration, mais il manquerait le design et certains messages que nous aimerions présenter à l'utilisateur. Mais passons d'abord par la configuration.

BCryptPasswordEncoder est aujourd'hui standard pour stocker le hachage d'un mot de passe avec son sel individuel dans un seul champ. Si nous n’avons pas d’exigence héritée, nous devrions utiliser celle-ci. De plus, nous fournissons l'AuthentificationManager sous forme de bean pour pouvoir l'intégrer à d'autres services.

En tant que troisième bean, nous créons SecurityFilterChain car c'est l'approche requise depuis le Spring 3.0. Nous devons configurer correctement CORS et CSRF pour fermer les vecteurs d'attaque correspondants. La configuration par défaut est généralement suffisante pour cela.

Dans notre classe de configuration, nous avons placé l'annotation @EnableMethodSecurity et protégerons les points de terminaison du contrôleur souhaités avec @PreAuthorize(...) plus tard. Par conséquent nous autorisons l'accès à l'ensemble de l'application avec permitAll(). Sans la sécurité basée sur les annotations, nous devrions également configurer les chemins d'accès aux ressources protégées à cet endroit.

Les méthodes formLogin() et logout() sont personnalisées pour notre contrôleur ultérieur afin que nous puissions toujours afficher un message approprié à l'utilisateur. Spring Security fournit automatiquement un point de terminaison pour la connexion auquel le nom d'utilisateur et le mot de passe peuvent être soumis via une requête POST. Ici, nous changeons le nom du champ du nom d'utilisateur en "email". La déconnexion est modifiée pour rediriger ensuite vers la page de connexion avec un paramètre.

Chargement des utilisateurs depuis notre base de données

Pour charger les utilisateurs de la table déjà créée, nous devons fournir une implémentation de UserDetailsService en tant que bean - il sera automatiquement trouvé et utilisé comme source d'utilisateurs.

@Service
public class HttpUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public HttpUserDetailsService(final UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public HttpUserDetails loadUserByUsername(final String username) {
        final User user = userRepository.findByEmailIgnoreCase(username);
        if (user == null) {
            log.warn("user not found: {}", username);
            throw new UsernameNotFoundException("User " + username + " not found");
        }
        final List<SimpleGrantedAuthority> roles = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
        return new HttpUserDetails(user.getId(), username, user.getHash(), roles);
    }

}

  Implémentation de UserDetailsService automatiquement utilisée comme source d'utilisateurs

Dans notre référentiel, nous devrions ajouter une méthode User findByEmailIgnoreCase(String email) pour exécuter une requête de recherche sur la base de données - ignorer les majuscules/minuscules permet à l'utilisateur de petites erreurs lors de la rédaction de son e-mail. Le rôle ici est toujours ROLE_USER pour chaque utilisateur. Comme nous n'avons pas de point de terminaison d'enregistrement disponible à ce stade, nous pouvons ajouter un simple chargeur de données avec notre application pour le moment. Le profil "local" est requis pour qu'il devienne actif.

@Component
@Profile("local")
public class UserLoader implements ApplicationRunner {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserLoader(final UserRepository userRepository,
                      final PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public void run(final ApplicationArguments args) {
        if (userRepository.count() != 0) {
            return;
        }
        final User user = new User();
        user.setEmail("test@test.com");
        user.setHash(passwordEncoder.encode("testtest"));
        userRepository.save(user);
    }

}

 Classe d'assistance pour initialiser un premier utilisateur localement

Add login controller

With this we can already add the LoginController. Since the POST endpoint is automatically provided by Spring Security, a GET endpoint is sufficient here to show the template to the user.

@Controller
public class AuthenticationController {

    @GetMapping("/login")
    public String login(@RequestParam(name = "loginRequired", required = false) final Boolean loginRequired,
            @RequestParam(name = "loginError", required = false) final Boolean loginError,
            @RequestParam(name = "logoutSuccess", required = false) final Boolean logoutSuccess,
            final Model model) {
        model.addAttribute("authentication", new AuthenticationRequest());
        if (loginRequired == Boolean.TRUE) {
            model.addAttribute(WebUtils.MSG_INFO, WebUtils.getMessage("authentication.login.required"));
        }
        if (loginError == Boolean.TRUE) {
            model.addAttribute(WebUtils.MSG_ERROR, WebUtils.getMessage("authentication.login.error"));
        }
        if (logoutSuccess == Boolean.TRUE) {
            model.addAttribute(WebUtils.MSG_INFO, WebUtils.getMessage("authentication.logout.success"));
        }
        return "authentication/login";
    }

}

  Backend for rendering the login page

The request parameters that we had already specified in our security configuration are converted to corresponding messages here. In our simple application from Bootify the corresponding helpers are already included. Here we also need the AuthenticationRequest object with getters and setters.

public class AuthenticationRequest {

    @NotNull
    @Size(max = 255)
    private String email;

    @NotNull
    @Size(max = 255)
    private String password;

}

The corresponding template for our controller could then look like this.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layout}">
    <head>
        <title>[[#{authentication.login.headline}]]</title>
    </head>
    <body>
        <div layout:fragment="content">
            <h1 class="mb-4">[[#{authentication.login.headline}]]</h1>
            <div th:replace="~{fragments/forms::globalErrors('authentication')}" />
            <form th:action="${requestUri}" method="post">
                <div th:replace="~{fragments/forms::inputRow(object='authentication', field='email')}" />
                <div th:replace="~{fragments/forms::inputRow(object='authentication', field='password', type='password')}" />
                <input type="submit" th:value="#{authentication.login.headline}" class="btn btn-primary mt-4" />
            </form>
        </div>
    </body>
</html>

As Thymeleaf doesn't allow direct access to request object anymore, we're providing the requestUri in the model.

@ModelAttribute("requestUri")
String getRequestServletPath(final HttpServletRequest request) {
    return request.getRequestURI();
}

_  Providing the requestUri - as part of the AuthenticationController or a general ControllerAdvice _

With this template we send a POST request to the /login endpoint. The INFO or ERROR messages are automatically displayed by the layout. All used messages have to be present in our messages.properties.

authentication.login.headline=Login
authentication.email.label=Email
authentication.password.label=Password
authentication.login.required=Please login to access this area.
authentication.login.error=Your login was not successful - please try again.
authentication.logout.success=Your logout was successful.
navigation.login=Login
navigation.logout=Logout

Last we can extend our layout.html. With this we also always show a login / logout link in the header. Spring Security also automatically provides a /logout endpoint, but we have to address it via POST.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
            xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <!-- ... -->    
    <a sec:authorize="!isAuthenticated()" th:href="@{/login}" class="nav-link">[[#{navigation.login}]]</a>
    <form sec:authorize="isAuthenticated()" th:action="@{/logout}" method="post" class="nav-link">
        <input th:value="#{navigation.logout}" type="submit" class="unset" />
    </form>
    <!-- ... -->
</html>

  Adding login / logout links to our layout

In the html tag we've extended the namespace to use the helpers from the thymeleaf-extras-springsecurity6 module. As a final step we only need to add the annotation @PreAuthorize("hasAuthority('ROLE_USER')") at our TodoListController.

Starting our application

With this we have all needed pieces of our puzzle together! Now we start our application and when we want to see the todo lists, we should be redirected to the login page. Here we can log in with test@test.com / testtest.

Form Login with Spring Boot and Thymeleaf
  Automatic redirect to the login

In the Free plan of Bootify, Spring Boot prototypes with its own database schema, REST API and frontend can be generated. In the Professional plan, among other things, Spring Security with the form-based login is available to generate the setup described here - exactly matching the created database and the selected settings. A registration endpoint and role source can be specified as well.

» See Features and Pricing

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn