概述

Spring Security 有三大功能可以概括为:

  • 认证(Authentication):确认用户身份,确保访问系统的主体是真实可信的。常用方式包括用户名/密码、Token、OAuth2 等。
  • 授权(Authorization):控制用户对资源或操作的访问权限,可以基于 URL、方法或角色进行精细化控制,例如使用注解 @PreAuthorize 或配置访问规则。
  • 防护(Protection):针对常见安全威胁提供防御措施,包括 CSRF 防护、防止 Session 固定攻击、点击劫持(Clickjacking)以及密码加密存储等,保障应用的整体安全性。

整个请求在进入应用之前都会沿着过滤器链依次经过各类 Filter,认证、授权和防护都在链内按顺序处理;只有当请求通过所有检查,才会到达应用的核心 Servlet。

核心逻辑

  • 认证产生认证对象
  • 授权访问认证对象进行权限检查

过滤器链运行流程

进入链

所有请求先到达 FilterChainProxy,这是 Spring Security 在 Servlet 容器里注册的唯一安全入口。它根据请求路径,选择合适的 SecurityFilterChain

链的结构

SecurityFilterChain 的内部其实就是一组按照顺序排列的 Filter。这些 Filter 都实现了 javax.servlet.Filter 接口,并遵循统一的调用规范:doFilter(ServletRequest request, ServletResponse response, FilterChain chain)。在执行过程中,请求会依次经过链上的各个 Filter,每一个 Filter 都可能对请求进行处理,例如:

  • 处理请求(比如认证、鉴权、上下文维护)
  • 决定是否继续往下走(调用 chain.doFilter)
  • 或者直接中断(返回响应,比如认证失败 401)。

链中节点特性

  • 拦截点:每个 Filter 都是一个检查点。
  • 条件执行:有的 Filter 只处理特定请求(比如 /login 时才进入认证 Filter)。
  • 职责单一:每个 Filter 做一件事(认证、CSRF、异常翻译、权限判断等)。
  • 可短路:一旦某个 Filter 决定返回响应,请求就不会继续向下传。

出链

如果没有被拦截、认证/授权成功,请求会一路穿过所有 Filter,最终进入应用的 Servlet(如 DispatcherServlet → Controller)。响应返回时,也会逆向经过这些 Filter(有些 Filter 会在响应阶段做处理,比如写回 SecurityContext)。

认证

AbstractAuthenticationProcessingFilter

由于都是在链内完成的,涉及认证的主要 Fliter 就是 AbstractAuthenticationProcessingFilter 接口的实现类了。他的实现类有很多,例如:

  • UsernamePasswordAuthenticationFilter :最常见的实现类,用于表单登录
  • OAuth2LoginAuthenticationFilter :处理基于 OAuth2 登录的认证请求

接口

实现类内部的操作包含了认证的全部流程,其主要涉及的组件接口如下:

数据封装:

  • Authentication :承载了认证请求和认证结果的数据。
  • UserDetails :用户的核心信息载体(用户名、密码、权限、账户是否过期/锁定等)。

工具服务组件:

  • AuthenticationManager :认证调度器,接收 Authentication 请求并分派给合适的 AuthenticationProvider
  • AuthenticationProvider :执行具体认证逻辑(如用户名密码校验)。常见实现是 DaoAuthenticationProvider
  • UserDetailsService :按用户名加载用户信息,返回 UserDetails
  • PasswordEncoder :用于密码的加密与匹配校验。
  • AuthenticationSuccessHandler / AuthenticationFailureHandler :登录成功或失败后的处理器(比如跳转页面、返回 JSON)。
  • AuthenticationEntryPoint :未登录时访问受保护资源的入口处理(比如返回 401)。

典型流程

请求进入 Spring Security FilterChain
|
└── AbstractAuthenticationProcessingFilter.doFilter()
    |
    ├── attemptAuthentication(request, response)
    |    |
    |    └── 调用 AuthenticationManager.authenticate(authenticationToken)
    |         |
    |         ├── 遍历所有 AuthenticationProvider
    |         |    |
    |         |    ├── 如果 provider.supports(token)
    |         |    |     └── 调用 provider.authenticate(token)
    |         |    |          |
    |         |    |          ├── AbstractUserDetailsAuthenticationProvider
    |         |    |          |     |
    |         |    |          |     ├── retrieveUser(username, token)
    |         |    |          |     |     └── 调用 UserDetailsService.loadUserByUsername()
    |         |    |          |     |           └── 返回 UserDetails(包含用户名、密码、权限等)
    |         |    |          |     |
    |         |    |          |     ├── additionalAuthenticationChecks(UserDetails, token)
    |         |    |          |     |     └── 比对密码(PasswordEncoder.matches())
    |         |    |          |     |
    |         |    |          |     └── 返回 Authentication(认证成功的对象)
    |         |    |
    |         |    └── 如果认证失败,抛出 AuthenticationException
    |         |
    |         └── 如果所有 provider 都不能认证,抛出 ProviderNotFoundException
    |
    ├── 如果认证成功
    |    ├── 将 Authentication 放入 SecurityContextHolder
    |    └── 调用 successfulAuthentication()
    |         └── 继续执行过滤器链
    |
    └── 如果认证失败
         └── 调用 unsuccessfulAuthentication()
              └── 返回错误响应 (如 401 Unauthorized)

