概述
本文基于实际项目需求,讲解如何替换 Spring Security 默认的登录页面,实现自定义登录界面,并处理静态资源拦截、国际化、表单参数定制等问题。
内容覆盖从依赖引入、安全配置到视图映射的全过程,并提供完整可运行的代码示例。
纲要
- 环境与依赖管理
spring-boot-starter-security、spring-boot-starter-thymeleafwebjars-locator-core与前端资源本地化
- 自定义登录页面
- 在
src/main/resources/templates/下创建login.html - 页面中引入 Bootstrap 等静态资源
- 在
- 视图控制器与资源映射
- 实现
WebMvcConfigurer,添加视图控制器映射 - 处理静态资源与
webjars的映射关系
- 实现
- 安全配置改造
- 配置
HttpSecurity,指定自定义登录页、放行静态资源 - 避免重定向循环:
permitAll()放行登录页
- 配置
- 国际化文本支持
- 创建
messages_zh_CN.properties - 配置 Spring 消息源,解决中文乱码
- 创建
- 表单字段与行为定制
- 自定义用户名、密码参数名
- 登录处理地址、成功/失败跳转逻辑
- 完整可运行项目演示
环境与依赖管理
在 pom.xml 中引入以下关键依赖。spring-boot-starter-security 提供安全框架支持,spring-boot-starter-thymeleaf 为模板引擎,用于渲染登录页面。webjars-locator-core 可以帮助我们直接在模板中引用 WebJars 中的静态资源而不需写死版本号。为让页面美观,引入 Bootstrap 的 WebJar。
xml
<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.13</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>custom-login-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>custom-login-demo</name>
<properties>
<java.version>11</java.version>
<bootstrap.version>4.6.2</bootstrap.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- WebJars 支持,无需在路径中写版本号 -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<!-- Bootstrap WebJar,用于前端样式 -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>${bootstrap.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
自定义登录页面
在 src/main/resources/templates/ 下新建 login.html。使用 Thymeleaf 模板语法,并引入 Bootstrap 样式。同时准备好表单,提交地址为 /login,方法为 POST,用户名、密码输入框的 name 属性分别使用 username 和 password(后续可通过配置修改)。
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="#{login.title}">登录</title>
<link rel="stylesheet" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}">
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-4">
<h3 class="text-center mb-4" th:text="#{login.header}">请登录</h3>
<form th:action="@{/login}" method="post">
<div class="form-group">
<label for="username" th:text="#{login.username}">用户名</label>
<input type="text" id="username" name="username" class="form-control" required autofocus>
</div>
<div class="form-group">
<label for="password" th:text="#{login.password}">密码</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary btn-block" th:text="#{login.submit}">登录</button>
</form>
</div>
</div>
</div>
</body>
</html>
视图控制器与资源映射
我们需要将 /login 这个 URL 映射到 login 视图(即 login.html),同时保证静态资源(如 webjars)能够被正常访问。这通过实现 WebMvcConfigurer 并重写 addViewControllers 和 addResourceHandlers 来完成。
java
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 将 /login 路径映射到名为 "login" 的视图
registry.addViewController("/login").setViewName("login");
// 可以继续添加其他视图控制器
registry.addViewController("/").setViewName("index");
}
}
一般情况下,Spring Boot 的自动配置已经能够正确处理 webjars-locator-core 的映射,因此无需手动配置 addResourceHandlers。若确需显式声明,可在同一配置类中增加:
java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
安全配置改造
定制登录页面后,必须在安全配置中进行相应声明,否则自定义页面无法生效。关键配置项包括:
- 指定登录页面 URL:
.loginPage("/login") - 表单登录的提交地址(通常与登录页面相同):
.loginProcessingUrl("/login") - 放行登录页面和静态资源,防止重定向循环:使用
permitAll()以及requestMatchers排除静态资源路径。 - 自定义用户名/密码参数名称(可选)。
以下是一个典型的 SecurityConfig 配置类,继承自 WebSecurityConfigurerAdapter(Spring Boot 2.x 版本)。
java
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 放行静态资源,避免被拦截
.requestMatchers(org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher("/webjars/**")).permitAll()
.antMatchers("/css/**", "/js/**", "/images/**").permitAll()
// 放行登录页面和登录请求
.antMatchers("/login").permitAll()
// 其余请求均需认证
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") // 自定义登录页
.loginProcessingUrl("/login") // 处理登录的 URL,与表单 action 一致
.usernameParameter("username") // 表单中用户名的 name
.passwordParameter("password") // 表单中密码的 name
.defaultSuccessUrl("/home", true) // 登录成功后的默认跳转
.failureUrl("/login?error") // 登录失败后的跳转
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/login?logout")
.permitAll();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
为了避免配置类中因 requestMatchers 的写法过于复杂,Spring Security 提供了便捷方法 web.ignoring() 来完全跳过某些路径的安全过滤器链,也可以用于静态资源。但由于这种方式会导致静态资源完全绕过安全上下文,仅推荐用于无认证需求的纯静态文件。上述配置已经足够演示。
国际化文本支持
登录页面中使用了 th:text="#{...}" 引用国际化消息。为此,我们需要在 src/main/resources 下创建消息文件,并在配置文件中指定基础名。
创建 messages_zh_CN.properties:
properties
login.title=自定义登录页
login.header=请登录
login.username=用户名
login.password=密码
login.submit=登录
同时在 application.properties(或 application.yml)中指定消息源的基础名称:
properties
spring.messages.basename=messages
spring.messages.encoding=UTF-8
为了确保浏览器能正确识别中文,也可在 login.html 头部保留 <meta charset="UTF-8">。
表单字段与行为定制
在 .formLogin() 配置块中,我们可以灵活调整与表单交互相关的参数:
usernameParameter("user"):如果前端输入框的name改为user,则需要此处同步。passwordParameter("pwd"):同理,修改密码字段名。loginProcessingUrl("/doLogin"):如果表单的action是/doLogin,则需要设置该值,默认是/login。successForwardUrl("/dashboard"):登录成功后执行服务端转发,而非重定向。failureForwardUrl("/login-error"):登录失败后的转发地址。defaultSuccessUrl("/home", true):总是重定向到指定 URL,而不是先前访问的受保护页面。- 登录成功处理器与失败处理器:适合前后端分离场景,返回 JSON 数据,后续章节介绍。
示例:修改用户名参数名为 email,密码参数名为 secret,前端需对应调整。
java
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/authenticate")
.usernameParameter("email")
.passwordParameter("secret")
.successHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":200,\"msg\":\"登录成功\"}");
})
.failureHandler((request, response, exception) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"登录失败\"}");
})
完整项目结构
最终的项目目录结构如下:
dir
src
└── main
├── java
│ └── com.example
│ ├── DemoApplication.java
│ └── config
│ ├── SecurityConfig.java
│ └── WebMvcConfig.java
└── resources
├── application.properties
├── messages_zh_CN.properties
└── templates
├── login.html
└── home.html (可选的首页)
DemoApplication.java 为 Spring Boot 启动类:
java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
home.html 示例(必须放在 templates 下,并在安全配置中允许认证后访问):
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>主页</title>
</head>
<body>
<h1 th:text="'欢迎,' + ${#authentication.name} + '!'"></h1>
<a th:href="@{/logout}">退出登录</a>
</body>
</html>
流程图示
用户访问受保护资源时,Spring Security 会自动重定向到自定义登录页,完成认证后跳转至目标页面。
后端服务 视图控制器 SpringSecurity 浏览器 后端服务 视图控制器 SpringSecurity 浏览器 #mermaid-svg-5xlaYhctZ9ijKTw7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5xlaYhctZ9ijKTw7 .error-icon{fill:#552222;}#mermaid-svg-5xlaYhctZ9ijKTw7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5xlaYhctZ9ijKTw7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5xlaYhctZ9ijKTw7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5xlaYhctZ9ijKTw7 .marker.cross{stroke:#333333;}#mermaid-svg-5xlaYhctZ9ijKTw7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5xlaYhctZ9ijKTw7 p{margin:0;}#mermaid-svg-5xlaYhctZ9ijKTw7 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5xlaYhctZ9ijKTw7 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5xlaYhctZ9ijKTw7 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-5xlaYhctZ9ijKTw7 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-5xlaYhctZ9ijKTw7 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-5xlaYhctZ9ijKTw7 .sequenceNumber{fill:white;}#mermaid-svg-5xlaYhctZ9ijKTw7 #sequencenumber{fill:#333;}#mermaid-svg-5xlaYhctZ9ijKTw7 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-5xlaYhctZ9ijKTw7 .messageText{fill:#333;stroke:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5xlaYhctZ9ijKTw7 .labelText,#mermaid-svg-5xlaYhctZ9ijKTw7 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .loopText,#mermaid-svg-5xlaYhctZ9ijKTw7 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5xlaYhctZ9ijKTw7 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-5xlaYhctZ9ijKTw7 .noteText,#mermaid-svg-5xlaYhctZ9ijKTw7 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-5xlaYhctZ9ijKTw7 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5xlaYhctZ9ijKTw7 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5xlaYhctZ9ijKTw7 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5xlaYhctZ9ijKTw7 .actorPopupMenu{position:absolute;}#mermaid-svg-5xlaYhctZ9ijKTw7 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-5xlaYhctZ9ijKTw7 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5xlaYhctZ9ijKTw7 .actor-man circle,#mermaid-svg-5xlaYhctZ9ijKTw7 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-5xlaYhctZ9ijKTw7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 认证成功 认证失败 访问 /api/user (受保护) 302 重定向到 /login GET /login 根据 ViewController 映射 返回 login.html 用户填写表单并提交 POST /login (用户名+密码) 校验凭证 302 重定向到之前请求的 /api/user (或 defaultSuccessUrl) GET /api/user 200 响应数据 302 重定向到 /login?error GET /login?error 显示错误信息
总结
本文详细介绍了如何从零开始定制 Spring Security 的登录页面,涵盖视图映射、安全规则配置、静态资源处理、国际化以及表单字段自定义等关键步骤。
所有代码均可在 Spring Boot 2.7.x 环境下直接运行,帮助开发者快速掌握传统模板引擎下的登录定制方案,为后续迁移至前后端分离的 OAuth2 认证体系打下基础。