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);
}
}
}
流程梳理
- 请求到达: 当一个请求到达 Spring Security 时,它会首先匹配最先定义的
antMatcher。 - 过滤器链选择: 根据
antMatcher的匹配结果,选择对应的过滤器链。 - 过滤器执行: 请求依次经过过滤器链中的各个过滤器。
- 对于
/admin/**和/user/**路径,UsernamePasswordAuthenticationFilter会处理登录请求,并调用AuthenticationManager进行认证。 - 对于
/app/**路径,TokenAuthenticationFilter会从请求头中获取 Token,验证 Token 的有效性,并设置SecurityContextHolder。
- 对于
- 授权验证: 经过认证后,Spring Security 会根据用户的角色和权限,以及请求的路径,进行授权验证。
- 访问资源: 如果用户通过了认证和授权,就可以访问对应的资源。
代码结构一览
以下表格总结了上述代码示例中的主要类和接口,以及它们的作用:
| 类/接口 | 作用 |
|---|---|
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 认证等。