Spring Boot 3迁移指南:Spring Security新写法与最佳实践

1. 项目概述

1.1 Spring Security到底是什么

我估计很多刚接触Spring Security的人跟我当初一样,打开官方文档看到一屏的Filter、AuthenticationManager、SecurityContext这些术语,那感觉就像面对着一个黑洞。我先用人话把它说清楚:Spring Security本质上就是一道放在你Web应用前面的"安检门",所有的HTTP请求进来之后,都得先过这道门。它负责回答两个最核心的问题——"你是谁"和"你能干什么",用学术点的说法就是认证(Authentication)和授权(Authorization)。

认证解决的是识别身份,比如你登录时提交的用户名密码,它得确认你确实是张三而不是李四;授权解决的是权限控制,比如确认你是张三之后,得判断你有没有资格访问 /admin 这个接口。这两个核心问题解决了,其实大部分Web应用的安保需求也就解决了。Spring Security厉害的地方在于,它把这两个问题背后的复杂逻辑——包括会话管理、密码加密、CSRF防御、记住我功能、跨域配置等等——全都封装成了一套可插拔的组件,你只需要通过配置把这些组件串起来就行了。

我见过很多Java开发者在项目里直接用拦截器(Interceptor)配合HandlerInterceptor来做登录校验,刚开始确实简单,但随着业务复杂化,你会发现自己在重复造轮子:密码加密方案各写各的,登录会话失效逻辑漏洞百出,接口权限散落在业务代码里。Spring Security至少在行业标准层面帮你把这些问题都规范了。

1.2 谁需要读这篇文章

这篇文章不是给那种已经能熟练改造Spring Security底层过滤器的老手看的,我面向的是下面这些人群:刚进入Java后端开发岗位不久、接手了公司项目但发现项目里配了一套看不懂的安全机制的新人;准备自己写个人项目但不知道登录校验怎么做得规范的学生;以及项目升级到Spring Boot 3后还在用老代码硬凑,发现启动直接报错的开发者。

文章会花大篇幅讲Spring Boot 3升级过程中配置代码的迁移问题,这是目前最容易让Spring Security新手和老手都头疼的地方。因为Spring Security在5.x到6.x之间做了几次比较大的重构,尤其是 WebSecurityConfigurerAdapter 这个类被彻底移除了,如果你在百度上看到的大部分教程都是2021年以前的,你照着抄在Spring Boot 3里大概率是启动不了的。

2. 核心概念拆解与新旧版本差异

2.1 必须搞清楚的三个核心组件

在动手写代码之前,我建议你先花十分钟理解这几个概念,不然你后面配置代码里的每个方法都会看着一头雾水。

第一个是 SecurityFilterChain 。它是整个安全机制的载体,你可以理解成是一个"安检流程清单"。Spring Boot启动的时候,会创建一个名字叫 springSecurityFilterChain 的Servlet过滤器,这个过滤器把Spring Security里所有的过滤器按顺序组织起来,形成一个链,HTTP请求按顺序穿过这条链上的每一个过滤器,只要任何一个过滤器不放行,请求就被拦截了。你要配置的"哪些路径需要登录才能访问"、"登录页面长什么样"、"放行哪些静态资源",最终都是往这条链上挂过滤器或者调整链上过滤器的行为。

第二个是 AuthenticationManager 。它是专门负责"校验身份"的组件。你提交用户名密码上来,Spring Security通过 AuthenticationManager 去找你配置的 UserDetailsService (这个服务负责根据用户名从数据库里查用户信息),然后把查出来的用户信息和提交的密码做比对。密码匹配通过,说明身份有效。

第三个是 SecurityContext 。它保存的是"当前请求到底是谁发起的"这样的上下文信息。一个用户认证成功之后,Spring Security会把认证结果放到 SecurityContext 里,后续的请求到达你的Controller时,你可以通过 SecurityContextHolder.getContext().getAuthentication() 拿到当前用户的信息。Spring Session的并发控制和会话失效,底层也是围绕这个上下文来管理的。

这三个概念搞清楚之后,你再看配置代码就会轻松很多,因为你会知道你在改的是谁的行为。

