技术简介
Spring Security 是一个帮你管理"谁能访问什么"的框架。举个现实的例子:你进公司大门需要刷工卡(认证 ------证明你是谁),进了门之后,只有特定楼层你才能进(授权------你能干什么)。Spring Security 就是做这两件事的:先确认用户身份,再根据身份决定允许访问哪些功能。它深度集成在 Spring 生态中,你只要加个依赖、写少量配置,就能给应用加上登录、权限控制这些能力,不用从零造轮子。
快速开始(5 分钟上手)
最简示例
下面是一个可完整运行的 Spring Boot + Spring Security 项目。你会看到:
- 访问受保护的
/hello接口,自动跳转到登录页 - 输入用户名
user、密码password后,返回 "Hello, User!" - 我们自定义了内存用户,避免随机密码带来的麻烦
项目结构 (新建一个 Maven 项目,按以下结构放置文件)
css
demo/
├── pom.xml
└── src/main/java/com/example/demo/
├── DemoApplication.java
├── SecurityConfig.java
└── HelloController.java
pom.xml (包含 Spring Boot 父工程、Web 和 Security 起步依赖)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
DemoApplication.java (Spring Boot 启动类)
java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
SecurityConfig.java (安全配置:定义内存用户,放行登录页,要求其他请求需认证)
java
package com.example.demo;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public InMemoryUserDetailsManager userDetailsManager() {
UserDetails user = User.withDefaultPasswordEncoder() // 仅演示用,生产必须用加密器
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated() // 所有请求都要认证
)
.formLogin(withDefaults()); // 使用默认表单登录
return http.build();
}
}
HelloController.java (受保护接口)
java
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, User!";
}
}
安装与运行
-
确保环境 :JDK 17+ 和 Maven 3.6+ 已安装。命令行执行
java -version和mvn -v能看到版本号。 -
创建项目目录,按上面结构把四个文件放进去。
-
在项目根目录(pom.xml 所在目录)打开终端,运行 :
bashmvn spring-boot:runMaven 会下载依赖并启动应用,看到
Started DemoApplication in ...就表示启动成功。
看到效果
- 浏览器访问
http://localhost:8080/hello - 页面会自动跳转到 Spring Security 默认登录页
/login - 输入用户名
user、密码password,点击登录 - 页面显示
Hello, User!
恭喜你,已经跑通了第一个带认证的应用。
必会基础
1. 快速关闭默认安全
一句话定义
有时你只想先跑通业务,不想立刻处理登录,可以通过配置关闭或放行所有请求。
代码示例
java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // 所有请求无需认证
)
.csrf(csrf -> csrf.disable()) // 同时关闭 CSRF,方便 POST 测试
.formLogin(form -> form.disable()); // 关闭表单登录
return http.build();
}
使用判断
- 典型场景:开发初期、纯 API 服务不想启用登录。
- 边界提醒 :千万别在生产环境对全部请求
permitAll(),否则等于裸奔。记住只开放真正需要公开的路径。
2. 定义内存用户(快速演示)
一句话定义
不连数据库,直接在代码里写死几个账号和角色,用于快速演示或测试。
代码示例
java
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("1234")
.roles("ADMIN")
.build();
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("1234")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
使用判断
- 典型场景:原型验证、编写安全规则时的临时用户库。
- 边界提醒 :
withDefaultPasswordEncoder()每次启动都会生成不一样的密码前缀,仅适合示例,千万不要 用于生产。生产环境必须用PasswordEncoder的加密实现,如BCryptPasswordEncoder。
3. 密码加密器(生产必备)
一句话定义
把用户的明文密码通过不可逆算法(如 BCrypt)处理后再存储,即使数据库泄露,攻击者也难以还原密码。
配置示例
java
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails user = User.builder()
.username("user")
.password(encoder.encode("123456")) // 密文存储
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
使用判断
- 典型场景 :任何正式的存储用户场景,必须使用
BCryptPasswordEncoder(或更高强度的 Argon2)。 - 边界提醒 :登录时 Spring Security 会自动用配置的
PasswordEncoder比对密码,你只需确保用户创建时用的是同一个 encoder 加密就行。如果用明文存储,认证会失败。
4. 基于角色的授权
一句话定义
限制某些 URL 只有拥有特定角色(如 ADMIN)的用户才能访问。
代码示例
java
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN") // 只有 ADMIN 角色能访问 /admin 下的路径
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") // USER 或 ADMIN 都能访问
.anyRequest().authenticated() // 其他请求至少需要登录
);
使用判断
- 典型场景:后台管理系统,普通用户看不了管理功能。
- 边界提醒 :
hasRole("ADMIN")会自动检查ROLE_ADMIN权限,所以给用户加角色时用roles("ADMIN")即可,不用写前缀。如果使用hasAuthority()则不会加前缀,两者不要搞混。
核心原理
1. 过滤器链(Filter Chain)
一句话定义
Spring Security 就是靠一组过滤器工作的,每个过滤器负责一个安全检查(如认证、授权、防攻击),请求只有穿过整条链才能到达你的 Controller。
核心机制
- 你可以把过滤器链想象成机场安检通道:先验证身份证(AuthenticationFilter),再查你有没有登机牌(AuthorizationFilter),最后才能登机(你的业务代码)。
SecurityFilterChainBean 就是用来配置这些过滤器的,HttpSecurity的每一个.xxx()背后都添加或调整了一个过滤器。
你必须知道的默认过滤器:
UsernamePasswordAuthenticationFilter------ 处理表单登录BasicAuthenticationFilter------ 处理 HTTP Basic 认证AuthorizationFilter------ 授权决策CsrfFilter------ 防御跨站请求伪造
2. SecurityContextHolder(安全上下文容器)
一句话定义
登录成功后,Spring Security 会把当前用户的信息存到一个全局可访问的"盒子"里,你在应用的任何地方都能拿到这次请求是谁发出的。
获取当前用户示例
java
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName(); // 拿到用户名
原理
默认用 ThreadLocal 存储,确保一个线程(一个请求)里存取的用户信息不会串到其他请求。请求结束后自动清理。
使用判断
- 典型场景:在 Service 层获取当前操作人,记日志、做数据权限等。
- 边界提醒 :不要在
ThreadLocal里放太多对象,且如果开启子线程,子线程拿不到主线程的安全上下文,需要额外配置(如DelegatingSecurityContextRunnable)。
实战模式
1. 自定义登录页
一句话定义
不用 Spring Security 默认的简陋登录页面,换成你自己设计的 HTML 登录页。
代码示例
java
http.formLogin(form -> form
.loginPage("/login.html") // 自定义登录页面地址
.loginProcessingUrl("/doLogin") // 表单提交地址,Spring Security 会接管认证
.defaultSuccessUrl("/index", true) // 登录成功后去的页面
.permitAll() // 登录页必须放行
);
对应你要准备一个 /static/login.html 页面,里面表单的 action 指向 /doLogin,用户名输入框 name 为 username,密码框 name 为 password。
使用判断
- 典型场景:需要统一品牌风格、加验证码等定制化登录界面。
- 边界提醒 :
loginPage指定的路径要和 controller 或静态资源路径一致,且一定记得对登录页 URL 调用permitAll(),否则会无限重定向。
2. 无状态 JWT 认证(API 项目主流方案)
一句话定义
服务器不保存用户登录状态(session),而是给客户端发一个签名令牌(JWT),后续请求带着令牌,服务器验签即可识别用户。
核心步骤
- 登录成功后生成 JWT 返回给前端
- 前端后续请求头带
Authorization: Bearer <token> - 自定义一个过滤器,从请求头提取 token,验证并设置安全上下文
- 配置
SecurityFilterChain为无状态,禁用 CSRF
过滤器骨架
java
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
// 解析 token 获得用户名/权限,构造 Authentication
// SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
安全配置
java
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
);
使用判断
- 典型场景:前后端分离项目、移动 App 接口。
- 边界提醒:JWT 一旦签发,服务端无法主动让它失效(除非引入黑名单机制),所以过期时间设短(如 15 分钟),并配合 refresh token 来换取新 token。
3. 方法级安全(@PreAuthorize)
一句话定义
不仅可以在 URL 层面控制,还能在 Service 方法上通过注解精准声明权限要求。
启用注解 (在配置类上加 @EnableMethodSecurity)
java
@Configuration
@EnableMethodSecurity
public class SecurityConfig { ... }
使用示例
java
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long id) { ... }
@PreAuthorize("hasAuthority('ORDER_READ') or #username == authentication.name")
public Order getOrderByUser(String username) { ... }
}
使用判断
- 典型场景:细粒度权限,比如"只有订单所属人或管理员才能查看"。
- 边界提醒:注解中支持 SpEL 表达式,可访问方法参数、返回值等,非常灵活,但表达式写得太复杂会降低可读性,建议控制长度。
进阶与避坑
1. CSRF 保护,何时关闭
核心概念
CSRF(跨站请求伪造)防护是 Spring Security 默认开启的,它会要求状态改变(POST/PUT/DELETE)请求带上一个特殊 token,以防恶意网站偷偷利用你的身份发起请求。
避坑
- 如果你做的是纯粹 REST API(前后端分离,使用 JWT,无 Cookie 认证) ,可以安全关闭:
http.csrf(csrf -> csrf.disable())。 - 如果你用表单登录 + Cookie/Session ,千万不要关,否则可能有 CSRF 攻击风险。关闭后若碰到 POST 请求 403,你首先该查的是是不是没带 CSRF token,而非直接关掉。
2. CORS 配置不当引发跨域问题
一句话定义
当前端地址和后端接口不同源(协议、域名、端口任一不同),浏览器会拦请求,需要后端配合声明允许哪些来源。
正确配置
java
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000")); // 前端的地址
config.setAllowedMethods(List.of("GET","POST"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
然后在 HttpSecurity 中启用:
java
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
边界提醒
不要同时使用 allowedOrigins("*") 和 allowCredentials(true),浏览器会直接拒绝。而且 Spring Security 的 CORS 配置需要配合 cors() 方法启用,单独写 CorsFilter Bean 可能不生效。
3. 忽略静态资源的安全拦截
一句话定义
你的 CSS、JS、图片等静态文件应该对所有用户开放,否则登录页可能连样式都加载不出来。
推荐写法
java
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/static/**", "/favicon.ico");
}
或者 在 authorizeHttpRequests 中放行:
java
auth.requestMatchers("/css/**", "/js/**", "/images/**").permitAll();
注意 :web.ignoring() 会完全跳过 Spring Security 过滤器链,性能更好,但请求里拿不到安全上下文。一般对纯静态资源用 ignoring,对需要记日志等需求用 permitAll。
4. 一次登录失败反复重试导致账号锁定
一句话定义
为防止暴力破解,应该限制短时间内登录失败次数,Spring Security 提供事件机制可轻松实现。
简单思路
实现 AuthenticationFailureHandler,在 onAuthenticationFailure 方法中记录某个用户的失败次数,达到阈值(如 5 次)就锁定一段时间。Spring Security 没有内置此功能,但留了清晰扩展点,网上有很多基于缓存(如 Redis)的实现,直接套用即可。
下一步
- 官方网站 :spring.io/projects/sp...
详细的参考文档、API 说明、样例项目都可以在这里找到。建议重点看Reference文档的Servlet Applications部分。 - 遇到报错的定位方式 :
- 先把日志级别调为 DEBUG:在
application.properties里加logging.level.org.springframework.security=DEBUG,重启后能看到过滤器链的详细决策过程。 - 常见"403 forbidden"先检查是否权限表达式写错;检查 CSRF 是否该带没带;检查角色前缀
ROLE_是否匹配正确。 - 登录后无限重定向,通常是登录页本身被保护且没放行,检查
loginPage是否permitAll()。
- 先把日志级别调为 DEBUG:在