前言

通过上一章节的讲解,相信大家已经认识了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防护:

CSRF隐藏字段

如果登出页是 a 连接形式,为了保证登出不会出现 404 的问题:

  1. 先关闭 csrf 防护:http.csrf(csrf -> csrf.disable())
  2. 登出按钮使用表单方式:th:action="@{/logout}"

自定义用户名密码

到这里有小伙伴又要问了,每次密码都是Spring Security自动生成的UUID,能否自定义用户名密码?答案是肯定的。Spring Security提供了在Spring Boot配置文件设置用户密码的功能:

# application.yml
spring:
  security:
    user:
      name: admin
      password: admin

登录成功/失败跳转问题

通过上述代码,可以看到登录成功后默认返回系统主页(index.html)。但业务需求可能需要跳转到别的页面,如何配置?

Spring Security配置类中 formLogin 提供了两个参数 defaultSuccessUrlfailureUrl 方便我们进行配置:

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中,有两个结果:SuccessFailure,最终交给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登录页,测试输入正确和不正确的账号密码,并观察浏览器控制台输出:

AJAX登录测试


结语

本章节介绍了如何通过Spring Security实现从配置自定义登录页面、表单登录处理逻辑的配置,并简单模拟了前后端分离的适配方案。

在接下来的章节中,我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术的方方面面。欢迎继续关注!

最后修改:2026 年 03 月 12 日
如果觉得我的文章对你有用,请随意赞赏