Spring Security

技术简介

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!";
    }
}

安装与运行

  1. 确保环境 :JDK 17+ 和 Maven 3.6+ 已安装。命令行执行 java -versionmvn -v 能看到版本号。

  2. 创建项目目录,按上面结构把四个文件放进去。

  3. 在项目根目录(pom.xml 所在目录)打开终端,运行

    bash 复制代码
    mvn spring-boot:run

    Maven 会下载依赖并启动应用,看到 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),最后才能登机(你的业务代码)。
  • SecurityFilterChain Bean 就是用来配置这些过滤器的,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),后续请求带着令牌,服务器验签即可识别用户。

核心步骤

  1. 登录成功后生成 JWT 返回给前端
  2. 前端后续请求头带 Authorization: Bearer <token>
  3. 自定义一个过滤器,从请求头提取 token,验证并设置安全上下文
  4. 配置 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 部分。
  • 遇到报错的定位方式
    1. 先把日志级别调为 DEBUG:在 application.properties 里加 logging.level.org.springframework.security=DEBUG,重启后能看到过滤器链的详细决策过程。
    2. 常见"403 forbidden"先检查是否权限表达式写错;检查 CSRF 是否该带没带;检查角色前缀 ROLE_ 是否匹配正确。
    3. 登录后无限重定向,通常是登录页本身被保护且没放行,检查 loginPage 是否 permitAll()
相关推荐
名字还没想好☜1 小时前
Spring Boot 全局异常处理:@ControllerAdvice 实战
java·spring boot·后端·spring·异常处理
还是鼠鼠1 小时前
AI掘金头条新闻系统 (Toutiao News)-缓存相关推荐新闻
后端·python·mysql·fastapi·web
Java内核笔记1 小时前
Spring Security 源码解析(八)方法安全授权与自定义扩展:@EnableMethodSecurity、AOP、SpEL 全链路
java·后端
逝水无殇2 小时前
C# 枚举(Enum)详解
开发语言·后端·c#
树獭叔叔2 小时前
从 Search 到 Extract:AI Agent 网页工具架构拆解
后端·agent·ai编程
梨子同志2 小时前
Nacos
后端
宠友信息2 小时前
Redis 内容社区源码架构优化与即时通讯数据一致性处理
spring boot·后端·websocket·mysql·uni-app
Csvn3 小时前
Python 开发技巧 · 高级装饰器 —— 从基础到工业级实战
后端
码云之上3 小时前
项目团队从 5 人扩到 15 人,我写了个 CLI 让 IDE 共享 AI 规则
前端·人工智能·后端