(以 UsernamePasswordAuthenticationFilter 为例)

  1. 接收请求

    • HttpServletRequest 里拿到用户名、密码。
    • 封装成 UsernamePasswordAuthenticationToken(一个实现了 Authentication 的对象,代表“待认证的凭证”)。
  2. 调用认证管理器

    Authentication authResult = this.getAuthenticationManager()
                                    .authenticate(authRequest);
    • 这里会进入 AuthenticationManager,通常是 ProviderManager
  3. Manager → Provider

    • ProviderManager 遍历 AuthenticationProvider,找到能处理 UsernamePasswordAuthenticationToken 的(一般是 DaoAuthenticationProvider)。
  4. Provider → Service + Encoder

    • DaoAuthenticationProvider 调用 UserDetailsService.loadUserByUsername() 查询用户信息。
    • 拿到 UserDetails,再用 PasswordEncoder.matches() 校验密码。
  5. 结果回传

    • 成功 → 返回一个带有用户信息和权限的 Authenticationauthenticated=true)。
    • 失败 → 抛异常,被 Filter 捕获后交给 AuthenticationFailureHandler
  6. Filter 收尾

    • 成功 → 调用 successfulAuthentication(),把认证信息放入 SecurityContextHolder
    • 失败 → 调用 unsuccessfulAuthentication(),返回错误信息。

JWT处理示例

jwt由于其认证等复杂过程都基于jwt的工具函数,通常已经包装完整,因此,如果采用jwt的工具函数,就很难再对整个认证过程进行像以上 Provider 等等的划分,因此,建议直接继承 OncePerRequestFilter ,构建一个基于jwt的过滤器,在过滤器中完成 jwt 的验证,处理,解析等完整流程,并将验证之后的数据存入上下文,达到和原本运行完 UsernamePasswordAuthenticationFilter 之后的一致结果。

示例:

public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtCoreUtil jwtCoreUtil; // 你封装的 JWT 工具类
    private final LocalTokenBlacklistService tokenBlacklistService; // 可选黑名单

    public JwtAuthFilter(JwtCoreUtil jwtCoreUtil, LocalTokenBlacklistService tokenBlacklistService) {
        this.jwtCoreUtil = jwtCoreUtil;
        this.tokenBlacklistService = tokenBlacklistService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws IOException, ServletException {

        String token = request.getHeader("Authorization");
        if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
            chain.doFilter(request, response); // 没有 token 就直接放行,让后续访问控制处理
            return;
        }

        token = token.substring(7); // 去掉 "Bearer " 前缀

        try {
            // 黑名单检查
            if (tokenBlacklistService != null && tokenBlacklistService.isBlacklisted(token)) {
                sendError(response, "Token revoked");
                return;
            }

            Claims claims = jwtCoreUtil.parseToken(token); // 解析 JWT
            validateClaims(claims);

            // 创建 Authentication 并放入 SecurityContext
            Authentication auth = createAuthentication(claims);
            SecurityContextHolder.getContext().setAuthentication(auth);

            chain.doFilter(request, response);

        } catch (ExpiredJwtException ex) {
            sendError(response, "Token expired");
        } catch (JwtException | IllegalArgumentException ex) {
            sendError(response, "Invalid token");
        } catch (Exception ex) {
            sendError(response, ex.getMessage());
        }
    }

    // 验证 JWT 必要字段
    protected void validateClaims(Claims claims) {
        if (claims.getSubject() == null) {
            throw new IllegalArgumentException("Missing subject");
        }
        // 可按需校验 roles / tenantId / email 等
    }

    // 构造 Authentication
    protected Authentication createAuthentication(Claims claims) {
        String username = claims.getSubject();
        List<String> roles = claims.get("roles", List.class);

        List<GrantedAuthority> authorities = Optional.ofNullable(roles)
                .orElse(Collections.emptyList())
                .stream()
                .filter(Objects::nonNull)
                .map(r -> new SimpleGrantedAuthority("ROLE_" + r))
                .collect(Collectors.toList());

        // 也可以在 principal 中封装 email / tenantId / userId
        return new UsernamePasswordAuthenticationToken(username, null, authorities);
    }

    // 统一返回错误
    private void sendError(HttpServletResponse response, String message) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write("{\\"code\\":401,\\"message\\":\\"" + message + "\\"}");
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        String path = request.getRequestURI();
        // 例:公共路径不验证
        return path.startsWith("/public/") || path.startsWith("/login");
    }
}

