【SpringSecurity】详细核心类与过滤器流程讲解和封装通用组件实战

Spring Security 全面介绍

1. 什么是 Spring Security?

Spring Security 是一个功能强大且高度可定制的认证和访问控制框架,是保护基于 Spring 的应用程序的标准工具。它是一个专注于为 Java 应用程序提供认证和授权的框架,实际上它是 Spring 生态系统中负责安全方面的重要成员。

核心特性

  • 全面的安全性:支持认证、授权和防护常见攻击
  • 与 Spring 生态系统深度集成
  • 适应多种环境:Web 应用、RESTful API、微服务
  • 高度可扩展性:几乎所有组件都可以定制或替换
  • 多种认证机制:表单认证、Basic 认证、OAuth2、LDAP、SAML 等

2. 基本概念

认证 (Authentication)

认证回答了"你是谁?"的问题,是确认用户身份的过程。

核心工作流程:

  1. 收集认证凭据(如用户名和密码)
  2. 验证凭据的有效性
  3. 对有效凭据发放安全令牌

授权 (Authorization)

授权回答了"你能做什么?"的问题,是确定用户是否有权执行特定操作的过程。

核心工作流程:

  1. 确定资源要求的权限
  2. 检查已认证用户是否拥有所需权限
  3. 授予或拒绝访问权限

主体 (Principal)

表示当前通过认证的用户,通常包含用户标识(如用户名)和授予的权限。

权限 (Authorities/Roles)

表示授予用户的特定权限,通常分为:

  • 角色 (Roles):代表用户组,如 ROLE_ADMIN
  • 权限 (Authorities):代表具体权限,如 READ_DATA

3. 核心架构

Spring Security 架构基于过滤器链模式,请求通过一系列专门的过滤器,各自负责安全流程的不同方面。

关键组件

Security Filters

过滤器链,按特定顺序执行的安全过滤器:

  • SecurityContextPersistenceFilter:管理 SecurityContext
  • UsernamePasswordAuthenticationFilter:处理表单登录
  • BasicAuthenticationFilter:处理 HTTP Basic 认证
  • ExceptionTranslationFilter:处理安全异常
  • FilterSecurityInterceptor:处理授权决策
Authentication Manager

认证管理器,主要实现是 ProviderManager,它维护一个 AuthenticationProvider 列表,依次尝试处理认证请求。

Authentication Providers

认证提供者,负责特定类型的认证,如:

  • DaoAuthenticationProvider:基于用户名密码的认证
  • JwtAuthenticationProvider:JWT 令牌验证
  • LdapAuthenticationProvider:LDAP 认证
UserDetailsService

负责加载用户数据,是自定义用户存储的主要扩展点。

Security Context

安全上下文,存储当前认证信息,通过 SecurityContextHolder 提供访问。

4. 常见认证机制

表单登录认证

最常见的认证方式,用户通过 HTML 表单提交凭据:

http
    .formLogin()
        .loginPage("/custom-login")
        .defaultSuccessUrl("/dashboard")
        .failureUrl("/login?error=true")
        .permitAll();

HTTP Basic 认证

简单的认证机制,通过 HTTP 头发送凭据:

http
    .httpBasic()
        .realmName("My API");

记住我功能

允许用户在会话过期后保持登录状态:

http
    .rememberMe()
        .key("uniqueAndSecret")
        .tokenValiditySeconds(86400);

OAuth 2.0 / OpenID Connect

用于实现单点登录和 API 授权:

http
    .oauth2Login()
        .loginPage("/oauth_login")
        .clientRegistrationRepository(clientRegistrationRepository)
        .authorizedClientService(authorizedClientService);

5. 授权模型

基于 URL 的授权

控制对 Web URL 的访问:

http
    .authorizeHttpRequests()
        .requestMatchers("/public/**").permitAll()
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .requestMatchers("/api/**").hasAuthority("API_ACCESS")
        .anyRequest().authenticated();

方法级安全

在服务层保护方法调用:

@PreAuthorize("hasRole('ADMIN') or #username == authentication.principal.username")
public UserDetails loadUserByUsername(String username) {
    // 实现逻辑
}

基于表达式的访问控制

使用 SpEL 表达式定义复杂授权规则:

http
    .authorizeHttpRequests()
        .requestMatchers("/orders/**").access("hasRole('USER') and @webSecurity.checkUserId(authentication, #id)");

6. 会话管理

