微服务架构中的通用认证与授权模块设计与深度分析
在微服务架构中,认证与授权是保障系统安全的核心。本文深入分析一个通用认证与授权模块的设计与实现,结合代码细节,探讨其技术选型、技术原理及优化空间,并通过模拟面试官的视角,从业务需求到代码实现进行全面剖析。
业务需求分析
需求背景
微服务架构下,各服务独立部署,需统一管理用户认证与授权:
- 统一认证:验证用户身份,确保请求合法。
- 动态授权:基于用户角色和权限,控制资源访问。
- 高复用性:模块需以JAR包形式供多个微服务引入。
- 安全性:支持内部调用校验(如Feign)、IP白名单等。
- 灵活性:支持配置无需认证的路径。
技术选型
-
认证机制:
- JWT Token:无状态,适合分布式环境,易于扩展。
- Feign客户端:调用远程认证服务,适配微服务架构。
-
授权机制:
- RBAC:基于角色的访问控制,简单且灵活。
- 远程权限服务:通过Feign调用,保持权限数据集中管理。
-
过滤器:
- Servlet Filter:Spring生态支持,适合拦截HTTP请求。
-
路径匹配:
- AntPathMatcher:Spring提供的路径匹配工具,支持Ant风格模式(如
/api/**)。
- AntPathMatcher:Spring提供的路径匹配工具,支持Ant风格模式(如
-
上下文管理:
- 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();
}
-
技术原理:
- 特定系统类型(
TYPE1、TYPE2)需权限校验,体现业务针对性。 - 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?
- A:
AntPathMatcher非线程安全,循环内创建避免并发问题。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:业务需求决定,
TYPE1和TYPE2可能为管理端或多店铺场景,需严格权限控制。其他类型(如普通用户)可能无需权限校验,优化性能。 - Follow-up:如何扩展到更复杂权限模型?
- A:可引入ABAC(属性访问控制),结合用户属性、资源属性动态判断。需扩展
checkRbac参数,增加属性校验逻辑。
Q:上下文清理为何用try-finally?
- A:
ThreadLocal数据线程隔离,但不自动清理。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:将
excludePathPatterns和FeignInsideAuthConfig移到配置中心(如Nacos),支持热更新。需监听配置变更,动态刷新过滤器配置。 - Follow-up:热更新如何保证线程安全?
- A:使用
CopyOnWriteArrayList存储配置,或通过ReentrantLock同步更新。
优化建议
-
性能:
- 引入本地缓存(如Caffeine)减少Feign调用。
- 优化路径匹配,使用前缀树替代遍历。
-
安全性:
- 支持动态IP白名单,适配云环境。
- 增加Feign调用超时与重试配置。
-
扩展性:
- 支持动态配置加载,从配置中心获取路径和白名单。
- 扩展RBAC到ABAC,支持更复杂权限模型。
-
可观测性:
- 集成Prometheus,监控认证成功率、耗时。
- 细化日志,区分INFO和ERROR。
总结
通用认证与授权模块通过Servlet Filter、AntPathMatcher和Feign客户端,实现了微服务架构下的统一安全管理。其设计兼顾复用性、安全性和扩展性,适配分布式环境。未来可通过缓存、异步处理和动态配置进一步优化,为复杂业务场景提供支持。