SpringSecurity身份验证实现完整指南

 更新时间:2025年12月31日 15:29:53   作者:eck_燃  
本文介绍了如何使用SpringSecurity实现基于表单的登录认证,包括配置SecurityConfig、自定义登录页面、处理认证成功和失败的场景、获取当前登录用户、实现登出功能,帮助开发者理解和应用SpringSecurity的核心功能和最佳实践,感兴趣的朋友跟随小编一起看看吧

Spring Security 表单登录完整指南

概述

Spring Security 是 Spring 生态中用于处理认证和授权的强大框架。本文以实际项目为例,详细讲解如何使用 Spring Security 实现基于表单的登录认证。

为什么选择 Spring Security?

  • 标准化: 提供企业级的安全解决方案
  • 自动化: 自动处理认证流程、Session 管理、CSRF 保护
  • 可扩展: 支持多种认证方式 (表单、OAuth2、JWT 等)
  • 久经考验: 经过大量生产环境验证

本文目标

通过本文,你将学会:

  1. 配置 Spring Security 的 formLogin
  2. 自定义登录页面和认证逻辑
  3. 处理登录成功和失败的场景
  4. 在业务代码中获取当前登录用户
  5. 实现登出功能

核心概念

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 默认会自动实现此功能!

工作原理:

  1. 用户访问 /protected/resource (未登录)
  2. Spring Security 将 /protected/resource 保存到 RequestCache
  3. 重定向到 /login
  4. 用户登录成功后,自动跳转回 /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身份验证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java8需要知道的4个函数式接口简单教程

    Java8需要知道的4个函数式接口简单教程

    这篇文章主要介绍了Java 8中引入的函数式接口,包括Consumer、Supplier、Predicate和Function,以及它们的用法和特点,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-03-03
  • SwiftUI中级List如何添加新内容(2020年教程)

    SwiftUI中级List如何添加新内容(2020年教程)

    这篇文章主要介绍了SwiftUI中级List如何添加新内容,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-01-01
  • Java虚拟机内存区域划分详解

    Java虚拟机内存区域划分详解

    这篇文章主要介绍了Java虚拟机内存区域划分,本文逻辑清晰,可以帮助我们更好的掌握虚拟机,对我们学习java来说是一种帮助,需要的朋友可以参考下
    2021-04-04
  • Java之next()、nextLine()区别及问题解决

    Java之next()、nextLine()区别及问题解决

    这篇文章主要介绍了Java之next()、nextLine()区别及问题解决,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • Java利用多线程复制文件

    Java利用多线程复制文件

    这篇文章主要介绍了Java利用多线程复制文件,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-09-09
  • 解析Java的JVM以及类与对象的概念

    解析Java的JVM以及类与对象的概念

    这篇文章主要介绍了解析Java的JVM以及类与对象的概念,是Java入门学习中的基础知识,需要的朋友可以参考下
    2015-09-09
  • Spring Security 和Apache Shiro你需要具备哪些条件

    Spring Security 和Apache Shiro你需要具备哪些条件

    这篇文章主要介绍了Spring Security 和Apache Shiro你需要具备哪些条件,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-07-07
  • 控制台显示java冒泡排序流程示例

    控制台显示java冒泡排序流程示例

    这篇文章主要介绍了控制台显示java冒泡排序流程示例,需要的朋友可以参考下
    2014-03-03
  • 实践讲解SpringBoot自定义初始化Bean+HashMap优化策略模式

    实践讲解SpringBoot自定义初始化Bean+HashMap优化策略模式

    本篇讲解了SpringBoot自定义初始化Bean+HashMap优化策略模式,通过实践的方式更通俗易懂,对此不了解的同学跟着小编往下看吧
    2021-09-09
  • Java中反射机制和作用详解

    Java中反射机制和作用详解

    这篇文章主要给大家介绍了关于Java中反射机制和作用的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-05-05

最新评论