数据的存储与加载

SecurityContextHolder

SecurityContextHolder
└── SecurityContext (接口)
    └── Authentication (接口)
        ├── Principal (Object)       // 当前用户对象,一般是 UserDetails 或自定义用户
        ├── Credentials (Object)     // 认证凭证,如密码或 token
        ├── Authorities (Collection<GrantedAuthority>)  // 权限列表
        └── isAuthenticated (boolean) // 是否认证通过

SecurityContextHolder 是 Spring Security 的全局上下文容器,用于在认证链中传递用户和权限信息,保证数据线程安全。

  • 认证链开始时:从请求中提取必要的凭证(如 JWT token、用户名密码)。验证凭证有效性,将基本的用户信息载入 Principal,并设置 isAuthenticated=false
  • 认证链结束时:完成完整验证(如查数据库加载用户详情、权限列表)。更新 Authentication 对象,设置 isAuthenticated=true,权限信息(Authorities)也完整载入。

载入后的访问:

  • 业务逻辑访问时:Controller 或 Service 层可通过 SecurityContextHolder.getContext().getAuthentication() 获取当前用户。可直接读取 Principal、权限列表以及认证状态。

@AuthenticationPrincipal

直接将当前认证用户对象注入到 Controller 方法参数中,避免手动从 SecurityContextHolder 获取。

@GetMapping("/me")
public String me(@AuthenticationPrincipal MyUser user) {
    return user.getEmail();
}

权限管理

权限管理的依据依旧在 Authentication 接口实现类的存储中,他有一个 Authorities 的成员数据,是一个 Collection<GrantedAuthority> 的权限列表,只需要在构建的时候载入角色该有的权限即可。

作为用户而言,通常可能有多个权限级别,例如读权限,写权限。因此这是一个序列而并不是一个类似角色的单个值。

授权概述

在 Spring Security 中,授权(Authorization)主要有 三类方式,根据粒度和应用场景划分:

授权方式 粒度 实现方式 核心组件
URL / 请求级 HttpSecurity + Filter FilterSecurityInterceptor
方法级 注解 + AOP MethodSecurityInterceptor
表达式 / 策略级 动态 SpEL + 自定义策略 PermissionEvaluatorAccessDecisionManager

授权的核心接口

AccessDecisionManager

决策者,决定某个请求是否允许访问。

常见实现:

  • AffirmativeBased(一个允许即可通过)
  • ConsensusBased(多数投票)
  • UnanimousBased(全票通过)
AccessDecisionVoter

投票者,单独判断是否允许访问。

int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes)

返回 ACCESS_GRANTED / ACCESS_DENIED / ACCESS_ABSTAIN

SecurityMetadataSource

提供安全元数据(URL / 方法 → 需要权限)。

例如:FilterInvocationSecurityMetadataSource(URL 授权)

FilterSecurityInterceptor

Spring Security Filter 链中的最后一个 Filter,用于做最终的授权判断。

流程:

  • 获取 SecurityMetadataSource 配置的资源权限
  • 调用 AccessDecisionManager 做决策
  • 决策成功 → 放行
  • 决策失败 → 抛出 AccessDeniedException

URL级别控制

请求进入 SecurityFilterChain
|
└── FilterSecurityInterceptor.doFilter()
     |
     ├── 调用 SecurityMetadataSource.getAttributes(request)
     |     └── 获取该 URL 需要的权限列表
     |
     ├── 调用 AccessDecisionManager.decide(authentication, request, attributes)
     |     |
     |     ├── 遍历 AccessDecisionVoter
     |     ├── 投票决定是否允许访问
     |     └── 返回允许或抛异常
     |
     ├── 如果允许
     |     └── chain.doFilter(request, response) 继续执行
     |
     └── 如果拒绝
           └── AccessDeniedHandler 处理(返回 403 或自定义页面)

实现

使用 HttpSecurity 进行静态 URL 控制

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // URL 权限配置
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            // 登录配置
            .formLogin()
            .and()
            // 注销配置
            .logout();

        return http.build();
    }
}

方法级别控制

请求调用方法
|
└── MethodSecurityInterceptor.invoke()
     |
     ├── 调用 SecurityMetadataSource.getAttributes(method)
     |     └── 获取该方法需要的权限元数据(角色 / 表达式)
     |
     ├── 获取 Authentication(当前用户身份 + 角色/权限)
     |
     ├── 调用 AccessDecisionManager.decide(authentication, method, attributes)
     |     |
     |     ├── 遍历 AccessDecisionVoter
     |     ├── 投票决定是否允许访问
     |     └── 返回允许或抛 AccessDeniedException
     |
     ├── 如果允许
     |     └── 执行目标方法
     |
     └── 如果拒绝
           └── 抛 AccessDeniedException

