Spring Boot 3.0 Security 6 customized UserDetailsService, dynamic permissions, Thymeleaf, password strength, expiration, lock, unlock, disable, historical new password edit distance, login log, Envers

In this tutorial, I will guide you on how to write code to protect web pages in a Spring Boot application using the Spring Security API with forms-based authentication. User details are stored in MySQL database and connected to the database using Spring JDBC. We'll start with the ProductManager project in this tutorial and add login and logout functionality to an existing spring boot project.

1. Create user table and virtual credentials

Credentials should be stored in the database. Spring Data JPA is used to automatically create tables. The ER diagram of the relationship between tables is as follows:

2. Configure data source properties

Next, specify the database connection information in the application properties file as follows: Update the URL, username, and password according to your MySQL database.

spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/product3?autoReconnect=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
#logging.level.root=WARN

3. Declare dependencies on Spring Security and MySQL JDBC Driver

To use Spring Security API for your project, declare the following dependencies in the pom.xml file: And to use JDBC with Spring Boot and MySQL: Note that the dependency versions are already defined by the Spring Boot Starter parent project.

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.0</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>net.codejava</groupId>
    <artifactId>ProductManagerUserDetailsServiceAuditBoot3.0</artifactId>
    <version>2.0</version>
    <name>ProductManagerUserDetailsServiceAuditBoot3.0</name>
    <description>Spring Boot CRUD Web App Example</description>
    <packaging>jar</packaging>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity6</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.passay</groupId>
            <artifactId>passay</artifactId>
            <version>1.6.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-text</artifactId>
            <version>1.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-envers</artifactId>
            <version>6.1.5.Final</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>


    </dependencies>

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

</project>

4. Configure Spring Security

To use Spring security with form-based authentication and CustomUserDetailsService, create the WebSecurityConfig class as follows:

package com.example;

import java.util.Arrays;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.HttpSessionEventPublisher;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private CustomLoginFailureHandler loginFailureHandler;
    @Autowired
    private CustomLoginSuccessHandler loginSuccessHandler;

    @Autowired

    private CustomUserDetailsService customUserDetailsService;

//    @Autowired
//    public void configAuthentication(AuthenticationManagerBuilder authBuilder) throws Exception {
//        authBuilder.userDetailsService(customUserDetailsService)
//                .passwordEncoder(new BCryptPasswordEncoder());
//
//    }
//requestMatchers("/**").
    @Bean
    VerifyCodeAuthenticationProvider authenticationProvider() {
        VerifyCodeAuthenticationProvider authenticationProvider = new VerifyCodeAuthenticationProvider();
        authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
        authenticationProvider.setUserDetailsService(customUserDetailsService);
        return authenticationProvider;
    }

    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return new ProviderManager(Arrays.asList(authenticationProvider()));
    }

    @Autowired
    CustomAuthorizationManager customAuthorizationManager;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests()
                .requestMatchers("/403").permitAll()
                .requestMatchers("/captcha_image").permitAll()
                .requestMatchers("/header").permitAll()
                .requestMatchers("/footer").permitAll()
                .requestMatchers("/sidebar").permitAll()
                .requestMatchers("/index2").permitAll()
                .requestMatchers("/index3").permitAll()
                .requestMatchers("/pages/**").permitAll()
                .requestMatchers("/css/**").permitAll()
                .requestMatchers("/js/**").permitAll()
                .requestMatchers("/assets/**").permitAll()
                .requestMatchers("/webjars/**").permitAll()
                .requestMatchers("/common/**").permitAll()
                .requestMatchers("/login").permitAll()
                .requestMatchers("/logout").permitAll()
                .requestMatchers("/verify").permitAll()
                .requestMatchers("/home").authenticated()
                .requestMatchers("/user/info").authenticated()
                .requestMatchers("/change/password").authenticated()
                .requestMatchers("/new/password").authenticated()
                .requestMatchers("/").authenticated()
                .anyRequest()
                //                .authenticated()
                .access(customAuthorizationManager)
                .and()
                .formLogin().loginPage("/login")
                .permitAll()
                .failureHandler(loginFailureHandler)
                .successHandler(loginSuccessHandler)
                .and()
                .logout().permitAll()
                .and()
                .exceptionHandling().accessDeniedPage("/403");
        http
                .sessionManagement()
                .maximumSessions(1)
                .expiredUrl("/")
                .maxSessionsPreventsLogin(false)
                .sessionRegistry(sessionRegistry());
        return http.build();
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        SessionRegistry sessionRegistry = new SessionRegistryImpl();
        return sessionRegistry;
    }

    // Register HttpSessionEventPublisher
    @Bean
    public static ServletListenerRegistrationBean httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
    }

    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

This security configuration class must be annotated with the @EnableWebSecurity annotation and is a subclass of the Web Security Configurator Adapter.

CustomUserDetailsService

package com.example;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    public UserDetails loadUserByUsername(String userName)
            throws UsernameNotFoundException {
        User domainUser = userRepository.findByUsername(userName);

        if (domainUser != null) {
            return new org.springframework.security.core.userdetails.User(
                    domainUser.getUsername(),
                    domainUser.getPassword(),
                    domainUser.getEnabled(),
                    domainUser.getAccountNonExpired(),
                    domainUser.getCredentialsNonExpired(),
                    domainUser.getAccountNonLocked(),
                    getAuthorities(domainUser.getRoles())
            );
        }
//        return null;
        throw new UsernameNotFoundException("User " + userName + " does not exist");
    }

    private Collection<? extends GrantedAuthority> getAuthorities(
            Collection<Role> roles) {
        List<GrantedAuthority> authorities
                = new ArrayList<>();
        for (Role role : roles) {
//            authorities.add(new SimpleGrantedAuthority(role.getName()));
            role.getPermissions().stream()
                    .map(p -> new SimpleGrantedAuthority(p.getName()))
                    .forEach(authorities::add);
        }

        return authorities;
    }

}

