SpringSecurity身份验证实现完整指南
Spring Security 表单登录完整指南
概述
Spring Security 是 Spring 生态中用于处理认证和授权的强大框架。本文以实际项目为例,详细讲解如何使用 Spring Security 实现基于表单的登录认证。
为什么选择 Spring Security?
- ✅ 标准化: 提供企业级的安全解决方案
- ✅ 自动化: 自动处理认证流程、Session 管理、CSRF 保护
- ✅ 可扩展: 支持多种认证方式 (表单、OAuth2、JWT 等)
- ✅ 久经考验: 经过大量生产环境验证
本文目标
通过本文,你将学会:
- 配置 Spring Security 的 formLogin
- 自定义登录页面和认证逻辑
- 处理登录成功和失败的场景
- 在业务代码中获取当前登录用户
- 实现登出功能
核心概念
1. 认证 (Authentication)
认证 是验证用户身份的过程,回答"你是谁?"的问题。
在 Spring Security 中,认证信息存储在 Authentication 对象中:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String username = authentication.getName(); // 获取用户名 Object principal = authentication.getPrincipal(); // 获取用户详情 Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 获取权限
2. SecurityContext
SecurityContext 是 Spring Security 的核心容器,用于存储当前用户的认证信息。
// 获取当前 SecurityContext SecurityContext context = SecurityContextHolder.getContext(); // 获取认证信息 Authentication auth = context.getAuthentication(); // 判断用户是否已认证 boolean isAuthenticated = auth != null && auth.isAuthenticated();
3. 过滤器链 (Filter Chain)
Spring Security 通过一系列过滤器来处理请求:
请求 → DisableEncodeUrlFilter
→ WebAsyncManagerIntegrationFilter
→ SecurityContextPersistenceFilter (从 Session 中恢复 SecurityContext)
→ HeaderWriterFilter
→ LogoutFilter (处理登出请求)
→ UsernamePasswordAuthenticationFilter (处理登录请求)
→ RequestCacheAwareFilter
→ SecurityContextHolderAwareRequestFilter
→ AnonymousAuthenticationFilter
→ SessionManagementFilter
→ ExceptionTranslationFilter
→ AuthorizationFilter (鉴权)
→ 业务代码
快速开始
步骤 1: 添加依赖
在 pom.xml 中添加 Spring Security 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>步骤 2: 配置 SecurityConfig
创建安全配置类:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 演示环境禁用 CSRF (生产环境建议启用)
.authorizeHttpRequests(auth -> auth
.antMatchers("/public/**").permitAll() // 公开路径
.antMatchers("/login").permitAll() // 登录页面允许匿名访问
.anyRequest().authenticated() // 其他路径需要认证
)
.formLogin(form -> form
.loginPage("/login") // 自定义登录页面
.loginProcessingUrl("/login") // 登录表单提交的 URL
.defaultSuccessUrl("/home", true) // 登录成功后跳转的页面
.failureUrl("/login?error") // 登录失败后跳转的页面
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout") // 登出 URL
.logoutSuccessUrl("/login?logout") // 登出成功后跳转的页面
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 使用 BCrypt 加密密码
}
@Bean
public UserDetailsService userDetailsService() {
// 内存用户存储 (演示用,生产环境应使用数据库)
UserDetails user = User.builder()
.username("admin")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}步骤 3: 创建登录页面
创建 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>关键点:
- 表单必须使用
POST方法 - 表单 action 必须是
/login(或配置的loginProcessingUrl) - 用户名字段必须命名为
username - 密码字段必须命名为
password
步骤 4: 创建 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 工作原理
当你配置 formLogin() 时,Spring Security 会自动注册 UsernamePasswordAuthenticationFilter:
1. 用户提交登录表单 (POST /login) ↓ 2. UsernamePasswordAuthenticationFilter 拦截请求 ↓ 3. 从请求中提取 username 和 password ↓ 4. 创建 UsernamePasswordAuthenticationToken (未认证) ↓ 5. 调用 AuthenticationManager.authenticate() ↓ 6. AuthenticationManager 委托给 DaoAuthenticationProvider ↓ 7. DaoAuthenticationProvider 调用 UserDetailsService.loadUserByUsername() ↓ 8. 获取 UserDetails,使用 PasswordEncoder 比对密码 ↓ 9. 认证成功: 创建已认证的 Authentication 对象 ↓ 10. 将 Authentication 存入 SecurityContext ↓ 11. SecurityContextPersistenceFilter 将 SecurityContext 保存到 Session ↓ 12. 重定向到 defaultSuccessUrl
2. 认证失败流程
1. 密码校验失败 ↓ 2. 抛出 BadCredentialsException ↓ 3. AuthenticationFailureHandler 处理异常 ↓ 4. 重定向到 failureUrl (/login?error) ↓ 5. LoginController 检测到 error 参数 ↓ 6. 在页面显示错误提示
3. 获取当前登录用户的三种方式
方式 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: 方法参数注入
@GetMapping("/profile")
public String profile(@AuthenticationPrincipal UserDetails user, Model model) {
String username = user.getUsername();
model.addAttribute("username", username);
return "profile";
}
方式 3: Principal 参数
@GetMapping("/profile")
public String profile(Principal principal, Model model) {
String username = principal.getName();
model.addAttribute("username", username);
return "profile";
}
4. Session 管理
Spring Security 默认会在登录成功后执行 Session Fixation Protection (会话固定攻击防护):
// 默认行为: 登录成功后更换 Session ID
.sessionManagement(session -> session
.sessionFixation().changeSessionId() // 默认值
)
// 其他选项:
.sessionFixation().none() // 不更换 (不安全)
.sessionFixation().newSession() // 创建新 Session (旧 Session 属性会丢失)
.sessionFixation().migrateSession() // 迁移 Session (保留旧属性)实战技巧
技巧 1: 自定义登录成功处理器
如果需要在登录成功后执行额外逻辑 (如记录日志、更新登录时间):
@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 + " 登录成功");
// 重定向到目标页面
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") // 表单参数名
)
登录表单添加复选框:
<label>
<input type="checkbox" name="remember-me"> 记住我
</label>
技巧 3: 限制同一用户的并发登录
.sessionManagement(session -> session
.maximumSessions(1) // 同一用户最多 1 个 Session
.maxSessionsPreventsLogin(true) // 达到上限后阻止新登录
.expiredUrl("/login?expired") // Session 过期后跳转的页面
)
技巧 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">
<!-- 仅认证用户可见 -->
<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')">
管理员专属内容
</div>技巧 5: 登录后跳转到之前访问的页面
Spring Security 默认会自动实现此功能!
工作原理:
- 用户访问
/protected/resource(未登录) - Spring Security 将
/protected/resource保存到RequestCache - 重定向到
/login - 用户登录成功后,自动跳转回
/protected/resource
如果需要禁用此功能,强制跳转到固定页面:
.formLogin(form -> form
.defaultSuccessUrl("/home", true) // 第二个参数 true 表示总是跳转到 /home
)
常见问题
问题 1: 登录后仍然跳转到登录页 (重定向循环)
原因: /login 路径没有配置为允许匿名访问
解决方案:
.authorizeHttpRequests(auth -> auth
.antMatchers("/login").permitAll() // 必须添加这一行
.anyRequest().authenticated()
)
问题 2: 登录失败没有显示错误信息
原因: Controller 没有处理 error 参数
解决方案:
@GetMapping("/login")
public String loginPage(@RequestParam(required = false) String error, Model model) {
if (error != null) {
model.addAttribute("error", "用户名或密码错误");
}
return "login";
}问题 3: CSRF token 验证失败
原因: 启用了 CSRF 保护但表单没有包含 CSRF token
解决方案 1: 禁用 CSRF (仅适用于演示环境)
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: 获取不到当前用户信息
原因: SecurityContext 没有正确存储到 Session
检查:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) {
System.out.println("SecurityContext 为空");
} else if (!auth.isAuthenticated()) {
System.out.println("用户未认证");
} else {
System.out.println("用户名: " + auth.getName());
}
问题 5: 静态资源 (CSS/JS/图片) 被拦截
解决方案: 配置静态资源路径为允许匿名访问
.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. 密码加密
永远不要明文存储密码! 使用 BCryptPasswordEncoder:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 创建用户时加密密码
String encodedPassword = passwordEncoder.encode("rawPassword");2. 使用数据库存储用户
生产环境应使用数据库而非内存存储:
@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()) // 数据库中已加密的密码
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}3. 区分开发和生产环境的配置
@Configuration
@Profile("dev")
public class DevSecurityConfig {
@Bean
public SecurityFilterChain devSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable(); // 开发环境禁用 CSRF
// ...
}
}
@Configuration
@Profile("prod")
public class ProdSecurityConfig {
@Bean
public SecurityFilterChain prodSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// ...
}
}4. 使用 HTTPS
生产环境必须使用 HTTPS:
http.requiresChannel(channel -> channel
.anyRequest().requiresSecure() // 强制使用 HTTPS
);
5. 日志记录
记录关键的安全事件:
@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 保护 (生产环境)
- ✅ 使用 HTTPS
- ✅ 密码加密存储
- ✅ 防止暴力破解 (限制登录尝试次数)
- ✅ Session 超时设置
- ✅ 安全的密码策略 (长度、复杂度)
// 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";
}
}在业务代码中获取当前用户
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) {
// 获取当前登录用户
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
model.addAttribute("username", username);
return "ratelimit";
}
}总结
本文通过实际代码示例,详细讲解了 Spring Security 表单登录的核心知识:
✅ 配置 SecurityConfig: 启用 formLogin,配置登录页面和成功/失败处理
✅ 自定义登录页面: 使用 Thymeleaf 创建登录表单
✅ 获取当前用户: 通过 SecurityContextHolder 获取认证信息
✅ 异常处理: 在 Controller 中处理登录错误
✅ 登出功能: 配置 logout URL 和成功跳转页面
✅ 实战技巧: Remember Me、并发控制、自定义成功处理器
✅ 最佳实践: 密码加密、HTTPS、日志记录、安全配置
掌握这些知识后,你可以在项目中灵活运用 Spring Security,构建安全可靠的认证系统。
到此这篇关于SpringSecurity身份验证实现的文章就介绍到这了,更多相关SpringSecurity身份验证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Spring Security 和Apache Shiro你需要具备哪些条件
这篇文章主要介绍了Spring Security 和Apache Shiro你需要具备哪些条件,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2020-07-07
实践讲解SpringBoot自定义初始化Bean+HashMap优化策略模式
本篇讲解了SpringBoot自定义初始化Bean+HashMap优化策略模式,通过实践的方式更通俗易懂,对此不了解的同学跟着小编往下看吧2021-09-09


最新评论