Die formularbasierte Anmeldung ist oft die erste Wahl, um das Web-Frontend einer Spring Boot-Anwendung zu schützen. Dadurch wird sichergestellt, dass bestimmte Bereiche unserer Anwendung nur dann zugänglich sind, wenn sich ein Benutzer mit einem Benutzernamen und einem Passwort authentifiziert hat und dieser Status in der Sitzung gespeichert wird. Welche Schritte sind erforderlich, um eine formularbasierte Anmeldung zu unserer Spring Boot-Anwendung hinzuzufügen?
Zuerst erstellen wir mit dem Bootify Builder eine einfache Spring Boot-Anwendung in der aktuellen Version 3.3.2 – dazu klicken wir einfach auf Projekt öffnen. Dort wählen wir Thymeleaf + Bootstrap als Frontend-Stack aus – Thymeleaf ist die am häufigsten verwendete Template-Engine von Spring Boot und ermöglicht serverseitiges Rendern. Bootstrap wird als WebJar in unsere App integriert. Wählen Sie eine beliebige Datenbank aus, mit der Sie eine Verbindung herstellen möchten. Eine eingebettete Datenbank reicht vorerst auch aus.
Im Entities Tab erstellen wir die Tabellen User sowie TodoList und verbinden sie mit einer N:1-Relation. Für die TodoList aktivieren wir die CRUD-Option für das Frontend – das wird der Bereich sein, den wir anschließend mit Spring Security absichern.
Vorschau unseres sehr einfachen Datenbankschemas
Jetzt können wir bereits die fertige Anwendung herunterladen und in unsere Lieblings-IDE importieren.
Die erste Version unserer Anwendung in IntelliJ
Die formularbasierte Anmeldung erfolgt mit Hilfe von Spring Security. Wir benötigen also zunächst die relevanten Abhängigkeiten, die wir zu unserem build.gradle bzw. pom.xml hinzufügen.
implementation('org.springframework.boot:spring-boot-starter-security') implementation('org.thymeleaf.extras:thymeleaf-extras-springsecurity6')
Das Modul spring-boot-starter-security integriert Spring Security. Mit thymeleaf-extras-springsecurity6 ist ein kleiner Helfer enthalten, der den Authentifizierungsstatus in unseren Thymeleaf-Vorlagen bereitstellt – dazu später mehr.
Damit können wir bereits die zentrale Sicherheitskonfiguration bereitstellen – hier direkt in unserer finalen Version.
@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(); } }
Unsere Konfiguration für den Formular-Login
Theoretisch kann Spring Security das Anmeldeformular mit weniger Konfiguration bereitstellen, aber dafür würden das Design und einige Nachrichten fehlen, die wir dem Benutzer präsentieren möchten. Aber gehen wir zuerst die Konfiguration durch.
BCryptPasswordEncoder ist heutzutage Standard, um den Hash eines Passworts zusammen mit seinem individuellen Salt in einem einzigen Feld zu speichern. Wenn wir keine Legacy-Anforderung haben, sollten wir diese verwenden. Außerdem stellen wir den AuthenticationManager als Bean zur Verfügung, um ihn in andere Dienste integrieren zu können.
Als dritte Bean erstellen wir die SecurityFilterChain, da dies seit Spring 3.0 der erforderliche Ansatz ist. Wir sollten sowohl CORS als auch CSRF richtig konfigurieren, um die entsprechenden Angriffsvektoren zu schließen. Hierfür reicht in der Regel die Standardkonfiguration aus.
In unserer Konfigurationsklasse haben wir die Annotation @EnableMethodSecurity platziert und werden die gewünschten Controller-Endpunkte später mit @PreAuthorize(...) schützen. Deshalb erlauben wir den Zugriff auf die gesamte Anwendung mit erlaubenAll(). Ohne die annotationsbasierte Sicherheit sollten wir auch an dieser Stelle die Pfade zu den geschützten Ressourcen konfigurieren.
Die Methoden formLogin() und logout() sind für unseren nachfolgenden Controller angepasst, sodass wir dem Benutzer immer eine entsprechende Meldung anzeigen können. Spring Security stellt automatisch einen Endpunkt für die Anmeldung bereit, an den Benutzername und Passwort per POST-Anfrage übermittelt werden können. Hier ändern wir den Namen des Benutzernamenfelds in „E-Mail“. Die Abmeldung wird so geändert, dass sie anschließend mit einem Parameter zur Anmeldeseite zurückgeleitet wird.
Um die Benutzer aus der bereits erstellten Tabelle zu laden, müssen wir eine Implementierung von UserDetailsService als Bean bereitstellen – diese wird automatisch gefunden und als Benutzerquelle verwendet.
@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); } }
UserDetailsService-Implementierung wird automatisch als Benutzerquelle verwendet
In unserem Repository sollten wir eine Methode User findByEmailIgnoreCase(String email) hinzufügen, um eine Suchabfrage in der Datenbank auszuführen – das Ignorieren von Groß-/Kleinschreibung ermöglicht es dem Benutzer, beim Schreiben seiner E-Mail kleine Fehler zu machen. Die Rolle ist hier immer ROLE_USER für jeden Benutzer. Da uns derzeit kein Registrierungsendpunkt zur Verfügung steht, können wir vorerst einen einfachen Datenlader zusammen mit unserer Anwendung hinzufügen. Damit es aktiv wird, ist das Profil „local“ erforderlich.
@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); } }
Hilfsklasse zum lokalen Initialisieren eines ersten Benutzers
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.
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.
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
Das obige ist der detaillierte Inhalt vonFormularanmeldung mit Spring Boot und Thymeleaf. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!