资讯详情

Spring Security过滤器链与JDBC数据库认证实战指南

📅 2026/9/29 22:23:04 | 华诺云谱 👁 阅读
Spring Security过滤器链与JDBC数据库认证实战指南
1. 先搞清楚Spring Security的过滤器链到底是怎么工作的很多同学学Spring Security上来就写配置类、重写方法跑通了就以为会了。结果有一天项目里加了一个自定义过滤器或者升级到Spring Boot 3发现原来的配置全废了才意识到根本没搞懂里面的核心机制。Spring Security的核心机制就是过滤器链Filter Chain。它不是一个过滤器而是一串过滤器按顺序执行像工厂流水线一样请求依次经过每个工位每个工位只干自己那一件事。认证、授权、CSRF防护、登录表单处理、异常处理全都是通过这一条链上的不同过滤器完成的。1.1 过滤器链的组成与加载逻辑Spring Security在启动时会自动构建一条过滤器链。以Spring Boot 3 Spring Security 6为例默认链上至少包含以下核心过滤器我按执行顺序列出来顺序过滤器职责1DisableEncodeUrlFilter禁用URL编码避免会话ID暴露2WebAsyncManagerIntegrationFilter集成异步请求的安全上下文3SecurityContextHolderFilter从SecurityContextRepository中加载安全上下文4HeaderWriterFilter写入安全响应头如X-Frame-Options5CorsFilter处理跨域请求6CsrfFilter处理CSRF防护7LogoutFilter处理登出请求8UsernamePasswordAuthenticationFilter处理表单登录认证9DefaultLoginPageGeneratingFilter生成默认登录页10BasicAuthenticationFilter处理HTTP Basic认证11AuthorizationFilter执行授权判断12ExceptionTranslationFilter捕获异常并转发13FilterSecurityInterceptor方法级/URL级安全拦截我第一次看这个列表的时候就在想为什么AuthorizationFilter放在ExceptionTranslationFilter前面后来踩坑才明白ExceptionTranslationFilter是负责把AccessDeniedException转换成403响应或者重定向到登录页的它必须在授权过滤器之后才能捕获到授权异常。这个顺序一旦乱了安全机制就会失灵。关键点来了这整条链是通过FilterChainProxy这个代理过滤器注册到容器的。Spring Boot会自动把FilterChainProxy注册为一个Servlet过滤器所以你在web.xml或者配置类里看不到一个名为springSecurityFilterChain的注册项但它真实存在。1.2 认证过滤器在链中的位置与执行流程用户提交用户名密码之后真正干活的过滤器是UsernamePasswordAuthenticationFilter。它从HttpServletRequest中取出username和password参数封装成UsernamePasswordAuthenticationToken对象然后交给AuthenticationManager去完成认证。AuthenticationManager本身不直接查数据库它是个调度者。它根据Token类型找到对应的AuthenticationProvider——对于用户名密码登录找到的是DaoAuthenticationProvider。DaoAuthenticationProvider再去调用UserDetailsService从数据库或者别的地方加载用户信息然后比对密码、检查账号状态最后把认证结果写回SecurityContext。这里有一条非常容易踩坑的链DaoAuthenticationProvider在比对密码时不是简单地拿输入的明文和数据库里的密文做equals而是用PasswordEncoder去校验。如果数据库里存的是BCrypt密文而你的配置里PasswordEncoder用了NoOpPasswordEncoder明文比较器那永远匹配失败。反过来也是同样的问题。所以JDBC认证实现的关键不光是把数据库连上、SQL写对还要理解这一整条调用链上每个环节的职责和数据流。我画的链路是这样客户端请求 - UsernamePasswordAuthenticationFilter - AuthenticationManager (ProviderManager) - DaoAuthenticationProvider - UserDetailsService (加载用户) - PasswordEncoder (密码校验) - SecurityContextHolder (写入认证结果)明白这条链后面写代码就是顺着这条线一个个填充实现而已。2. 基于JDBC的认证实现思路与方案选型2.1 为什么不用内存用户而用JDBCSpring Security入门教程里最常见的就是在配置类里这样写Bean public UserDetailsService userDetailsService() { UserDetails user User.withUsername(admin) .password({noop}123456) .roles(ADMIN) .build(); return new InMemoryUserDetailsManager(user); }这段代码能跑但它有一个致命问题用户数据写死在代码里。改一个密码要重新编译、重新打包、重新部署这在任何真实项目里都是不可接受的。真实项目的用户数据一定在数据库里。用JDBC做认证本质上是把UserDetailsService的实现从内存换成了数据库查询。Spring Boot提供了一套完整的JDBC用户存储方案你要么直接用它内置的JdbcUserDetailsManager要么自定义一个UserDetailsService实现用JdbcTemplate去数据库里捞用户。两种方式各有适用场景。2.2 数据库表设计与密码存储方案先说密码存储。这是安全底线。明文密码在数据库里裸奔的项目我见过不止一个一旦数据库泄露所有用户账号直接暴露。Spring Security内置的BCryptPasswordEncoder就是干这个的——它是BCrypt哈希算法自带盐同一个密码每次加密结果都不同暴力破解成本极高。然后是表结构。如果你使用Spring Security内置的JdbcUserDetailsManager它默认期望以下两张表存在CREATE TABLE users ( username VARCHAR(50) NOT NULL PRIMARY KEY, password VARCHAR(100) NOT NULL, enabled TINYINT NOT NULL DEFAULT 1 ); CREATE TABLE authorities ( username VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users(username) );这是Spring Security的默认schema。很多人不知道在classpath下加一个schema.sql内容是这两条建表语句应用启动时Spring Boot的脚本初始化机制会自动执行。但现实项目中很少有用户表刚好叫users、字段刚好叫username和password。你大概率有自己的用户表比如sys_user。那就有两条路第一条配置JdbcUserDetailsManager的查询SQL让它的查询语句适配你的表结构。 第二条实现自定义UserDetailsService完全掌控查询逻辑。我的建议是表结构离Spring Security默认schema比较远的项目直接走自定义UserDetailsService。因为JdbcUserDetailsManager的适配SQL改起来比较绕而且只能处理单表查询一旦涉及联表查询比如用户-角色-权限三表关联它就无能为力了。自定义UserDetailsService的核心就是实现这个接口方法public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }你只需在这个方法里做三件事根据用户名从数据库查出用户记录把数据库记录转换成UserDetails对象设置用户拥有的权限就这么简单。JdbcTemplate负责第一步User对象负责第二步。3. 实操Spring Boot 3中完整落地JDBC认证3.1 工程结构与依赖引入先看依赖。基于Spring Boot 3.2.x Spring Security 6.2.x需要引入以下依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意如果你引入了spring-boot-starter-jdbcSpring Boot会利用DataSourceAutoConfiguration自动配置数据源。如果你项目里用的是MyBatis或者JPA它内部已经传递依赖了这个starter数据源自动配置依然生效。这里有一个最容易被新手忽略的点Spring Boot 3整个安全配置的API都变了。如果你在网上搜索教程看到有类继承WebSecurityConfigurerAdapter的那都是Spring Boot 2时代的写法在Spring Boot 3里这个类已经删除了。现在的标准写法是定义SecurityFilterChain的Bean。再提一个IDEA里的坑有些同学新建Spring Boot项目时IDEA自动下载Maven依赖会失败尤其是mysql-connector-j这个依赖偶尔会报download from maven failed。遇到这种情况先检查Maven私服配置再看本地仓库是否损坏。最常见的解决方法是删掉本地仓库通常位于~/.m2/repository下对应的失败目录然后重新刷新Maven项目让IDEA重新下载。3.2 数据源配置与数据库准备接下来配置application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/security_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver这里单独说一下driver-class-name。可能有人发现明明不写driver-class-name也能跑因为Spring Boot能从URL里推断驱动类。但建议还是显式写出来尤其是在多数据源场景下不写容易串驱动。另外MySQL 8之后的驱动类名是com.mysql.cj.jdbc.Driver老写法com.mysql.jdbc.Driver已经废弃了。然后准备测试数据。我的做法是在resources目录下放一个schema.sql和data.sql这样应用启动时数据库表结构和测试数据会自动初始化-- schema.sql CREATE TABLE IF NOT EXISTS sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, enabled TINYINT DEFAULT 1 );-- data.sql INSERT INTO sys_user (username, password, enabled) VALUES (admin, {bcrypt}$2a$10$dXJ3SW6G7P5lGz6VqZcZ6eCwQxMOvZSlVN7OZx1XHhE4wNOUuO5lO, 1), (user, {bcrypt}$2a$10$dXJ3SW6G7P5lGz6VqZcZ6eCwQxMOvZSlVN7OZx1XHhE4wNOUuO5lO, 1);注意看密码前面那一段{bcrypt}前缀。这是Spring Security 5之后引入的PasswordEncoder Delegation机制。Spring Security会根据这个前缀自动选择对应的PasswordEncoder去做校验。如果你不想在前缀上做文章可以统一在配置里指定一个PasswordEncoder Bean。那这段密文怎么生成的最方便的办法是写一个临时测试类用BCryptPasswordEncoder生成public class PasswordGenerator { public static void main(String[] args) { BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); String rawPassword 123456; String encodedPassword encoder.encode(rawPassword); System.out.println(encodedPassword); } }跑一次把输出粘到SQL里就行了。我实际项目中也是这么干的没有比这更快的办法。3.3 自定义UserDetailsService与JdbcTemplate实现现在一步步写代码。首先自定义一个UserDetailsService实现类Service public class JdbcUserDetailsService implements UserDetailsService { private final JdbcTemplate jdbcTemplate; public JdbcUserDetailsService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String sql SELECT username, password, enabled FROM sys_user WHERE username ?; ListMapString, Object list jdbcTemplate.queryForList(sql, username); if (list null || list.isEmpty()) { throw new UsernameNotFoundException(用户不存在: username); } MapString, Object row list.get(0); String dbUsername (String) row.get(username); String dbPassword (String) row.get(password); Integer enabled (Integer) row.get(enabled); return User.withUsername(dbUsername) .password(dbPassword) .disabled(enabled 0) .authorities(ROLE_USER) .build(); } }有人会问查询一条记录为什么不用queryForObject我在实际开发中踩过坑。queryForObject配合RowMapper在查询结果为空时会抛EmptyResultDataAccessException这倒也能捕获。但是当数据库字段有NULL或者类型映射不对时RowMapper里的getString很容易因为SQL中间结果的问题报错不如queryForList拿到List之后自己再做判断灵活代码也好调试。权限那里我先硬编码ROLE_USER。真实项目里应该是联表查询比如从sys_role表查出用户名对应的角色再拼成ROLE_ADMIN这样的字符串。我演示项目里简化了但是思路是一样的。3.4 安全配置类的写法重点讲Spring Boot 3的迁移变化接下来是Spring Security的配置类。这是Spring Boot 3改动最大的一块我重点讲。Spring Boot 2时代的标准写法是继承WebSecurityConfigurerAdapter重写configure(HttpSecurity http)方法。Spring Security 6把这条路彻底堵死了现在的写法是直接定义SecurityFilterChain BeanConfiguration EnableWebSecurity public class SecurityConfig { private final UserDetailsService userDetailsService; public SecurityConfig(UserDetailsService userDetailsService) { this.userDetailsService userDetailsService; } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(/login, /css/**, /js/**, /register).permitAll() .requestMatchers(/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .formLogin(form - form .loginPage(/login) .loginProcessingUrl(/login) .usernameParameter(username) .passwordParameter(password) .defaultSuccessUrl(/index, true) .permitAll() ) .logout(logout - logout .logoutUrl(/logout) .logoutSuccessUrl(/login?logout) ) .userDetailsService(userDetailsService); return http.build(); } }关于这段配置有几个点我必须单独拉出来讲清楚因为这些就是新人最容易卡住的地方。第一个注意点关于密码加密器PasswordEncoder的注入方式。很多教程会在过滤链里加上.and()或者是把PasswordEncoder作为参数传给http其实不需要。只要你声明了PasswordEncoder BeanSpring Security会自动把它装配到DaoAuthenticationProvider里。前提是——你声明的UserDetailsService Bean也要被Spring Security感知到。第二个注意点DaoAuthenticationProvider会自动使用容器中的UserDetailsService。但在某些情况下Spring Security可能没有自动装配你的Bean。我怎么解决的我习惯显式在配置里调用.userDetailsService(userDetailsService)这样做的好处是明确的告诉Spring Security用我这个UserDetailsService不要自己去猜测。第三个注意点关于登录页。如果你的项目有自定义登录页面loginPage(/login)配置好后还需要一个Controller来处理/login这个GET请求返回login.html页面。如果你不想写ControllerSpring Security也提供了默认登录页只要不配置loginPage系统自动生成一个。第四个注意点Spring Boot 3中csrf默认开启。这意味着POST请求比如登录如果没有携带CSRF Token会被拒绝——返回403。开发阶段最简单的解法是暂时关闭CSRFhttp.csrf(csrf - csrf.disable());但这里我明确提醒你上线前务必评估CSRF的风险。对于公开的、无状态的服务关闭CSRF可能问题不大但如果是基于Session的有状态应用CSRF防护是必须开的。关闭只是开发调试方便。再看登录表单的UsernameParameter和PasswordParameter。如果前端登录页的输入框name属性就是username和password这两个配置可以省略。但如果你的前端input写的是nameuser和namepwd那这里就必须对应修改否则Spring Security取不到参数登录必然失败。这段配置里还涉及一个静态资源放行问题。在实际项目中CSS、JS、图片这些资源一般都要放行不然页面样式加载不出来登录页一团乱。我的经验是尽量用具体的路径匹配如果是Spring Boot项目/static目录下的资源默认映射到/css/、/js/、/images/等路径所以上面配置里我把这些路径都放行了。最后是关键的一点Spring Security 6用requestMatchers代替了antMatchers。Spring Security 5.8开始antMatchers标记为废弃Spring Security 6删掉了。网上老教程里写的.antMatchers(/admin/)在Spring Boot 3里直接编译报错要统一改成.requestMatchers(/admin/)。这个坑我自己就踩过搜索的时候发现很多代码还是旧的写法照着敲了一遍发现根本编译不过。3.5 认证流程串联验证配置完成后整个认证流程是这样的用户打开/login页面输入用户名密码点击提交请求发到/login处理URLloginProcessingUrl指定的UsernamePasswordAuthenticationFilter拦截请求取出参数构造UsernamePasswordAuthenticationToken交给AuthenticationManagerProviderManager找到DaoAuthenticationProviderDaoAuthenticationProvider调用我们的JdbcUserDetailsServiceJdbcTemplate执行SQL从sys_user表查出用户转换成UserDetailsDaoAuthenticationProvider用BCryptPasswordEncoder比对密码把认证结果写入SecurityContextHolder跳转到defaultSuccessUrl指定的页面我在本地用Spring Boot 3.2 MySQL 8实测过整个流程跑通非常顺。唯一让我折腾了一会的是密码前缀的问题——因为我在数据库里存的密文格式带{bcrypt}前缀而我的PasswordEncoder Bean是BCryptPasswordEncoder校验时Spring Security发现前缀是{bcrypt}就会委托给对应的DelegatingPasswordEncoder处理。如果你的密文不带任何前缀那就会直接使用你配置的BCryptPasswordEncoder进行比对。4. 常见问题排查与避坑实录4.1 登录页刷新后无限重定向这个是我见过最多的问题。现象是输入正确账号密码后页面一直在登录页和某个地址之间来回跳转最终浏览器报重定向次数过多。原因通常是defaultSuccessUrl配置出错。比如defaultSuccessUrl(/index, true)中/index这个地址要求用户必须登录才能访问而登录成功后Spring Security要重定向到/index结果又触发认证检查发现没有认证信息又跳回登录页无限循环。解决思路有两条。一是把defaultSuccessUrl指向一个permitAll的地址比如首页或欢迎页。二是检查SecurityContext里到底有没有写入认证信息——我在调试时习惯加一个Controller端点返回SecurityContextHolder.getContext().getAuthentication()的内容一眼就能看出认证是否成功。还有一个隐藏原因登录成功但SecurityContext没有持久化到Session。如果通过Spring Session或者分布式Session方案Session的序列化问题可能导致安全上下文丢失。遇到这种情况检查是否有实现HttpSessionEventPublisher的Listener注册Spring Security需要这个Listener来清理Session中的安全上下文。4.2 CSRF导致登录/注册接口403Spring Boot 3中CSRF默认是开启的。如果你用Postman测试登录接口返回403根本不用怀疑别的地方大概率就是CSRF拦截。排查方法很简单看响应体里有没有CSRF相关的错误信息。再一个打开浏览器开发者工具提交登录表单看请求参数里有没有名为_csrf的字段。没有的话说明前端没有带上Token。开发阶段我的做法是直接关闭CSRF。但是关闭之后表单登录方式会受影响吗其实不会登录流程本身不强制要求CSRF只是作为防护手段。如果没有关闭前端需要从后端拿到CSRF Token并附在表单或请求头里。这个流程对接比较繁琐所以开发阶段关闭是合理的。4.3 密码一直报错不匹配登录时提示Bad credentials但用户名是存在的密码也是正确的。这种情况90%是密码编码问题。我整理了几种常见场景你们可以对号入座场景原因解决方式数据库密码是明文配置了BCryptPasswordEncoder编码器类型不匹配改用NoOpPasswordEncoder或重新生成密文数据库密码是BCrypt但配置了NoOpPasswordEncoder编码器类型不匹配改用BCryptPasswordEncoder密码前面有{noop}前缀但没配置DelegatingPasswordEncoder不支持该前缀去掉前缀或用DelegatingPasswordEncoder密码里不小心带了空格或换行数据问题清理数据字段长度不够插入时被截断数据库字段长度不足把password字段长度扩展到100我在第一次实现JDBC认证时就是被第二个场景坑的。我数据库里存的是BCrypt密文但那个时候不懂PasswordEncoder机制用了DelegatingPasswordEncoder的默认行为而密文里没有前缀所以它直接走BCrypt理论上应该能匹配。后来发现问题是JdbcTemplate返回的字段名不对——MySQL查询结果字段名是大写还是小写取决于数据库配置和SQL别名。如果SQL没写别名某些情况下取出来的是USERNAME而不是usernameMap按下标取值直接拿到null。这提醒大家用Map接收JdbcTemplate查询结果时字段名的坑一定要小心。我的经验是SQL里显式写别名SELECT username AS username, password AS password, enabled AS enabled FROM sys_user WHERE username ?这样不管数据库怎么处理字段名查询结果一定是小写。4.4 自定义过滤器不生效有时候项目需要在Spring Security过滤器链上插入自定义过滤器比如处理上传PDF文件时的XSS攻击、或者打点日志。结果配置了OrderedFilterRegistrationBean后发现过滤器根本不执行。这里要用一个绕不过去的概念FilterRegistrationBean的注册顺序和Spring Security过滤器链的关系。如果你直接使用Servlet的WebFilter注解注册了一个过滤器它是作为独立的Servlet过滤器运行跑在Spring Security的FilterChainProxy之前或之后取决于Order注解。如果你想让过滤器作为Spring Security过滤器链的一部分就需要在HttpSecurity配置里加http.addFilterBefore(new MyCustomFilter(), UsernamePasswordAuthenticationFilter.class);这个区别很多教程不强调但实际工作里非常重要。比如你想在认证之前对请求做一些预处理如参数清洗那就用addFilterBefore放在UsernamePasswordAuthenticationFilter之前。想在认证之后拿到用户信息后做业务处理就放在它之后。我在处理XSS攻击时就遇到了这个问题。项目里需要对上传的PDF文件做内容检查我最初用WebFilter注册全局过滤器结果请求先经过了Spring Security的认证检查如果此时用户未认证请求直接被重定向到登录页了我的过滤器根本没机会执行。后来把这部分逻辑改成了在Spring Security链内添加过滤器放在认证之前执行问题就解决了。4.5 JdbcTemplate注入失败或数据源初始化失败最后一个常见问题应用启动时数据源初始化报错。特别是MySQL版本升级到8之后驱动类、时间戳时区问题都可能导致启动失败。报错信息通常包含Unable to obtain connection from database或者Access denied for user。前者一般是URL或者防火墙问题后者是账号密码问题。时区问题在MySQL 8里尤其典型。URL里必须加上serverTimezoneAsia/Shanghai否则启动时可能报Bad connection或者Unable to load authentication plugin。还有一个很隐蔽的问题有些版本的Spring Boot对MySQL 8的驱动兼容性有问题报this version of the jdbc driver is only compatible with elasticsearch version其实这是依赖冲突导致驱动类加载错乱。遇到这种问题先检查pom里有没有多余的JDBC驱动依赖或者用mvn dependency:tree看看实际生效的驱动版本。我通常的做法是去掉所有多余的驱动依赖只保留一个mysql-connector-j然后清理Maven本地仓库中该依赖的缓存重新构建。这一套在绝大多数情况下都能解决问题。最后再分享一个小技巧我在实际项目中调试Spring Security时很少去断点跟源码通常两个技巧就够用了。第一个是开启日志。在application.yml加一行logging: level: org.springframework.security: DEBUG这个能让你看到认证过程每一步的执行情况——哪个过滤器在跑、从哪个Provider走、密码校验成功还是失败、SecurityContext里最后写入了什么。排查认证问题比看一百遍源码都高效。第二个是在项目里临时加一个Controller端点可以随时查看当前登录用户信息RestController public class UserController { GetMapping(/me) public Object me(Authentication authentication) { return authentication; } }登录之后请求这个接口返回的JSON里包括用户名、权限、认证时间等全部信息。接口如果返回403说明还没认证返回详细数据说明认证链路已经通了。Spring Security的过滤器链和JDBC认证结合在一起是绝大多数Java Web项目的安全基石。这个机制搞明白了后面再做权限管理、OAuth2扩展、JWT整合都是在同一个架构上打补丁不会走弯路。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