基于Filter+RBAC+AntPathMatcher的微服务架构中的通用认证与授权模块设计与深度分析

372 阅读8分钟

微服务架构中的通用认证与授权模块设计与深度分析

在微服务架构中,认证与授权是保障系统安全的核心。本文深入分析一个通用认证与授权模块的设计与实现,结合代码细节,探讨其技术选型、技术原理及优化空间,并通过模拟面试官的视角,从业务需求到代码实现进行全面剖析。

业务需求分析

需求背景

微服务架构下,各服务独立部署,需统一管理用户认证与授权:

  • 统一认证:验证用户身份,确保请求合法。
  • 动态授权:基于用户角色和权限,控制资源访问。
  • 高复用性:模块需以JAR包形式供多个微服务引入。
  • 安全性:支持内部调用校验(如Feign)、IP白名单等。
  • 灵活性:支持配置无需认证的路径。

技术选型

  1. 认证机制

    • JWT Token:无状态,适合分布式环境,易于扩展。
    • Feign客户端:调用远程认证服务,适配微服务架构。
  2. 授权机制

    • RBAC:基于角色的访问控制,简单且灵活。
    • 远程权限服务:通过Feign调用,保持权限数据集中管理。
  3. 过滤器

    • Servlet Filter:Spring生态支持,适合拦截HTTP请求。
  4. 路径匹配

    • AntPathMatcher:Spring提供的路径匹配工具,支持Ant风格模式(如/api/**)。
  5. 上下文管理

    • ThreadLocal:存储用户信息,线程安全,适合请求上下文。

代码分析

模块功能

模块通过AuthFilter实现认证与授权:

  • Token校验:验证请求头中的Token。
  • 路径排除:支持配置无需认证的路径。
  • Feign校验:验证内部调用的密钥和IP。
  • RBAC授权:动态校验权限。
  • 上下文管理:存储用户信息,供下游使用。
核心代码解析
@Component
public class AuthFilter implements Filter {
    @Autowired
    private AuthConfigAdapter authConfigAdapter;
    @Autowired
    private HttpHandler httpHandler;
    @Autowired
    private AuthFeignClient authFeignClient;
    @Autowired
    private RbacFeignClient rbacFeignClient;
    @Autowired
    private FeignInsideAuthConfig feignInsideAuthConfig;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        if (!feignRequestCheck(req)) {
            httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
            return;
        }

        if (Auth.TOKEN_CHECK_URI.equals(req.getRequestURI())) {
            chain.doFilter(req, resp);
            return;
        }

        List<String> excludePathPatterns = authConfigAdapter.excludePathPatterns();
        if (CollectionUtil.isNotEmpty(excludePathPatterns)) {
            for (String excludePathPattern : excludePathPatterns) {
                AntPathMatcher pathMatcher = new AntPathMatcher();
                if (pathMatcher.match(excludePathPattern, req.getRequestURI())) {
                    chain.doFilter(req, resp);
                    return;
                }
            }
        }

        String accessToken = req.getHeader("Authorization");
        if (StrUtil.isBlank(accessToken)) {
            httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
            return;
        }

        ServerResponseEntity<UserInfoBO> userInfoResponse = authFeignClient.checkToken(accessToken);
        if (!userInfoResponse.isSuccess()) {
            httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
            return;
        }

        UserInfoBO userInfo = userInfoResponse.getData();
        if (!checkRbac(userInfo, req.getRequestURI(), req.getMethod())) {
            httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
            return;
        }

        try {
            AuthUserContext.set(userInfo);
            chain.doFilter(req, resp);
        } finally {
            AuthUserContext.clean();
        }
    }
}
1. 过滤器初始化
  • 技术原理@Component注册Spring Bean,@Autowired注入依赖,体现IoC。
  • 设计考量AuthConfigAdapter使用适配器模式,允许微服务自定义排除路径,增强扩展性。
2. Feign请求校验
private boolean feignRequestCheck(HttpServletRequest req) {
    if (!req.getRequestURI().startsWith(FeignInsideAuthConfig.FEIGN_INSIDE_URL_PREFIX)) {
        return true;
    }
    String feignInsideSecret = req.getHeader(feignInsideAuthConfig.getKey());
    if (StrUtil.isBlank(feignInsideSecret) || !Objects.equals(feignInsideSecret, feignInsideAuthConfig.getSecret())) {
        return false;
    }
    List<String> ips = feignInsideAuthConfig.getIps();
    ips.removeIf(StrUtil::isBlank);
    if (CollectionUtil.isNotEmpty(ips) && !ips.contains(IpHelper.getIpAddr())) {
        logger.error("ip not in ip White list: {}, ip, {}", ips, IpHelper.getIpAddr());
        return false;
    }
    return true;
}
  • 技术原理

    • URL前缀匹配:通过字符串前缀判断Feign请求,简单高效。
    • 密钥校验:使用配置的密钥,防止未授权调用。
    • IP白名单:通过IpHelper.getIpAddr()获取客户端IP,与白名单比较。
  • 设计考量

    • 双重校验(密钥+IP)提升安全性。
    • 日志记录非法IP,便于审计。
3. 路径排除
  • AntPathMatcher

    • Spring提供的路径匹配工具,支持Ant风格模式:

      • ?:匹配单个字符。
      • *:匹配0或多个字符。
      • **:匹配0或多个路径段。
    • 例:/api/**匹配/api/users/api/users/123,但不匹配/admin/users

  • 技术原理

    • AntPathMatcher.match()基于正则表达式,高效匹配URI。
    • 每次循环创建新实例,避免线程安全问题。
  • 设计考量

    • 支持动态配置排除路径,适合公共API或静态资源。
    • 遍历匹配效率较低,可优化为前缀树或缓存。
4. Token校验
  • 技术原理

    • Authorization头获取Token,符合HTTP标准。
    • Feign调用远程服务,适配分布式架构。
  • 设计考量

    • 校验失败直接返回UNAUTHORIZED,符合RESTful规范。
    • 可引入本地缓存减少Feign调用。
5. RBAC授权
public boolean checkRbac(UserInfoBO userInfo, String uri, String method) {
    if (!Objects.equals(SysTypeEnum.TYPE1.value(), userInfo.getSysType()) && 
        !Objects.equals(SysTypeEnum.TYPE2.value(), userInfo.getSysType())) {
        return true;
    }
    ServerResponseEntity<Boolean> response = rbacFeignClient.checkPermission(
        userInfo.getUserId(), userInfo.getSysType(), uri, userInfo.getIsAdmin(), 
        HttpMethodEnum.valueOf(method.toUpperCase()).value());
    return response.isSuccess() && response.getData();
}
  • 技术原理

    • 特定系统类型(TYPE1TYPE2)需权限校验,体现业务针对性。
    • Feign调用权限服务,保持权限数据集中管理。
  • 设计考量

    • HTTP方法转换为枚举,规范化输入。
    • 可优化为批量校验,减少Feign调用。
6. 上下文管理
  • 技术原理

    • AuthUserContext基于ThreadLocal,确保线程隔离。
    • try-finally清理上下文,防止内存泄漏。
  • 设计考量

    • 适合请求级上下文传递,易于下游服务获取用户信息。

模拟面试官拷问

1. 业务需求与技术选型

Q:为什么选择Servlet Filter而非Spring Security?

  • A:Servlet Filter轻量,适合简单拦截逻辑。Spring Security功能强大但配置复杂,增加学习成本。本模块目标是轻量级JAR包,Filter更易集成。
  • Follow-up:Filter如何处理高并发?
  • A:Filter本身线程安全,doFilter每次处理独立请求。AntPathMatcher创建新实例避免并发问题。需注意Feign调用的性能瓶颈,可引入缓存或异步处理。

Q:为什么用AntPathMatcher而非正则表达式?

  • A:AntPathMatcher是Spring内置工具,语法简洁(/**等),性能优化好,适合URI匹配。正则表达式更灵活但编写复杂,易出错。
  • Follow-up:AntPathMatcher的性能如何?
  • A:基于正则编译,单次匹配O(n)。遍历所有排除路径时复杂度为O(n*m),n为路径长度,m为排除路径数。可通过前缀树优化到O(n)。

2. 代码细节

Q:为什么每次循环创建新的AntPathMatcher?

  • AAntPathMatcher非线程安全,循环内创建避免并发问题。Spring文档建议按需创建。
  • Follow-up:这会导致性能问题吗?
  • A:创建成本低(无复杂初始化),但频繁创建可优化。解决方案是将AntPathMatcher作为单例,同步访问,或预编译路径模式。

Q:Feign请求校验的双重机制(密钥+IP)是否必要?

  • A:必要。密钥校验防止未授权调用,IP白名单限制调用来源,结合使用提升安全性。微服务内部调用易被伪造,双重校验降低风险。
  • Follow-up:IP白名单如何应对动态IP场景?
  • A:当前实现不支持动态IP,可扩展为从配置中心动态加载白名单,或支持CIDR格式匹配。

Q:Token校验为何不本地解析JWT?

  • A:远程校验将Token解析逻辑集中到认证服务,便于统一管理(如密钥轮换)。本地解析需各服务同步密钥,增加维护成本。

  • Follow-up:远程调用失败怎么办?

  • A:当前直接返回UNAUTHORIZED,可优化为:

    • 重试机制:Feign内置重试。
    • 降级策略:本地缓存最近验证的Token。
    • 熔断器:如Hystrix,防止服务雪崩。

Q:RBAC校验为何只针对特定系统类型?

  • A:业务需求决定,TYPE1TYPE2可能为管理端或多店铺场景,需严格权限控制。其他类型(如普通用户)可能无需权限校验,优化性能。
  • Follow-up:如何扩展到更复杂权限模型?
  • A:可引入ABAC(属性访问控制),结合用户属性、资源属性动态判断。需扩展checkRbac参数,增加属性校验逻辑。

Q:上下文清理为何用try-finally

  • AThreadLocal数据线程隔离,但不自动清理。try-finally确保每次请求后清理,防止内存泄漏,尤其在Tomcat线程池复用场景。
  • Follow-up:有无替代方案?
  • A:可使用Spring的RequestContextHolder,但仍需清理。或改用传递式上下文(如Header传递用户信息),避免ThreadLocal。

3. 优化与扩展

Q:如何优化Feign调用性能?

  • A

    • 缓存:使用Caffeine缓存Token和权限校验结果,设置合理TTL。
    • 批量调用:将多个权限校验合并为单次Feign调用。
    • 异步处理:Feign支持异步客户端,降低阻塞时间。
  • Follow-up:缓存失效如何处理?

  • A:失效后重新调用远程服务,需配置缓存刷新策略(如定时刷新高频Token)。

Q:如何支持动态配置?

  • A:将excludePathPatternsFeignInsideAuthConfig移到配置中心(如Nacos),支持热更新。需监听配置变更,动态刷新过滤器配置。
  • Follow-up:热更新如何保证线程安全?
  • A:使用CopyOnWriteArrayList存储配置,或通过ReentrantLock同步更新。

优化建议

  1. 性能

    • 引入本地缓存(如Caffeine)减少Feign调用。
    • 优化路径匹配,使用前缀树替代遍历。
  2. 安全性

    • 支持动态IP白名单,适配云环境。
    • 增加Feign调用超时与重试配置。
  3. 扩展性

    • 支持动态配置加载,从配置中心获取路径和白名单。
    • 扩展RBAC到ABAC,支持更复杂权限模型。
  4. 可观测性

    • 集成Prometheus,监控认证成功率、耗时。
    • 细化日志,区分INFO和ERROR。

总结

通用认证与授权模块通过Servlet Filter、AntPathMatcher和Feign客户端,实现了微服务架构下的统一安全管理。其设计兼顾复用性、安全性和扩展性,适配分布式环境。未来可通过缓存、异步处理和动态配置进一步优化,为复杂业务场景提供支持。