User

package com.example;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import lombok.Data;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.envers.Audited;

@Data
@Entity
@DynamicUpdate
@Audited
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    private Boolean enabled = true;

    private Boolean accountNonExpired = true;

    private Boolean credentialsNonExpired = true;

    private Boolean accountNonLocked = true;

    private String email;

    private String name;

    private String homepage;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "user_role",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();

    @Column(name = "password_changed_time")
    private Date passwordChangedTime;

    @Column(name = "failed_attempt")
    private int failedAttempt;

    @Column(name = "lock_time")
    private Date lockTime;

    @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinTable(
            name = "user_history",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "history_id")
    )
    private Set<History> historys = new HashSet<>();

}

Roles and permissions work together

5. Login verification process

package com.example;

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

@Controller
public class LoginController {

    @GetMapping("/login")
    public String viewLoginPage() {
        // custom logic before showing login page...

        return "login";
    }

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

    @GetMapping("/403")
    public String view403Page() {
        return "403";
    }
}
package com.example;

import com.google.code.kaptcha.Constants;

import java.util.Objects;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class VerifyCodeAuthenticationProvider extends DaoAuthenticationProvider {

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        //获取当前请求
        HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String code = req.getParameter("kaptcha");//从当前请求中拿到code参数
        System.out.println("!!!code=" + code);
        String verifyCode = (String) req.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);//从session中获取生成的验证码字符串
        System.out.println("!!!" + Constants.KAPTCHA_SESSION_KEY + "=" + verifyCode);
        //比较验证码是否相同
        if (StringUtils.isBlank(code) || StringUtils.isBlank(verifyCode) || !Objects.equals(code, verifyCode)) {
            throw new AuthenticationServiceException("验证码错误!");
        }
        super.additionalAuthenticationChecks(userDetails, authentication);//调用父类DaoAuthenticationProvider的方法做密码的校验
    }
}

kaptcha verification code

package com.example;

import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.Producer;

/**
 * @author will
 */
@Controller
public class KaptchaController {

    @Autowired
    private Producer captchaProducer;

    @GetMapping("/captcha_image")
    public void getKaptchaImage(HttpServletResponse response, HttpSession session) throws Exception {
        response.setDateHeader("Expires", 0);
        // Set standard HTTP/1.1 no-cache headers.
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        // Set IE extended HTTP/1.1 no-cache headers (use addHeader).
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        // Set standard HTTP/1.0 no-cache header.
        response.setHeader("Pragma", "no-cache");
        // return a jpeg
        response.setContentType("image/jpeg");
        // create the text for the image
        String capText = captchaProducer.createText();
        // store the text in the session
        // request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);

        // save captcha to session
        session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);

        // create the image with the text
        BufferedImage bi = captchaProducer.createImage(capText);
        ServletOutputStream out = response.getOutputStream();

        // write the data out
        ImageIO.write(bi, "jpg", out);
        try {
            out.flush();
        } finally {
            out.close();
        }
    }
}
package com.example;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.Properties;

@Component
public class KaptchaConfig {

    @Bean
    public DefaultKaptcha getDefaultKaptcha() {
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.textproducer.font.color", "black");
        properties.put("kaptcha.image.width", "150");
        properties.put("kaptcha.image.height", "40");
        properties.put("kaptcha.textproducer.font.size", "30");
        properties.put("kaptcha.session.key", "verifyCode");
        properties.put("kaptcha.textproducer.char.space", "5");
//        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);

        return defaultKaptcha;
    }

}

6. Login page

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Bootstrap 5 Sign In Form with Image Example</title>
        <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
              integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
                integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
        crossorigin="anonymous"></script>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
    </head>

    <body>

        <form th:action="@{/login}" method="post">
            <div class="container-fluid vh-100" style="margin-top:50px">
                <div class="" style="margin-top:50px">
                    <div class="rounded d-flex justify-content-center">
                        <div class=" col-md-4 col-sm-12 shadow-lg p-5 bg-light">
                            <div th:if="${param.error}">
                                <p class="text-danger">[[${session.SPRING_SECURITY_LAST_EXCEPTION.message}]]</p>
                            </div>

                            <div th:if="${param.logout}">
                                <p class="text-warning">You have been logged out.</p>
                            </div>
                            <div class="text-center">
                                <h3 class="text-primary">请登录</h3>
                            </div>
                            <div class="p-4">
                                <div class="input-group mb-3">
                                    <span class="input-group-text bg-secondary"><i
                                            class="bi bi-person-fill text-white"></i></span>
                                    <input id="username" type="text" name="username" required class="form-control" placeholder="用户名">
                                </div>
                                <div class="input-group mb-3">
                                    <span class="input-group-text bg-secondary"><i
                                            class="bi bi-key-fill text-white"></i></span>
                                    <input  id="password" type="password" name="password" required class="form-control" placeholder="密码">
                                </div>
                                <div class="input-group mb-3">
                                    <span class="input-group-text bg-secondary"><i
                                            class="bi bi-lock-fill text-white"></i></span>
                                    <input type="text" name="kaptcha"  class="form-control" placeholder="输入下图中的校验码">
                                </div>
                                <div class="input-group mb-3">
                                    <span class="input-group-text bg-secondary"><i
                                            class="bi bi-image-fill text-white"></i></span>
                                    <img alt="Click the picture to refresh!" class="pointer" th:src="@{/captcha_image}"
                                         onclick="this.src = '/captcha_image?d=' + new Date() * 1">
                                </div>
                                <div class="col-12">
                                    <button type="submit" class="btn btn-primary px-4 float-end mt-4">登录</button>
                                </div>

                            </div>
                        </div>
                    </div>
                </div>
            </div>
    </body>

