Spring Security:定制Filter Chain实现微服务中的细粒度鉴权

大家好,今天我们来深入探讨如何利用 Spring Security 定制 Filter Chain,以实现微服务架构下的细粒度鉴权。在微服务架构中,鉴权不再是单体应用中简单的一层拦截,而是需要考虑服务间的调用、权限的动态变化、以及更精细化的资源访问控制。Spring Security 强大的可定制性为我们提供了灵活的解决方案。

一、微服务鉴权面临的挑战

在单体应用中,通常使用一个全局的 Filter 或 Interceptor 来完成身份验证和授权。但在微服务架构下,这种方式存在诸多问题:

  • 重复鉴权逻辑: 每个服务都需要实现相似的鉴权逻辑,导致代码冗余和维护困难。
  • 权限同步问题: 当权限发生变化时,需要更新所有服务的权限配置,容易出现不一致。
  • 粗粒度鉴权: 无法针对服务内部的不同资源进行细粒度控制,例如限制用户只能访问某个服务的特定接口或数据。
  • 服务间信任问题: 服务间的调用需要建立信任关系,确保调用方具有访问权限。

二、Spring Security Filter Chain 的核心概念

Spring Security 的核心在于 Filter Chain。一个 Filter Chain 由多个 Filter 组成,每个 Filter 负责处理特定的安全逻辑,例如身份验证、授权、CSRF 防护等。请求会依次经过 Filter Chain 中的每个 Filter,最终到达目标资源。

理解几个关键概念:

  • SecurityFilterChain 定义了一组 Filter 的执行顺序和匹配规则。可以配置多个 SecurityFilterChain,每个 SecurityFilterChain 对应不同的请求路径或匹配条件。
  • Filter 实现 javax.servlet.Filter 接口,负责执行具体的安全逻辑。Spring Security 提供了许多内置的 Filter,例如 UsernamePasswordAuthenticationFilterAuthorizationFilter 等。
  • Authentication 代表已认证的用户信息,包含用户名、密码、权限等。
  • GrantedAuthority 代表用户的权限,例如 ROLE_ADMINREAD_RESOURCE 等。
  • AuthenticationManager 负责验证用户的身份,例如通过用户名和密码验证。
  • AccessDecisionManager 负责根据用户的权限和请求资源,决定是否允许访问。

三、定制 Filter Chain 实现细粒度鉴权

要实现微服务中的细粒度鉴权,我们需要定制 Filter Chain,使其能够处理以下逻辑:

  1. 身份验证: 验证用户的身份,通常通过 JWT (JSON Web Token) 或 OAuth 2.0 等协议。
  2. 权限解析: 从 JWT 或 OAuth 2.0 令牌中解析用户的权限信息。
  3. 资源权限匹配: 将用户的权限与请求的资源进行匹配,判断用户是否具有访问权限。
  4. 服务间鉴权: 验证服务间的调用请求,确保调用方具有访问权限。

下面我们通过代码示例来演示如何定制 Filter Chain:

1. 定义权限枚举:

public enum Authority {
    READ_USER,
    WRITE_USER,
    READ_PRODUCT,
    WRITE_PRODUCT,
    // 其他权限
}

2. 创建 JWT 验证 Filter:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            String token = authorizationHeader.substring(7); // Remove "Bearer " prefix

            try {
                Claims claims = Jwts.parserBuilder()
                        .setSigningKey(jwtSecret.getBytes())
                        .build()
                        .parseClaimsJws(token)
                        .getBody();

                String username = claims.getSubject();
                List<String> authorities = (List<String>) claims.get("authorities"); // Assuming "authorities" is the claim name for authorities

                if (username != null && authorities != null) {
                    List<GrantedAuthority> grantedAuthorities = authorities.stream()
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());

                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, grantedAuthorities);
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }

            } catch (Exception e) {
                // Token is invalid or expired
                SecurityContextHolder.clearContext(); // Clear context if token is invalid
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("Invalid JWT token.");
                return; // Stop processing the filter chain
            }
        }

        filterChain.doFilter(request, response); // Continue processing the filter chain
    }
}

3. 配置 Spring Security:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // Disable CSRF for API
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Set session management to stateless
            .authorizeHttpRequests(authz -> authz
                .requestMatchers(HttpMethod.GET, "/products/**").hasAuthority("READ_PRODUCT")
                .requestMatchers(HttpMethod.POST, "/products/**").hasAuthority("WRITE_PRODUCT")
                .requestMatchers(HttpMethod.GET, "/users/**").hasAuthority("READ_USER")
                .requestMatchers(HttpMethod.POST, "/users/**").hasAuthority("WRITE_USER")
                .anyRequest().authenticated() // All other requests require authentication
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // Add JWT filter before UsernamePasswordAuthenticationFilter

        return http.build();
    }
}