实现

  • @PreAuthorize

功能:方法调用前进行权限检查,可以使用表达式,例如 hasRole('ROLE')hasAuthority('AUTH')、SpEL 表达式等。

@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
    // 只有 ADMIN 角色能执行
}
  • @PostAuthorize

功能:方法调用后进行权限检查,可以访问方法返回值 (returnObject)。适合根据返回内容控制权限。

@PostAuthorize("returnObject.owner == authentication.name")
public User getUser(Long id) {
    // 方法执行后,会检查返回的 User 是否属于当前用户
    return userService.findById(id);
}
  • @PreFilter

功能:方法调用前过滤集合或数组参数,只保留满足条件的元素。

@PreFilter("filterObject.owner == authentication.name")
public void processUsers(List<User> users) {
    // 只处理属于当前用户的 User 对象
}
  • @PostFilter

功能:方法调用后过滤集合或数组返回值,只保留满足条件的元素。

@PostFilter("filterObject.owner == authentication.name")
public List<User> listUsers() {
    // 返回列表时,只保留属于当前用户的对象
    return userService.findAll();
}

原理

@PreAuthorize("hasRole('ADMIN')") 为例:

切面拦截:

  • PreInvocationAuthorizationAdviceVoterMethodSecurityInterceptor 拦截方法调用
  • 从方法上的注解解析出 ConfigAttribute(Spring 内部会生成一个 RoleConfigAttribute

AccessDecisionManager 执行:

  • 默认是 AffirmativeBased:只要有一个 voter 投 ACCESS_GRANTED 就允许
  • 将当前 Authentication(JWT 解析后放在 SecurityContextHolder 的对象)和方法上解析出的角色 ConfigAttribute 一起传入

投票逻辑(RoleVoter):

  • RoleVoter 会把 ConfigAttribute 的角色(如 "ROLE_ADMIN") 和 Authentication.getAuthorities() 里的权限比对
  • 投票规则:如果 authentication.getAuthorities() 中包含 ConfigAttribute,投 ACCESS_GRANTED 否则投 ACCESS_DENIEDABSTAIN(视 Voter 类型而定)
  • 默认 RoleVoter 会自动加 "ROLE_" 前缀,所以你的 JWT 权限要加 ROLE_ 前缀才能生效

表达式 / 策略的权限控制

权限判断请求
|
└── AccessDecisionManager.decide(authentication, resource, attributes)
     |
     ├── 遍历 AccessDecisionVoter 列表
     |     |
     |     ├── ExpressionVoter
     |     |     └── 使用表达式解析器解析权限表达式(如 @PreAuthorize、hasRole、hasAuthority)
     |     |           ├─ 解析表达式
     |     |           ├─ 对比当前用户 Authentication 的权限 / 角色
     |     |           └─ 投票:允许 / 拒绝 / 弃权
     |     |
     |     ├── RoleVoter
     |     |     └── 判断用户角色是否匹配资源角色要求
     |     |
     |     └── CustomVoter
     |           └── 自定义策略逻辑判断(如业务条件、用户属性、时间约束等)
     |
     ├── 汇总投票结果
     |     ├─ 多数投票或一致通过 → 允许访问
     |     └─ 否则 → 拒绝访问
     |
     └── 返回决策结果
           ├─ 允许 → 放行请求 / 执行方法
           └─ 拒绝 → 抛 AccessDeniedException 或交给 AccessDeniedHandler 处理

实现

例子:

@Service
public class UserService {

    @PreAuthorize("hasRole('ADMIN') or #userId == principal.id")
    public void updateUser(Long userId) {
        // 只有管理员或操作自己信息的用户可以调用
    }

    @PreAuthorize("isAuthenticated()")
    public void viewProfile() {
        // 只要登录用户就可以访问
    }
}

原理

表达式权限控制:用 SpEL 表达式 判断用户是否有访问权限,灵活且可组合。

  • 典型场景:方法级或 URL 级权限控制
  • 核心接口:AccessDecisionManager + Voter 模型

策略级权限控制:底层用 AccessDecisionManager 聚合多个 Voter 的投票结果来决策。

Voter 类型举例:

  • RoleVoter:检查用户角色(hasRole('ADMIN')
  • AuthenticatedVoter:检查用户是否已认证(isAuthenticated()
  • PreInvocationAuthorizationAdviceVoter:处理表达式注解(@PreAuthorize / @PostAuthorize