</html>

5. Test login and logout

Start the Spring Boot application and visit http://localhost:8080 in a web browser. You will see the customized login page appear:

 Now enter the correct username admin and password admin, you will see the home page as follows:

 And note the welcome message followed by the username. The user is now authenticated to use the application. Click the "Logout" button and you will see the customized login page appear, which means we have successfully implemented login and logout to our Spring Boot application.

Customize dynamic permission verification from database

package com.example;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.envers.Audited;

@Data
@Entity
@DynamicUpdate
@Audited
public class Permission {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    private String uri;
    private String method;

    @Override
    public String toString() {
        return this.name;
    }
}
package com.example;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PermissionRepository extends JpaRepository<Permission, Long> {

    public Permission findByName(String name);

}
package com.example;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

@Component
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    @Autowired
    private PermissionRepository permissionRepository;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
        HttpServletRequest request = requestAuthorizationContext.getRequest();
        //获取用户认证信息
        System.out.println(authentication.get().getAuthorities());
        Object principal = authentication.get().getPrincipal();
        System.out.println(principal.getClass());
        //判断数据是否为空 以及类型是否正确
        if (null != principal && principal instanceof org.springframework.security.core.userdetails.User) {
            String username = ((org.springframework.security.core.userdetails.User) principal).getUsername();
            System.out.println(username);

        }

        String requestURI = request.getRequestURI();
        System.out.println(requestURI);
        String method = request.getMethod();
        System.out.println(method);

        Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
        boolean hasPermission = false;
        for (GrantedAuthority authority : authorities) {
            String authorityname = authority.getAuthority();
            System.out.println(authority.getAuthority());
            Permission permission = permissionRepository.findByName(authorityname);
            System.out.println(permissionRepository.findByName(authorityname));
            if (null != permission && permission.getMethod().equals(request.getMethod()) && antPathMatcher.match(permission.getUri(), request.getRequestURI())) {
                hasPermission = true;

                break;
            }
        }
        System.out.println(hasPermission);
        return new AuthorizationDecision(hasPermission);
    }

}
package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Date;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;

@Component
public class CustomLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Autowired
    private UserLoginService userLoginService;
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private LoginLogRepository loginLogRepository;

    private static final long PASSWORD_EXPIRATION_TIME = 30L * 24L * 60L * 60L * 1000L;    // 30 days

    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        String username = userDetails.getUsername();
        User user = userRepository.getByUsername(username);
        if (user.getFailedAttempt() > 0) {
            userLoginService.resetFailedAttempts(user.getUsername());
        }
        System.out.println(request.getRemoteAddr());
        System.out.println(request.getSession().getId());
        LoginLog loginLog = new LoginLog();
        loginLog.setUsername(username);
        loginLog.setDescription("登录成功");
        loginLog.setIp(request.getRemoteAddr());
        loginLog.setEventtime(new Date());
        loginLog.setSessionid(request.getSession().getId());
        loginLogRepository.save(loginLog);

        if (user.getPasswordChangedTime() == null) {
            String redirectURL = request.getContextPath() + "/change/password";
            response.sendRedirect(redirectURL);

        } else {
            long currentTime = System.currentTimeMillis();
            long lastChangedTime = user.getPasswordChangedTime().getTime();

            if (currentTime > lastChangedTime + PASSWORD_EXPIRATION_TIME) {
                System.out.println("User:" + user.getUsername() + ":password expired");
                System.out.println("Last Time password changed:" + user.getPasswordChangedTime());
                System.out.println("Current Time:" + new Date());

                String redirectURL = request.getContextPath() + "/change/password";
                response.sendRedirect(redirectURL);
            } else {

                System.out.println(request.getContextPath() + user.getHomepage());
                response.sendRedirect(request.getContextPath() + user.getHomepage());
//        super.onAuthenticationSuccess(request, response, authentication);
            }
        }
    }
}
package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import org.springframework.security.authentication.CredentialsExpiredException;

@Component
public class CustomLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private UserLoginService userLoginService;
    @Autowired
    private LoginLogRepository loginLogRepository;

    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        if (exception instanceof CredentialsExpiredException) {
            // 保存异常信息到会话属性,供页面显示
            saveException(request, exception);
            String userName = request.getParameter("username");
            // 跳转到修改密码页面
            response.sendRedirect("/changepassword?error&username=" + userName);
        }

        String username = request.getParameter("username");
        User user = userRepository.getByUsername(username);
        if (user != null) {
            LoginLog loginLog = new LoginLog();
            loginLog.setUsername(username);
            loginLog.setDescription("登录失败");
            loginLog.setIp(request.getRemoteAddr());
            loginLog.setEventtime(new Date());
            loginLog.setSessionid(request.getSession().getId());
            loginLogRepository.save(loginLog);
            if (user.getEnabled() && user.getAccountNonLocked()) {
                if (user.getFailedAttempt() < userLoginService.MAX_FAILED_ATTEMPTS - 1) {
                    System.out.println("user.getFailedAttempt()=" + user.getFailedAttempt());
                    userLoginService.increaseFailedAttempts(user);
                } else {

                    userLoginService.lock(user);
                    exception = new LockedException("your account has been locked due to 3 failed attempt"
                            + " It will be unclocked after 15 minutes");
                    System.out.println(exception);
                    System.out.println("userLoginService.lock(user)");
                }
            } else if (!user.getAccountNonLocked()) {
                if (userLoginService.unlockWhenTimeExpired(user)) {
                    exception = new LockedException("your account has been unclock ."
                            + " please try to login again");
                    System.out.println(exception);
                }
            }
        }

        super.setDefaultFailureUrl("/login?error");
        super.onAuthenticationFailure(request, response, exception);
    }
}

