CC 咖啡猫的工作空间 Coding Space

Spring Security 深入原理

Spring Security 是 Spring 生态中的认证授权框架,核心是过滤器链(Filter Chain)。所有请求先经过过滤器链处理,认证通过后才到达业务代码。


1. 核心概念

1.1 认证(Authentication)vs 授权(Authorization)

概念 说明 问题
认证 验证"你是谁" 登录时验证用户名密码
授权 验证"你能做什么" RBAC:判断用户有没有访问接口的权限

1.2 核心组件

组件 职责
SecurityFilterChain 过滤器链,Spring Security 的核心
Authentication 认证信息,存放用户身份
AuthenticationManager 认证管理器,校验认证信息
UserDetailsService 加载用户数据(从数据库/内存)
PasswordEncoder 密码加密与验证
SecurityContext 持有当前 Authentication 的上下文
AccessDecisionManager 授权决策,决定能否访问资源

2. 过滤器链原理

2.1 过滤器链结构

Spring Security 的请求处理流程:

HTTP 请求
    ↓
Filter Chain(过滤器链)
    ↓
① WebAsyncManagerIntegrationFilter
② SecurityContextPersistenceFilter   → 从 Session 加载 SecurityContext
③ HeaderWriterFilter                 → 添加安全响应头
④ CsrfFilter                         → CSRF 防护
⑤ LogoutFilter                       → 处理登出
⑥ UsernamePasswordAuthenticationFilter → 用户名密码认证
⑦ DefaultLoginPageGeneratingFilter   → 生成登录页
⑧ BasicAuthenticationFilter          → HTTP Basic 认证
⑨ RequestCacheAwareFilter            → 请求缓存
⑩ SecurityContextHolderAwareAwareFilter
⑪ AnonymousAuthenticationFilter      → 未认证用户设为匿名
⑫ SessionManagementFilter            → Session 管理
⑬ ExceptionTranslationFilter         → 包装异常为 HTTP 响应
⑭ FilterSecurityInterceptor          → 最后一关:权限校验
    ↓
通过 → 业务代码
不通过 → 403 Forbidden / 302 重定向到登录页

过滤器按顺序执行,前面的过滤器处理完,后续才有机会。

2.2 一个请求经过过滤器链的全流程

请求 GET /admin/users
    ↓
FilterSecurityInterceptor 检查:需要 ADMIN 角色
    ↓
SecurityContext 中没有 Authentication
    ↓
AnonymousAuthenticationFilter 塞入匿名 Authentication
    ↓
AccessDecisionManager 投票:匿名用户没有 ADMIN 角色
    ↓
投票结果:拒绝访问
    ↓
ExceptionTranslationFilter 捕获
    ↓
判断是否已认证:
  未认证 → 重定向到 /login 页面
  已认证但权限不足 → 403 Forbidden

2.3 自定义过滤器

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(myCustomFilter(), UsernamePasswordAuthenticationFilter.class)
            // 在 UsernamePasswordAuthenticationFilter 之前添加自定义过滤器
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/home")
            );
        return http.build();
    }
}

3. 认证流程

3.1 UsernamePasswordAuthenticationFilter

// 登录请求 POST /login,携带 username & password
// UsernamePasswordAuthenticationFilter 拦截

UsernamePasswordAuthenticationFilter 内部:
    ↓
attemptAuthentication(request, response)
    ↓
从 request 获取 username 和 password
    ↓
创建 UsernamePasswordAuthenticationToken(未认证)
    ↓
交给 AuthenticationManager.authenticate(token)
    ↓
认证成功:
      1. SecurityContextHolder.getContext().setAuthentication(Authentication)
      2. 调用 AuthenticationSuccessHandler
    认证失败:
      1. SecurityContext 清空
      2. 调用 AuthenticationFailureHandler

3.2 AuthenticationManager 认证管理器

默认实现是 ProviderManager,内部维护多个 AuthenticationProvider

AuthenticationManager(ProviderManager)
    ↓
provider1.canSupport(认证类型)? → DaoAuthenticationProvider → 查数据库
provider2.canSupport(认证类型)? → JwtAuthenticationProvider → JWT 验证
provider3.canSupport(认证类型)? → OAuth2AuthenticationProvider → OAuth2
    ↓
遍历找到能处理的 Provider,执行认证

3.3 UserDetailsService

加载用户数据:

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        // UserDetails 包含用户名、密码、角色列表
        return org.springframework.security.core.userdetails.User
            .withUsername(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRoles().toArray(new String[0]))
            .build();
    }
}

3.4 PasswordEncoder 密码加密

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 推荐:BCrypt(带盐,强度自适应)
        return new BCryptPasswordEncoder();
    }
}

// 使用
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
    return username -> {
        // 注册时:加密存储
        String encoded = encoder.encode("plainPassword");
        // 登录时:Spring Security 自动比对
    };
}

不要用 MD5/SHA-1:这些没盐且可被彩虹表破解。BCrypt 是业界标准。


4. 授权流程

4.1 基于配置文件的授权

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**", "/login").permitAll()  // 公开
                .requestMatchers("/admin/**").hasRole("ADMIN")           // 需要 ADMIN 角色
                .requestMatchers("/user/**").hasAnyRole("ADMIN", "USER") // ADMIN 或 USER
                .anyRequest().authenticated()                            // 其他需登录
            );
        return http.build();
    }
}

4.2 基于注解的方法级授权

