Spring Security自定义过滤器链解决多登录入口鉴权问题

Spring Security 自定义过滤器链解决多登录入口鉴权问题

大家好,今天我们来深入探讨一个在实际开发中经常遇到的问题:如何利用 Spring Security 的自定义过滤器链来优雅地解决多登录入口的鉴权问题。

背景:单体应用的挑战

在传统的单体应用中,我们往往只有一个登录页面,用户通过用户名和密码进行身份验证。Spring Security 默认的配置通常足以满足需求。但随着业务的扩展,我们可能会面临以下情况:

  • 多种用户角色: 例如,管理员、普通用户、供应商等,他们需要不同的权限和访问控制。
  • 多个登录入口: 例如,管理后台有单独的登录页面,用户App 有独立的登录页面,甚至第三方 OAuth 登录。
  • 不同的认证方式: 例如,普通用户使用用户名/密码,管理员使用 LDAP 认证,App 用户使用 Token 认证。

如果将所有逻辑都塞到一个过滤器中,代码会变得臃肿、难以维护,并且扩展性很差。因此,我们需要一种更加灵活、可扩展的方案。Spring Security 的自定义过滤器链机制正是为此而生的。

核心思想:职责分离,按需定制

Spring Security 的过滤器链本质上是一个责任链模式的实现。请求会依次经过一系列过滤器,每个过滤器负责处理特定的认证或授权逻辑。我们可以根据不同的登录入口和用户角色,定义不同的过滤器链,每个链包含特定的过滤器,只处理特定类型的请求。

实践:构建多登录入口的 Spring Security