In the thymeleaf view file, the connection menu is displayed based on permissions

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta content="width=device-width, initial-scale=1.0" name="viewport">
        <title>Dashboard - Admin Bootstrap Template</title>
        <meta name="robots" content="noindex, nofollow">
        <meta content="" name="description">
        <meta content="" name="keywords">
        <link href="/assets/img/favicon.png" rel="icon">
        <link href="/assets/img/apple-touch-icon.png" rel="apple-touch-icon">
        <link href="/assets/css/bootstrap.min.css" rel="stylesheet">
        <link href="/assets/css/bootstrap-icons.css" rel="stylesheet">
        <link href="/assets/css/boxicons.min.css" rel="stylesheet">
        <link href="/assets/css/quill.snow.css" rel="stylesheet">
        <link href="/assets/css/quill.bubble.css" rel="stylesheet">
        <link href="/assets/css/remixicon.css" rel="stylesheet">
        <link href="/assets/css/simple-datatables.css" rel="stylesheet">
        <link href="/assets/css/style.css" rel="stylesheet">
    </head>
    <body>
        <header id="header" class="header fixed-top d-flex align-items-center" th:replace="fragments/header :: header">
        </header>
        <aside id="sidebar" class="sidebar" th:replace="fragments/sidebar :: sidebar">
        </aside>
        <main id="main" class="main">
            <div class="pagetitle">
                <nav>
                    <ol class="breadcrumb">
                        <li class="breadcrumb-item"><a href="index.html">主页</a></li>
                        <li class="breadcrumb-item active">用户管理</li>
                    </ol>
                </nav>
            </div>
            <section class="section dashboard">
                <div class="row">
                    <div class="col-lg-12">
                        <div class="card">
                            <div class="card-body">

                                <br/>
                                <br/>
                                <div class="table-title">
                                    <div class="row">
                                        <div class="col-sm-6">
                                            <h2> <b>用户管理</b></h2>
                                        </div>
                                        <div class="col-sm-6">
                                            <div sec:authorize="hasAnyAuthority('permission_create')">
                                                <a href="/user/new" class="btn btn-success" ><i class="bi bi-person-plus" data-toggle="tooltip" title="添加用户"></i></a>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <table class="table table-striped table-hover">
                                    <thead >
                                        <tr>
                                            <th>ID</th>
                                            <th>User Name</th>
                                            <th>E-mail</th>
                                            <th>Name</th>
                                            <th>Home Page</th>
                                            <th>Roles</th>
                                            <th>Actions</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <tr th:each="user: ${listUsers}">
                                            <td th:text="${user.id}">User ID</td>
                                            <td th:text="${user.username}">User Name</td>
                                            <td th:text="${user.email}">E-mail</td>
                                            <td th:text="${user.name}">Name</td>
                                            <td th:text="${user.homepage}">Home Page</td>
                                            <td th:text="${user.roles}">Roles</td>
                                            <td>
                                                <a sec:authorize="hasAuthority('user_edit')" th:href="@{'/user/edit/' + ${user.id}}" class="edit" ><i class="bi bi-pencil" data-toggle="tooltip" title="编辑"></i></a>
                                                <a sec:authorize="hasAuthority('user_edit')" th:href="@{'/user/resetpassword/' + ${user.id}}" class="reset" ><i class="bi bi-key" data-toggle="tooltip" title="重置密码"></i></a>
                                                <a sec:authorize="hasAuthority('user_delete')" th:href="@{'/user/delete/' + ${user.id}}" class="delete" ><i class="bi bi-trash" data-toggle="tooltip" title="删除"></i></a>


                                            </td>
                                        </tr>
                                    </tbody>
                                </table>



                            </div>
                        </div>
                    </div>
            </section>
        </main>
        <footer id="footer" class="footer" th:replace="fragments/footer :: footer">
            <div class="copyright"> &copy; Copyright <strong><span>Compnay Name</span></strong>. All Rights Reserved</div>
            <div class="credits"> with love <a href="https://freeetemplates.com/">FreeeTemplates</a></div>
        </footer>
        <a href="#" class="back-to-top d-flex align-items-center justify-content-center"><i class="bi bi-arrow-up-short"></i></a>
        <script src="/assets/js/apexcharts.min.js"></script>
        <script src="/assets/js/bootstrap.bundle.min.js"></script>
        <script src="/assets/js/chart.min.js"></script>
        <script src="/assets/js/echarts.min.js"></script>
        <script src="/assets/js/quill.min.js"></script>
        <script src="/assets/js/simple-datatables.js"></script>
        <script src="/assets/js/tinymce.min.js"></script>
        <script src="/assets/js/validate.js"></script>
        <script src="/assets/js/main.js"></script>

    </body>
</html>

 Thymeleaf集成Bootstrap 5 Free Admin Dashboard Template 1 - freeetemplates