2.2 Spring Boot 3中配置迁移的核心变化

现在来说说热词里提到的Spring Boot 3配置迁移问题。这是目前搜索Spring Security相关教程时最大的痛点,也是我要重点讲的部分。

在Spring Boot 2时代,我们写安全配置最常见的方式是继承 WebSecurityConfigurerAdapter 这个类,然后重写它的 configure(HttpSecurity http) 方法。我印象中2019年到2021年之间,市面上几乎99%的Spring Security教程都是这个写法。网上搜索出来的代码样例基本是:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/**").permitAll();
    }
}

这个写法在Spring Security 5.x时代是可以正常工作的。但Spring Boot 3基于Spring Security 6.x,这个类已经被彻底移除了。你如果在新项目里沿用这个写法,启动时会直接抛 java.lang.AbstractMethodError 或者因为找不到 WebSecurityConfigurerAdapter 类而编译失败。

新的推荐写法是直接声明一个 SecurityFilterChain 的 @Bean 。基于组件的注册方式,整体的思路是"把配置行为变成组件的定义"。

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/login").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index")
                .permitAll()
            )
            .logout(logout -> logout.logoutUrl("/logout"));
        return http.build();
    }
}

除了这个最核心的变化,配置迁移中还涉及几个特别容易翻车的小点。

第一, antMatchers() 变成了 requestMatchers() 。 antMatchers 在Spring Security 6里还能用但已经标了弃用,而且官方明确建议切换到 requestMatchers ,同时路径匹配规则从Ant风格逐步转向了 PathPattern 风格,写通配符的时候要注意写法上的差异。

第二,方法名变了。 authorizeRequests() 和 permitAll() 这些老方法在6.x中仍然保留,但在代码提示里会出现过期标记。新的链式调用风格是 authorizeHttpRequests() ,配合Lambda表达式,代码的配置块更清晰。我在迁移项目的时候发现直接用新风格改起来其实很快,就是一个方法名替换的活儿。

第三,默认行为变了。Spring Boot 3里的Spring Security默认几乎把常见攻击的防御都打开了,CSRF默认开启,登录表单默认生成,X-Frame-Options默认拒绝iframe嵌入。这也是好事,但如果你在写前后端分离项目,遇到POST请求被403挡回来,八成就是CSRF没关或者没配置好。

第四,依赖坐标也变了。老的 spring-boot-starter-security 坐标不变,但如果你直接依赖 spring-security-config 或 spring-security-web 这些底层模块,需要注意版本要匹配Spring Boot 3的版本管理,最好直接用BOM来统一版本。

3. 实操:从零搭建一个Spring Boot 3安全项目

3.1 环境准备与依赖引入

动手之前先把环境准备好。我的建议是直接创建Spring Boot 3.x项目,JDK版本至少17以上。这里给出我实测可用的版本组合,方便你参考。

组件 版本建议
JDK 17或21
Spring Boot 3.2.x或3.3.x
Spring Security 由Spring Boot BOM统一管理(6.2.x)
构建工具 Maven或Gradle均可

引入依赖时,如果你只需要最基础的登录认证能力,先引入这三个就够了: spring-boot-starter-web 、 spring-boot-starter-security ,还有一个用来连接数据库的 spring-boot-starter-jdbc 或者 spring-boot-starter-data-jpa 。不过为了保持文章聚焦,先不引入数据库相关的东西,用内存用户测试,等把流程跑通了再接数据库。

3.2 最简配置与认证逻辑实现

新建一个Configuration类,我给它起名叫 SecurityConfiguration 。夫妻先配置一个 SecurityFilterChain 的Bean,这是新版的核心。

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.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/home", "/css/**", "/js/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").hasRole("USER")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
            );
        return http.build();
    }
}

注意这里我用 /admin/** 和 /user/** 做了两个不同的权限层级,一个需要ADMIN角色,一个需要USER角色。这样配置之后,所有未登录用户访问受保护资源时会被引导到 /login 页面。这个页面需要你自己准备。顺便说一句, requestMatchers 方法可以接收多个路径参数,也可以接收 AntPathRequestMatcher 对象,灵活度很高。

接着我们需要声明用户信息。在没接数据库的时候,用 InMemoryUserDetailsManager 来定义两个测试账号是最方便的。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;

@Configuration
public class UserConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails admin = User.withUsername("admin")
                .password("{noop}admin123")
                .roles("ADMIN", "USER")
                .build();
        UserDetails user = User.withUsername("user")
                .password("{noop}user123")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(admin, user);
    }
}

这里有一个非常关键的细节,我先说一下,后面还会反复提到: {noop} 前缀表示这个密码是明文存储,没有任何加密。Spring Security 5之后默认要求密码必须有某种编码格式,如果你直接在 .password() 里写纯字符串而不用 {noop} 或者没给PasswordEncoder,启动时会报错提示"No PasswordEncoder mapped for the id null"。生产环境当然不能这么干,这里只是为了先把流程跑通。

3.3 登录页面与Controller配置

接下来我们加一个简单的Controller来验证效果,同时配置登录页。先用Thymeleaf模板引擎来渲染页面。

加入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Controller代码:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class PageController {

    @GetMapping("/")
    public String home() {
        return "home";
    }

    @GetMapping("/admin")
    public String admin() {
        return "admin";
    }

    @GetMapping("/user")
    public String user() {
        return "user";
    }

    @GetMapping("/login")
    public String login() {
        return "login";
    }
}

在 src/main/resources/templates/ 目录下建 home.html 、 admin.html 、 user.html 和 login.html 四个页面。login页面里放一个表单,注意表单的提交地址和字段名称必须跟Spring Security默认约定一致:提交地址是 /login ,用户名和密码字段名分别是 username 和 password 。如果字段名不同,你需要自定义认证参数,我建议先按默认来。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>登录</title>
</head>
<body>
<h1>登录</h1>
<form action="/login" method="post">
    <div>
        <label>用户名</label>
        <input type="text" name="username"/>
    </div>
    <div>
        <label>密码</label>
        <input type="password" name="password"/>
    </div>
    <button type="submit">登录</button>
</form>
</body>
</html>

一个很容易让新手困惑的点是:我明明没有写处理 /login POST请求的Controller方法,为什么这个表单提交能生效?答案就是Spring Security内部的登录过滤器自动处理了这个地址的POST请求。当用户提交表单时,过滤器拿到用户名和密码,调用 AuthenticationManager 认证,认证成功后再把用户信息写入SecurityContext,然后重定向到 defaultSuccessUrl 指定的地址。这就是框架的魅力——很多逻辑你不需要写,但要理解它的存在。

做完这些,启动项目,访问 http://localhost:8080/ 可以看到公开的home页面。访问 /admin 时会被重定向到登录页,因为该路径需要ADMIN角色。登录成功之后,跳回原来想访问的页面。整个过程就是一个完整的认证加授权闭环。

4. 实战难点:密码加密与用户权限数据加载

4.1 PasswordEncoder的正确用法

刚才演示的时候为了快速跑通流程用了 {noop} 明文密码。实际项目里密码绝不可能这么存,数据库泄露一次就全完了。Spring Security提供了一个 PasswordEncoder 接口,业内最常用的是 BCryptPasswordEncoder 。BCrypt算法加盐且计算耗时可控,抵抗暴力破解的能力很强。

在Spring Security 6中,你可以直接声明一个 PasswordEncoder 的Bean。注意,一旦你声明了它,之前 UserDetailsService 里的密码就必须用它来编码。

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

在初始化测试用户时,可以改成这样。不过为了后面的实操,我还是手动编码一下,直接把用户数据放到里面。

UserDetails admin = User.withUsername("admin")
        .password(passwordEncoder().encode("admin123"))
        .roles("ADMIN", "USER")
        .build();

如果你使用 UserDetailsService 从数据库查询用户,查出来的密码也一定是数据库中存储的BCrypt密文。登录时,Spring Security会自动调用 passwordEncoder.matches(rawPassword, encodedPassword) 来做校验,你不需要在Service层手动比对密码。这一点也很重要,如果你自己写LoginService又调了一次 matches ,可能会因为密码已经被比对过了导致两套逻辑冲突。

4.2 自定义UserDetailsService的接入方式

内存用户只适合演示,真实项目里都是从数据库读用户。我们定义一个实现 UserDetailsService 接口的Service,重写 loadUserByUsername 方法。

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class DatabaseUserDetailsService implements UserDetailsService {

    // 假设这里注入了UserMapper或者UserRepository
    private final UserRepository userRepository;

    public DatabaseUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .roles(user.getRoles().split(","))
                .build();
    }
}

Spring Security在登录时发现容器里有一个 UserDetailsService 的实现类,就会自动用这个Service来加载用户信息,链接数据库直接验证用户身份,不再需要你手动调用。这也是为什么很多人配置完数据库用户表之后,登录逻辑一句代码都不用改就能生效的原因。

这里容易出现的一个问题是权限设计。很多人把权限和角色混为一谈,实际上Spring Security里角色(Role)是特殊的权限,角色名称自动带有 ROLE_ 前缀,在代码中使用时写 hasRole("ADMIN") 而不是 hasAuthority("ROLE_ADMIN") 。如果你数据库中存的是不带前缀的权限标识,可以用 hasAuthority 等方法做权限级别的控制。

5. 前后端分离场景下的JSON登录与鉴权配置

5.1 禁用表单登录,改用JSON登录接口

现在很多项目是前后端分离的,前端登录页面是Vue或React,后端只提供JSON接口。这种情况下默认的formLogin默认返回HTML重定向就不合适了,需要配置JSON登录。

做法主要有两种:一是写一个登录Controller接收JSON体,手动调用 AuthenticationManager 来完成认证;二是自定义登录成功和失败处理器,让Spring Security内部的过滤器返回JSON数据。我倾向于用第一种,逻辑更直观,也更好排查问题。

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;

    public AuthController(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody LoginRequest request) {
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                request.getUsername(), request.getPassword());
        Authentication authentication = authenticationManager.authenticate(token);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        Map<String, Object> result = new HashMap<>();
        result.put("code", 0);
        result.put("message", "登录成功");
        result.put("username", authentication.getName());
        return result;
    }
}

这个方案的优点是简单直白,登录成功之后认证信息就在当前会话中生效了,配合基于Session的认证机制完全够用。缺点是没有充分发挥Spring Security过滤器链已经内置好的"登录即处理"的优势,需要自己写Controller层的逻辑。

如果你希望接口只接受JSON请求,拒绝表单提交,可以在 HttpSecurity 里关闭formLogin:

http.formLogin(form -> form.disable());

同时关闭CSRF,因为前后端分离项目如果不用Session存储Token,而是用JWT之类的无状态令牌,那么CSRF攻击的场景就会变化,这种情况下需要关闭默认开启的CSRF防御,不然POST请求会被403拦截:

http.csrf(csrf -> csrf.disable());

5.2 JWT无状态鉴权的快速集成思路

如果项目要做成纯无状态接口服务,用JWT来替代Session是最常见的方案。JWT本质上就是把用户信息和过期时间用签名算法加密成一个字符串,服务端不保存会话状态,每次请求时前端把Token放在请求头 Authorization: Bearer xxx 里,服务端解密验证Token有效之后,直接认定请求者的身份。

在Spring Security里接入JWT,标准套路是写一个过滤器继承 OncePerRequestFilter ,在这个过滤器里解析请求头中的JWT,如果Token有效就把用户的认证信息设置到 SecurityContext 中。然后在过滤器链上把这个过滤器放在 UsernamePasswordAuthenticationFilter 之前。

这里有个小细节是登录成功之后你自己生成Token,千万不要在过滤器里既解析Token又校验密码,那就是职责混乱了。我的建议是新建一个 JwtAuthenticationFilter ,并且配置它在前。JWT的解析如果失败直接放行,让后面的过滤器决定用户到底是匿名还是被拦截。

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final String secretKey;

    public JwtAuthenticationFilter(String secretKey) {
        this.secretKey = secretKey;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            try {
                Claims claims = Jwts.parser()
                        .setSigningKey(secretKey)
                        .parseClaimsJws(token)
                        .getBody();
                String username = claims.getSubject();
                List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_ADMIN"));
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(username, null, authorities);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } catch (Exception ignored) {
                // Token无效或过期,SecurityContext保持未认证状态即可
            }
        }
        filterChain.doFilter(request, response);
    }
}

然后在配置类里把过滤器塞进去就可以了:

http.addFilterBefore(new JwtAuthenticationFilter(secretKey), UsernamePasswordAuthenticationFilter.class);

5.3 会话并发控制与记住我功能

如果你还在用Session方案,有个比较隐蔽但是很实用的配置是会话并发控制。默认情况下Spring Security允许多个端同时登录同一个账号。有些业务场景不允许,比如同一个账号只能在一处登录,这时候配置一下 sessionManagement 就行。

http.sessionManagement(session -> session
    .maximumSessions(1)
    .expiredUrl("/login?expired=true")
);

配置了 maximumSessions(1) 之后,用同一个账号在新设备登录时,旧设备访问任何受保护资源都会被弹出登录页面或者被踢下线。

记住我功能也是个小细节。注册了这个功能之后,用户勾选"记住我",关闭浏览器重新打开之后登录状态还在。它的实现原理是发送一个加密过的Cookie,里面包含用户名和过期时间,下一次请求访问时,过滤器从这个Cookie恢复出用户信息。配置方式很简单:

http.rememberMe(remember -> remember
    .key("uniqueAndSecretKey")
    .tokenValiditySeconds(86400 * 7)
);

一个注意点是 key 必须持久化稳定,如果应用重启后key变了,之前发的所有记住我Cookie都会失效,用户全都需要重新登录。

6. 真实踩坑:Spring Boot 3迁移中的常见问题

6.1 antMatchers消失与路径匹配差异

这个坑是目前最常见的一个。网上老教程里是的 authorizeRequests().antMatchers("/admin/**") ,在Spring Boot 3里把项目一启动就直接报错,或者代码里疯狂划黄线提示过时。

解决方案是把 authorizeRequests() 改成 authorizeHttpRequests() ,把 antMatchers() 改成 requestMatchers() 。代码风格建议用新版Lambda写法。

还有一个更隐蔽的地方是路径匹配规则的细微差别。Spring Security 6默认的路径解析器是 PathPattern ,它跟老的AntPathMatcher在匹配规则上有区别。比如 /admin/* 在老风格里匹配一层路径,新的PathPattern也差不多是这个语义,但叠加通配符时建议实际测试一下,最好把配置里的路径统一到 /** 和 /* 这两种常见写法上,能避开绝大多数差异问题。

6.2 CSRF导致的403问题

前后端分离项目中,排查问题花费时间最多的可能就是请求被无缘无故403了。明明用户名密码都正确,接口就是调不通。这个问题十有八九是CSRF防护在拦截。

Spring Security 6默认开启CSRF防护, POST 、 PUT 、 DELETE 这些有副作用的请求都会被要求携带CSRF Token。你用表单开发的时候,Spring Security会自动在模板渲染时把CSRF Token放到请求里,Thymeleaf不用你额外处理。但如果你用Postman测接口、用Vue发Ajax请求,Token就不存在了,请求直接403。

如果项目仍然走Session认证且没有同源策略限制,那我建议保留CSRF防御,然后在请求头里带上Token。如果项目是无状态JWT方案,直接关闭CSRF也说得过去,因为JWT放在消息头里天然防护了CSRF攻击,接外部请求时不依赖浏览器自动携带Cookie。注意权衡。

6.3 迁移到Spring Boot 3后登录验证码失效

还有一个我从多个项目里见过的现象:升级到Spring Boot 3之后,原本能正常显示的验证码图片登录流程突然进不去登录页,或者验证码服务接口直接返回401。原因是在Spring Security 6中,默认未认证的请求进入登录页,动态生成的验证码图片接口往往位于放行列表之外,导致请求被打回。只要在过滤链配置里把验证码生成接口放进 permitAll() 列表即可。

/captcha , /code/image 这些路径全部放行,认证之后接口访问就不受影响了。

6.4 启动报错"PasswordEncoder must not be null"或循环依赖

如果启动时报 PasswordEncoder 相关错误,多半是 UserDetailsService 加载用户时密码没有指定编码器。检查两个点:容器里有没有声明 PasswordEncoder 的Bean;用户密码字段是否有 {noop} 前缀或是否用BCrypt编码过。

如果报循环依赖错误,尤其是自动配置了数据源和SecurityConfig之间互相依赖,一般检查一下是否在 UserDetailsService 的Bean里引用了当前配置类中的方法,导致两个Bean互相依赖。建议把 PasswordEncoder 单独放在一个配置类里,避免和安全配置类耦合。

7. 项目小结与个人经验

7.1 为什么推荐使用新写法

用Spring Boot 3写安全配置,刚上手的新发明最好不要再去网上翻老教程。官方文档现在已经是组件注册的写法,而且 WebSecurityConfigurerAdapter 这种继承方式本身就是对框架的一种"侵入式"使用,它让你覆盖父类方法,父类方法的逻辑对使用者不透明;而新的 SecurityFilterChain Bean方式更符合"组合优于继承"的思想,配置就是声明,扩展就是往链路里加过滤器,排查问题时链路也更清晰。

7.2 快速上手路径建议

如果你是想快速在项目中用起来,我建议按这个顺序来:先把最简的 SecurityFilterChain 配置跑通页面表单登录;然后替换成数据库用户服务和BCrypt密码;接着给REST接口加JSON认证;最后根据需要再接JWT或者引入OAuth2。每一步验证一次,不要试图一次把全部功能都堆上去,不然出了问题排查起来会无从下手。

7.3 从0到1的最终参考配置

我最后把所有内容浓缩一下,给出一个可以继续扩展的最终参考配置。

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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**", "/", "/home", "/css/**", "/js/**", "/captcha/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").hasRole("USER")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
            )
            .sessionManagement(session -> session
                .maximumSessions(1)
                .expiredUrl("/login?expired=true")
            );
        return http.build();
    }
}

我个人在实际操作中的体会是,Spring Security入门的第一道坎不是API的用法本身,而是心态。它的过滤器链确实抽象,配置项也很多,但只要你抓住一个核心链路——请求进来经过过滤器,过滤器里调用认证管理器,认证管理器加载用户信息并判断密码是否匹配,匹配之后把结果塞进安全上下文,后续请求就能拿到用户身份——那么不管后面的JWT、OAuth2、方法级权限怎么变,你就都不会乱了。先把这条链用最小配置跑通,剩下的都是往链上挂东西。

公司财务管理系统(源码+数据库+论文+答辩ppt一整套齐全)java开发springboot框架javaweb,可做计算机毕业设计或课程设计 本系统分为员工、管理员两个用户角色。 员工功能: 1. 注册登录:填写员工账号、密码、姓名、部门职位等信息完成注册,账号密码登录系统。 2. 请假管理:填写请假标题、原因、时间、类型提交请假申请,查看本人请假记录与审核回复。 3. 考勤查看:查看个人考勤信息,查看出勤、请假、迟到、早退、缺勤统计数据。 4. 薪资查询:查看每月薪资详情,查看底薪、绩效、奖金、扣款以及实发工资等信息。 5. 薪资异议申请:对薪资有疑问时提交薪资异议申请,查看申请审核状态与管理员回复。 6. 个人中心:修改个人头像、手机号等资料,修改登录密码。 管理员功能: 1. 员工管理:查询、新增、编辑、删除员工账号,维护员工部门、职位等基础信息。 2. 请假信息管理:查看全部员工请假申请,搜索筛选请假记录,审核请假申请并填写回复。 3. 考勤信息管理:录入、编辑、删除员工考勤数据,按年月统计员工出勤相关信息。 4. 薪资信息管理:录入员工每月薪资数据,维护底薪、绩效、奖金、扣款等薪资记录。 5. 薪资异议处理:查看员工提交的薪资异议申请,审核申请内容,填写处理回复。 6. 系统管理:维护首页轮播图,发布系统公告,查看系统操作日志。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值