为了更好地理解,我们通过一个具体的例子来说明。假设我们有以下需求:

  • 普通用户: 使用用户名/密码登录,访问 /user/** 路径。
  • 管理员: 使用用户名/密码登录,访问 /admin/** 路径。
  • App 用户: 使用 Token 登录,访问 /app/** 路径。

我们需要三个独立的过滤器链,分别处理这三种类型的登录请求。

1. 定义用户角色和权限

首先,我们需要定义用户角色和权限。Spring Security 提供了 GrantedAuthority 接口来表示权限。我们可以使用枚举类来定义角色:

public enum Role {
    ROLE_USER,
    ROLE_ADMIN,
    ROLE_APP
}

2. 创建自定义过滤器

接下来,我们需要创建自定义的过滤器来处理不同类型的登录请求。

2.1 用户名/密码认证过滤器 (UserAuthenticationFilter)

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class UserAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public UserAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl, "POST")); // 只处理POST请求
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        // 创建 UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

        // 交给 AuthenticationManager 进行认证
        return getAuthenticationManager().authenticate(authRequest);
    }
}

2.2 Token 认证过滤器 (TokenAuthenticationFilter)

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private final String tokenHeader = "Authorization"; // 从请求头中获取Token

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

        String token = request.getHeader(tokenHeader);

        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7); // 去掉 "Bearer " 前缀

            // 验证Token的有效性 (这里需要调用Token验证服务)
            if (isValidToken(token)) {
                String username = getUsernameFromToken(token); // 从Token中获取用户名

                // 创建 Authentication 对象
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        username, null, Collections.singletonList(() -> "ROLE_APP")); //  赋予 ROLE_APP 角色

                // 将 Authentication 对象放入 SecurityContextHolder
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(request, response);
    }

    private boolean isValidToken(String token) {
        // TODO: 实现Token验证逻辑,例如调用Token验证服务
        // 这里只是一个示例,实际应用中需要根据具体的Token机制进行验证
        return true;
    }

    private String getUsernameFromToken(String token) {
        // TODO: 实现从Token中获取用户名的逻辑
        // 这里只是一个示例,实际应用中需要根据具体的Token机制进行解析
        return "appUser";
    }
}

3. 配置 AuthenticationManager

AuthenticationManager 负责处理认证请求。我们需要配置不同的 AuthenticationProvider 来处理不同类型的认证。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class AuthenticationConfig {

    @Autowired
    private UserDetailsService userDetailsService; // 用于加载用户信息的服务

    @Bean
    public AuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder()); // 设置密码加密器
        provider.setUserDetailsService(userDetailsService); // 设置 UserDetailsService
        return provider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 使用 BCrypt 加密密码
    }

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

4. 配置 WebSecurityConfigurerAdapter

这是配置 Spring Security 的核心部分。我们需要继承 WebSecurityConfigurerAdapter 并重写 configure(HttpSecurity http) 方法。

import org.springframework.beans.factory.annotation.Autowired;
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.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class MultiHttpSecurityConfig {

    @Configuration
    @Order(1) // 管理员的配置优先级最高
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private AuthenticationManager authenticationManager;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .antMatcher("/admin/**") // 只处理 /admin/** 路径
                    .authorizeRequests()
                    .anyRequest().hasRole("ADMIN") // 需要 ROLE_ADMIN 角色
                    .and()
                    .formLogin() // 使用表单登录
                    .loginProcessingUrl("/admin/login") // 登录请求处理URL
                    .defaultSuccessUrl("/admin/dashboard") // 登录成功后的跳转URL
                    .failureUrl("/admin/login?error") // 登录失败后的跳转URL
                    .permitAll()
                    .and()
                    .logout()
                    .logoutUrl("/admin/logout")
                    .permitAll()
                    .and()
                    .csrf().disable(); // 关闭 CSRF 保护 (这里为了简化配置,实际应用中需要开启)
        }
    }

    @Configuration
    @Order(2) // 普通用户的配置优先级次之
    public static class UserSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private AuthenticationManager authenticationManager;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .antMatcher("/user/**") // 只处理 /user/** 路径
                    .authorizeRequests()
                    .anyRequest().hasRole("USER") // 需要 ROLE_USER 角色
                    .and()
                    .formLogin() // 使用表单登录
                    .loginProcessingUrl("/user/login") // 登录请求处理URL
                    .defaultSuccessUrl("/user/profile") // 登录成功后的跳转URL
                    .failureUrl("/user/login?error") // 登录失败后的跳转URL
                    .permitAll()
                    .and()
                    .logout()
                    .logoutUrl("/user/logout")
                    .permitAll()
                    .and()
                    .csrf().disable(); // 关闭 CSRF 保护 (这里为了简化配置,实际应用中需要开启)
        }
    }

    @Configuration
    @Order(3) // App 用户的配置优先级最低
    public static class AppSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            TokenAuthenticationFilter tokenAuthenticationFilter = new TokenAuthenticationFilter();
            http
                    .antMatcher("/app/**") // 只处理 /app/** 路径
                    .authorizeRequests()
                    .anyRequest().hasRole("APP") // 需要 ROLE_APP 角色
                    .and()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置为无状态,不创建Session
                    .and()
                    .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // 将 TokenAuthenticationFilter 添加到过滤器链中
                    .csrf().disable(); // 关闭 CSRF 保护
        }
    }
}

关键点解释:

  • @Order 注解: 用于指定配置类的优先级。优先级高的配置类会先执行。
  • antMatcher 方法: 用于指定当前配置类处理的请求路径。
  • authorizeRequests 方法: 用于配置授权规则。
  • formLogin 方法: 用于配置表单登录。
  • sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS): 对于 App 用户,我们使用 Token 认证,因此需要设置为无状态,不创建 Session。
  • addFilterBefore 方法: 用于将自定义的 TokenAuthenticationFilter 添加到过滤器链中。UsernamePasswordAuthenticationFilter.class 表示将 TokenAuthenticationFilter 添加到 UsernamePasswordAuthenticationFilter 之前。

5. 创建 UserDetailsService

UserDetailsService 负责从数据库或其他数据源中加载用户信息。

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Collections;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 模拟从数据库中加载用户信息
        if ("admin".equals(username)) {
            return new User("admin", new BCryptPasswordEncoder().encode("admin"), Collections.singletonList(() -> "ROLE_ADMIN"));
        } else if ("user".equals(username)) {
            return new User("user", new BCryptPasswordEncoder().encode("user"), Collections.singletonList(() -> "ROLE_USER"));
        } else {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
    }
}

流程梳理

  1. 请求到达: 当一个请求到达 Spring Security 时,它会首先匹配最先定义的 antMatcher
  2. 过滤器链选择: 根据 antMatcher 的匹配结果,选择对应的过滤器链。
  3. 过滤器执行: 请求依次经过过滤器链中的各个过滤器。
    • 对于 /admin/**/user/** 路径,UsernamePasswordAuthenticationFilter 会处理登录请求,并调用 AuthenticationManager 进行认证。
    • 对于 /app/** 路径,TokenAuthenticationFilter 会从请求头中获取 Token,验证 Token 的有效性,并设置 SecurityContextHolder
  4. 授权验证: 经过认证后,Spring Security 会根据用户的角色和权限,以及请求的路径,进行授权验证。
  5. 访问资源: 如果用户通过了认证和授权,就可以访问对应的资源。

代码结构一览

以下表格总结了上述代码示例中的主要类和接口,以及它们的作用:

类/接口 作用
Role 定义用户角色
UserAuthenticationFilter 处理用户名/密码认证
TokenAuthenticationFilter 处理 Token 认证
AuthenticationManager 认证管理器,负责处理认证请求
AuthenticationProvider 认证提供者,用于实现具体的认证逻辑
UserDetailsService 用户信息服务,用于从数据库或其他数据源中加载用户信息
WebSecurityConfigurerAdapter Web 安全配置适配器,用于配置 Spring Security 的行为

扩展与改进

  • 自定义 AuthenticationProvider: 如果需要支持更复杂的认证方式,可以自定义 AuthenticationProvider
  • OAuth 2.0 集成: 可以集成 OAuth 2.0 来支持第三方登录。
  • 细粒度的权限控制: 可以使用 Spring Security 的表达式语言来实现更细粒度的权限控制。
  • 异常处理: 需要添加适当的异常处理机制,例如处理认证失败、授权失败等情况。

总结

通过自定义过滤器链,我们可以将复杂的认证和授权逻辑分解成多个独立的模块,每个模块只负责处理特定的任务。这种方式可以提高代码的可读性、可维护性和可扩展性,使得 Spring Security 能够更好地适应各种复杂的应用场景。希望今天的分享能够帮助大家更好地理解和应用 Spring Security。

关键点回顾

  • 使用 @Order 注解定义多个 WebSecurityConfigurerAdapter,实现不同的过滤器链。
  • 使用 antMatcher 方法指定每个过滤器链处理的请求路径。
  • 自定义过滤器来处理不同的认证方式,例如用户名/密码认证、Token 认证等。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注