header.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"  xmlns:sec="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
            <meta content="width=device-width, initial-scale=1.0" name="viewport">
                <title>Dashboard - Admin Bootstrap Template</title>
                <meta name="robots" content="noindex, nofollow">
                    <meta content="" name="description">
                        <meta content="" name="keywords">
                            <link href="assets/img/favicon.png" rel="icon">
                                <link href="assets/img/apple-touch-icon.png" rel="apple-touch-icon">
                                    <link href="https://fonts.gstatic.com" rel="preconnect">
                                        <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
                                            <link href="assets/css/bootstrap.min.css" rel="stylesheet">
                                                <link href="assets/css/bootstrap-icons.css" rel="stylesheet">
                                                    <link href="assets/css/boxicons.min.css" rel="stylesheet">
                                                        <link href="assets/css/quill.snow.css" rel="stylesheet">
                                                            <link href="assets/css/quill.bubble.css" rel="stylesheet">
                                                                <link href="assets/css/remixicon.css" rel="stylesheet">
                                                                    <link href="assets/css/simple-datatables.css" rel="stylesheet">
                                                                        <link href="assets/css/style.css" rel="stylesheet">
                                                                            </head>
                                                                            <body>

                                                                                <header id="header" class="header fixed-top d-flex align-items-center" th:fragment="header">
                                                                                    <form th:action="@{/logout}" method="post" th:hidden="true" name="logoutForm">
                                                                                        <input type="submit" value="Logout" />
                                                                                    </form>
                                                                                    <div class="d-flex align-items-center justify-content-between"> <a href="index.html" class="logo d-flex align-items-center"> <img src="/assets/img/logo.png" alt=""> <span class="d-none d-lg-block">CRUD和RBAC系统</span> </a> <i class="bi bi-list toggle-sidebar-btn"></i></div>

                                                                                    <nav class="header-nav ms-auto">
                                                                                        <ul class="d-flex align-items-center">
                                                                                            <li class="nav-item d-block d-lg-none"> <a class="nav-link nav-icon search-bar-toggle " href="#"> <i class="bi bi-search"></i> </a></li>


                                                                                            <li class="nav-item dropdown pe-3">
                                                                                                <a class="nav-link nav-profile d-flex align-items-center pe-0" href="#" data-bs-toggle="dropdown"> <img src="/assets/img/profile.png" alt="Profile" class="rounded-circle"> <span class="d-none d-md-block dropdown-toggle ps-2"><span sec:authentication="name">Username</span></span> </a>
                                                                                                <ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow profile">
                                                                                                    <li class="dropdown-header">
                                                                                                        <h6><span sec:authentication="name">Username</span></h6>
                                                                                                    </li>
                                                                                                    <li>
                                                                                                        <hr class="dropdown-divider">
                                                                                                    </li>
                                                                                                    <li> <a class="dropdown-item d-flex align-items-center" href="/user/info"> <i class="bi bi-person"></i> <span>账号信息</span> </a></li>
                                                                                                    <li>
                                                                                                        <hr class="dropdown-divider">
                                                                                                    </li>
                                                                                                    <li> <a href="javascript: document.logoutForm.submit()" class="dropdown-item d-flex align-items-center" > <i class="bi bi-box-arrow-right"></i> <span>注销</span> </a>
                                                                                                    </li>

                                                                                                </ul>
                                                                                            </li>
                                                                                        </ul>
                                                                                    </nav>
                                                                                </header>
                                                                            </body>
                                                                            </html>

footer.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"  xmlns:sec="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
            <meta content="width=device-width, initial-scale=1.0" name="viewport">
                <title>Dashboard - Admin Bootstrap Template</title>
                <meta name="robots" content="noindex, nofollow">
                    <meta content="" name="description">
                        <meta content="" name="keywords">
                            <link href="assets/img/favicon.png" rel="icon">
                                <link href="assets/img/apple-touch-icon.png" rel="apple-touch-icon">
                                    <link href="https://fonts.gstatic.com" rel="preconnect">
                                        <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
                                            <link href="assets/css/bootstrap.min.css" rel="stylesheet">
                                                <link href="assets/css/bootstrap-icons.css" rel="stylesheet">
                                                    <link href="assets/css/boxicons.min.css" rel="stylesheet">
                                                        <link href="assets/css/quill.snow.css" rel="stylesheet">
                                                            <link href="assets/css/quill.bubble.css" rel="stylesheet">
                                                                <link href="assets/css/remixicon.css" rel="stylesheet">
                                                                    <link href="assets/css/simple-datatables.css" rel="stylesheet">
                                                                        <link href="assets/css/style.css" rel="stylesheet">
                                                                            </head>
                                                                            <body>


                                                                                <footer id="footer" class="footer"  th:fragment="footer">
                                                                                    <div class="copyright"> &copy; Copyright <strong><span>CRUD和RBAC系统</span></strong>. All Rights Reserved</div>
                                                                                    <div class="credits"> with love <a href="https://freeetemplates.com/">FreeeTemplates</a></div>
                                                                                </footer>
                                                                            </body>
                                                                            </html>

