在本教程中,我将指导您如何编写代码,以使用具有基于表单的身份验证的Spring安全API来保护Spring Boot应用程序中的网页。用户详细信息存储在MySQL数据库中,并使用春季JDBC连接到数据库。我们将从本教程中的 ProductManager 项目开始,向现有的弹簧启动项目添加登录和注销功能。
1. 创建用户表和虚拟凭据
凭据应存储在数据库中,因此让我们创建新表:
-- --------------------------------------------------------
-- 主机: 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 */;
-- 导出 sales2 的数据库结构
CREATE DATABASE IF NOT EXISTS `sales2` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `sales2`;
-- 导出 表 sales2.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=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 正在导出表 sales2.product 的数据:~1 rows (大约)
INSERT INTO `product` (`id`, `brand`, `madein`, `name`, `price`) VALUES
(1, '1', '1', '1', 1);
-- 导出 表 sales2.users 结构
CREATE TABLE IF NOT EXISTS `users` (
`user_id` int NOT NULL AUTO_INCREMENT,
`username` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`role` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL,
`enabled` tinyint DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 正在导出表 sales2.users 的数据:~0 rows (大约)
INSERT INTO `users` (`user_id`, `username`, `password`, `role`, `enabled`) VALUES
(1, 'namhm', '$2a$10$U8bbzqZof07fvyVE.GwUR.ULMl4wKQerriNAkT1/j.hJ5yfNYuFBK', 'ROLE_USER', 1),
(2, 'admin', '$2a$10$U8bbzqZof07fvyVE.GwUR.ULMl4wKQerriNAkT1/j.hJ5yfNYuFBK', 'ROLE_ADMIN', 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/sales2?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.5</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>net.codejava</groupId>
<artifactId>ProductManagerMaximumSessionsCaptcha</artifactId>
<version>2.0</version>
<name>ProductManagerMaximumSessionsCaptcha</name>
<description>Spring Boot CRUD Web App Example</description>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4. 配置 JDBC 身份验证详细信息
要将 Spring 安全性与基于表单的身份验证和 JDBC 结合使用,请按如下方式创建 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
public void configAuthentication(AuthenticationManagerBuilder authBuilder) throws Exception {
authBuilder.jdbcAuthentication()
.dataSource(dataSource)
.passwordEncoder(new BCryptPasswordEncoder())
.usersByUsernameQuery("select username, password, enabled from users where username=?")
.authoritiesByUsernameQuery("select username, role from users where username=?");
}
@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("/edit/*", "/delete/*").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.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());
}
}
此安全配置类必须使用@EnableWebSecurity注释进行批注,并且是 Web 安全配置器适配器的子类。数据源对象的实例将由Spring框架创建并注入:
@Autowired
private DataSource dataSource;
它将从应用程序属性文件中读取数据库连接信息。要使用JDBC配置身份验证,请编写以下方法:
@Autowired
public void configAuthentication(AuthenticationManagerBuilder authBuilder) throws Exception {
authBuilder.jdbcAuthentication()
.dataSource(dataSource)
.passwordEncoder(new BCryptPasswordEncoder())
.usersByUsernameQuery("select username, password, enabled from users where username=?")
.authoritiesByUsernameQuery("SELECT users.username,permissions.name FROM users,roles,users_roles,permissions,roles_permissions WHERE users.username=users_roles.username AND roles.name=users_roles.role_name AND roles.name=roles_permissions.role_name AND permissions.name=roles_permissions.permission AND users.username=?");
}
如您所见,我们需要指定密码编码器(建议使用BCrypt),数据源和两个SQL语句:第一个根据用户名选择用户,第二个选择用户的角色。请注意,Spring安全性要求列名必须是用户名,密码,启用和角色。为了配置基于表单的身份验证,我们重写了 configure(HttpSecurity) 方法,如下所示:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/edit/*", "/delete/*").hasAnyAuthority("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.logout().permitAll()
.and()
.exceptionHandling().accessDeniedPage("/403");
}
在这里,我们指定所有请求都必须进行身份验证,这意味着用户必须登录才能使用该应用程序。使用Spring安全性提供的默认登录表单。要显示已登录用户的用户名,请在Thymeleaf模板文件中编写以下代码:
<div sec:authorize="isAuthenticated()">
Welcome <b><span sec:authentication="name">Username</span></b>
<i><span sec:authentication="principal.authorities">Roles</span></i>
</div>
并添加注销按钮:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Logout" />
</form>
如您所见,Spring Security将处理应用程序的登录和注销。我们不必编写重复的代码,只需指定一些配置即可。
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;
}
}
package com.example;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import com.google.code.kaptcha.Constants;
public class KaptchaAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
final static Logger logger = LoggerFactory.getLogger(KaptchaAuthenticationFilter.class);
private String servletPath;
public KaptchaAuthenticationFilter(String servletPath, String failureUrl) {
super(servletPath);
this.servletPath = servletPath;
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(failureUrl));
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
if ("POST".equalsIgnoreCase(req.getMethod()) && servletPath.equals(req.getServletPath())) {
String expect = (String) req.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
if (expect != null && !expect.equalsIgnoreCase(req.getParameter("kaptcha"))) {
unsuccessfulAuthentication(req, res, new InsufficientAuthenticationException("Incorrect captcha code."));
return;
}
}
chain.doFilter(request, response);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
return null;
}
}
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,您将看到主页如下:

再其他浏览器中再打开登录

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

返回之前的页面点击创建新产品,会自动跳转到登录界面,是因为新会话踢掉了旧会话,如果 修改.maxSessionsPreventsLogin(false)为 .maxSessionsPreventsLogin(true)则在旧会话未退出时,新登录会失败。

并注意欢迎消息后跟用户名。用户现在已通过身份验证以使用该应用程序。单击“注销”按钮,您将看到自定义的登录页面出现,这意味着我们已成功实现登录并注销到我们的Spring Boot应用程序。
结论:
到目前为止,您已经学会了使用基于表单的身份验证和数据库内凭据来保护Spring Boot应用程序。您会看到 Spring 安全性使实现登录和注销功能变得非常容易,并且非常方便。为方便起见,您可以下载下面的示例项目。
下载源码:GitHub - allwaysoft/ProductManagerMaximumSessionsCaptcha
1万+

被折叠的 条评论
为什么被折叠?



