news 2026/9/28 13:26:58

Spring Cloud Gateway登录校验实战:自定义过滤器与JWT鉴权全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Cloud Gateway登录校验实战:自定义过滤器与JWT鉴权全解析

微服务拆着拆着,大家迟早会遇到同一个尴尬:登录校验到底放哪?单体的时代一个Session过滤器搞定一切,拆成微服务之后,用户服务管登录,订单服务要验身份,商品服务也要知道操作人是谁——总不能每个服务各写一套校验逻辑,到时候密钥、规则、异常处理全都对不上,维护起来就是一场灾难。Spring Cloud Gateway作为整个微服务体系的统一入口,天然最适合干这件事。这篇教程我直接围绕网关登录校验展开,重点带你写自定义过滤器——GlobalFilter和GatewayFilter,从原理到落地代码一路捋清楚,并把我在实际项目里踩过的坑一并交代了。

这套内容适合谁看?正在搭Spring Cloud微服务、手头有网关场景但还没做统一鉴权的同学,或者已经在用Gateway但总觉得过滤器执行顺序、路径匹配这些细节不够通透的人。我会先把整体设计思路讲明白,再逐个拆解过滤器的区别和登录校验的实现细节,最后给出一套可以直接抄的代码,以及几个典型的排查案例。

1. 网关登录校验的整体设计思路

1.1 为什么登录校验必须下沉到网关

在做微服务拆分的时候,很多人容易陷入一个误区:把业务拆得很细,却把通用逻辑留在各个服务里重复实现。登录校验就是典型的通用逻辑。想一想,一张订单的创建接口需要判断用户是否登录,商品的详情接口可能也需要暴露给登录用户看到不同的价格,这些场景在每个服务里都存在。

如果把校验逻辑放在每个微服务内部,你会遇到三连问题:第一,代码重复,每个服务都要引入JWT解析依赖、写一遍过滤器或拦截器;第二,标准不统一,A服务校验失败返回401,B服务返回403,前端对接起来很痛苦;第三,扩展性差,如果将来公司要求统一启用某种风控策略或者签名校验,你得改多少个服务?

网关这层就完美规避了上述问题。请求进入系统的大门只有一个,就是Gateway网关。在这个节点完成登录校验,相当于给整个系统的后端服务装上了一道统一的安检门。用户请求到达网关时,先判断这个路径需不需要登录,如果需要就验证携带的令牌是否合法,合法才放行到下游服务,否则直接返回401。下游服务拿到的请求,已经是经过了认证的请求,各业务模块不必再关心“你是谁”,只需要关心“你能干什么”。

这里要额外说明一点:网关做的是认证(Authentication),也就是确认身份。而授权(Authorization),也就是判断有没有权限执行某个操作,这个建议放在业务层做。因为权限模型往往和具体业务强相关,是用户角色判断还是资源归属判断,网关这种通用组件很难写死。简单项目可以在网关做角色级的粗粒度控制,但一旦权限逻辑复杂起来,硬塞进网关只会把网关代码搞得又臭又长。

1.2 过滤器家族:GlobalFilter 与 GatewayFilter 的分工

Spring Cloud Gateway的过滤机制是它最灵活的地方。在网关里,过滤器可以分为两大类:GlobalFilter和GatewayFilter,字面上都有“Filter”但职责很不一样。

GlobalFilter是全局过滤器,它对所有转发到网关的请求生效,只要网关接收了请求,就会进入全局过滤器的处理链。因为这种“无差别覆盖”的特性,统一登录校验、全局日志追踪、请求耗时统计这些横切逻辑,几乎都会用GlobalFilter来实现。你不需要在路由配置里挂任何额外的东西,只要把GlobalFilter实现类注册成Spring Bean,它就自动生效了。

GatewayFilter则是针对特定路由生效的过滤器。它的粒度更细,只对配置了该过滤器的路由起作用。比如你希望某个服务接口在转发前做参数签名校验,但其他服务不需要,那就很适合用GatewayFilter绑到那条路由上。另一个常见场景是灰度发布,只对灰度版本的服务路由附加某些特殊处理逻辑。

打个不太严谨但好记的比方:GlobalFilter像小区门口的保安,每个进小区的人他都要过目;GatewayFilter像单元楼的门禁,只有该单元的住户才需要交互。一个是全覆盖,一个是按需触发。