sidebar.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"  xmlns:sec="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
            <meta content="width=device-width, initial-scale=1.0" name="viewport">
                <title>Dashboard - Admin Bootstrap Template</title>
                <meta name="robots" content="noindex, nofollow">
                    <meta content="" name="description">
                        <meta content="" name="keywords">
                            <link href="assets/img/favicon.png" rel="icon">
                                <link href="assets/img/apple-touch-icon.png" rel="apple-touch-icon">
                                    <link href="https://fonts.gstatic.com" rel="preconnect">
                                        <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
                                            <link href="assets/css/bootstrap.min.css" rel="stylesheet">
                                                <link href="assets/css/bootstrap-icons.css" rel="stylesheet">
                                                    <link href="assets/css/boxicons.min.css" rel="stylesheet">
                                                        <link href="assets/css/quill.snow.css" rel="stylesheet">
                                                            <link href="assets/css/quill.bubble.css" rel="stylesheet">
                                                                <link href="assets/css/remixicon.css" rel="stylesheet">
                                                                    <link href="assets/css/simple-datatables.css" rel="stylesheet">
                                                                        <link href="assets/css/style.css" rel="stylesheet">
                                                                            </head>
                                                                            <body>



                                                                                <aside id="sidebar" class="sidebar" th:fragment="sidebar">
                                                                                    <ul class="sidebar-nav" id="sidebar-nav">
                                                                                        <li class="nav-item"> <a class="nav-link " href="index.html"> <i class="bi bi-grid"></i> <span>Dashboard</span> </a></li>
                                                                                        <li class="nav-item">
                                                                                            <a class="nav-link collapsed" data-bs-target="#components-nav" data-bs-toggle="collapse" href="#"> <i class="bi bi-menu-button-wide"></i><span>系统管理</span><i class="bi bi-chevron-down ms-auto"></i> </a>
                                                                                            <ul id="components-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
                                                                                                <li sec:authorize="hasAuthority('user')"><a th:href="@{/user}"><i class="bi bi-circle"></i><span>用户管理</span> </a></li>
                                                                                                <li sec:authorize="hasAuthority('role')"><a th:href="@{/role}"><i class="bi bi-circle"></i><span>角色管理</span> </a></li>
                                                                                                <li sec:authorize="hasAuthority('permission')"><a th:href="@{/permission}"><i class="bi bi-circle"></i><span>权限管理</span> </a></li>
                                                                                                <li> <a href="components-breadcrumbs.html"> <i class="bi bi-circle"></i><span>Breadcrumbs</span> </a></li>
                                                                                                <li> <a href="components-buttons.html"> <i class="bi bi-circle"></i><span>Buttons</span> </a></li>
                                                                                                <li> <a href="components-cards.html"> <i class="bi bi-circle"></i><span>Cards</span> </a></li>
                                                                                                <li> <a href="components-carousel.html"> <i class="bi bi-circle"></i><span>Carousel</span> </a></li>
                                                                                                <li> <a href="components-list-group.html"> <i class="bi bi-circle"></i><span>List group</span> </a></li>
                                                                                                <li> <a href="components-modal.html"> <i class="bi bi-circle"></i><span>Modal</span> </a></li>
                                                                                                <li> <a href="components-tabs.html"> <i class="bi bi-circle"></i><span>Tabs</span> </a></li>
                                                                                                <li> <a href="components-pagination.html"> <i class="bi bi-circle"></i><span>Pagination</span> </a></li>
                                                                                                <li> <a href="components-progress.html"> <i class="bi bi-circle"></i><span>Progress</span> </a></li>
                                                                                                <li> <a href="components-spinners.html"> <i class="bi bi-circle"></i><span>Spinners</span> </a></li>
                                                                                                <li> <a href="components-tooltips.html"> <i class="bi bi-circle"></i><span>Tooltips</span> </a></li>
                                                                                            </ul>
                                                                                        </li>
                                                                                        <li class="nav-item">
                                                                                            <a class="nav-link collapsed" data-bs-target="#forms-nav" data-bs-toggle="collapse" href="#"> <i class="bi bi-journal-text"></i><span>Forms</span><i class="bi bi-chevron-down ms-auto"></i> </a>
                                                                                            <ul id="forms-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
                                                                                                <li> <a href="forms-elements.html"> <i class="bi bi-circle"></i><span>Form Elements</span> </a></li>
                                                                                                <li> <a href="forms-layouts.html"> <i class="bi bi-circle"></i><span>Form Layouts</span> </a></li>
                                                                                                <li> <a href="forms-editors.html"> <i class="bi bi-circle"></i><span>Form Editors</span> </a></li>
                                                                                                <li> <a href="forms-validation.html"> <i class="bi bi-circle"></i><span>Form Validation</span> </a></li>
                                                                                            </ul>
                                                                                        </li>
                                                                                        <li class="nav-item">
                                                                                            <a class="nav-link collapsed" data-bs-target="#tables-nav" data-bs-toggle="collapse" href="#"> <i class="bi bi-layout-text-window-reverse"></i><span>Tables</span><i class="bi bi-chevron-down ms-auto"></i> </a>
                                                                                            <ul id="tables-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
                                                                                                <li> <a href="tables-general.html"> <i class="bi bi-circle"></i><span>General Tables</span> </a></li>
                                                                                                <li> <a href="tables-data.html"> <i class="bi bi-circle"></i><span>Data Tables</span> </a></li>
                                                                                            </ul>
                                                                                        </li>
                                                                                        <li class="nav-item">
                                                                                            <a class="nav-link collapsed" data-bs-target="#charts-nav" data-bs-toggle="collapse" href="#"> <i class="bi bi-bar-chart"></i><span>Charts</span><i class="bi bi-chevron-down ms-auto"></i> </a>
                                                                                            <ul id="charts-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
                                                                                                <li> <a href="charts-chartjs.html"> <i class="bi bi-circle"></i><span>Chart.js</span> </a></li>
                                                                                                <li> <a href="charts-apexcharts.html"> <i class="bi bi-circle"></i><span>ApexCharts</span> </a></li>
                                                                                                <li> <a href="charts-echarts.html"> <i class="bi bi-circle"></i><span>ECharts</span> </a></li>
                                                                                            </ul>
                                                                                        </li>
                                                                                        <li class="nav-item">
                                                                                            <a class="nav-link collapsed" data-bs-target="#icons-nav" data-bs-toggle="collapse" href="#"> <i class="bi bi-gem"></i><span>Icons</span><i class="bi bi-chevron-down ms-auto"></i> </a>
                                                                                            <ul id="icons-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
                                                                                                <li> <a href="icons-bootstrap.html"> <i class="bi bi-circle"></i><span>Bootstrap Icons</span> </a></li>
                                                                                                <li> <a href="icons-remix.html"> <i class="bi bi-circle"></i><span>Remix Icons</span> </a></li>
                                                                                                <li> <a href="icons-boxicons.html"> <i class="bi bi-circle"></i><span>Boxicons</span> </a></li>
                                                                                            </ul>
                                                                                        </li>
                                                                                        <li class="nav-heading">Pages</li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="users-profile.html"> <i class="bi bi-person"></i> <span>Profile</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-faq.html"> <i class="bi bi-question-circle"></i> <span>F.A.Q</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-contact.html"> <i class="bi bi-envelope"></i> <span>Contact</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-register.html"> <i class="bi bi-card-list"></i> <span>Register</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-login.html"> <i class="bi bi-box-arrow-in-right"></i> <span>Login</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-error-404.html"> <i class="bi bi-dash-circle"></i> <span>Error 404</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-blank.html"> <i class="bi bi-file-earmark"></i> <span>Blank</span> </a></li>
                                                                                    </ul>
                                                                                </aside>
                                                                            </body>
                                                                            </html>