@RestController
public class AdminController {

    // 只有 ADMIN 角色才能访问
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin/users")
    public List<User> getUsers() { ... }

    // 表达式:用户名是当前用户本人或者是 ADMIN
    @PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
    @GetMapping("/user/{username}")
    public User getUser(@PathVariable String username) { ... }

    // 按钮级别权限:数据库中定义了按钮权限的用户才能访问
    @PreAuthorize("@permissionService.hasPermission('user:export')")
    @GetMapping("/user/export")
    public void exportUsers() { ... }
}

4.3 权限投票机制 AccessDecisionManager

// 决策方式:多数票胜出
public class AffirmativeBased extends AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        int grant = 0;
        for (AccessDecisionVoter voter : decisionVoters) {
            int result = voter.vote(authentication, object, attributes);
            if (result == ACCESS_GRANTED) return;  // 有票同意就放行
        }
        throw new AccessDeniedException("无权限访问");
    }
}

Voter 的实现:

  • RoleHierarchyVoter:角色层级(ADMIN 包含 USER 权限)
  • AuthenticatedVoter:判断 IS_AUTHENTICATED_FULLY
  • CustomVoter:自定义投票逻辑

5. Session 管理

5.1 Session 并发控制

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)               // 同一个用户最多一个会话
            .maxSessionsPreventsLogin(false)   // false=踢掉前一个用户,true=禁止新登录
        );
}

maximumSessions(1) 效果:第二个设备登录后,第一个设备被踢下线。

5.2 Session 并发问题

用户 A 登录(Session1 在服务器)
    ↓
Session1 被别人异地登录,A 被踢下线
    ↓
A 再次登录(Session2)
    ↓
Session1 还存活(A 的旧会话)
    ↓
如果 maximumSessions 设置为 1,Session1 会被标记为"已过期"但未销毁
    ↓
下次 A 访问时,过期 Session 被检测到,触发 Session 并发逻辑

5.3 Redis Session 共享

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
@Configuration
@EnableRedisHttpSession  // Session 存 Redis
public class SessionConfig {}

多节点部署时,用户请求可能打到不同服务器,Session 共享保证用户体验一致。


6. 登录后的用户信息获取

6.1 在 Controller 中获取当前用户

@RestController
public class UserController {

    // 方式1:通过 SecurityContext
    @GetMapping("/me")
    public User me() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String username = auth.getName();  // 用户名
        return userService.findByUsername(username);
    }

    // 方式2:通过 @AuthenticationPrincipal 注解(推荐)
    @GetMapping("/me2")
    public UserDetails me2(@AuthenticationPrincipal UserDetails user) {
        return user;
    }

    // 方式3:Controller 方法参数注入
    @GetMapping("/me3")
    public User me3(Authentication auth) {
        return userService.findByUsername(auth.getName());
    }
}

6.2 在 Service 中获取

@Service
public class OrderService {

    public Order createOrder() {
        // 从 SecurityContext 获取当前用户名
        String username = SecurityContextHolder.getContext()
            .getAuthentication().getName();
        User user = userMapper.findByUsername(username);
        // ...
    }
}

7. Spring Security 与微服务

7.1 微服务认证的两种模式

模式 原理 适用场景
网关鉴权 网关统一验证 Token,其他服务信任网关 内部微服务
JWT 无状态 每个服务独立验证 JWT 跨团队、开放 API
OAuth2 + JWT OAuth2 发放 JWT,服务独立验证 第三方接入

7.2 JWT 认证流程

用户登录 → 验证用户名密码 → 颁发 JWT Token
    ↓
后续请求:Authorization: Bearer <token>
    ↓
过滤器链中:JwtAuthenticationFilter 拦截
    ↓
验证 JWT 签名 + 过期时间
    ↓
提取用户信息 → 创建 Authentication
    ↓
SecurityContext 设置 Authentication
    ↓
请求继续到业务代码

7.3 Spring Security OAuth2 Resource Server

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
            );
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthorityConverter grantedAuthoritiesConverter = new JwtGrantedAuthorityConverter();
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }
}

8. 常见问题

8.1 @PreAuthorize 不生效

检查是否开启了全局方法安全注解:

@Configuration
@EnableMethodSecurity  // 必须加!否则 @PreAuthorize 不生效
public class SecurityConfig {}

8.2 静态资源被拦截

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
    .anyRequest().authenticated()
)

静态资源放在 src/main/resources/static/ 下。

8.3 跨域配置与 Security 冲突

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins("http://localhost:3000")
            .allowCredentials(true);
    }
}

// 同时在 Security 中配置
http.cors(cors -> {});  // 启用 CORS

8.4 密码加密后登录仍然失败

常见原因:

  1. 注册时加密,登录时却用 NoOpPasswordEncoder(不加密)比对
  2. 前后端密码传输编码不一致
// 确保 PasswordEncoder 注册正确
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

9. Spring Security 与 Shiro 对比

维度 Spring Security Apache Shiro
与 Spring 集成 原生集成 需要集成
过滤器链 完整过滤器链 Filter 链
权限粒度 方法级(@PreAuthorize) 方法级 + 注解
OAuth2 原生支持 无,需扩展
JWT 需配合 Resource Server 需扩展
学习曲线 陡峭 平缓
复杂度

结论

  • Spring 全家桶项目 → Spring Security(生态最契合)
  • 非 Spring 项目或追求简单 → Shiro