前言
通过上一章节的讲解,相信大家已经认识了Spring Security安全框架。在我们创建的第一个演示项目中,相信大家也发现了默认表单登录的局限性。Spring Security默认提供的登录页虽然快速可用,但存在三大问题:
- 界面风格与业务系统不匹配
- 登录成功/失败处理逻辑固定
- 缺乏扩展能力(如验证码、多因子认证)
本章节我们将对Spring Security默认表单进行登录定制,并深度改造处理逻辑。
改造准备
在之前的Maven项目中创建第二个子模块,命名为login-spring-security。由于需要自定义登录页,还需要引入thymeleaf模板框架:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>完整的Maven项目结构如下:

开始登录页改造
创建自定义登录页
首先,我们需要自定义一个带验证码的登录页。在resources/templates目录下创建login.html:
<!-- src/main/resources/templates/login.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>企业级登录系统</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
<div class="container d-flex justify-content-center align-items-center vh-100">
<div class="w-100" style="max-width: 400px;">
<div class="card">
<div class="card-body">
<h2 class="card-title text-center mb-4">登录</h2>
<form th:action="@{/login}" method="post">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" name="username" id="username" placeholder="请输入用户名">
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" name="password" id="password" placeholder="请输入密码">
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">登录</button>
</div>
<p class="mt-3 text-center"><a href="#">忘记密码?</a></p>
</form>
</div>
</div>
</div>
</div>
</body>
</html>创建默认首页
添加一个默认首页index.html,显示登出按钮:
<!-- src/main/resources/templates/index.html -->
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>企业级登录系统</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
<h1>Hello Security</h1>
<!-- 测试过程不需要关闭csrf防护 -->
<form th:action="@{/login}" method="post">
<button type="submit" class="btn btn-primary">Log Out</button>
</form>
<!-- 测试过程需要关闭csrf防护 否则404 -->
<a th:href="@{/logout}">Log Out</a>
</body>
</html>添加Controller配置
添加Controller配置首页以及登录页:
@Controller
public class DemoTowController {
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/")
public String index() {
return "index";
}
}配置Spring Security
最后对Spring Security进行配置:
@Configuration
public class BasicSecurityConfig {
// 配置安全策略
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // 自定义登录页路径
.permitAll() // 不需要对login认证
)
.logout(withDefaults())
.csrf(csrf -> csrf.disable()); // 关闭csrf防护
return http.build();
}
}测试效果
测试访问默认主页,由于主页被拦截会自动跳转到login登录页:

输入正确用户名密码后,自动返回主页,点击登出按钮自动回到登录页:

特别说明:
注意登录页以及主页登出,action采用@{}生成URL,Spring Security会自动帮我们生成name为 _csrf 的隐藏表单,作用于csrf防护:

如果登出页是 a 连接形式,为了保证登出不会出现 404 的问题:
- 先关闭 csrf 防护:
http.csrf(csrf -> csrf.disable()) - 登出按钮使用表单方式:
th:action="@{/logout}"
自定义用户名密码
到这里有小伙伴又要问了,每次密码都是Spring Security自动生成的UUID,能否自定义用户名密码?答案是肯定的。Spring Security提供了在Spring Boot配置文件设置用户密码的功能:
# application.yml
spring:
security:
user:
name: admin
password: admin登录成功/失败跳转问题
通过上述代码,可以看到登录成功后默认返回系统主页(index.html)。但业务需求可能需要跳转到别的页面,如何配置?
Spring Security配置类中 formLogin 提供了两个参数 defaultSuccessUrl 和 failureUrl 方便我们进行配置:
http.formLogin(form -> form
.loginPage("/login") // 自定义登录页路径
.defaultSuccessUrl("/home", true) // 登录成功后跳转路径
.failureUrl("/login?error=true") // 登录失败后跳转路径
.permitAll() // 不需要对login认证
)自定义登出
登出配置和登录基本相同:
http.logout(logout -> logout
.logoutUrl("/logout") // 自定义登出URL
.logoutSuccessUrl("/login?logout") // 登出成功跳转
)前后端分离适配方案
上述案例针对的是前后端在一个整体中的情况。针对现在前后端分离的项目,我们如何进行改造?需要处理以下问题:
- 用户登录成功返回成功JSON / 失败返回对应JSON
- 用户登出成功返回成功JSON / 失败返回对应JSON
原理说明
首先引入官方介绍图如下:

在身份认证管理器AuthenticationManager中,有两个结果:Success和Failure,最终交给AuthenticationSuccessHandler以及AuthenticationFailureHandler处理器处理。
简单总结:
- 登录成功调用:AuthenticationSuccessHandler
- 登录失败调用:AuthenticationFailureHandler
自定义处理器实现
通过上面的讲解,我们只需要自定义这两个处理器即可:
@Configuration
public class BasicSecurityConfig {
// 配置安全策略
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/ajaxLogin").permitAll() // ajax登录页不需要认证
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // 自定义登录页路径
.successHandler(loginSuccessHandler())
.failureHandler(loginFailureHandler())
.permitAll() // 不需要对login认证
)
.logout(withDefaults())
.csrf(csrf -> csrf.disable()); // 关闭csrf防护
return http.build();
}
// 自定义登录成功处理器
@Bean
public AuthenticationSuccessHandler loginSuccessHandler() {
return (request, response, authentication) -> {
if (isAjaxRequest(request)) {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"code\":200, \"message\":\"认证成功\"}");
} else {
response.sendRedirect("/");
}
};
}
// 自定义登录失败处理器
@Bean
public AuthenticationFailureHandler loginFailureHandler() {
return (request, response, exception) -> {
if (isAjaxRequest(request)) {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"code\":401, \"message\":\"认证失败\"}");
} else {
response.sendRedirect("/login?error=true");
}
};
}
// 判断是否ajax请求
public boolean isAjaxRequest(HttpServletRequest request) {
String xRequestedWith = request.getHeader("X-Requested-With");
return "XMLHttpRequest".equals(xRequestedWith);
}
}创建AJAX登录页
新增一个ajaxLogin.html,使用ajax发送请求(为了测试方便,这里简单创建一个,不使用VUE等工程了):
<!-- src/main/resources/templates/ajaxLogin.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>企业级登录系统</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<div class="container d-flex justify-content-center align-items-center vh-100">
<div class="w-100" style="max-width: 400px;">
<div class="card">
<div class="card-body">
<h2 class="card-title text-center mb-4">登录</h2>
<form>
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" name="username" id="username" placeholder="请输入用户名">
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" name="password" id="password" placeholder="请输入密码">
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">登录</button>
</div>
<p class="mt-3 text-center"><a href="#">忘记密码?</a></p>
</form>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$('form').submit(function (event) {
event.preventDefault();
var username = $('#username').val();
var password = $('#password').val();
$.ajax({
type: 'POST',
url: '/login',
data: {
username: username,
password: password
},
success: function (response) {
console.log(response)
if(response.code == 200){
window.location.href = '/';
}
}
})
})
})
</script>
</body>
</html>添加Controller路由
在Controller中追加页面展示:
@GetMapping("/ajaxLogin")
public String ajaxLogin() {
return "ajaxLogin";
}测试效果
最后启动Spring Boot项目,访问/ajaxLogin登录页,测试输入正确和不正确的账号密码,并观察浏览器控制台输出:

结语
本章节介绍了如何通过Spring Security实现从配置自定义登录页面、表单登录处理逻辑的配置,并简单模拟了前后端分离的适配方案。
在接下来的章节中,我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术的方方面面。欢迎继续关注!