list_user.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta content="width=device-width, initial-scale=1.0" name="viewport">
        <title>Dashboard - Admin Bootstrap Template</title>
        <meta name="robots" content="noindex, nofollow">
        <meta content="" name="description">
        <meta content="" name="keywords">
        <link href="/assets/img/favicon.png" rel="icon">
        <link href="/assets/img/apple-touch-icon.png" rel="apple-touch-icon">
        <link href="/assets/css/bootstrap.min.css" rel="stylesheet">
        <link href="/assets/css/bootstrap-icons.css" rel="stylesheet">
        <link href="/assets/css/boxicons.min.css" rel="stylesheet">
        <link href="/assets/css/quill.snow.css" rel="stylesheet">
        <link href="/assets/css/quill.bubble.css" rel="stylesheet">
        <link href="/assets/css/remixicon.css" rel="stylesheet">
        <link href="/assets/css/simple-datatables.css" rel="stylesheet">
        <link href="/assets/css/style.css" rel="stylesheet">
    </head>
    <body>
        <header id="header" class="header fixed-top d-flex align-items-center" th:replace="fragments/header :: header">
        </header>
        <aside id="sidebar" class="sidebar" th:replace="fragments/sidebar :: sidebar">
        </aside>
        <main id="main" class="main">
            <div class="pagetitle">
                <nav>
                    <ol class="breadcrumb">
                        <li class="breadcrumb-item"><a href="index.html">主页</a></li>
                        <li class="breadcrumb-item active">用户管理</li>
                    </ol>
                </nav>
            </div>
            <section class="section dashboard">
                <div class="row">
                    <div class="col-lg-12">
                        <div class="card">
                            <div class="card-body">

                                <br/>
                                <br/>
                                <div class="table-title">
                                    <div class="row">
                                        <div class="col-sm-6">
                                            <h2> <b>用户管理</b></h2>
                                        </div>
                                        <div class="col-sm-6">
                                            <div sec:authorize="hasAnyAuthority('permission_create')">
                                                <a href="/user/new" class="btn btn-success" ><i class="bi bi-person-plus" data-toggle="tooltip" title="添加用户"></i></a>
                                                <!-- <a href="#deleteEmployeeModal" class="btn btn-danger" data-toggle="modal"><i class="material-icons">&#xE15C;</i> <span>Delete</span></a>						 -->
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <table class="table table-striped table-hover">
                                    <thead >
                                        <tr>
                                            <th>ID</th>
                                            <th>User Name</th>
                                            <th>E-mail</th>
                                            <th>Name</th>
                                            <th>Home Page</th>
                                            <th>Roles</th>
                                            <th>Actions</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <tr th:each="user: ${listUsers}">
                                            <td th:text="${user.id}">User ID</td>
                                            <td th:text="${user.username}">User Name</td>
                                            <td th:text="${user.email}">E-mail</td>
                                            <td th:text="${user.name}">Name</td>
                                            <td th:text="${user.homepage}">Home Page</td>
                                            <td th:text="${user.roles}">Roles</td>
                                            <td>
                                                <a sec:authorize="hasAuthority('user_edit')" th:href="@{'/user/edit/' + ${user.id}}" class="edit" ><i class="bi bi-pencil" data-toggle="tooltip" title="编辑"></i></a>
                                                <a sec:authorize="hasAuthority('user_edit')" th:href="@{'/user/resetpassword/' + ${user.id}}" class="reset" ><i class="bi bi-key" data-toggle="tooltip" title="重置密码"></i></a>
                                                <a sec:authorize="hasAuthority('user_delete')" th:href="@{'/user/delete/' + ${user.id}}" class="delete" ><i class="bi bi-trash" data-toggle="tooltip" title="删除"></i></a>


                                            </td>
                                        </tr>
                                    </tbody>
                                </table>



                            </div>
                        </div>
                    </div>
            </section>
        </main>
        <footer id="footer" class="footer" th:replace="fragments/footer :: footer">
            <div class="copyright"> &copy; Copyright <strong><span>Compnay Name</span></strong>. All Rights Reserved</div>
            <div class="credits"> with love <a href="https://freeetemplates.com/">FreeeTemplates</a></div>
        </footer>
        <a href="#" class="back-to-top d-flex align-items-center justify-content-center"><i class="bi bi-arrow-up-short"></i></a>
        <script src="/assets/js/apexcharts.min.js"></script>
        <script src="/assets/js/bootstrap.bundle.min.js"></script>
        <script src="/assets/js/chart.min.js"></script>
        <script src="/assets/js/echarts.min.js"></script>
        <script src="/assets/js/quill.min.js"></script>
        <script src="/assets/js/simple-datatables.js"></script>
        <script src="/assets/js/tinymce.min.js"></script>
        <script src="/assets/js/validate.js"></script>
        <script src="/assets/js/main.js"></script>

    </body>
</html>

 

Historical password storage and reuse detection

package com.example;

import org.apache.commons.text.similarity.LevenshteinDistance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;
import java.util.Date;
import java.util.List;
import java.util.Set;

@Controller

