首页 / Spring Boot 入门教程 / 安全配置进阶

Spring Boot 入门教程

安全配置进阶

本教程共 48 篇 · 第 35 篇 · 更新于 2026-08-13 · 约 5 分钟阅读

Spring BootSecurityFilterChainCSRFCORS权限表达式EndpointRequest多过滤器链

本节目标:掌握 SecurityFilterChain 的多链与授权表达式,理解 CSRF 和 CORS 的取舍,学会给 Actuator 端点配置安全规则。

一条请求的安检之旅

SecurityFilterChain 本质是一条过滤器链。请求进来,按顺序过安检:

  1. 认证过滤器:解析凭证,建立身份
  2. 授权检查:比对身份和访问规则
  3. 异常处理:认证失败跳登录,授权失败返回 403
  4. 最后才到你的控制器

HttpSecurity 就是配置这条链的 DSL。每调一个方法,就多装一个「安检设备」。

上一节的配置能跑,但真实项目往往同时有页面和 API,规则要拆开。下面逐个进阶。

多链配置:不同路径,不同规则

一个应用可能同时有 /api/**(无状态 API)和页面(表单登录)。用多个 SecurityFilterChain Bean 加 securityMatcher 分流:

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class MultiChainConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()))
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable());
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(withDefaults());
        return http.build();
    }
}

@Order 数字小的先匹配。securityMatcher 不匹配的链直接跳过,落到下一条。

Warning

链的顺序写反是高频事故。API 链不写 securityMatcher 时,它会吞掉所有请求,页面链永远不生效。

权限表达式

authorizeHttpRequests 里支持 SpEL 表达式:

表达式含义
permitAll()完全放行
authenticated()登录即可
hasRole("ADMIN")有 ROLE_ADMIN 角色
hasAuthority("user:read")有指定权限
hasAnyRole("ADMIN", "USER")任一角色
access("hasRole('ADMIN') and ipAddress('10.0.0.0/8')")组合条件

hasRolehasAuthority 的区别:hasRole("ADMIN") 自动补 ROLE_ 前缀,hasAuthority("ADMIN") 按原样匹配。

两种风格对应两种权限模型:

  • 角色模型:粗粒度,ROLE_ADMINROLE_USER,适合中小项目
  • 权限模型:细粒度,user:readorder:delete,适合权限点多的系统

方法级同样能用表达式:

@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public void update(Long id) { }

#id 引用方法参数,authentication.principal 取当前用户。这是面试高频考点。

CSRF:表单的守护,API 的包袱

CSRF(跨站请求伪造):你登录着银行,黑客网页偷偷发一个转账 POST。Cookie 自动带上,银行以为是你。

Spring Security 默认开启 CSRF 防护:表单里注入隐藏 token,服务端校验。浏览器表单场景必须保留。

纯 API 场景没有 Cookie,CSRF 失去意义,可以关掉:

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

判断标准很简单:客户端是不是浏览器?浏览器页面里发起的请求,CSRF 防护别关。

Warning

只对「非浏览器客户端」的服务关 CSRF。开着 CSRF 时,用 POST 调 actuator 的 shutdown、loggers 端点会收到 403,这是防护在起作用,不是故障。

CORS:跨域放行

前后端分离时,浏览器跨域请求需要服务端声明允许。Security 的 CORS 配置:

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://myapp.example.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

然后在 HttpSecurity 上启用:

http.cors(withDefaults());

浏览器发复杂请求前会先发一个 OPTIONS 预检,问服务端「允不允许这么跨」。上面的配置回答了这个问题,预检通过才发正式请求。

Note

allowedOrigins 别写 * 还开 allowCredentials(true),浏览器会拒绝这种组合。精确列出前端域名更安全。
也可以直接在控制器或 MVC 配置里写 @CrossOrigin,但全局规则统一放 Security 更清晰。

与 Actuator 集成

有了自定义 SecurityFilterChain,Actuator 端点也归你管。用 EndpointRequest 精确匹配端点路径:

package com.example.demo.config;

import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
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.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class ActuatorSecurityConfig {

    @Bean
    public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth.anyRequest().hasRole("ENDPOINT_ADMIN"))
            .httpBasic(withDefaults());
        return http.build();
    }
}

这样 /actuator/** 全部要求 ENDPOINT_ADMIN 角色。静态资源用 PathRequest.toStaticResources().atCommonLocations() 放行:

http.authorizeHttpRequests(auth -> auth
    .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
    .anyRequest().authenticated()
);

EndpointRequest.to("health") 只匹配指定端点,toAnyEndpoint() 匹配全部。端点路径换了 base-path 也能正确匹配,因为它读的是 management.endpoints.web.base-path 配置。

Note

只要存在任何 SecurityFilterChain Bean,Spring Boot 对 Actuator 的默认安全规则就退出,端点安全完全由你的配置决定。
未配置时,Boot 4.x 默认只暴露 /health,其余端点不可见。

记住我:免登录的折中

会话一断,用户就要重新登录。rememberMe 是折中方案:登录成功后签发一个签名 Cookie,有效期内重新访问自动恢复登录态。

http.rememberMe(withDefaults());

登录表单加一个勾选框:

<input type="checkbox" name="remember-me"/>

安全提醒:记住我 Cookie 相当于长期凭证,泄露了就能冒充登录。敏感系统建议不开,或缩短有效期。

自定义认证逻辑

想加验证码校验、对接第三方账号体系,实现 AuthenticationProvider

package com.example.demo.security;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class DemoAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) {
        String name = authentication.getName();
        String password = authentication.getCredentials().toString();
        if ("admin".equals(name) && "admin123".equals(password)) {
            return new UsernamePasswordAuthenticationToken(name, password,
                    List.of(new SimpleGrantedAuthority("ROLE_ADMIN")));
        }
        return null; // 交给下一个 Provider 处理
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
    }
}

认证失败返回 null 表示「我不处理」,Spring Security 会尝试下一个 Provider。定义了 AuthenticationProviderAuthenticationManager Bean 后,默认的 UserDetailsService 自动配置退出。

安全响应头

SecurityFilterChain 还会自动加响应头:X-Content-Type-OptionsCache-Control、HSTS(HTTPS 时)等。

frameOptions 默认 DENY,防止点击劫持。业务上确实需要被 iframe 嵌入时再放开:

http.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));

这些头是免费的纵深防御,别随手关。只有明确冲突(比如要嵌入 iframe)才调整。

初学者常栽的坑

  • 多链顺序反了:API 链没写 securityMatcher,吞掉所有请求
  • hasRole 写权限名:hasRole("user:read") 会补成 ROLE_user:read,永远匹配不上
  • CSRF 忘了关:纯 API 项目表单登录页能进,POST 接口全是 403
  • CORS 配置了但接口还是跨域报错:检查 .cors(withDefaults()) 有没有写,两处缺一不可
  • 自定义 Provider 返回 null 却以为认证失败:null 是「不处理」,要抛 BadCredentialsException 才是明确拒绝

本节小结

  • SecurityFilterChain 是过滤器链,@Order + securityMatcher 做多链分流
  • 权限表达式区分 hasRolehasAuthority,可组合 access(...)
  • CSRF 默认开启,浏览器场景保留,纯 API 可关
  • CORS 用 CorsConfigurationSource Bean + .cors(withDefaults())
  • Actuator 端点用 EndpointRequest 纳入安全规则