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

开始登录页改造
创建自定义登录页
首先,我们需要自定义一个带验证码的登录页。在resources/templates目录下创建login.html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <!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,显示登出按钮:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <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> <form th:action="@{/login}" method="post"> <button type="submit" class="btn btn-primary">Log Out</button> </form> <a th:href="@{/logout}">Log Out</a> </body> </html>
|
添加Controller配置
添加Controller配置首页以及登录页:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Controller public class DemoTowController { @GetMapping("/login") public String login() { return "login"; } @GetMapping("/") public String index() { return "index"; } }
|
配置Spring Security
最后对Spring Security进行配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Configuration public class BasicSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .permitAll() ) .logout(withDefaults()) .csrf(csrf -> csrf.disable()); 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配置文件设置用户密码的功能:
1 2 3 4 5 6
| spring: security: user: name: admin password: admin
|
登录成功/失败跳转问题
通过上述代码,可以看到登录成功后默认返回系统主页(index.html)。但业务需求可能需要跳转到别的页面,如何配置?
Spring Security配置类中 formLogin 提供了两个参数 defaultSuccessUrl 和 failureUrl 方便我们进行配置:
1 2 3 4 5 6
| http.formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/home", true) .failureUrl("/login?error=true") .permitAll() )
|
自定义登出
登出配置和登录基本相同:
1 2 3 4
| http.logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") )
|
前后端分离适配方案
上述案例针对的是前后端在一个整体中的情况。针对现在前后端分离的项目,我们如何进行改造?需要处理以下问题:
- 用户登录成功返回成功JSON / 失败返回对应JSON
- 用户登出成功返回成功JSON / 失败返回对应JSON
原理说明
首先引入官方介绍图如下:

在身份认证管理器AuthenticationManager中,有两个结果:Success和Failure,最终交给AuthenticationSuccessHandler以及AuthenticationFailureHandler处理器处理。
简单总结:
- 登录成功调用:AuthenticationSuccessHandler
- 登录失败调用:AuthenticationFailureHandler
自定义处理器实现
通过上面的讲解,我们只需要自定义这两个处理器即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| @Configuration public class BasicSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/ajaxLogin").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .successHandler(loginSuccessHandler()) .failureHandler(loginFailureHandler()) .permitAll() ) .logout(withDefaults()) .csrf(csrf -> csrf.disable()); 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"); } }; } public boolean isAjaxRequest(HttpServletRequest request) { String xRequestedWith = request.getHeader("X-Requested-With"); return "XMLHttpRequest".equals(xRequestedWith); } }
|
创建AJAX登录页
新增一个ajaxLogin.html,使用ajax发送请求(为了测试方便,这里简单创建一个,不使用VUE等工程了):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| <!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中追加页面展示:
1 2 3 4
| @GetMapping("/ajaxLogin") public String ajaxLogin() { return "ajaxLogin"; }
|
测试效果
最后启动Spring Boot项目,访问/ajaxLogin登录页,测试输入正确和不正确的账号密码,并观察浏览器控制台输出:

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