4. 解释说明:

  • JwtAuthenticationFilter:这是一个自定义的 Filter,用于从请求头中提取 JWT,验证 JWT 的有效性,并解析 JWT 中包含的用户名和权限信息。然后,它将这些信息设置到 SecurityContextHolder 中,以便后续的授权过程可以使用。
  • SecurityConfig:这是一个配置类,用于配置 Spring Security。
    • csrf().disable():禁用 CSRF 保护,因为我们是在 API 场景下使用 JWT,不需要 CSRF 保护。
    • sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS):设置 Session 创建策略为 STATELESS,因为我们使用 JWT 进行身份验证,不需要使用 Session。
    • authorizeHttpRequests():配置请求的授权规则。
      • .requestMatchers(HttpMethod.GET, "/products/**").hasAuthority("READ_PRODUCT"):允许具有 READ_PRODUCT 权限的用户访问 /products 路径下的所有 GET 请求。
      • .requestMatchers(HttpMethod.POST, "/products/**").hasAuthority("WRITE_PRODUCT"):允许具有 WRITE_PRODUCT 权限的用户访问 /products 路径下的所有 POST 请求。
      • .anyRequest().authenticated():所有其他的请求都需要进行身份验证。
    • addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class):将 JwtAuthenticationFilter 添加到 UsernamePasswordAuthenticationFilter 之前。这意味着 JwtAuthenticationFilter 会先于 UsernamePasswordAuthenticationFilter 执行,先进行JWT验证。

5. 服务间鉴权:

服务间鉴权通常使用 Mutual TLS (mTLS) 或 API Key 等方式。

  • Mutual TLS (mTLS): 每个服务都拥有自己的证书,服务间的调用需要验证对方的证书,确保调用方是可信任的服务。

    • 配置步骤:
      1. 为每个服务生成证书。
      2. 配置 Spring Boot 应用的 server.ssl 属性,指定证书和密钥库。
      3. 配置 RestTemplateWebClient,使其使用客户端证书进行调用。
  • API Key: 每个服务都分配一个唯一的 API Key,服务间的调用需要在请求头中携带 API Key,接收方验证 API Key 的有效性,确保调用方是授权的服务。

    • 实现步骤:
      1. 生成 API Key 并存储在数据库或配置中心。
      2. 创建 Filter 或 Interceptor,从请求头中提取 API Key。
      3. 验证 API Key 的有效性,如果无效则拒绝请求。

四、更精细化的鉴权方式

除了基于权限的鉴权外,还可以使用以下方式实现更精细化的鉴权:

  • 基于角色的访问控制 (RBAC): 将权限赋予角色,然后将角色分配给用户。
  • 基于属性的访问控制 (ABAC): 根据用户的属性、资源的属性和环境属性来决定是否允许访问。例如,只有在特定时间段内,特定部门的用户才能访问某个资源。
  • Spring Expression Language (SpEL): 使用 SpEL 表达式来定义复杂的权限规则。可以在 @PreAuthorize@PostAuthorize 注解中使用 SpEL 表达式。

五、代码示例:基于SpEL的权限控制

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @PreAuthorize("hasRole('ADMIN') or #username == authentication.name")
    public String getProduct(String productId, String username) {
        // Only ADMIN role or the user with the same username can access the product
        return "Product " + productId;
    }
}

在这个例子中,getProduct 方法使用了 @PreAuthorize 注解,它使用 SpEL 表达式来定义权限规则。只有具有 ADMIN 角色的用户或者用户名与请求用户名相同的用户才能访问该方法。authentication.name 会自动解析为当前用户的用户名。

六、一些需要注意的点

  • 避免在 Filter 中执行耗时操作: Filter 的性能直接影响整个应用的性能,因此应避免在 Filter 中执行耗时的操作,例如访问数据库或调用外部服务。
  • 正确处理异常: 在 Filter 中捕获异常,并返回合适的 HTTP 状态码和错误信息。
  • 保护 JWT Secret: JWT Secret 是用于签名 JWT 的密钥,必须妥善保管,避免泄露。可以使用环境变量或配置中心来存储 JWT Secret。
  • 考虑 Token 的过期时间: 设置合理的 Token 过期时间,避免 Token 被长期滥用。
  • 使用 HTTPS: 使用 HTTPS 协议来保护 Token 在传输过程中的安全。
  • 服务间认证方案选择: 根据实际情况选择合适的认证方案,例如 mTLS 或 API Key。
  • 监控和审计: 完善的监控和审计机制可以帮助我们及时发现和处理安全问题。

七、表格:各种鉴权方式的对比

鉴权方式 优点 缺点 适用场景
JWT 轻量级,无状态,易于扩展 需要妥善保管 Secret,Token 泄露会导致安全风险 单点登录,API 鉴权
OAuth 2.0 支持第三方授权,用户体验好 配置复杂,需要维护 Authorization Server 第三方应用授权,开放 API
mTLS 安全性高,双向认证 配置复杂,需要维护证书 服务间调用,高安全性场景
API Key 简单易用 安全性相对较低,容易被伪造 服务间调用,对安全性要求不高的场景
RBAC 易于管理,角色权限分离 权限控制粒度较粗 权限管理较为简单的场景
ABAC 权限控制粒度细,灵活性高 配置复杂,性能开销大 权限管理复杂的场景,例如金融、医疗等
SpEL 可以自定义复杂的权限规则 学习成本高,容易出错 需要灵活的权限控制策略,例如基于用户属性、资源属性和环境属性进行判断的场景

八、总结:细粒度鉴权的关键

定制 Spring Security Filter Chain 是实现微服务细粒度鉴权的关键。我们需要根据实际需求选择合适的身份验证方式、权限管理策略和资源保护机制。通过精细化的权限控制,可以有效保护微服务架构中的敏感数据和资源,确保系统的安全性和可靠性。

希望今天的分享对大家有所帮助。谢谢!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