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、方法级权限怎么变,你就都不会乱了。先把这条链用最小配置跑通,剩下的都是往链上挂东西。

399

被折叠的 条评论
为什么被折叠?