会话创建策略

控制会话的创建方式:

http
    .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

可选策略:

  • ALWAYS:总是创建会话
  • NEVER:不主动创建会话
  • IF_REQUIRED:需要时创建(默认)
  • STATELESS:不创建或使用会话(适用于 REST API)

并发会话控制

限制用户同时活跃的会话数:

http
    .sessionManagement()
        .maximumSessions(1)
        .maxSessionsPreventsLogin(true);

会话固定保护

防止会话固定攻击:

http
    .sessionManagement()
        .sessionFixation().migrateSession();

7. 保护常见攻击

CSRF 保护

默认启用,保护表单提交免受跨站请求伪造:

http
    .csrf()  // 默认启用
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

跨域资源共享 (CORS)

启用跨域支持:

http
    .cors()
        .configurationSource(corsConfigurationSource());

安全头部配置

添加安全相关的 HTTP 头:

http
    .headers()
        .frameOptions().deny()
        .xssProtection().block()
        .contentSecurityPolicy("script-src 'self'");

8. 实际应用示例

基础 Web 应用配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {
  
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/home", "/css/**", "/js/**").permitAll()
                .requestMatchers("/user/**").hasRole("USER")
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
                .defaultSuccessUrl("/dashboard")
            )
            .logout(logout -> logout
                .permitAll()
                .logoutSuccessUrl("/login?logout")
            );
          
        return http.build();
    }
  
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

自定义用户存储

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;
  
    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
  
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
          
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            user.isEnabled(),
            true, true, true,
            mapRolesToAuthorities(user.getRoles())
        );
    }
  
    private Collection<? extends GrantedAuthority> mapRolesToAuthorities(Set<Role> roles) {
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .collect(Collectors.toSet());
    }
}

REST API 安全配置

@Configuration
@EnableWebSecurity
public class ApiSecurityConfig {
  
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .httpBasic(Customizer.withDefaults());
          
        return http.build();
    }
}

9. 高级特性

方法安全

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
    // 配置代码
}

@Service
public class UserService {
  
    @PreAuthorize("hasRole('ADMIN')")
    public List<User> findAllUsers() {
        // 实现逻辑
    }
  
    @PostAuthorize("returnObject.username == authentication.name")
    public User getUser(String id) {
        // 实现逻辑
    }
}

多租户安全

@Bean
public TenantContextHolder tenantContextHolder() {
    return new TenantContextHolder();
}

@Bean
public UserDetailsService userDetailsService(DataSource dataSource, TenantContextHolder holder) {
    return username -> {
        String tenant = holder.getTenant();
        // 基于租户获取用户
    };
}

事件监听

@Component
public class AuthenticationEventListener {
  
    private static final Logger logger = LoggerFactory.getLogger(AuthenticationEventListener.class);
  
    @EventListener
    public void onSuccess(AuthenticationSuccessEvent event) {
        logger.info("User logged in: {}", event.getAuthentication().getName());
    }
  
    @EventListener
    public void onFailure(AuthenticationFailureBadCredentialsEvent event) {
        logger.warn("Login failed for user: {}", event.getAuthentication().getName());
    }
}

10. 最佳实践

1. 使用强密码哈希

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);  // 更高的强度
}

2. 保持依赖更新

定期更新 Spring Security 版本,以获取安全修复和新功能。

3. 使用多种防御机制

不要仅依赖一种安全机制,应组合使用认证、授权、CSRF 保护等。

4. 最小权限原则

默认拒绝访问,只明确允许必要的权限:

.anyRequest().denyAll()  // 而不是 .authenticated()

5. 安全日志记录

记录所有重要的安全事件,但避免记录敏感信息:

@EventListener
public void handleBadCredentials(AuthenticationFailureBadCredentialsEvent event) {
    logger.warn("Failed login attempt from IP: {}", request.getRemoteAddr());
    // 不要记录密码!
}

6. 适当的错误处理

不要泄露敏感信息:

.failureHandler((request, response, exception) -> {
    response.sendRedirect("/login?error=true");  // 通用错误,不指明原因
})

11. 生态系统集成

Spring Boot 集成

Spring Boot 自动配置大部分 Spring Security 功能:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

OAuth2 客户端集成

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

资源服务器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

12. 性能和可扩展性考虑

  • 使用适当的会话策略(无状态 vs. 有状态)
  • 缓存用户详情和权限检查
  • 在负载均衡环境中注意会话复制/共享
  • 考虑分布式会话存储(Redis, Hazelcast)

Spring Security 拦截器链工作步骤

作为一名资深软件工程师,我将详细分析 Spring Security 的拦截器链(Filter Chain)工作流程。Spring Security 使用一系列过滤器来实现其安全功能,这些过滤器按特定顺序组织成拦截器链。

拦截器链概述

Spring Security 的拦截器链是由 FilterChainProxy 管理的一系列 SecurityFilterChain 对象,每个 SecurityFilterChain 包含多个安全过滤器。

标准过滤器执行顺序

以下是典型的 Spring Security 过滤器链执行顺序(从先到后):

  1. WebAsyncManagerIntegrationFilter

    • 将 SecurityContext 集成到 Spring 异步处理中
    • 确保异步请求中可以访问 SecurityContext
  2. SecurityContextPersistenceFilter

    • 在请求开始时从 SecurityContextRepository(通常是 HTTP Session)中恢复 SecurityContext
    • 在请求结束时将 SecurityContext 保存回 SecurityContextRepository
    • 使安全上下文在整个请求中可用
  3. HeaderWriterFilter

    • 向响应添加安全相关 HTTP 头
    • 如 X-XSS-Protection, X-Frame-Options, X-Content-Type-Options 等
  4. CsrfFilter

    • 提供 CSRF(跨站请求伪造)保护
    • 验证 POST/PUT/DELETE 等请求中的 CSRF token
  5. LogoutFilter

    • 处理 /logout 路径的请求
    • 执行用户注销逻辑,清除认证信息和会话
  6. UsernamePasswordAuthenticationFilter

    • 处理表单登录尝试(通常是 /login POST 请求)
    • 提取用户名和密码并创建认证令牌
    • 委托给 AuthenticationManager 进行实际验证
  7. DefaultLoginPageGeneratingFilter

    • 如果没有自定义登录页,生成默认登录页面
    • 处理 /login GET 请求
  8. DefaultLogoutPageGeneratingFilter

    • 生成默认注销页面
    • 处理 /logout GET 请求
  9. BasicAuthenticationFilter

    • 处理 HTTP Basic 认证头
    • 提取凭据并尝试认证
  10. RequestCacheAwareFilter

    • 处理请求缓存
    • 如果用户在登录前访问受保护资源,登录后可以重定向到原始 URL
  11. SecurityContextHolderAwareRequestFilter

    • 包装 HttpServletRequest,添加安全相关方法
    • 实现 Servlet API 安全方法
  12. AnonymousAuthenticationFilter

    • 如果当前没有认证信息,创建匿名用户认证
    • 确保 SecurityContext 总是有 Authentication 对象
  13. SessionManagementFilter

    • 检测会话相关问题
    • 处理会话固定保护、并发会话控制等
  14. ExceptionTranslationFilter

    • 捕获安全异常并转换为适当的 HTTP 响应
    • 处理 AccessDeniedException 和 AuthenticationException
  15. FilterSecurityInterceptor

    • 最后一道防线,保护 HTTP 资源
    • 使用 AccessDecisionManager 确定是否允许当前请求访问资源
    • 在这里应用具体的访问控制决策

工作流程详解

  1. 请求到达

    • 请求首先进入 FilterChainProxy
    • FilterChainProxy 找到匹配当前请求的第一个 SecurityFilterChain
  2. 上下文准备

    • SecurityContextPersistenceFilter 检索或创建 SecurityContext
    • SecurityContext 存储在 SecurityContextHolder
  3. 认证流程

    • 如果请求包含认证信息(如登录请求),相应的认证过滤器处理认证
    • 认证成功后,Authentication 对象被放入 SecurityContext
  4. 授权检查

    • FilterSecurityInterceptor 检查用户是否有权限访问请求的资源
    • 使用 SecurityMetadataSource 获取资源所需权限
    • 使用 AccessDecisionManager 决定是否授予访问权限
  5. 异常处理

    • 如果发生安全异常,ExceptionTranslationFilter 进行处理
    • 认证异常触发认证流程(通常重定向到登录页)
    • 授权异常产生 403 禁止访问响应
  6. 请求完成

    • 所有过滤器处理完毕后,请求传递给实际的应用程序处理器
    • 请求处理完成后,SecurityContextPersistenceFilter 保存 SecurityContext
    • SecurityContextHolder 被清理

自定义配置示例

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
}

这个配置会设置适当的过滤器并配置它们的行为。

调试技巧

要查看实际应用的过滤器链,可以设置日志级别:

logging.level.org.springframework.security.web.FilterChainProxy=DEBUG

这将输出每个请求应用的确切过滤器链。

Spring Security 的拦截器链设计体现了职责分离原则,每个过滤器专注于特定安全功能,共同构成了一个强大而灵活的安全框架。

Spring Security 核心类详解

作为资深软件工程师,我将深入介绍 Spring Security 的核心类结构。这些核心类共同构成了 Spring Security 的基础架构,理解它们有助于掌握整个框架的工作原理。

认证核心类

1. Authentication(接口)

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();
    Object getDetails();
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean isAuthenticated);
}
  • 职责: 代表认证请求或已认证的主体
  • 关键方法:
    • getPrincipal(): 获取主体身份(通常是 UserDetails)
    • getCredentials(): 获取凭证(如密码)
    • getAuthorities(): 获取授予的权限
    • isAuthenticated(): 判断是否已认证
  • 常见实现: UsernamePasswordAuthenticationToken, JwtAuthenticationToken

2. AuthenticationManager(接口)

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
  • 职责: 处理认证请求,是认证架构的核心接口
  • 工作方式: 接收一个 Authentication 对象,验证后返回完全填充的 Authentication
  • 主要实现: ProviderManager

3. ProviderManager

public class ProviderManager implements AuthenticationManager {
    private List<AuthenticationProvider> providers;
    private AuthenticationManager parent;
  
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 遍历所有 provider 尝试认证
    }
}
  • 职责: AuthenticationManager 的主要实现
  • 工作流程: 维护 AuthenticationProvider 列表,依次尝试认证

4. AuthenticationProvider(接口)

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    boolean supports(Class<?> authentication);
}
  • 职责: 执行特定类型的认证
  • 关键实现:
    • DaoAuthenticationProvider: 基于用户名密码的认证
    • JwtAuthenticationProvider: JWT令牌认证

5. UserDetailsService(接口)

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • 职责: 从数据源加载用户信息
  • 使用场景: 被 AuthenticationProvider 调用以获取用户数据

6. UserDetails(接口)

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}
  • 职责: 提供核心用户信息
  • 特点: 框架对用户概念的抽象,与应用用户模型解耦
  • 常用实现: User 和自定义实现

安全上下文管理

7. SecurityContext(接口)

public interface SecurityContext extends Serializable {
    Authentication getAuthentication();
    void setAuthentication(Authentication authentication);
}
  • 职责: 存储当前线程的安全信息
  • 主要实现: SecurityContextImpl

8. SecurityContextHolder

public class SecurityContextHolder {
    private static SecurityContextHolderStrategy strategy;
  
    public static SecurityContext getContext() {
        return strategy.getContext();
    }
  
    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }
  
    public static void clearContext() {
        strategy.clearContext();
    }
}
  • 职责: 提供对当前 SecurityContext 的访问
  • 存储策略: 支持 ThreadLocal, InheritableThreadLocal 和全局模式

9. SecurityContextRepository(接口)

public interface SecurityContextRepository {
    SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);
    void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);
    boolean containsContext(HttpServletRequest request);
}
  • 职责: 在请求之间持久化 SecurityContext
  • 主要实现: HttpSessionSecurityContextRepository

授权核心类

10. AccessDecisionManager(接口)

public interface AccessDecisionManager {
    void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException;
    boolean supports(ConfigAttribute attribute);
    boolean supports(Class<?> clazz);
}
  • 职责: 做出访问控制决策
  • 主要实现:
    • AffirmativeBased: 只要有一个投票者同意即通过
    • ConsensusBased: 基于多数原则
    • UnanimousBased: 要求全体一致同意

11. AccessDecisionVoter(接口)

public interface AccessDecisionVoter<S> {
    int ACCESS_GRANTED = 1;
    int ACCESS_ABSTAIN = 0;
    int ACCESS_DENIED = -1;

    boolean supports(ConfigAttribute attribute);
    boolean supports(Class<?> clazz);
    int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}
  • 职责: 对访问请求进行投票
  • 主要实现: RoleVoter, WebExpressionVoter

12. FilterSecurityInterceptor

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    private FilterInvocationSecurityMetadataSource securityMetadataSource;
  
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        // 拦截请求并应用安全检查
    }
}
  • 职责: HTTP 资源的安全拦截器
  • 工作流程: 从 SecurityMetadataSource 获取配置属性,调用 AccessDecisionManager 做决策

过滤器链管理

13. FilterChainProxy

public class FilterChainProxy extends GenericFilterBean {
    private List<SecurityFilterChain> filterChains;
  
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        // 找到匹配的过滤器链并执行
    }
}
  • 职责: Spring Security 过滤器的主入口点
  • 特点: 管理多个 SecurityFilterChain 实例

14. SecurityFilterChain(接口)

public interface SecurityFilterChain {
    boolean matches(HttpServletRequest request);
    List<Filter> getFilters();
}
  • 职责: 持有与特定请求匹配的过滤器集合
  • 主要实现: DefaultSecurityFilterChain

关键过滤器

15. UsernamePasswordAuthenticationFilter

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    // 处理表单登录
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
        // 提取用户名密码并创建认证令牌
    }
}
  • 职责: 处理表单登录认证

16. ExceptionTranslationFilter

public class ExceptionTranslationFilter extends GenericFilterBean {
    private AccessDeniedHandler accessDeniedHandler;
    private AuthenticationEntryPoint authenticationEntryPoint;
  
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        // 捕获安全异常并转换为HTTP响应
    }
}
  • 职责: 转换 Spring Security 异常为 HTTP 响应
  • 处理逻辑:
    • AuthenticationException → AuthenticationEntryPoint
    • AccessDeniedException → AccessDeniedHandler

17. SecurityContextPersistenceFilter

public class SecurityContextPersistenceFilter extends GenericFilterBean {
    private SecurityContextRepository repo;
  
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        // 在请求前加载上下文,请求后保存上下文
    }
}
  • 职责: 管理 SecurityContext 的生命周期

配置核心类

18. WebSecurityConfigurerAdapter (在Spring Security 5.7+已废弃)

public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
    protected void configure(HttpSecurity http) throws Exception {
        // 配置HTTP安全
    }
  
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 配置认证
    }
}
  • 职责: 提供安全配置的便捷基类
  • : 在新版中使用组件化配置代替

19. HttpSecurity

public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity> {
    // 提供流式API配置HTTP安全
    public HttpSecurity authorizeRequests() {...}
    public HttpSecurity formLogin() {...}
    // 更多配置方法...
}
  • 职责: 配置 HTTP 请求级别的安全特性
  • 特点: 提供流式 API 进行安全配置

20. SecurityBuilder & SecurityConfigurer

public interface SecurityBuilder<O> {
    O build() throws Exception;
}

public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
    void init(B builder) throws Exception;
    void configure(B builder) throws Exception;
}
  • 职责: 构建器模式的核心接口,支持模块化安全配置

实际应用示例

@Configuration
@EnableWebSecurity
public class SecurityConfig {
  
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );
          
        return http.build();
    }
  
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
          
        return new InMemoryUserDetailsManager(user);
    }
}

理解这些核心类及其关系,能让你更透彻掌握 Spring Security 的工作方式,并更有效地进行安全配置和定制化开发。无论是标准认证流程还是实现自定义安全机制,这些类都是框架的基础构建块。

通用鉴权模块实战

我们新建一个单独的模块security-contract,封装Security的相关组件,这样就能在我们自己的项目中引入。
在这里插入图片描述

依赖管理

<?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 http://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.4</version>
        <relativePath/>
    </parent>

    <groupId>com.xujie</groupId>
    <artifactId>security-contract</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>23</maven.compiler.source>
        <maven.compiler.target>23</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 新版本的 JJWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson -->
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <!-- pom.xml -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.4.0</version>
        </dependency>

    </dependencies>
</project>

相关类代码

IgnoreUrlsConfig 白名单配置类

@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "secure")
public class IgnoreUrlsConfig {

    private List<String> ignored = new ArrayList<>();

}

UserSecurityConfiguration 用户鉴权核心配置类


public class UserSecurityConfiguration {

    private final StringRedisTemplate redisTemplate;

    private final MyAuthenticationTokenFilter tokenFilter;

    private List<String> ingoreList;

    private final SecurityContextFilter securityContextFilter;

    public UserSecurityConfiguration(StringRedisTemplate stringRedisTemplate, List<String> ingoreList, SecurityContextFilter securityContextFilter) {
        this.redisTemplate = stringRedisTemplate;
        this.tokenFilter = new MyAuthenticationTokenFilter(stringRedisTemplate);
        this.ingoreList = ingoreList;
        this.securityContextFilter = securityContextFilter;
    }

    @Bean
    public SecurityFilterChain jwtFilterChain(HttpSecurity http) throws Exception {
        http
                .cors(cors -> cors.configurationSource(corsConfigurationSource())) // 开启跨域
                .csrf(AbstractHttpConfigurer::disable)  // 对API禁用CSRF
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers(ingoreList.toArray(String[]::new)).permitAll()
                        .requestMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN")
                        .anyRequest().authenticated()
                )
                .exceptionHandling(exceptions -> exceptions
                        .authenticationEntryPoint((request, response, ex) -> {
                            response.setCharacterEncoding("UTF-8");
                            response.setStatus(401);
                            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                            ErrorInfo errorInfo = new ErrorInfo(HttpStatus.UNAUTHORIZED.value(), "未登录或登录已过期,请重新登录!");
                            ObjectMapper mapper = new ObjectMapper();
                            mapper.writeValue(response.getWriter(), errorInfo);
                            response.getWriter().flush();
                        })
                        .accessDeniedHandler((request, response, ex) -> {
                            response.setContentType("application/json");
                            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                            response.getWriter().write("{\"error\":\"Forbidden\",\"message\":\"" + ex.getMessage() + "\"}");
                        })
                )
                .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(securityContextFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:*")); // 允许的前端域
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); // 允许的HTTP方法
        configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With")); // 允许的请求头
        configuration.setExposedHeaders(List.of("Authorization")); // 允许前端访问的响应头
        configuration.setAllowCredentials(true); // 允许发送凭证
        configuration.setMaxAge(3600L); // 预检请求缓存时间(秒)

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration); // 对所有路径应用此配置
        return source;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    public record ErrorInfo(int status, String message) {
    }
}

AuthenticationConstant 常量类

public class AuthenticationConstant {

    public static final String HEAD_AUTH = "Authorization";
    public static final String HEAD_USER_ID = "userId";
    public static final String REDIS_KEY = "Auth:%s";

}

MyAuthenticationTokenFilter Token验证拦截器


@Slf4j
public class MyAuthenticationTokenFilter extends OncePerRequestFilter {
    private final StringRedisTemplate redisTemplate;

    public MyAuthenticationTokenFilter(StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = stringRedisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader(AuthenticationConstant.HEAD_AUTH);
        // 判断Token是否为空
        if (token == null) {
            SecurityContextHolder.getContext().setAuthentication(null);
            filterChain.doFilter(request, response);
        }
        Long userId = JWTUtil.getUserIdFromTokenUnsafe(token);
        // TODO 暂时不开启Redis 验证
//        String key = String.format(AuthenticationConstant.REDIS_KEY, userId);
//        if (!redisTemplate.hasKey(key)) {
//            SecurityContextHolder.getContext().setAuthentication(null);
//            filterChain.doFilter(request, response);
//        }
        String userSign = "user123password";
        if (userSign != null && !userSign.isEmpty()) {
            try {
                JWTUtil.verifyToken(token, userSign);
                request.setAttribute(AuthenticationConstant.HEAD_USER_ID, userId);
            } catch (Exception e) {
                SecurityContextHolder.getContext().setAuthentication(null);
            } finally {
                filterChain.doFilter(request, response);
            }
        }


    }
}

SecurityContextFilter 上下文填充拦截器


public abstract class SecurityContextFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            Long userId = (Long) request.getAttribute(AuthenticationConstant.HEAD_USER_ID);
            if (userId == null) {
                SecurityContextHolder.getContext().setAuthentication(null);
                filterChain.doFilter(request, response);
                return;
            }
            Collection<GrantedAuthority> userAuth = getAuths(getUserAuth(userId), getUserRoles(userId));
            UsernamePasswordAuthenticationToken securityContextFilter = new UsernamePasswordAuthenticationToken(userId, null, userAuth);
            SecurityContextHolder.getContext().setAuthentication(securityContextFilter);
        } finally {
            filterChain.doFilter(request, response);
        }
    }


    protected abstract List<String> getUserRoles(Long userId);

    protected abstract List<String> getUserAuth(Long userId);

    private Collection<GrantedAuthority> getAuths(List<String> auths, List<String> userRoles) {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        if (auths == null) {
            return authorities;
        }
        for (String auth : auths) {
            authorities.add(new SimpleGrantedAuthority(auth));
        }
        for (String userRole : userRoles) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + userRole));
        }
        return authorities;
    }
}

JWTUtil 工具类


public class JWTUtil {


    /**
     * 从用户密码生成安全的JWT密钥
     *
     * @param userPassword 用户密码
     * @return 安全的JWT密钥
     */
    public static SecretKey generateSecureKeyFromPassword(String userPassword) throws NoSuchAlgorithmException {
        // 1. 组合用户密码
        String combined = userPassword;
        // 2. 使用SHA-256对组合字符串进行哈希处理
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hashedBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8));

        // 3. 确保密钥长度足够(SHA-256已经产生32字节/256位的输出)
        return Keys.hmacShaKeyFor(hashedBytes);
    }

    /**
     * 创建JWT令牌
     */
    public static String createToken(String username, String userPassword) throws NoSuchAlgorithmException {
        SecretKey key = generateSecureKeyFromPassword(userPassword);

        return Jwts.builder()
                .setSubject(username)
                .setId(UUID.randomUUID().toString())
                .signWith(key)
                .compact();
    }

    /**
     * 创建JWT令牌
     */
    public static String createToken(String username, String userPassword, Map<String, String> claims) throws NoSuchAlgorithmException {
        SecretKey key = generateSecureKeyFromPassword(userPassword);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(username)
                .setId(UUID.randomUUID().toString())
                .signWith(key)
                .compact();
    }

    /**
     * 验证并解析令牌
     */
    public static void verifyToken(String token, String sign) throws NoSuchAlgorithmException {
        try {
            SecretKey key = generateSecureKeyFromPassword(sign);
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            throw new IllegalArgumentException("Token is not valid");
        }
    }


    /**
     * 不验证签名,直接解析JWT获取对应的Claim
     * 警告:此方法不验证令牌的真实性,仅用于读取
     */
    private static String parseTokenWithoutVerification(String token, String claimKey) {
        // 将token拆分成头部、负载和签名
        String[] parts = token.split("\\.");
        if (parts.length != 3) {
            throw new IllegalArgumentException("Invalid token format");
        }
        DecodedJWT decode = JWT.decode(token);
        Map<String, Claim> claims = decode.getClaims();
        Claim claim = claims.get(claimKey);
        if (claim == null) {
            throw new IllegalArgumentException("Invalid claim");
        }
        return claim.asString();
    }

    /**
     * 不验证签名,直接获取用户ID
     */
    public static Long getUserIdFromTokenUnsafe(String token) {
        String userId = parseTokenWithoutVerification(token, "userId");
        return Long.valueOf(userId);
    }

    public static void main(String[] args) {
        try {
            // 模拟用户密码
            String userPassword = "user123password";
            String username = "john_doe";

            // 创建令牌
            Map<String, String> claimsMap = new HashMap<>();
            claimsMap.put("userId", "1001");
            String jwt = createToken(username, userPassword, claimsMap);
            System.out.println("生成的JWT: " + jwt);

            // 获取Token的subject
            Long userId = JWTUtil.getUserIdFromTokenUnsafe(jwt);
            System.out.println("UserId: " + userId);

            // 验证令牌
            verifyToken(jwt, userPassword);
            System.out.println("验证成功");
            // 验证使用错误密码
            try {
                verifyToken(jwt, "wrong_password");
                System.out.println("这行不应该执行");
            } catch (Exception e) {
                System.out.println("使用错误密码验证失败(预期行为): " + e.getMessage());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

引入自己模块使用

WebSecurityConfig 配置类

@Configuration
public class WebSecurityConfig extends UserSecurityConfiguration {


    public WebSecurityConfig(StringRedisTemplate stringRedisTemplate, IgnoreUrlsConfig ignoreUrlsConfig, MySecurityContextFilter mySecurityContextFilter) {
        super(stringRedisTemplate, ignoreUrlsConfig.getIgnored(), mySecurityContextFilter);
    }
}

MySecurityContextFilter 实现用户角色和权限接口

@Component
public class MySecurityContextFilter extends SecurityContextFilter {


    @Override
    protected List<String> getUserRoles(Long userId) {
        return List.of("testRole");
    }

    @Override
    protected List<String> getUserAuth(Long userId) {
        return List.of("testAuth");
    }
}

配置白名单

secure:
  ignored:
    - /test
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小徐Chao努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值