public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @GetMapping("/user/new")
    public String showRegistrationForm(Model model) {
        model.addAttribute("user", new UserDTO());

        return "user/new_user";
    }

    @GetMapping("/user")
    public String listUsers(Model model) {
        List<User> listUsers = userRepository.findAll();
        model.addAttribute("listUsers", listUsers);

        return "user/list_user";
    }

    @GetMapping("/user/edit/{id}")
    public String editUser(@PathVariable("id") Long id, Model model) {
        User user = userService.get(id);
        List<Role> listRoles = userService.listRoles();
        model.addAttribute("user", user);
        model.addAttribute("listRoles", listRoles);
        return "user/edit_user";
    }

    @PostMapping("/user/save")
    public String saveUser(@Valid @ModelAttribute("user") UserDTO user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "user/new_user";
        }
        userService.saveUser(user);

        return "redirect:/user";
    }

    @PostMapping("/user/update")
    public String updateUser(User user) {
        User repoUser = userRepository.findById(user.getId()).orElse(null);

        if (repoUser != null) {
            repoUser.setUsername(user.getUsername());
            repoUser.setEmail(user.getEmail());
            repoUser.setName(user.getName());
            repoUser.setEnabled(user.getEnabled());
            repoUser.setAccountNonLocked(user.getAccountNonLocked());
            repoUser.setHomepage(user.getHomepage());
            repoUser.setRoles(user.getRoles());
            userRepository.save(repoUser);
        }

        return "redirect:/user";
    }

    @GetMapping("/user/resetpassword/{id}")
    public String showResetPasswordForm(@PathVariable("id") Long id, Model model) {
        User user = userRepository.findById(id).orElse(null);
        UserResetPasswordDTO userResetPasswordDTO = new UserResetPasswordDTO();
        if (user != null) {
            userResetPasswordDTO.setId(user.getId());
            userResetPasswordDTO.setUsername(user.getUsername());
            model.addAttribute("user", userResetPasswordDTO);
            return "user/reset_password";
        }
        return "redirect:/user";
    }

    @PostMapping("/user/savepassword")
    public String savePassword(@Valid @ModelAttribute("user") UserResetPasswordDTO user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "user/reset_password";
        }
        User repoUser = userRepository.findById(user.getId()).orElse(null);

        if (repoUser != null) {
            repoUser.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
            Date passwordChangedTime = new Date();

            repoUser.setPasswordChangedTime(passwordChangedTime);
            userRepository.save(repoUser);
        }

        return "redirect:/user";
    }

    @RequestMapping("/user/delete/{id}")
    public String deleteUser(@PathVariable(name = "id") Long id) {
        userRepository.deleteById(id);

        return "redirect:/user";
    }

    @GetMapping("user/info")
    public String userProfile(Model model) {

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        System.out.println(auth.getName());
        User user = userRepository.getByUsername(auth.getName());
        model.addAttribute("user", user);

        return "user/user_profile";
    }

    @GetMapping("/change/password")

    public String changePassword(Model model) {

        model.addAttribute("userDTO", new UserChangePasswordDTO());
        return "user/password_update";

    }

    @PostMapping("/new/password")
    public String
            newPassword(@Valid @ModelAttribute("userDTO") UserChangePasswordDTO userChangePasswordDTO, BindingResult bindingResult, Model model) {
        if (bindingResult.hasErrors()) {
            return "user/password_update";

        }

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (userChangePasswordDTO.getNewPass().equals(userChangePasswordDTO.getConfirmPass())) {
            User user = userService.findUserByUsername(auth.getName());
            boolean status = userService.isPasswordValid(userChangePasswordDTO.getPassword(), user.getPassword());

            if (status) {

                LevenshteinDistance levenshteinWithThreshold = new LevenshteinDistance(3);
                // Returns -1 since the actual distance, 4, is higher than the threshold
                System.out.println("Levenshtein distance: " + levenshteinWithThreshold.apply(userChangePasswordDTO.getNewPass(), userChangePasswordDTO.getPassword()));
                if (levenshteinWithThreshold.apply(userChangePasswordDTO.getNewPass(), userChangePasswordDTO.getPassword()) == -1) {
                    Set<History> setHistorysCheck = user.getHistorys();
                    boolean check = true;
                    for (History hist : setHistorysCheck) {
                        System.out.print(hist.getPassword());
                        if (bCryptPasswordEncoder.matches(userChangePasswordDTO.getNewPass(), hist.getPassword())) {
                            check = false;
                            break;
                        }
                    }

                    if (check) {

                        System.out.println(user.getHistorys());
                        History history = new History();
                        System.out.println("userChangePasswordDTO.getNewPass()=" + userChangePasswordDTO.getNewPass());
                        history.setPassword(bCryptPasswordEncoder.encode(userChangePasswordDTO.getNewPass()));
                        System.out.println(history);
                        Set<History> setHistorys = user.getHistorys();
                        setHistorys.add(history);
                        user.setHistorys(setHistorys);
                        System.out.println(user.getHistorys());
                        userService.changePassword(user, userChangePasswordDTO);
                        return "login";
                    } else {
                        model.addAttribute("passMatched", "New password was same as history..!");
                        return "user/password_update";
                    }
                } else {
                    model.addAttribute("passMatched", "New password need 4 diff with Current password..!");
                    return "user/password_update";
                }
            } else {

                model.addAttribute("wrongPass", "Current password was wrong..!");
                return "user/password_update";
            }

        } else {
            model.addAttribute("passMatched", "Password doesn't matched..!");
            return "user/password_update";
        }

    }

}

 

 

 

 

in conclusion:

So far, you have learned to secure your Spring Boot application using forms-based authentication and in-database credentials. You can see that Spring security makes implementing login and logout functionality very easy and very convenient. For your convenience, you can download the sample project below.

源码:allwaysoft/ProductManagerUserDetailsServiceAuditBoot3.0 · GitHub

Guess you like

Origin blog.csdn.net/allway2/article/details/128050891