Spring Security 定制UserDetailsService,动态uri权限,Thymeleaf,限制密码强度、过期、错误密码锁定超时自动解锁、禁用历史密码、新密码和现密码差异要求编辑距离

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

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

凭据应存储在数据库中,因此让我们创建新表,表间关系ER图如下:

-- --------------------------------------------------------
-- 主机:                           127.0.0.1
-- 服务器版本:                        8.0.22 - MySQL Community Server - GPL
-- 服务器操作系统:                      Win64
-- HeidiSQL 版本:                  12.1.0.6537
-- --------------------------------------------------------

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;


-- 导出 product3 的数据库结构
CREATE DATABASE IF NOT EXISTS `product3` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `product3`;

-- 导出  表 product3.history 结构
CREATE TABLE IF NOT EXISTS `history` (
  `id` int NOT NULL AUTO_INCREMENT,
  `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 正在导出表  product3.history 的数据:~17 rows (大约)
INSERT INTO `history` (`id`, `password`) VALUES
	(1, NULL),
	(2, NULL),
	(3, NULL),
	(4, NULL),
	(5, NULL),
	(6, NULL),
	(7, '$2a$10$f8BTEI8AkiBQV60MvzpnVehdo9.AM6Zevvg8817dj5qbNuu.p3B22'),
	(8, '$2a$10$9.ZG.D.e8u4NhGkGCVVUcOqpOB1b6WpCM7hEdawqIFo1G7Mg6/bt.'),
	(9, '$2a$10$IK/CKJLp5jH7hQYjfTjZIeAPPNuI2rGQuEWMk8Z.Of9QkUvQC28X6'),
	(10, '$2a$10$Lv.4MczeZKZNJyi58HVtp.0wztSxduK53L7O836KQS5mxItAoQsRG'),
	(11, '$2a$10$7jbJuk9kHXXlZQOspT2T0O/Quj/zrMZC0CrPgQvUGSrhZRoub7kUO'),
	(12, '$2a$10$bh68pHYzneQEWElmQRT/qOp/00WldBypIacHESvZGMHZ3vBJiN88u'),
	(13, '$2a$10$8m5rM1JZ4RLcnOpuKXFtlOeXxHxtWHk7PuXumOJFGFnRA6o5hxz7.'),
	(14, '$2a$10$VSsuBdX6sM2jtCAZ/gOhIut.aqfoymVYceSIUP6ncVMNDJmT2VjJi'),
	(15, '$2a$10$.wqeNise86FpzDlZHrWISenOnOXryn71JNBlrhAM0Nv6Ae6M739t6'),
	(16, '$2a$10$fdl7T4Gx37W8YANpb.JaF.D04vxecmMGlidcPHhziCtRpI1dXj3Dy'),
	(17, '$2a$10$VcsIbKiV7VFHHFZZAMUtKeHl5HKlaB45jCdfoLAXgQd1i8rIwC2N.'),
	(18, '$2a$10$OrlRVA057ZlHM9MNJmWTzO26vawbNu3O.dViXuN8bLfBCpyDY20ni'),
	(19, '$2a$10$qyi1TCu6X/eAAdfXQqfmtuZF.zdYEMC2ZprdyFmBQVRshLUjLZpEC');

-- 导出  表 product3.permission 结构
CREATE TABLE IF NOT EXISTS `permission` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `uri` varchar(8192) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `method` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 正在导出表  product3.permission 的数据:~27 rows (大约)
INSERT INTO `permission` (`id`, `name`, `description`, `uri`, `method`) VALUES
	(3, 'product_create', '增加产品', '/product/new', 'GET'),
	(4, 'product_delete', '删除产品', '/product/delete/*', 'GET'),
	(5, 'product_save', '保存产品', '/product/save', 'POST'),
	(9, 'product', '列表产品', '/product', 'GET'),
	(10, 'product_edit', '编辑产品', '/product/edit/*', 'GET'),
	(11, 'register', '注册用户', '/register', 'GET'),
	(12, 'process_register', '保存用户', '/process_register', 'POST'),
	(13, 'user', '列表用户', '/user', 'GET'),
	(14, 'user/info', 'user/info', '/user/info', 'GET'),
	(15, 'user_save', '保存用户', '/user/save', 'POST'),
	(16, 'user_edit', '编辑用户', '/user/edit/*', 'GET'),
	(17, 'permission', '读取权限', '/permission', 'GET'),
	(18, 'permission_create', '增加权限', '/permission/new', 'GET'),
	(19, 'permission_delete', '删除权限', '/permission/delete/*', 'GET'),
	(20, 'permission_edit', '编辑权限', '/permission/edit/*', 'GET'),
	(21, 'permission_save', '保存权限', '/permission/save', 'POST'),
	(23, 'test1', '测试1', '/test1', 'GET1'),
	(24, 'user_delete', '删除用户', '/user/delete/*', 'GET'),
	(25, 'user_create', '增加用户', '/user/new', 'GET'),
	(26, 'role', '读取角色', '/role', 'GET'),
	(27, 'role_create', '创建角色', '/role/new', 'GET'),
	(28, 'role_save', '保存角色', '/role/save', 'POST'),
	(29, 'role_edit', '编辑角色', '/role/edit/*', 'GET'),
	(30, 'role_delete', '删除角色', '/role/delete/*', 'GET'),
	(31, 'user_update', '更新用户(不含密码)', '/user/update', 'POST'),
	(32, 'user_resetpassword', '重置密码', '/user/resetpassword/*', 'GET'),
	(33, 'user_savepassword', '保存重置密码', '/user/savepassword', 'POST'),
	(34, 'order_create', '创建订单', '/order/new', 'GET');

-- 导出  表 product3.product 结构
CREATE TABLE IF NOT EXISTS `product` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `brand` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `madein` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `price` float NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 正在导出表  product3.product 的数据:~4 rows (大约)
INSERT INTO `product` (`id`, `brand`, `madein`, `name`, `price`) VALUES
	(6, '6', '6', '61', 6),
	(7, '7', '7', '7', 7),
	(9, '66', '66', '66', 66),
	(14, '666', '中国', 'new', 111);

-- 导出  表 product3.role 结构
CREATE TABLE IF NOT EXISTS `role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 正在导出表  product3.role 的数据:~3 rows (大约)
INSERT INTO `role` (`id`, `name`, `description`) VALUES
	(1, 'ADMIN', 'Administrator role'),
	(2, 'USER_P1', 'Perfil 1'),
	(3, 'USER_P2', 'Perfil 2');

-- 导出  表 product3.role_permission 结构
CREATE TABLE IF NOT EXISTS `role_permission` (
  `id` int NOT NULL AUTO_INCREMENT,
  `role_id` int NOT NULL DEFAULT '0',
  `permission_id` int NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `role_id_permission_id` (`role_id`,`permission_id`),
  KEY `FK_role_permission_permission` (`permission_id`),
  CONSTRAINT `FK_role_permission_permission` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`id`),
  CONSTRAINT `FK_role_permission_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 正在导出表  product3.role_permission 的数据:~31 rows (大约)
INSERT INTO `role_permission` (`id`, `role_id`, `permission_id`) VALUES
	(1, 1, 3),
	(2, 1, 4),
	(3, 1, 5),
	(4, 1, 9),
	(5, 1, 10),
	(11, 1, 11),
	(12, 1, 12),
	(13, 1, 13),
	(14, 1, 14),
	(16, 1, 15),
	(15, 1, 16),
	(17, 1, 17),
	(18, 1, 18),
	(19, 1, 19),
	(20, 1, 20),
	(22, 1, 21),
	(23, 1, 24),
	(24, 1, 25),
	(25, 1, 26),
	(26, 1, 27),
	(29, 1, 28),
	(28, 1, 29),
	(27, 1, 30),
	(35, 1, 31),
	(36, 1, 32),
	(37, 1, 33),
	(8, 2, 5),
	(6, 2, 9),
	(7, 2, 10),
	(10, 3, 4),
	(9, 3, 9);

-- 导出  表 product3.user 结构
CREATE TABLE IF NOT EXISTS `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `name` varchar(65) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `enabled` bit(1) DEFAULT b'1',
  `homepage` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '',
  `password_changed_time` datetime(6) DEFAULT NULL,
  `account_non_locked` bit(1) DEFAULT b'1',
  `failed_attempt` int DEFAULT '0',
  `lock_time` datetime(6) DEFAULT NULL,
  `account_non_expired` bit(1) NOT NULL,
  `credentials_non_expired` bit(1) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 正在导出表  product3.user 的数据:~14 rows (大约)
INSERT INTO `user` (`id`, `username`, `email`, `name`, `password`, `enabled`, `homepage`, `password_changed_time`, `account_non_locked`, `failed_attempt`, `lock_time`, `account_non_expired`, `credentials_non_expired`) VALUES
	(1, 'admin', 'admin@admin.com', '系统管理员', '$2a$10$6uilpgI7dsrj9q/Z3lN5GeKZhECGvhjIf6FwrhHdMLodiBxIEvM3y', b'1', '', '2022-11-18 16:15:05.186000', b'1', 0, NULL, b'1', b'1'),
	(2, 'u1', 'u1@example.com', 'User P1', '$2a$10$7lsSw2BeeDCmm6gYJBdJfuotFoDkfggWq8pWTjj7awbwywNdGHZ3u', b'1', 'home', NULL, b'1', 0, NULL, b'0', b'0'),
	(3, 'u2', 'u2@example.com', 'User P2', '$2a$10$2/LSmp3YoEOT97KzgrYODen7I88ErBovM2Qehw9DL1dW9DZ7DZSAm', b'1', '', NULL, b'1', 0, NULL, b'0', b'0'),
	(11, 'allway222', 'allway222@1.com', 'allway222', '$2a$10$WTg/WDKmYaYZUbFl39Z6au78WOG8LI9bcnvbbLs/.t5MjzwrBD60u', b'1', 'home', NULL, b'1', 0, NULL, b'0', b'0'),
	(12, 'allway666', 'allway666@1.com', 'allway66622', '$2a$10$Jeky6hyUSN3i0u6IPF8squDtjkFwHL7WzP3rRoF4QOEwWfVAzi/c2', b'1', 'home', NULL, b'1', 0, NULL, b'0', b'0'),
	(13, 'allway777', 'allway777@1.com', 'allway777', '$2a$10$W0TWw1.9lBYG7PW8KsEWcuNFGiOJAOobr55bKMQD8rf68GVppUH4S', b'1', 'home', NULL, b'1', 0, NULL, b'0', b'0'),
	(14, 'test', '1@1.com', 'test', '$2a$10$RMVwmkanbTyghH3tiyyIqO94OAqzDPa8X9sb3AVQY7OGHPFOJefUS', b'1', '', '2022-11-18 16:47:14.309000', b'1', 0, NULL, b'0', b'0'),
	(25, 'user102', 'Password@123.com', 'user102', '$2a$10$hZvr4Ao7ofNFdXHjPkRWXuCuuUUJmImhJjVAiiWxTBKRF2Rx5Z8RG', b'1', '', '2022-11-15 15:17:05.924000', b'1', 0, NULL, b'0', b'0'),
	(26, 'user103', 'Password@123.com', 'user103', '$2a$10$E6Zy8hSM6EVV53G8PjRyoeGdVi6Rj0hkkSQHvZPARoKTwwFD0kWza', b'1', '', '2022-11-16 12:20:26.190000', b'1', 0, NULL, b'0', b'0'),
	(27, 'user200', '1@1.com', 'user200', '$2a$10$8gy2nX5/nTfCPEmr8KPNfuOpQHgH9.2h6NQG0uBdlBzlK5n3jz6OC', b'1', '', '2022-11-16 14:45:42.963000', b'1', 0, NULL, b'0', b'0'),
	(28, 'user300', '1@1.com', 'user300', '$2a$10$0k1AJFxJ4/LbFyRvjH5KHONlZKDAoB34R8ze9wVcpIfkXChXPUnzu', b'1', '', '2022-11-16 14:56:20.506000', b'1', 0, NULL, b'0', b'0'),
	(30, 'user500', '1@1.com', 'user500', '$2a$10$LJc24EMmR9JsKnc6uDBpLufeKZK/jIonsLlGf5n67Tn9cOm6LvLfK', b'1', '', '2022-11-16 15:31:32.021000', b'1', 0, NULL, b'0', b'0'),
	(32, 'user700', '1@1.com', 'user700', '$2a$10$IWgF7b6xbv1Am9tiL.QvaehLJcyFChX.dAluPIfTMjvFIc4cDH9Dm', b'1', '', '2022-11-18 16:13:29.637000', b'1', 0, NULL, b'1', b'1'),
	(33, 'user800', '1@1.com', 'user800', '$2a$10$Q7JUDcptZuT0f5xsM1hdqOPwRFjSFEiYflq7ZOcwWBH/SeDZUqS1O', b'1', '', '2022-11-20 12:59:42.560000', b'0', 2, '2022-11-20 13:02:40.871000', b'1', b'1');

-- 导出  表 product3.user_history 结构
CREATE TABLE IF NOT EXISTS `user_history` (
  `user_id` int NOT NULL,
  `history_id` int NOT NULL,
  PRIMARY KEY (`user_id`,`history_id`),
  KEY `FKmyn0e5qi70qvka9b47oaedacw` (`history_id`),
  CONSTRAINT `FKaa6ilb6iqih95bntoeyysb2pc` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
  CONSTRAINT `FKmyn0e5qi70qvka9b47oaedacw` FOREIGN KEY (`history_id`) REFERENCES `history` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 正在导出表  product3.user_history 的数据:~18 rows (大约)
INSERT INTO `user_history` (`user_id`, `history_id`) VALUES
	(1, 1),
	(1, 2),
	(1, 3),
	(1, 4),
	(1, 5),
	(1, 6),
	(1, 7),
	(1, 8),
	(1, 9),
	(1, 10),
	(1, 11),
	(26, 12),
	(26, 13),
	(1, 14),
	(1, 15),
	(30, 16),
	(30, 17),
	(1, 18),
	(14, 19);

-- 导出  表 product3.user_role 结构
CREATE TABLE IF NOT EXISTS `user_role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL DEFAULT '0',
  `role_id` int NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id_role_id` (`user_id`,`role_id`),
  KEY `FK_user_role_role` (`role_id`),
  CONSTRAINT `FK_user_role_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`),
  CONSTRAINT `FK_user_role_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=97 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 正在导出表  product3.user_role 的数据:~18 rows (大约)
INSERT INTO `user_role` (`id`, `user_id`, `role_id`) VALUES
	(93, 1, 1),
	(92, 1, 2),
	(91, 1, 3),
	(2, 2, 2),
	(3, 3, 3),
	(43, 11, 1),
	(44, 12, 1),
	(26, 13, 1),
	(90, 14, 1),
	(60, 25, 1),
	(64, 26, 1),
	(63, 26, 2),
	(62, 26, 3),
	(76, 27, 1),
	(75, 28, 1),
	(87, 30, 1),
	(86, 30, 2),
	(85, 30, 3),
	(89, 32, 1),
	(96, 33, 1);

/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 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一起使用:请注意,依赖项版本已由弹簧启动初学者父项目定义。

<?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>2.7.4</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>net.codejava</groupId>
    <artifactId>ProductManagerJDBCAuthenticationManuallyAuthenticateCaptchaAccess</artifactId>
    <version>2.0</version>
    <name>ProductManagerJDBCAuthenticationManuallyAuthenticateCaptchaAccess</name>
    <description>ProductManagerJDBCAuthentication</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-springsecurity5</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </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 javax.servlet.Filter;
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.session.HttpSessionEventPublisher;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private CustomLoginFailureHandler loginFailureHandler;
    @Autowired
    private CustomLoginSuccessHandler successHandler;

    @Autowired

    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder authBuilder) throws Exception {
        authBuilder.userDetailsService(customUserDetailsService)
                .passwordEncoder(new BCryptPasswordEncoder());

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        Filter filter = new KaptchaAuthenticationFilter("/login", "/login?error");
        http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);

        http.authorizeRequests()
                .antMatchers("/captcha_image**").permitAll()
                .antMatchers("/header").permitAll()
                .antMatchers("/footer").permitAll()
                .antMatchers("/sidebar").permitAll()
                .antMatchers("/index2").permitAll()
                .antMatchers("/index3").permitAll()
                .antMatchers("/pages/**").permitAll()
                .antMatchers("/css/**").permitAll()
                .antMatchers("/js/**").permitAll()
                .antMatchers("/assets/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/common/**").permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/logout").permitAll()
                .antMatchers("/verify").permitAll()
                .antMatchers("/home").authenticated()
                .antMatchers("/user/info").authenticated()
                .antMatchers("/change/password").authenticated()
                .antMatchers("/new/password").authenticated()
                .antMatchers("/").authenticated()
                .anyRequest()
                .access("@rbacService.hasPermission(request , authentication)")
                .and()
                .formLogin().loginPage("/login")
                .permitAll()
                .failureHandler(loginFailureHandler)
                .successHandler(successHandler)
                .and()
                .logout().permitAll()
                .and()
                .exceptionHandling().accessDeniedPage("/403");
        http
                .sessionManagement()
                .maximumSessions(1)
                .expiredUrl("/")
                .maxSessionsPreventsLogin(false)
                .sessionRegistry(sessionRegistry());
    }

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

当不使用WebSecurityConfigurerAdapter 时最大会话限制不起作用,故此处仍使用废弃的WebSecurityConfigurerAdapter

此安全配置类必须使用@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);

        boolean enabled = true;
        boolean accountNonExpired = true;
        boolean credentialsNonExpired = true;
        boolean accountNonLocked = true;

        return new org.springframework.security.core.userdetails.User(
                domainUser.getUsername(),
                domainUser.getPassword(),
                domainUser.isEnabled(),
                domainUser.isAccountNonExpired(),
                domainUser.isCredentialsNonExpired(),
                domainUser.isAccountNonLocked(),
                getAuthorities(domainUser.getRoles())
        );
    }

    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 java.util.Date;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import lombok.Data;
import org.hibernate.annotations.DynamicUpdate;

@Data
@Entity
@DynamicUpdate

public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer 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 CommonController {

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

        return "login";
    }
}

kaptcha验证码

package com.example;

import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.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 javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;

@Data
@Entity
public class Permission {

    @Id
    private Long id;
    private String name;
    private String description;
    private String uri;
    private String method;

}
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 javax.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;

/**
 * RBAC模型实现Security,即通过角色对用户进行分组,在对每个角色进行权限授权就, 进而简化用户权限分配以及管理。 需要的表: user:
 * 用户信息表 保存有的用户id,用户名、密码、账号、状态、salt加盐 role:角色表 user_role:用户角色关联表 permission: 权限表
 * 保存的有权限相关信息 role_permission: 角色与权限管理表
 */
public interface RbacService {

    //用户判断当前请求是否有操作权限
    boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
package com.example;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

@Component("rbacService")
public class RbacServiceImpl implements RbacService {

    @Autowired
    private PermissionRepository permissionRepository;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {

        //获取用户认证信息
        System.out.println(authentication.getAuthorities());
        Object principal = authentication.getPrincipal();
        System.out.println(principal.getClass());
        //判断数据是否为空 以及类型是否正确
        if (null != principal && principal instanceof User) {
            String username = ((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.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 hasPermission;
    }
}

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

    </body>
</html>

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

header.html

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

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

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


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

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

footer.html

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


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

sidebar.html

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



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

list_user.html

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

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


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



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

    </body>
</html>

 

历史密码保存和重用检测

package com.example;

import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.validation.Valid;
import org.apache.commons.text.similarity.LevenshteinDistance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.ModelAndView;

@Controller

public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    /**
     * 注入身份认证管理器
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    @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") Integer 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);
        repoUser.setUsername(user.getUsername());
        repoUser.setEmail(user.getEmail());
        repoUser.setName(user.getName());
        repoUser.setEnabled(user.isEnabled());
        repoUser.setAccountNonLocked(user.isAccountNonLocked());
        repoUser.setHomepage(user.getHomepage());
        repoUser.setRoles(user.getRoles());
        userRepository.save(repoUser);

        return "redirect:/user";
    }

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

    @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);

        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") Integer id) {
        userRepository.deleteById(id);

        return "redirect:/user";
    }

//    @RequestMapping(value = "user/info", method = RequestMethod.GET)
//    public ModelAndView userProfile() {
//        ModelAndView modelAndView = new ModelAndView();
//        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
//        System.out.println(auth.getName());
//        User user = userRepository.getByUsername(auth.getName());
//        modelAndView.addObject("user", user);
//        modelAndView.addObject("userName", user.getUsername());
//        modelAndView.setViewName("user-profile");
//        return modelAndView;
//    }
    @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";
    }

//    @RequestMapping(value = "/change/password", method = RequestMethod.GET)
//    public ModelAndView changePassword() {
//        ModelAndView modelAndView = new ModelAndView();
//        modelAndView.addObject("userDTO", new UserChangePasswordDTO());
//        modelAndView.setViewName("password-update");
//        return modelAndView;
//    }
    @GetMapping("/change/password")

    public String changePassword(Model model) {

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

    }

//    @RequestMapping(value = "/new/password", method = RequestMethod.POST)
//    public ModelAndView newPassword(@Valid @ModelAttribute("userDTO") UserChangePasswordDTO userChangePasswordDTO, BindingResult bindingResult) {
//        if (bindingResult.hasErrors()) {
//            ModelAndView modelAndView = new ModelAndView();
//            modelAndView.addObject("userDTO", new UserChangePasswordDTO());
//            modelAndView.setViewName("password-update");
//            return modelAndView;
//        }
//        ModelAndView modelAndView = new ModelAndView();
//        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 == true) {
//
//                userService.changePassword(user, userChangePasswordDTO);
//                modelAndView.setViewName("login");
//            } else {
//                modelAndView.addObject("wrongPass", "Current possword was wrong..!");
//                modelAndView.setViewName("password-update");
//            }
//
//        } else {
//            modelAndView.addObject("passMatched", "Password doesn't matched..!");
//            modelAndView.setViewName("password-update");
//        }
//        return modelAndView;
//    }
    @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 == true) {

                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 == true) {

                        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 wtih 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 安全性使实现登录和注销功能变得非常容易,并且非常方便。为方便起见,您可以下载下面的示例项目。

下载源码:https://github.com/allwaysoft/ProductManagerMaximumSessionsCaptchaNewUserDetailsService

<think>我们正在整合MyBatis、Spring 7(即Spring Framework 7)、Spring Security 6Thymeleaf。由于Spring 7Spring Security 6是最新版本,需要注意它们之间的兼容性以及配置方式的变化(例如,Spring Security 6默认不再使用WebSecurityConfigurerAdapter,而是使用组件式配置)。 主要步骤: 1. 创建项目并添加依赖 2. 配置数据源MyBatis 3. 创建用户权限的实体类、Mapper接口及映射文件 4. 实UserDetailsService以从数据库加载用户 5. 配置Spring Security 6. 创建Thymeleaf登录页面 7. 创建控制器处理登录请求页面跳转 注意:Spring Security 6中,密码必须使用密码编码器(PasswordEncoder)进行加密存储,且认证时也需要使用相同的编码器。 下面逐步说明: ### 1. 创建项目并添加依赖 使用Maven或Gradle构建项目。以下是Maven的核心依赖(请根据实际Spring Boot版本选择,因为Spring 7可能需要Spring Boot 3.x以上): ```xml <dependencies> <!-- Spring Boot Starter Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.0</version> <!-- 使用Spring Boot 3.1.0,它支持Spring Framework 6 --> </dependency> <!-- Spring Boot Starter Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>3.1.0</version> </dependency> <!-- Spring Boot Starter Thymeleaf --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>3.1.0</version> </dependency> <!-- MyBatis Spring Boot Starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.2</version> <!-- 确保兼容Spring Boot 3.x --> </dependency> <!-- 数据库驱动(例如MySQL) --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> <!-- Lombok(可选) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.26</version> <optional>true</optional> </dependency> </dependencies> ``` ### 2. 配置数据源MyBatis 在`application.properties`或`application.yml`中配置数据源MyBatis: ```properties # 数据源配置 spring.datasource.url=jdbc:mysql://localhost:3306/your_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8 spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # MyBatis配置 mybatis.mapper-locations=classpath:mapper/*.xml mybatis.type-aliases-package=com.example.demo.entity ``` ### 3. 创建实体类Mapper 假设有用户表(user)权限表(permission),用户实体类可以如下(这里使用Lombok简化): ```java @Entity public class User { private Long id; private String username; private String password; // 注意:密码数据库中应该是加密存储的 // 其他字段... // gettersetter } ``` 权限实体类: ```java public class Permission { private Long id; private String percode; // 权限代码,例如"ROLE_ADMIN" // 其他字段... } ``` 创建Mapper接口: ```java @Mapper public interface UserMapper { @Select("SELECT * FROM user WHERE username = #{username}") User findByUsername(String username); } @Mapper public interface PermissionMapper { @Select("SELECT p.* FROM permission p JOIN user_permission up ON p.id=up.permission_id WHERE up.user_id=#{userId}") List<Permission> findByUserId(Long userId); } ``` ### 4. 实UserDetailsService 创建一个服务类实`UserDetailsService`接口,用于从数据库加载用户信息。同时,我们让自定义的UserDetails实Spring Security的`UserDetails`接口,或者直接使用Spring Security提供的`User`类(推荐)。 这里我们使用第二种方式:在自定义用户实体类中实`UserDetails`接口,或者直接返回Spring Security的`User`对象。为了方便,我们选择在服务类中构建`UserDetails`对象。 创建`MyUserDetailsService`: ```java @Service public class MyUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private PermissionMapper permissionMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } // 查询用户权限 List<Permission> permissions = permissionMapper.findByUserId(user.getId()); List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (Permission permission : permissions) { authorities.add(new SimpleGrantedAuthority(permission.getPercode())); } // 注意:这里使用Spring Security的User类,密码应该是加密后的,且认证时会使用密码编码器进行匹配 return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), // 数据库存储的密码应该是经过加密的 authorities ); } } ``` ### 5. 配置Spring SecuritySpring Security 6中,我们不再继承`WebSecurityConfigurerAdapter`,而是使用`SecurityFilterChain` Bean进行配置。 创建安全配置类: ```java @Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private MyUserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { // 使用BCryptPasswordEncoder作为密码编码器 return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/css/**", "/js/**").permitAll() // 允许登录页面静态资源 .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") // 自定义登录页面 .defaultSuccessUrl("/home", true) .permitAll() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .permitAll() ) .userDetailsService(userDetailsService); // 设置自定义的UserDetailsService return http.build(); } } ``` ### 6. 创建Thymeleaf登录页面 在`resources/templates`下创建`login.html`: ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>登录</title> </head> <body> <div th:if="${param.error}">用户名或密码错误</div> <form th:action="@{/login}" method="post"> <div> <label>用户名: <input type="text" name="username"/></label> </div> <div> <label>密码: <input type="password" name="password"/></label> </div> <div> <input type="submit" value="登录"/> </div> </form> </body> </html> ``` ### 7. 创建控制器 创建一个控制器处理登录页面主页的请求: ```java @Controller public class HomeController { @GetMapping("/login") public String login() { return "login"; } @GetMapping("/home") public String home() { return "home"; } } ``` ### 注意事项 - 在数据库中存储的密码必须是加密后的(使用BCryptPasswordEncoder加密),否则认证会失败。 - 注册用户时,也需要使用相同的密码编码器对密码进行加密存储。 ### 测试 启动应用,访问`http://localhost:8080/login`,输入数据库中的用户名密码密码是加密存储的,但输入时用明文,Spring Security自动用相同的编码器进行匹配)。 ### 引用说明 以上实参考了Spring Security官方文档以及常见的整合方式[^1][^2][^3][^4]。 §§相关问题§§ 1. 如何在Spring Security 6中配置自定义登录页面? 2. Spring Security 6中如何实基于数据库的角色权限控制? 3. 使用Thymeleaf时如何根据用户权限显示不同的内容? 4. 在Spring Security 6中如何实记住我(Remember-Me)功能?</think>### 整合 MyBatis、Spring 7、Spring Security 6 Thymeleaf登录功能 #### **1. 项目依赖配置(pom.xml)** ```xml <dependencies> <!-- Spring 7 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.0</version> <!-- Spring Boot 3.x 对应 Spring 6 --> </dependency> <!-- Spring Security 6 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>3.1.0</version> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.2</version> </dependency> <!-- Thymeleaf --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>3.1.0</version> </dependency> <!-- 数据库驱动(示例使用MySQL) --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> </dependencies> ``` #### **2. 数据库建模** ```sql CREATE TABLE users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, -- 存储加密后的密码 enabled BOOLEAN DEFAULT true ); CREATE TABLE authorities ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, FOREIGN KEY (username) REFERENCES users(username) ); ``` #### **3. 实 UserDetails 实体类** ```java @Entity public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "authorities", joinColumns = @JoinColumn(name = "username")) private List<SimpleGrantedAuthority> authorities; // 实 UserDetails 接口方法 @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } // Getters and Setters } ``` #### **4. MyBatis Mapper 接口** ```java @Mapper public interface UserMapper { @Select("SELECT * FROM users WHERE username = #{username}") @Results({ @Result(property = "authorities", column = "username", many = @Many(select = "findAuthoritiesByUsername")) }) User findByUsername(String username); @Select("SELECT authority FROM authorities WHERE username = #{username}") List<SimpleGrantedAuthority> findAuthoritiesByUsername(String username); } ``` #### **5. 自定义 UserDetailsService** ```java @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) { User user = userMapper.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } return user; // 直接返回实了UserDetails的实体 } } ``` #### **6. Spring Security 配置类** ```java @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/css/**", "/login").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/home") .permitAll() ) .logout(logout -> logout .logoutSuccessUrl("/login?logout") ); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // 强制使用密码加密 } } ``` #### **7. Thymeleaf 登录页面(login.html)** ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>登录</title> </head> <body> <div th:if="${param.error}">用户名或密码错误</div> <form th:action="@{/login}" method="post"> <input type="text" name="username" placeholder="用户名" required/> <input type="password" name="password" placeholder="密码" required/> <button type="submit">登录</button> </form> </body> </html> ``` #### **8. 控制器处理登录请求** ```java @Controller public class LoginController { @GetMapping("/login") public String loginPage() { return "login"; // 返回Thymeleaf模板 } @GetMapping("/home") public String homePage() { return "home"; // 登录成功后的主页 } } ``` #### **9. 关键配置(application.yml)** ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/your_db username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false # 开发时禁用缓存 ``` ### 实要点说明 1. **密码加密**:必须使用`PasswordEncoder`加密存储密码Spring Security 6 强制要求加密[^2] 2. **实体继承**:用户实体直接实`UserDetails`接口,避免数据转换[^1] 3. **权限加载**:通过MyBatis的`@Many`注解实关联查询加载权限 4. **安全配置**:使用`SecurityFilterChain`替代过时的`WebSecurityConfigurerAdapter`[^3] 5. **Thymeleaf整合**:模板中可使用`sec:`命名空间显示权限信息(需添加`thymeleaf-extras-springsecurity6`依赖) ### 测试流程 1. 向`users`表插入用户(密码需BCrypt加密) ```sql INSERT INTO users(username, password) VALUES ('admin', '$2a$10$6u3Zb7sSJ5J0Qd1XvY9wB.7zK8lA3cB1dF6hG2jD3kL4mN5oP6qR7s'); ``` 2. 启动应用访问 `http://localhost:8080/login` 3. 输入凭证后跳转到 `/home` > 提示:可使用`BCryptPasswordEncoder`的`encode("rawPassword")`方法生成加密密码
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值