SpringSecurity身份驗證實現(xiàn)完整指南
Spring Security 表單登錄完整指南
概述
Spring Security 是 Spring 生態(tài)中用于處理認(rèn)證和授權(quán)的強(qiáng)大框架。本文以實際項目為例,詳細(xì)講解如何使用 Spring Security 實現(xiàn)基于表單的登錄認(rèn)證。
為什么選擇 Spring Security?
- ? 標(biāo)準(zhǔn)化: 提供企業(yè)級的安全解決方案
- ? 自動化: 自動處理認(rèn)證流程、Session 管理、CSRF 保護(hù)
- ? 可擴(kuò)展: 支持多種認(rèn)證方式 (表單、OAuth2、JWT 等)
- ? 久經(jīng)考驗: 經(jīng)過大量生產(chǎn)環(huán)境驗證
本文目標(biāo)
通過本文,你將學(xué)會:
- 配置 Spring Security 的 formLogin
- 自定義登錄頁面和認(rèn)證邏輯
- 處理登錄成功和失敗的場景
- 在業(yè)務(wù)代碼中獲取當(dāng)前登錄用戶
- 實現(xiàn)登出功能
核心概念
1. 認(rèn)證 (Authentication)
認(rèn)證 是驗證用戶身份的過程,回答"你是誰?"的問題。
在 Spring Security 中,認(rèn)證信息存儲在 Authentication 對象中:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String username = authentication.getName(); // 獲取用戶名 Object principal = authentication.getPrincipal(); // 獲取用戶詳情 Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 獲取權(quán)限
2. SecurityContext
SecurityContext 是 Spring Security 的核心容器,用于存儲當(dāng)前用戶的認(rèn)證信息。
// 獲取當(dāng)前 SecurityContext SecurityContext context = SecurityContextHolder.getContext(); // 獲取認(rèn)證信息 Authentication auth = context.getAuthentication(); // 判斷用戶是否已認(rèn)證 boolean isAuthenticated = auth != null && auth.isAuthenticated();
3. 過濾器鏈 (Filter Chain)
Spring Security 通過一系列過濾器來處理請求:
請求 → DisableEncodeUrlFilter
→ WebAsyncManagerIntegrationFilter
→ SecurityContextPersistenceFilter (從 Session 中恢復(fù) SecurityContext)
→ HeaderWriterFilter
→ LogoutFilter (處理登出請求)
→ UsernamePasswordAuthenticationFilter (處理登錄請求)
→ RequestCacheAwareFilter
→ SecurityContextHolderAwareRequestFilter
→ AnonymousAuthenticationFilter
→ SessionManagementFilter
→ ExceptionTranslationFilter
→ AuthorizationFilter (鑒權(quán))
→ 業(yè)務(wù)代碼
快速開始
步驟 1: 添加依賴
在 pom.xml 中添加 Spring Security 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>步驟 2: 配置 SecurityConfig
創(chuàng)建安全配置類:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 演示環(huán)境禁用 CSRF (生產(chǎn)環(huán)境建議啟用)
.authorizeHttpRequests(auth -> auth
.antMatchers("/public/**").permitAll() // 公開路徑
.antMatchers("/login").permitAll() // 登錄頁面允許匿名訪問
.anyRequest().authenticated() // 其他路徑需要認(rèn)證
)
.formLogin(form -> form
.loginPage("/login") // 自定義登錄頁面
.loginProcessingUrl("/login") // 登錄表單提交的 URL
.defaultSuccessUrl("/home", true) // 登錄成功后跳轉(zhuǎn)的頁面
.failureUrl("/login?error") // 登錄失敗后跳轉(zhuǎn)的頁面
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout") // 登出 URL
.logoutSuccessUrl("/login?logout") // 登出成功后跳轉(zhuǎn)的頁面
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 使用 BCrypt 加密密碼
}
@Bean
public UserDetailsService userDetailsService() {
// 內(nèi)存用戶存儲 (演示用,生產(chǎn)環(huán)境應(yīng)使用數(shù)據(jù)庫)
UserDetails user = User.builder()
.username("admin")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}步驟 3: 創(chuàng)建登錄頁面
創(chuàng)建 src/main/resources/templates/login.html:
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<h2>用戶登錄</h2>
<!-- 錯誤提示 -->
<div th:if="${error}" style="color: red;">
用戶名或密碼錯誤
</div>
<!-- 登出成功提示 -->
<div th:if="${logout}" style="color: green;">
您已成功退出登錄
</div>
<!-- 登錄表單 -->
<form method="post" th:action="@{/login}">
<div>
<label>用戶名:</label>
<input type="text" name="username" required autofocus>
</div>
<div>
<label>密碼:</label>
<input type="password" name="password" required>
</div>
<button type="submit">登錄</button>
</form>
</body>
</html>關(guān)鍵點:
- 表單必須使用
POST方法 - 表單 action 必須是
/login(或配置的loginProcessingUrl) - 用戶名字段必須命名為
username - 密碼字段必須命名為
password
步驟 4: 創(chuàng)建 LoginController
@Controller
public class LoginController {
@GetMapping("/login")
public String loginPage(@RequestParam(required = false) String error,
@RequestParam(required = false) String logout,
Model model) {
if (error != null) {
model.addAttribute("error", "用戶名或密碼錯誤");
}
if (logout != null) {
model.addAttribute("logout", "您已成功退出登錄");
}
return "login";
}
}注意:
- 只需要處理
GET /login展示登錄頁面 POST /login由 Spring Security 自動處理,不需要編寫代碼
深入理解
1. formLogin 工作原理
當(dāng)你配置 formLogin() 時,Spring Security 會自動注冊 UsernamePasswordAuthenticationFilter:
1. 用戶提交登錄表單 (POST /login) ↓ 2. UsernamePasswordAuthenticationFilter 攔截請求 ↓ 3. 從請求中提取 username 和 password ↓ 4. 創(chuàng)建 UsernamePasswordAuthenticationToken (未認(rèn)證) ↓ 5. 調(diào)用 AuthenticationManager.authenticate() ↓ 6. AuthenticationManager 委托給 DaoAuthenticationProvider ↓ 7. DaoAuthenticationProvider 調(diào)用 UserDetailsService.loadUserByUsername() ↓ 8. 獲取 UserDetails,使用 PasswordEncoder 比對密碼 ↓ 9. 認(rèn)證成功: 創(chuàng)建已認(rèn)證的 Authentication 對象 ↓ 10. 將 Authentication 存入 SecurityContext ↓ 11. SecurityContextPersistenceFilter 將 SecurityContext 保存到 Session ↓ 12. 重定向到 defaultSuccessUrl
2. 認(rèn)證失敗流程
1. 密碼校驗失敗 ↓ 2. 拋出 BadCredentialsException ↓ 3. AuthenticationFailureHandler 處理異常 ↓ 4. 重定向到 failureUrl (/login?error) ↓ 5. LoginController 檢測到 error 參數(shù) ↓ 6. 在頁面顯示錯誤提示
3. 獲取當(dāng)前登錄用戶的三種方式
方式 1: SecurityContextHolder (推薦)
@Controller
public class MyController {
@GetMapping("/profile")
public String profile(Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
model.addAttribute("username", username);
return "profile";
}
}方式 2: 方法參數(shù)注入
@GetMapping("/profile")
public String profile(@AuthenticationPrincipal UserDetails user, Model model) {
String username = user.getUsername();
model.addAttribute("username", username);
return "profile";
}
方式 3: Principal 參數(shù)
@GetMapping("/profile")
public String profile(Principal principal, Model model) {
String username = principal.getName();
model.addAttribute("username", username);
return "profile";
}
4. Session 管理
Spring Security 默認(rèn)會在登錄成功后執(zhí)行 Session Fixation Protection (會話固定攻擊防護(hù)):
// 默認(rèn)行為: 登錄成功后更換 Session ID
.sessionManagement(session -> session
.sessionFixation().changeSessionId() // 默認(rèn)值
)
// 其他選項:
.sessionFixation().none() // 不更換 (不安全)
.sessionFixation().newSession() // 創(chuàng)建新 Session (舊 Session 屬性會丟失)
.sessionFixation().migrateSession() // 遷移 Session (保留舊屬性)實戰(zhàn)技巧
技巧 1: 自定義登錄成功處理器
如果需要在登錄成功后執(zhí)行額外邏輯 (如記錄日志、更新登錄時間):
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
// 記錄登錄日志
String username = authentication.getName();
System.out.println("用戶 " + username + " 登錄成功");
// 重定向到目標(biāo)頁面
response.sendRedirect("/home");
}
}在 SecurityConfig 中使用:
@Autowired
private CustomAuthenticationSuccessHandler successHandler;
.formLogin(form -> form
.loginPage("/login")
.successHandler(successHandler) // 使用自定義處理器
)技巧 2: 記住我 (Remember Me)
.rememberMe(remember -> remember
.key("unique-key") // 加密密鑰
.tokenValiditySeconds(604800) // Token 有效期 (7天)
.rememberMeParameter("remember-me") // 表單參數(shù)名
)
登錄表單添加復(fù)選框:
<label>
<input type="checkbox" name="remember-me"> 記住我
</label>
技巧 3: 限制同一用戶的并發(fā)登錄
.sessionManagement(session -> session
.maximumSessions(1) // 同一用戶最多 1 個 Session
.maxSessionsPreventsLogin(true) // 達(dá)到上限后阻止新登錄
.expiredUrl("/login?expired") // Session 過期后跳轉(zhuǎn)的頁面
)
技巧 4: 在 Thymeleaf 中使用 Spring Security
添加依賴:
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>在模板中使用:
<html xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<!-- 僅認(rèn)證用戶可見 -->
<div sec:authorize="isAuthenticated()">
歡迎,<span sec:authentication="name">用戶</span>!
<a th:href="@{/logout}" rel="external nofollow" >退出</a>
</div>
<!-- 僅匿名用戶可見 -->
<div sec:authorize="!isAuthenticated()">
<a th:href="@{/login}" rel="external nofollow" >登錄</a>
</div>
<!-- 僅特定角色可見 -->
<div sec:authorize="hasRole('ADMIN')">
管理員專屬內(nèi)容
</div>技巧 5: 登錄后跳轉(zhuǎn)到之前訪問的頁面
Spring Security 默認(rèn)會自動實現(xiàn)此功能!
工作原理:
- 用戶訪問
/protected/resource(未登錄) - Spring Security 將
/protected/resource保存到RequestCache - 重定向到
/login - 用戶登錄成功后,自動跳轉(zhuǎn)回
/protected/resource
如果需要禁用此功能,強(qiáng)制跳轉(zhuǎn)到固定頁面:
.formLogin(form -> form
.defaultSuccessUrl("/home", true) // 第二個參數(shù) true 表示總是跳轉(zhuǎn)到 /home
)
常見問題
問題 1: 登錄后仍然跳轉(zhuǎn)到登錄頁 (重定向循環(huán))
原因: /login 路徑?jīng)]有配置為允許匿名訪問
解決方案:
.authorizeHttpRequests(auth -> auth
.antMatchers("/login").permitAll() // 必須添加這一行
.anyRequest().authenticated()
)
問題 2: 登錄失敗沒有顯示錯誤信息
原因: Controller 沒有處理 error 參數(shù)
解決方案:
@GetMapping("/login")
public String loginPage(@RequestParam(required = false) String error, Model model) {
if (error != null) {
model.addAttribute("error", "用戶名或密碼錯誤");
}
return "login";
}問題 3: CSRF token 驗證失敗
原因: 啟用了 CSRF 保護(hù)但表單沒有包含 CSRF token
解決方案 1: 禁用 CSRF (僅適用于演示環(huán)境)
http.csrf().disable();
解決方案 2: 在表單中添加 CSRF token
<form method="post" th:action="@{/login}">
<!-- Spring Security 會自動注入 CSRF token -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<!-- 其他表單字段 -->
</form>
使用 Thymeleaf 的 th:action 會自動添加 CSRF token,無需手動添加!
問題 4: 獲取不到當(dāng)前用戶信息
原因: SecurityContext 沒有正確存儲到 Session
檢查:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) {
System.out.println("SecurityContext 為空");
} else if (!auth.isAuthenticated()) {
System.out.println("用戶未認(rèn)證");
} else {
System.out.println("用戶名: " + auth.getName());
}
問題 5: 靜態(tài)資源 (CSS/JS/圖片) 被攔截
解決方案: 配置靜態(tài)資源路徑為允許匿名訪問
.authorizeHttpRequests(auth -> auth
.antMatchers("/css/**", "/js/**", "/images/**").permitAll()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)
或者使用 WebSecurity.ignoring():
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/css/**", "/js/**", "/images/**");
}
最佳實踐
1. 密碼加密
永遠(yuǎn)不要明文存儲密碼! 使用 BCryptPasswordEncoder:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 創(chuàng)建用戶時加密密碼
String encodedPassword = passwordEncoder.encode("rawPassword");2. 使用數(shù)據(jù)庫存儲用戶
生產(chǎn)環(huán)境應(yīng)使用數(shù)據(jù)庫而非內(nèi)存存儲:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用戶不存在"));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 數(shù)據(jù)庫中已加密的密碼
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}3. 區(qū)分開發(fā)和生產(chǎn)環(huán)境的配置
@Configuration
@Profile("dev")
public class DevSecurityConfig {
@Bean
public SecurityFilterChain devSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable(); // 開發(fā)環(huán)境禁用 CSRF
// ...
}
}
@Configuration
@Profile("prod")
public class ProdSecurityConfig {
@Bean
public SecurityFilterChain prodSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// ...
}
}4. 使用 HTTPS
生產(chǎn)環(huán)境必須使用 HTTPS:
http.requiresChannel(channel -> channel
.anyRequest().requiresSecure() // 強(qiáng)制使用 HTTPS
);
5. 日志記錄
記錄關(guān)鍵的安全事件:
@Component
@Slf4j
public class AuthenticationEventListener {
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
String username = event.getAuthentication().getName();
log.info("用戶登錄成功: {}", username);
}
@EventListener
public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
String username = event.getAuthentication().getName();
Exception exception = event.getException();
log.warn("用戶登錄失敗: {}, 原因: {}", username, exception.getMessage());
}
}6. 避免常見安全漏洞
- ? 啟用 CSRF 保護(hù) (生產(chǎn)環(huán)境)
- ? 使用 HTTPS
- ? 密碼加密存儲
- ? 防止暴力破解 (限制登錄嘗試次數(shù))
- ? Session 超時設(shè)置
- ? 安全的密碼策略 (長度、復(fù)雜度)
// Session 超時配置 (application.yml)
server:
servlet:
session:
timeout: 30m # 30 分鐘無操作自動登出完整示例代碼
SecurityConfig.java
package org.example.demoboot.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.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.antMatchers("/public/**").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/ratelimit/**").authenticated()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/ratelimit", true)
.failureUrl("/login?error")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
UserDetails test = User.builder()
.username("test")
.password(passwordEncoder().encode("test123"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, test);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}LoginController.java
package org.example.demoboot.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.ui.Model;
@Controller
public class LoginController {
@GetMapping("/login")
public String loginPage(@RequestParam(required = false) String error,
@RequestParam(required = false) String logout,
Model model) {
if (error != null) {
model.addAttribute("error", "用戶名或密碼錯誤");
}
if (logout != null) {
model.addAttribute("logout", "您已成功退出登錄");
}
return "login";
}
}在業(yè)務(wù)代碼中獲取當(dāng)前用戶
package org.example.demoboot.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/ratelimit")
public String home(Model model) {
// 獲取當(dāng)前登錄用戶
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
model.addAttribute("username", username);
return "ratelimit";
}
}總結(jié)
本文通過實際代碼示例,詳細(xì)講解了 Spring Security 表單登錄的核心知識:
? 配置 SecurityConfig: 啟用 formLogin,配置登錄頁面和成功/失敗處理
? 自定義登錄頁面: 使用 Thymeleaf 創(chuàng)建登錄表單
? 獲取當(dāng)前用戶: 通過 SecurityContextHolder 獲取認(rèn)證信息
? 異常處理: 在 Controller 中處理登錄錯誤
? 登出功能: 配置 logout URL 和成功跳轉(zhuǎn)頁面
? 實戰(zhàn)技巧: Remember Me、并發(fā)控制、自定義成功處理器
? 最佳實踐: 密碼加密、HTTPS、日志記錄、安全配置
掌握這些知識后,你可以在項目中靈活運(yùn)用 Spring Security,構(gòu)建安全可靠的認(rèn)證系統(tǒng)。
到此這篇關(guān)于SpringSecurity身份驗證實現(xiàn)的文章就介紹到這了,更多相關(guān)SpringSecurity身份驗證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring中DAO被循環(huán)調(diào)用的時候數(shù)據(jù)不實時更新的解決方法
這篇文章主要介紹了Spring中DAO被循環(huán)調(diào)用的時候數(shù)據(jù)不實時更新的解決方法,需要的朋友可以參考下2014-08-08
百度翻譯API使用詳細(xì)教程(前端vue+后端springboot)
這篇文章主要給大家介紹了關(guān)于百度翻譯API使用的相關(guān)資料,百度翻譯API是百度面向開發(fā)者推出的免費翻譯服務(wù)開放接口,任何第三方應(yīng)用或網(wǎng)站都可以通過使用百度翻譯API為用戶提供實時優(yōu)質(zhì)的多語言翻譯服務(wù),需要的朋友可以參考下2024-02-02
Java實現(xiàn)數(shù)據(jù)脫敏(Desensitization)的操作指南
數(shù)據(jù)脫敏是指通過對敏感數(shù)據(jù)進(jìn)行部分或完全隱藏處理,保護(hù)敏感信息在存儲和使用過程中的安全性,常見的應(yīng)用場景包括日志記錄、接口返回、報表展示、數(shù)據(jù)分析等,本文給大家介紹了Java實現(xiàn)數(shù)據(jù)脫敏(Desensitization)的操作指南,需要的朋友可以參考下2025-02-02
如何實現(xiàn)自定義SpringBoot的Starter組件
這篇文章主要介紹了實現(xiàn)自定義SpringBoot的Starter組件的示例代碼,想要自定義starter組件,首先要了解springboot是如何加載starter的,也就是springboot的自動裝配機(jī)制原理,本文結(jié)合示例代碼詳細(xì)講解,需要的朋友可以參考下2023-02-02