2. 自定义过滤器的核心细节与原理

2.1 GlobalFilter 与 GatewayFilter 的区别详解

很多初学者会把这两个过滤器的概念搞混,这里我用一张对比表把关键差异列出来,后续看代码的时候心里也更有谱。

对比维度GlobalFilterGatewayFilter
作用范围所有经过网关的路由仅绑定到特定路由
生效方式实现接口并注册为Bean后自动生效需要在路由配置中显式指定
配置位置Java代码中完成注册路由配置的filters段中声明
典型场景登录校验、日志链路、限流特定服务的签名校验、灰度逻辑
有没有Order有,实现Ordered接口指定执行顺序有,同一个路由内多个过滤器也有顺序

两条腿走路的时候要特别注意执行顺序的问题。Spring Cloud Gateway的过滤器链是有顺序的,Order值越小越先执行。内置了很多过滤器,NettyRoutingFilter之类的默认Order都不小。自定义GlobalFilter的时候,如果想让它尽早执行,Order要设成负数,比如-100。如果设成正数,很可能请求已经经过路由匹配甚至开始转发链路了,你拦截的意义就弱了。

还有一个特别容易踩的点:GlobalFilter和GatewayFilter虽然叫“过滤器”,但它们的语法是基于Spring WebFlux的响应式编程模型,方法签名返回的是Mono<Void>,而不是传统Servlet环境下的void。这意味着你不能在过滤器里直接做阻塞式操作,比如用Thread.sleep、调用同步的JDBC接口,这会把事件循环线程卡住。想查Redis或者访问数据库,请务必用响应式客户端。

2.2 登录校验的完整链路设计

有了过滤器的理解,现在来设计登录校验的完整链路。这套链路是目前微服务项目里最常见也最稳妥的做法:

第一步,客户端发起请求,带上前端保存的令牌,一般放在Authorization请求头里,格式是Bearer <token>。

第二步,请求到达网关,GlobalFilter先取到请求路径,拿这个路径和白名单做比对。白名单里的路径比如登录接口、注册接口、验证码接口、静态资源,是允许匿名访问的,直接放行到下游。白名单之外的路径,必须要有合法令牌。

第三步,从Authorization头中取出令牌,做三件事来验证真伪:一是验签,确认这个令牌确实是服务端签发的;二是看有效期,确认令牌没有过期;三是按业务需求检查Redis中的会话状态,比如用户是否被强制下线,令牌是否在黑名单里。

第四步,验证通过后,把令牌里携带的用户ID、用户角色等信息解析出来,写入转发请求的Header中,然后透传给下游微服务。这样下游服务根本不关心令牌是什么,直接读X-User-Id请求头就能知道当前操作人。

第五步,验证失败或者令牌缺失,直接返回401响应,并附带一条明确的错误信息提示。前端拿到401就知道要跳登录页或者刷新令牌了,谁还会去关心后端服务长什么样?

这套链路看得复杂,其实核心就是“白名单放行 + 令牌校验 + 身份透传”。在代码里,GlobalFilter一个类就能承载全部逻辑。

2.3 JWT 令牌:网关校验的信任基础

登录校验绕不开令牌方案,而JWT是目前网关鉴权场景里使用最广泛的选择。JWT全称是JSON Web Token,它本质上是一串由三部分组成的字符串:Header、Payload、Signature。Header里声明了令牌的算法类型,Payload里存放业务声明的字段,比如userId、userName、角色等,Signature则是用密钥对前两部分进行签名的结果。

JWT有个很实用的特性:它是自包含的。网关拿到JWT,不需要去用户服务里查数据库,只要用服务端密钥验一下签名,再检查一下Expiration时间,就能确认令牌是否合法。这种设计在微服务的高频调用场景下非常友好,验签的耗时和本地计算相比几乎可以忽略,不像Session方案每一次校验都要求一次Redis或者DB查询。

但自包含特性也带来了一个先天弱点:JWT一旦签发,在有效期内很难主动让它失效。Redis+JWT双校验模式可以缓解这个问题。网关在验签通过之后,再查一次Redis,看看该用户的token是否还在有效会话集合里。如果用户修改密码、被管理员封禁或者主动退出登录,服务端可以删除Redis中的会话记录,网关就能立刻把这个令牌判死。安全性要求高的系统,我强烈建议加上这一层。

