Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers

在本教程中,我将指导您如何编写代码,以使用具有基于表单的身份验证的Spring安全API来保护Spring Boot应用程序中的网页。用户详细信息存储在MySQL数据库中,并使用春季JDBC连接到数据库。我们将从本教程中的 ProductManager 项目开始,向现有的弹簧启动项目添加登录和注销功能。

1. 创建用户表和虚拟凭据

凭据应存储在数据库中,使用了Spring Data JPA 自动创建表,表间关系ER图如下:

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第1张图片

2. 配置数据源属性

接下来,在应用程序属性文件中指定数据库连接信息,如下所示:根据您的MySQL数据库更新URL,用户名和密码。

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. 声明弹簧安全性和 MySQL JDBC 驱动程序的依赖关系

要将Spring安全API用于项目,请在pom.xml文件中声明以下依赖项:并且要将JDBC与弹簧启动和MySQL一起使用:请注意,依赖项版本已由弹簧启动初学者父项目定义。



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        3.0.0
         
    
    net.codejava
    ProductManagerUserDetailsServiceAuditBoot3.0
    2.0
    ProductManagerUserDetailsServiceAuditBoot3.0
    Spring Boot CRUD Web App Example
    jar

    
        1.8
    

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


    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


4. 配置 Spring  Security

要将 Spring 安全性与基于表单的身份验证和 CustomUserDetailsService 结合使用,请按如下方式创建 WebSecurityConfig 类:

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();
    }
}

此安全配置类必须使用@EnableWebSecurity注释进行批注,并且是 Web 安全配置器适配器的子类。

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 getAuthorities(
            Collection roles) {
        List 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 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 historys = new HashSet<>();

}

角色和权限同时起作用

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第2张图片

5.登录验证过程

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验证码

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.登录页面



    
        
        
        
        Bootstrap 5 Sign In Form with Image Example
        
        
        
    

    

        

[[${session.SPRING_SECURITY_LAST_EXCEPTION.message}]]

You have been logged out.

请登录

Click the picture to refresh!

5. 测试登录和注销

启动Spring Boot应用程序并访问 http://localhost:8080 在Web浏览器中,您将看到自定义的登录页面出现:

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第3张图片

 现在输入正确的用户名admin和密码admin,您将看到主页如下:

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第4张图片

 并注意欢迎消息后跟用户名。用户现在已通过身份验证以使用该应用程序。单击“注销”按钮,您将看到自定义的登录页面出现,这意味着我们已成功实现登录并注销到我们的Spring Boot应用程序。

自定义从数据库中获取动态权限验证

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 {

    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 {

    @Autowired
    private PermissionRepository permissionRepository;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public AuthorizationDecision check(Supplier 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 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);
    }
}

thymeleaf视图文件中,根据权限显示连接菜单



    
        
        
        Dashboard - Admin Bootstrap Template
        
        
        
        
        
        
        
        
        
        
        
        
        
    
    
        
        
        


用户管理

ID User Name E-mail Name Home Page Roles Actions
User ID User Name E-mail Name Home Page Roles
with love FreeeTemplates

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

header.html



    
        
            
                Dashboard - Admin Bootstrap Template
                
                    
                        
                            
                                
                                    
                                        
                                            
                                                
                                                    
                                                        
                                                            
                                                                
                                                                    
                                                                        
                                                                            
                                                                            

                                                                                
                                                                            
                                                                            

footer.html



    
        
            
                Dashboard - Admin Bootstrap Template
                
                    
                        
                            
                                
                                    
                                        
                                            
                                                
                                                    
                                                        
                                                            
                                                                
                                                                    
                                                                        
                                                                            
                                                                            


                                                                                
with love FreeeTemplates

sidebar.html



    
        
            
                Dashboard - Admin Bootstrap Template
                
                    
                        
                            
                                
                                    
                                        
                                            
                                                
                                                    
                                                        
                                                            
                                                                
                                                                    
                                                                        
                                                                            
                                                                            



                                                                                
                                                                            
                                                                            

list_user.html



    
        
        
        Dashboard - Admin Bootstrap Template
        
        
        
        
        
        
        
        
        
        
        
        
        
    
    
        
        
        


用户管理

ID User Name E-mail Name Home Page Roles Actions
User ID User Name E-mail Name Home Page Roles
with love FreeeTemplates

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第5张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第6张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第7张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第8张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第9张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第10张图片

 Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第11张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第12张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第13张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第14张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第15张图片

历史密码保存和重用检测

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 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 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 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 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";
        }

    }

}

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第16张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第17张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第18张图片

Spring Boot 3.0 Security 6定制UserDetailsService,动态权限,Thymeleaf,密码强度、过期、锁定、解锁、禁用、历史新密码编辑距离、登录日志、Envers_第19张图片

结论:

到目前为止,您已经学会了使用基于表单的身份验证和数据库内凭据来保护Spring Boot应用程序。您会看到 Spring 安全性使实现登录和注销功能变得非常容易,并且非常方便。为方便起见,您可以下载下面的示例项目。

源码:allwaysoft/ProductManagerUserDetailsServiceAuditBoot3.0 · GitHub

你可能感兴趣的:(数据库,java,spring)