在本教程中,我将指导您如何编写代码,以使用具有基于表单的身份验证的Spring安全API来保护Spring Boot应用程序中的网页。用户详细信息存储在MySQL数据库中,并使用春季JDBC连接到数据库。我们将从本教程中的 ProductManager 项目开始,向现有的弹簧启动项目添加登录和注销功能。
1. 创建用户表和虚拟凭据
凭据应存储在数据库中,使用了Spring Data JPA 自动创建表,表间关系ER图如下:
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一起使用:请注意,依赖项版本已由弹簧启动初学者父项目定义。
<?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. 配置 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<? 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<>();
}
角色和权限同时起作用
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.登录页面
<!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/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/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. 测试登录和注销
启动Spring Boot应用程序并访问 http://localhost:8080 在Web浏览器中,您将看到自定义的登录页面出现:
现在输入正确的用户名admin和密码admin,您将看到主页如下:
并注意欢迎消息后跟用户名。用户现在已通过身份验证以使用该应用程序。单击“注销”按钮,您将看到自定义的登录页面出现,这意味着我们已成功实现登录并注销到我们的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<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);
}
}
thymeleaf视图文件中,根据权限显示连接菜单
<!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"> © 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"> © 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"></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"> © 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>
历史密码保存和重用检测
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";
}
}
}
结论:
到目前为止,您已经学会了使用基于表单的身份验证和数据库内凭据来保护Spring Boot应用程序。您会看到 Spring 安全性使实现登录和注销功能变得非常容易,并且非常方便。为方便起见,您可以下载下面的示例项目。
源码:allwaysoft/ProductManagerUserDetailsServiceAuditBoot3.0 · GitHub