Spring Security自定義用戶名密碼登錄頁面全過程
在現(xiàn)代 Web 應用開發(fā)中,安全性是不可忽視的重要環(huán)節(jié)。Spring Security 作為 Spring 生態(tài)中最成熟、最廣泛使用的安全框架,為開發(fā)者提供了強大的認證(Authentication)與授權(quán)(Authorization)能力。默認情況下,Spring Security 會提供一個自動生成的登錄頁面,但在實際項目中,我們幾乎總是需要根據(jù)業(yè)務需求和 UI/UX 設計,定制屬于自己的登錄界面。
本文將深入探討如何在 Spring Boot 項目中使用 Spring Security 實現(xiàn)完全自定義的用戶名密碼登錄頁面。我們將從基礎配置開始,逐步構(gòu)建一個完整的登錄流程,涵蓋表單設計、后端處理、錯誤提示、記住我功能、CSRF 防護、登錄成功/失敗跳轉(zhuǎn)策略等核心內(nèi)容,并通過代碼示例和流程圖幫助你徹底掌握這一關(guān)鍵技能。
為什么需要自定義登錄頁面?
默認的 Spring Security 登錄頁面雖然功能完整,但存在以下問題:
- 外觀簡陋:無法匹配企業(yè)級應用的 UI 風格;
- 響應式缺失:不支持移動端適配;
- 多語言困難:難以集成國際化(i18n);
- 擴展性差:無法添加驗證碼、第三方登錄等額外功能;
- 安全細節(jié)暴露:默認路徑和參數(shù)可能被攻擊者利用。
因此,自定義登錄頁面不僅是美觀需求,更是安全性和用戶體驗的必要實踐。
小知識:Spring Security 的默認登錄頁面路徑是 /login,提交表單的 action 是 /login,用戶名字段為 username,密碼字段為 password。這些都可以被覆蓋。
項目初始化
我們使用 Spring Boot 3.x(基于 Jakarta EE 9+,使用 jakarta.* 包而非 javax.*)來搭建項目。確保你的開發(fā)環(huán)境已安裝 JDK 17+ 和 Maven/Gradle。
依賴配置(Maven)
<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>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
</dependencies>Thymeleaf 官方文檔 提供了與 Spring Security 集成的詳細說明。
基礎安全配置
首先,我們需要禁用 Spring Security 的默認登錄頁面,并指定我們自己的登錄頁面路徑。
創(chuàng)建 SecurityConfig 類
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.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // 指定自定義登錄頁
.loginProcessingUrl("/do-login") // 表單提交地址
.usernameParameter("username") // 用戶名參數(shù)名
.passwordParameter("password") // 密碼參數(shù)名
.defaultSuccessUrl("/dashboard", true) // 登錄成功后跳轉(zhuǎn)
.failureUrl("/login?error=true") // 登錄失敗跳轉(zhuǎn)
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.permitAll()
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("123456")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
注意:User.withDefaultPasswordEncoder() 僅用于演示!生產(chǎn)環(huán)境必須使用強哈希算法(如 BCrypt)。
自定義登錄頁面(Thymeleaf)
我們在 src/main/resources/templates 目錄下創(chuàng)建 login.html 文件。
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>用戶登錄</title>
<link rel="stylesheet" th:href="@{/css/login.css}" rel="external nofollow" >
</head>
<body>
<div class="login-container">
<h2>系統(tǒng)登錄</h2>
<!-- 錯誤信息提示 -->
<div th:if="${param.error}" class="alert alert-error">
用戶名或密碼錯誤,請重試!
</div>
<!-- 成功登出提示 -->
<div th:if="${param.logout}" class="alert alert-success">
您已成功退出系統(tǒng)。
</div>
<form th:action="@{/do-login}" method="post">
<!-- CSRF Token 自動注入 -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="form-group">
<label for="username">用戶名</label>
<input type="text" id="username" name="username" required autofocus/>
</div>
<div class="form-group">
<label for="password">密碼</label>
<input type="password" id="password" name="password" required/>
</div>
<div class="form-group remember-me">
<input type="checkbox" id="remember-me" name="remember-me"/>
<label for="remember-me">記住我</label>
</div>
<button type="submit">登錄</button>
</form>
</div>
</body>
</html>關(guān)鍵點說明:
th:action="@{/do-login}":表單提交到/do-login,與loginProcessingUrl一致;- CSRF Token 通過 Thymeleaf 自動注入,防止跨站請求偽造;
name="remember-me"是 Spring Security “記住我”功能的默認參數(shù)名;- 使用
param.error和param.logout獲取 URL 參數(shù)以顯示提示。
靜態(tài)資源處理
為了讓登錄頁面樣式正常加載,需在 SecurityConfig 中放行靜態(tài)資源路徑:
.requestMatchers("/login", "/css/**", "/js/**", "/images/**", "/favicon.ico").permitAll()
并在 src/main/resources/static/css 下創(chuàng)建 login.css:
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.login-container {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
width: 300px;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
}
.form-group input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.remember-me {
display: flex;
align-items: center;
}
.remember-me input[type="checkbox"] {
margin-right: 0.5rem;
}
button {
width: 100%;
padding: 0.75rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
.alert {
padding: 0.75rem;
margin-bottom: 1rem;
border-radius: 4px;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
}
.alert-success {
background-color: #d4edda;
color: #155724;
}登錄流程解析
理解 Spring Security 的登錄流程對調(diào)試和擴展至關(guān)重要。
關(guān)鍵組件說明:
UsernamePasswordAuthenticationFilter:負責攔截/do-login請求;UserDetailsService:加載用戶信息(可對接數(shù)據(jù)庫);AuthenticationProvider:驗證密碼是否匹配;- 成功/失敗處理器決定跳轉(zhuǎn)邏輯。
實現(xiàn)數(shù)據(jù)庫用戶認證
前面我們使用了內(nèi)存用戶,現(xiàn)在將其替換為數(shù)據(jù)庫查詢。
1. 添加 JPA 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>2. 創(chuàng)建 User 實體
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private boolean enabled = true;
// getters and setters
}
3. 創(chuàng)建 UserRepository
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<AppUser, Long> {
AppUser findByUsername(String username);
}
4. 自定義 UserDetailsService
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public CustomUserDetailsService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用戶不存在: " + username);
}
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 已加密
.authorities("ROLE_USER")
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(!user.isEnabled())
.build();
}
}
5. 更新 SecurityConfig
移除 userDetailsService() 方法,注入 CustomUserDetailsService:
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// ... 其他配置不變
http.userDetailsService(customUserDetailsService);
return http.build();
}
6. 密碼編碼器配置
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
安全建議:永遠不要存儲明文密碼!BCrypt 是目前推薦的密碼哈希算法。
處理登錄成功與失敗
Spring Security 允許我們自定義登錄成功和失敗的行為,而不僅僅是跳轉(zhuǎn)。
自定義成功處理器
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// 可記錄日志、更新最后登錄時間等
System.out.println("用戶 " + authentication.getName() + " 登錄成功");
// 默認跳轉(zhuǎn)
response.sendRedirect("/dashboard");
}
}
自定義失敗處理器
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
System.out.println("登錄失敗: " + exception.getMessage());
// 可記錄失敗次數(shù)、鎖定賬戶等
response.sendRedirect("/login?error=" + exception.getClass().getSimpleName());
}
}
在 SecurityConfig 中注冊
@Autowired
private CustomAuthenticationSuccessHandler successHandler;
@Autowired
private CustomAuthenticationFailureHandler failureHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/do-login")
.successHandler(successHandler)
.failureHandler(failureHandler)
.permitAll()
);
// ...
}
應用場景:
- 成功時記錄 IP、設備信息;
- 失敗時限制嘗試次數(shù),防止暴力.破 解;
- 根據(jù)角色跳轉(zhuǎn)不同首頁。
“記住我”功能實現(xiàn)
“記住我”允許用戶在關(guān)閉瀏覽器后仍保持登錄狀態(tài)(通常通過持久化 Cookie 實現(xiàn))。
1. 數(shù)據(jù)庫表結(jié)構(gòu)
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);2. 配置 RememberMeServices
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 自動建表(僅首次)
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.rememberMe(remember -> remember
.key("myAppKey") // 必須設置,用于加密
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(7 * 24 * 3600) // 7天
.userDetailsService(customUserDetailsService)
);
// ...
}
原理:登錄時勾選“記住我”,服務器生成 series 和 token 存入數(shù)據(jù)庫,并設置 Cookie。下次訪問時,通過 Cookie 中的憑證自動登錄。
CSRF 防護與自定義表單
Spring Security 默認啟用 CSRF(跨站請求偽造)防護,要求每個修改狀態(tài)的請求(POST/PUT/DELETE)攜帶有效的 CSRF Token。
在 Thymeleaf 表單中,我們通過以下方式自動注入:
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>如果你使用的是純 HTML 或 AJAX,需手動處理:
AJAX 示例(JavaScript)
// 從 meta 標簽獲取 CSRF Token
var csrfToken = document.querySelector("meta[name='_csrf']").getAttribute("content");
var csrfHeader = document.querySelector("meta[name='_csrf_header']").getAttribute("content");
// 發(fā)送請求時添加 Header
fetch('/do-login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
[csrfHeader]: csrfToken
},
body: 'username=admin&password=123456'
});對應的 HTML 需添加:
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>重要:不要禁用 CSRF!除非你明確知道自己在做什么(如 REST API 使用 JWT)。
國際化(i18n)支持
為了讓登錄頁面支持多語言,我們可以集成 Spring 的 MessageSource。
1. 創(chuàng)建 messages.properties
# src/main/resources/messages.properties login.title=用戶登錄 login.username=用戶名 login.password=密碼 login.remember=記住我 login.button=登錄 error.bad.credentials=用戶名或密碼錯誤
2. messages_zh_CN.properties
login.title=用戶登錄 login.username=用戶名 login.password=密碼 login.remember=記住我 login.button=登錄 error.bad.credentials=用戶名或密碼錯誤
3. 在 Thymeleaf 中使用
<h2 th:text="#{login.title}">用戶登錄</h2>
<label for="username" th:text="#{login.username}">用戶名</label>
<!-- 其他類似 -->
<div th:if="${param.error}" class="alert alert-error" th:text="#{error.bad.credentials}"></div>
4. 配置 LocaleResolver
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.CHINA);
return resolver;
}
提示:瀏覽器通過 Accept-Language 頭自動選擇語言。
登錄驗證碼集成(可選)
雖然 Spring Security 本身不提供驗證碼,但我們可以輕松集成。
1. 添加 Kaptcha 依賴(示例)
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>2. 配置 Kaptcha
@Bean
public DefaultKaptcha kaptcha() {
DefaultKaptcha kaptcha = new DefaultKaptcha();
Properties props = new Properties();
props.setProperty("kaptcha.border", "no");
props.setProperty("kaptcha.textproducer.font.color", "black");
props.setProperty("kaptcha.textproducer.char.space", "10");
kaptcha.setConfig(new Config(props));
return kaptcha;
}
3. 驗證碼 Controller
@GetMapping("/captcha")
public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("image/jpeg");
String capText = kaptcha.createText();
request.getSession().setAttribute("captcha", capText);
BufferedImage image = kaptcha.createImage(capText);
ImageIO.write(image, "jpg", response.getOutputStream());
}
4. 修改登錄表單
<img th:src="@{/captcha}" onclick="this.src='/captcha?' + Math.random()" alt="驗證碼"/>
<input type="text" name="captcha" placeholder="請輸入驗證碼" required/>5. 自定義 AuthenticationFilter
繼承 UsernamePasswordAuthenticationFilter,在 attemptAuthentication 中校驗驗證碼。
注意:驗證碼校驗應在密碼校驗之前,避免無效請求消耗資源。
常見問題與調(diào)試技巧
1. 登錄后無限重定向
原因:defaultSuccessUrl 指向的頁面未放行,導致再次跳轉(zhuǎn)到登錄頁。
? 解決:確保 /dashboard 被 authenticated() 保護,且用戶有權(quán)限訪問。
2. CSRF Token 無效
現(xiàn)象:提交表單報 403 Forbidden。
? 檢查:
- 表單是否包含
<input type="hidden" th:name="${_csrf.parameterName}" ... /> - 是否使用了正確的 POST 方法;
- Session 是否過期(Token 綁定 Session)。
3. 密碼不匹配
? 檢查:
- 數(shù)據(jù)庫存儲的密碼是否經(jīng)過 BCrypt 加密;
PasswordEncoder是否與加密時一致;- 不要使用
User.withDefaultPasswordEncoder()。
4. 日志調(diào)試
在 application.properties 中開啟安全日志:
logging.level.org.springframework.security=DEBUG
安全最佳實踐
強制 HTTPS:生產(chǎn)環(huán)境務必使用 HTTPS,防止密碼嗅探。
http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
密碼策略:前端+后端雙重校驗(長度、復雜度)。
賬戶鎖定:多次失敗后臨時鎖定(可通過
DaoAuthenticationProvider擴展)。
Session 管理:
http.sessionManagement(session -> session
.maximumSessions(1)
.expiredUrl("/login?expired=true")
);
安全頭設置:
http.headers(headers -> headers
.frameOptions().deny()
.contentTypeOptions().and()
.xssProtection().and()
.cacheControl()
);
OWASP Top 10 是 Web 安全的權(quán)威指南,值得每位開發(fā)者閱讀。
總結(jié)
通過本文,我們系統(tǒng)地學習了如何在 Spring Security 中實現(xiàn)完全自定義的用戶名密碼登錄頁面。從基礎配置、頁面設計、數(shù)據(jù)庫集成,到高級功能如“記住我”、CSRF 防護、國際化和驗證碼,每一步都配有可運行的代碼示例。
自定義登錄頁面不僅是 UI 的替換,更是安全架構(gòu)的重要組成部分。合理的錯誤處理、日志記錄、會話管理和輸入驗證,能顯著提升應用的安全性與用戶體驗。
記?。?strong>安全不是功能,而是持續(xù)的過程。隨著業(yè)務發(fā)展,不斷審視和加固你的認證機制,才能抵御日益復雜的網(wǎng)絡威脅。
下一步建議:
- 集成 OAuth2 / 微信登錄;
- 實現(xiàn)多因素認證(MFA);
- 使用 JWT 替代表單登錄(適用于前后端分離)。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
使用原生JDBC動態(tài)解析并獲取表格列名和數(shù)據(jù)的方法
這篇文章主要介紹了使用原生JDBC動態(tài)解析并獲取表格列名和數(shù)據(jù),本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-08-08
詳解使用spring cloud config來統(tǒng)一管理配置文件
這篇文章主要介紹了詳解使用spring cloud config來統(tǒng)一管理配置文件,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12
Java解析照片拿到GPS位置數(shù)據(jù)的詳細步驟
這篇文章主要介紹了Java解析照片拿到GPS位置數(shù)據(jù),本文給大家介紹代碼環(huán)境及核心代碼,代碼簡單易懂,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03
java 裝飾模式(Decorator Pattern)詳解
這篇文章主要介紹了java 裝飾模式(Decorator Pattern)詳解的相關(guān)資料,需要的朋友可以參考下2016-10-10