3. 实操过程与核心代码实现

3.1 网关工程的基础环境准备

演示前先把工程搭起来。创建一个Spring Boot项目,Spring Boot版本用2.7.x或者3.x都可以,注意Spring Cloud版本要与之对应。这里我以Spring Cloud 2021.0.x + Spring Boot 2.7.x为例,这部分组合在实际生产里用得最普遍,资料也多。

pom.xml里引入网关核心依赖,不要引入spring-boot-starter-web,Spring Cloud Gateway基于WebFlux,如果同时引入Web MVC,启动会直接冲突报错,这是新手常踩的坑。

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> </dependency>

Redis响应式客户端是为了做令牌黑名单、会话状态校验用的,如果你的项目没到那个规模,也可以先不加,纯JWT验签也能跑通。但既然要写完整的生产级方案,我建议把Redis加上,反正依赖不大。

路由的基础配置写在application.yml里,下面这个是示例,把用户服务的路由挂上去,同时声明两个白名单路径。

server: port: 8080 spring: application: name: gateway-service cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path=/api/user/** - id: order-service uri: lb://order-service predicates: - Path=/api/order/** auth: white-list: - /api/auth/login - /api/auth/register jwt: secret: your-secret-key-please-change-in-production expire: 7200000

lb://前缀表示要负载均衡到这个服务名,这要求项目里已经引入了服务发现组件比如Nacos或Eureka,并且微服务能正常注册进来。没有注册中心的话可以先用uri: http://localhost:8081指向具体地址做本地联调,能把过滤器的逻辑跑起来再说。

3.2 自定义 GlobalFilter 实现登录校验

当前期准备做完之后,最核心的工作就是编写GlobalFilter过滤器了。先实现一个JWT工具类,负责令牌的生成和验证。实际项目中,生成令牌通常在用户服务里,网关这边只需要验签的方法,但工具类内部把两者都写了也没关系。

@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private Long expire; private SecretKey getSecretKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String generateToken(Map<String, Object> claims) { Date now = new Date(); Date expirationDate = new Date(now.getTime() + expire); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(expirationDate) .signWith(getSecretKey(), SignatureAlgorithm.HS256) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(getSecretKey()) .build() .parseClaimsJws(token) .getBody(); } }

注意这里的secret至少需要32位字节长度,否则使用HS256算法时会报错,因为该算法要求密钥长度不低于256位。很多人在本地测试图省事写个"abc"当密钥,启动一调用就抛WeakKeyException或者类似签名相关的异常,排查半天发现是密钥长度不够。

接着写登录校验的核心过滤器。这个类实现GlobalFilter和Ordered两个接口,核心逻辑按前文设计的链路来。

@Component public class AuthGlobalFilter implements GlobalFilter, Ordered { @Autowired private JwtUtil jwtUtil; @Autowired private ReactiveStringRedisTemplate redisTemplate; @Value("#{'${auth.white-list}'.split(',')}") private List<String> whiteList; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getURI().getPath(); // 白名单路径直接放行 if (whiteList.stream().anyMatch(path::startsWith)) { return chain.filter(exchange); } // 获取请求头中的令牌 String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StringUtils.isBlank(authHeader) || !authHeader.startsWith("Bearer ")) { return unauthorized(exchange, "missing or invalid token"); } String token = authHeader.substring(7); Claims claims; try { claims = jwtUtil.parseToken(token); } catch (Exception e) { return unauthorized(exchange, "token parse failed"); } String userId = claims.get("userId", String.class); // 可选增强:验证Redis中的会话状态 return redisTemplate.hasKey("login:token:" + userId).flatMap(exists -> { if (!Boolean.TRUE.equals(exists)) { return unauthorized(exchange, "token expired in redis"); } // 把用户信息透传给下游服务 ServerWebExchange newExchange = exchange.mutate() .request(builder -> builder.header("X-User-Id", userId)) .build(); return chain.filter(newExchange); }); } private Mono<Void> unauthorized(ServerWebExchange exchange, String message) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); DataBuffer buffer = exchange.getResponse().bufferFactory() .wrap(("{\"code\":401,\"msg\":\"" + message + "\"}").getBytes(StandardCharsets.UTF_8)); return exchange.getResponse().writeWith(Mono.just(buffer)); } @Override public int getOrder() { return -100; } }

代码里几个关键点我展开说一下。首先,白名单的匹配方式是startsWith,不是全路径等值。因为登录接口可能是/api/auth/login,而验证码接口可能是/api/auth/captcha,都属于/api/auth/这个前缀下,用前缀匹配会把它们一并放行,配置和维护都比较省事。如果你的某个接口路径存在歧义,再精确到全路径即可。

其次,透传用户信息的写法是exchange.mutate()返回一个新的ServerWebExchange。这里有个细节,请求头在mutate时通过request.builder操作,可以新增或者覆盖Header。下游服务只要约定好读取X-User-Id这个Header,就能拿到当前操作人的ID,不需要再解析JWT。服务之间调用链路如果有多层,这种透传的设计能省掉很多重复工作。

最后,也是响应式编程比较特殊的点:过滤器不能再通过return chain.filter(exchange)一放到底,需要中间查询的时候就通过Reacto的flatMap组合操作。Redis的hasKey返回的是Mono<Boolean>,所以后续的放行或拒绝逻辑都要写进flatMap回调里。如果你一不留神在里面调了exists.block(),网关上配置的工作线程池很容易被拖垮,高并发场景下Gateway的吞吐会直线下降,这是WebFlux和传统Servlet思维的显著差异。

3.3 自定义 GatewayFilter 绑定到指定路由

上一节演示了GlobalFilter,它的覆盖范围是全路由。但有的场景只希望某个路由做额外校验,或者做轻量级的定制处理,这时候就该上GatewayFilter了。我举一个很典型的例子:订单服务的关键接口要求额外的签名头,其他服务不要求,你就可以写一个专属订单服务的GatewayFilter。

GatewayFilter的自定义有两种姿势。第一种是直接在Java代码中组装路由时添加过滤器,第二种是通过实现GatewayFilterFactory接口,把过滤器做成可配置的Bean,在yml里声明。第二种方式更灵活,也更符合Gateway框架的风格,我用第二种来演示。

@Component public class SignatureGatewayFilterFactory extends AbstractGatewayFilterFactory<SignatureGatewayFilterFactory.Config> { public SignatureGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { String sign = exchange.getRequest().getHeaders().getFirst("X-Sign"); if (StringUtils.isBlank(sign) || !config.getSecret().equals(sign)) { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().setComplete(); } return chain.filter(exchange); }; } @Override public List<String> shortcutFieldOrder() { return Collections.singletonList("secret"); } public static class Config { private String secret; public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } } }

然后在路由配置里这样引用:

spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/order/** filters: - Signature=my-sign-secret

这里实现的过滤器工厂类名为SignatureGatewayFilterFactory,Gateway框架会自动截取类名前缀Signature作为yml中的过滤器配置名。配置中的Signature=my-sign-secret相当于给Config对象的secret属性赋值。这是GatewayFilter和GlobalFilter在配置方式上的最大不同:GatewayFilter是声明式的,用起来更像一个可叠加的插件;GlobalFilter则是全自动生效的。

实际项目中,GatewayFilter更常见的用途其实是重写路径、添加响应头、请求限流这一类局部策略,和登录校验的主流程配合起来效果更好。比如给某个路由单独加一个“验证验证码”的逻辑,或者给某条链路加一个“灰度版本号”响应头,都很方便。

3.4 前端配合:401 处理与令牌传递

网关把终极任务完成了一部分,但还有一个关键环节是前端的配合。你在网关返回401的时候需要有一个约定好的响应格式,前端统一拦截。如果网关返回的401格式和业务返回的格式不一致,前端要么做两层解析,要么就得吞掉错误信息,体验很差。

建议网关的401响应统一返回JSON格式,包含code、msg两个字段,就像前面unauthorized方法里写的那样。前端的axios拦截器或者fetch封装里,遇到HTTP 401就触发一次全局的“跳转登录页”动作,同时把当前页面的地址记录下来,登录成功后可以跳回来。这种做法比每个页面自己判断状态码要省心得多。

令牌传递也一样。前端每次请求都要把令牌放到Authorization头里,一般来说是登录成功后把JWT存到localStorage或pinia/vuex等前端状态管理器里,然后在请求拦截器里统一给请求头附加。不要只在某几个请求里手动加,切换页面之后容易漏掉,在HTTP客户端层做成全局的才算真正省心。

4. 常见问题与排查技巧实录

4.1 过滤器静默失效,怎么排查

我在项目里见过太多次这种情况:代码写了,类也建了,但请求就是不进过滤器。排查看似无从下手,其实套路很固定。第一个要查的就是组件是否被Spring容器扫描到。如果你把过滤器类放在了启动类所在包之外,又没通过@ComponentScan指定扫描路径,那这个过滤器就不会被注册,所有逻辑自然都不生效。这问题在多人开发、分包不规范的项目里尤其频繁。

第二个要查的是Order值。自定义的GlobalFilter往往不只一个,团队里每个人可能都写了自己的过滤器。如果别人的过滤器Order比你的小,他会先执行,并且他可能在你的过滤器之前就返回了响应,你的逻辑就被跳过了。排查时可以通过在过滤器的entry和exit处打印日志确认自己的过滤器到底有没有被调用,在日志里看到[AuthGlobalFilter] token check start这样的输出,问题就清楚了一大半。

第三个隐藏坑在路由匹配上。网关的全局过滤器虽然对所有路由生效,但如果路径根本没有匹配到任何路由,请求会在路由匹配阶段直接返回404,根本走不到后续的过滤器逻辑。尤其是自定义GlobalFilter的Order设为了正数,会排在路由转发之后,这种情况下你甚至连请求什么时候失败的都看不明白。所以全局过滤器要尽早执行,Order用负数。

4.2 请求放行了但拿不到用户信息

有一种常见现象:过滤器确实执行了,token也校验通过了,但是下游服务拿不到X-User-Id这个请求头。排查之后发现原因往往出在代码细节上,比如用了exchange.getRequest().getHeaders().set()而不是通过exchange.mutate()去传递新增请求头。ServerWebExchange的Request实例默认是不可变的,直接在原对象上改请求头是不行的,必须通过mutate方法构建一个新的exchange,新的Request才会带上你加的header。

另一个原因可能是服务间的路由配置用了StripPrefix之类的过滤器,它会把请求路径的第一段前缀去掉。请求头本身不受StripPrefix影响,但如果下游服务收到请求后被另一个框架层拦截器清洗了请求头,那也可能看不到自定义头。这时候建议下游服务统一约定从X-User-Id读用户信息,并且在入口处把自定义请求头打印出来,确认网关到下游这一跳是否完整透传。

4.3 CORS 跨域与 OPTIONS 预检拦截

前端项目在本地开发时,地址往往和网关地址不一样,跨域请求就来了。网关作为后端入口,如果跨域配置没做好,浏览器预检请求会被自己的登录校验过滤器拦住,前端控制台报出一堆红色CORS错误。这个坑非常经典:OPTIONS请求不带Authorization头,而你的过滤器看到没有令牌直接就返回401了,浏览器自然认为这个跨域请求不可用。

解决办法是在全局配置里统一处理跨域,让OPTIONS请求直接放行。Spring Cloud Gateway提供了内置的CorsWebFilter,也可以通过配置文件设置spring.cloud.gateway.globalcors.cors-configurations。参考配置如下:

spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowedOriginPatterns: "*" allowedMethods: "*" allowedHeaders: "*" allowCredentials: true

但要注意,CORS配置是针对路由转发阶段的。如果你自定义的GlobalFilterOrder设得很靠前,请求可能在CORS处理之前就被你的过滤器拦截,OPTIONS请求依然会被拦死。我见过有人在这上面折腾了一整天。更稳妥的做法是在自定义过滤器里主动判断一下请求方法,如果是OPTIONS请求,直接放行,不参与登录校验逻辑。

if ("OPTIONS".equalsIgnoreCase(exchange.getRequest().getMethodValue())) { return chain.filter(exchange); }

这段代码放在过滤器的最前面,CORS问题能规避掉大半。

4.4 JWT 校验中的那些坑

最后集中说一下JWT校验的过程中我遇到过的实际问题。最经常出问题的是签发和验签使用了两套密钥。用户服务签发token用的是A密钥,网关验签时配置的却是B密钥,结果就是所有合法token在网关这里全部验签失败,返回401。这个问题的排查最简单,把两边的secret配置打出来对比就知道了,但恰恰因为这个错误太低级,很多人反而不容易想到。

第二个坑是token过期时间的时区或者时钟偏移问题。服务端签发token的机器时钟比网关验签的机器快了几秒钟,导致token刚签发出来就被网关判定为过期。项目里的通用做法是给过期时间预留一个宽容窗口,比如校验时不直接判断exp,而是判断exp + 5秒是否大于当前时间。写工业级代码的时候,千万不要低估服务器之间那几秒的时间漂移。

第三个坑和token的缓存有关。如果网关集群部署了多个实例,你在Redis里存了token状态,但有些实例没有接入同一个Redis,就会出现部分请求401。解决方案是确保Redis为一个统一共享的实例或者集群,不要在每台网关本地存会话状态。只要你用JWT + Redis校验的姿势,这个点就必须格外注意。

5. 一点心得

网关这套登录校验方案,我自己在几个项目里落地过,最大的感受就是:代码本身不难,难的是把过滤器的执行时机和响应式模型搞明白。GlobalFilter做统一登录校验,GatewayFilter做局部定制,两者各司其职,配合JWT自包含特性和Redis的状态管理,整个微服务体系的认证入口就清晰了。

如果你是在老项目里改造,建议先加白名单,把所有现有接口放行,然后逐个业务模块切换成必须登录才能访问的状态,这样不会出现一次改造导致全站不可用的情况。这一期内容到这里,下一篇我想写网关的限流和灰度发布,到时候见。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 13:26:50

USB2.0物理层调试为何必须用示波器看波形

1. 为什么USB2.0物理层信号非得用示波器“亲眼看见”&#xff1f;你有没有遇到过这种情况&#xff1a;USB设备插上去&#xff0c;主机识别不了&#xff0c;设备管理器里要么显示“未知设备”&#xff0c;要么干脆没反应&#xff1b;或者能识别&#xff0c;但传输速度卡在12Mbps…

作者头像 李华
网站建设 2026/9/28 13:25:16

STM32F103串口DMA接收FE/NE错误自愈方案

1. 串口DMA接收踩坑背景与问题定位1.1 为什么串口DMA接收总在“莫名其妙”出错STM32F103 这颗芯片在工控、传感器采集、通信网关里出镜率极高&#xff0c;HAL 库又把 UART 的初始化门槛压得很低&#xff0c;CubeMX 点几下就能生成一套“看起来能用”的串口 DMA 接收代码。但真正…

作者头像 李华
网站建设 2026/9/28 13:24:50

关系代数核心:投影与外连接在SQL中的落地实践

“关系代数”这四个字&#xff0c;大概是数据库原理课睡眠率最高的部分。希腊字母、集合符号、抽象运算定义&#xff0c;当年背完就忘&#xff0c;工作后更觉得“直接写SQL就好了”。但我在线上排查过一个慢查询&#xff0c;优化器把三层子查询拆成了笛卡尔积连接&#xff0c;那…

作者头像 李华
网站建设 2026/9/28 13:24:21

RS485通信调试掉包原因全解析:硬件链路、软件配置与抗干扰实战

1. 为什么485通信总在调试时“掉包”&#xff1f;——从单片机引脚到终端电阻的全链路拆解你手里的STC89C52或者STM32F103&#xff0c;串口TX/RX接上MAX485模块后&#xff0c;一通电就发不出数据&#xff1b;或者能发但对方收不到&#xff0c;偶尔又突然能通几帧&#xff1b;用…

作者头像 李华
网站建设 2026/9/28 13:24:21

Zemax偏振敏感散射仿真:从BSDF到杂散光分析

做光学系统设计的朋友应该都有这种经历&#xff1a;暗室里实测的杂散光分布&#xff0c;跟 Zemax 里仿真的结果怎么都对不上&#xff0c;尤其在镜头前加一道偏振片之后&#xff0c;鬼像的亮度和位置能差出好几个量级。很多人第一反应是测量条件有问题&#xff0c;但我在几个项目…

作者头像 李华
网站建设 2026/9/28 13:22:56

MySQL索引深度解析:从B+树原理到慢查询优化实战

做SQL优化这些年&#xff0c;我最大的体会就是&#xff1a;索引这东西&#xff0c;谁都会建&#xff0c;但真正能用好的人不多。很多人一遇到慢查询&#xff0c;第一反应是"加个索引"&#xff0c;结果加了之后发现查询还是慢&#xff0c;甚至更慢了&#xff1b;去面试…

作者头像 李华