news 2026/10/13 3:59:32

Spring MVC通用数据权限校验框架设计与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring MVC通用数据权限校验框架设计与实践

做过几年后端的人,应该都遇到过这种场景:同一个接口,不同角色登录进来,能看到的数据完全不一样。老板要看全公司的经营数据,部门主管只能看自己部门的,普通员工只能看自己的单子。这其实就是典型的数据权限校验,属于鉴权里面最容易让人头疼的一块。

我自己的做法是沉淀了一个基于Spring MVC的通用数据权限校验(鉴权)框架,日常业务代码只需要加一个注解,权限条件自动生效,不需要在每个Mapper里手写一堆where判断。这篇东西就围绕这个框架的使用示例来写,按照我实际跑过的项目来拆解:整个框架的设计思路、核心细节、具体怎么落地配置,以及我在广西移动互联网中心(虚拟环境)里调试时踩过的那些和网上教程不太一样的坑。

先说清楚定位:这套框架解决的是“登录用户能看哪些数据”的问题,不是“用户能不能登录”的问题。前者是数据权限,后者是身份认证,两个不是一个东西,别混。

1. 项目整体设计与思路拆解

刚开始想自己写数据权限的时候,我的第一反应是直接在Service层里写:

if ("admin".equals(roleCode)) { // 查全部 } else { // 只查部门 }

三个角色还好,等角色变成七八个、业务又分成订单、客户、报表好几块的时候,代码就乱成一锅粥了。后面静下心复盘,才发现核心问题其实只有一个:业务代码不该关心当前用户能看哪些数据,它只负责把“约束条件”带上查询里就行。

整套框架的想法就很直白:把数据权限的解析过程抽出来,做成一个通用的组件。业务接口上标记一个注解,拦截器自动解析当前登录人,从ThreadLocal里拿用户上下文,找出他对应的数据范围(全部、本部门、本人、本部门及以下等),最后再把权限条件以SQL片段的形式塞给业务查询。业务方完全不用感知权限逻辑,整个过程中业务代码零侵入。

1.1 为什么用Spring MVC拦截器而不是过滤器

实话讲,我也试过用Filter来实现,毕竟Servlet Filter理论上是入口,管得更早。真正上手才发现两个问题:

第一个问题:Filter在Spring管理之外。你在Web.xml里注册的Filter默认拿不到Spring的Bean,非要在Filter里用ApplicationContext手工去getBean,写起来极其别扭,尤其到了Spring Boot集成阶段,配置也不够统一。

第二个问题:Filter拿不到HandlerMethod。过滤器的世界里只有路径,没有方法的概念。而Spring MVC的核心——HandlerInterceptor——是能拿到HandlerMethod的,进而能拿到方法上的注解、一次请求所在Controller的类信息、参数注解,这对鉴权框架来说是刚需。

你可以理解为:Filter管的是“请求是否合法”,HandlerInterceptor管的是“这个请求正在调用哪个方法”。后者天然更贴近业务语义。

所以框架选择了HandlerInterceptor:

  • 在preHandle里解析请求和当前用户;
  • 将权限上下文写入ThreadLocal;
  • 在afterCompletion里清理上下文,防止线程池复用导致数据串线。

1.2 为什么要有“通用”这两个字

我把很多项目里的数据权限实现翻了个遍,发现各自的写法千奇百怪但本质都差不多:要么直接在每个ServiceImpl里循环判断角色,要么把临时写好的SQL片段复制粘贴到好几个Mapper里。一旦权限规则变化,东改一句西改一句,改漏了老板那边又不满意。

通用的思路就是把权限规则抽象成几个维度:

  • 权限类型:全部数据、本部门及以下、本部门、本人、自定义字段;
  • 匹配结构:用户表、部门表、角色表之间的关联关系;
  • 拼接策略:把权限范围最终翻译成一段WHERE条件。

业务开发完全不用关心这些维度怎么实现,只需要在Controller接口上标记注解,通用框架内部完成维度解析和SQL拼接。这就是“通用”两个字的价值。

这个方案选型的另一个好处是,它可以无缝对接很多项目现有的账号体系。用户表是现成的,角色表是现成的,部门表也是现成的。框架只需读配置、做关联,不需要去改动用户的登录流程,落地成本极低。

1.3 框架的核心模块划分

整个框架被我拆成了四个模块,按生效顺序排列:

  1. 注解模块:定义@DataScope注解,作用在Controller方法上,标记这个接口需要做数据权限校验;
  2. 拦截模块:DataScopeInterceptor,负责在请求进入时读取用户信息、解析注解,并初始化权限上下文;
  3. 解析模块:根据用户数据权限类型生成SQL条件片段,替换掉语句里的占位符;
  4. 上下文模块:ThreadLocal保存当前请求的权限数据,业务层通过静态方法获取并拼接到查询里。

模块之间用服务接口解耦,所有实现类都由Spring管理。后面你想把部门维度改成自定义维度,只需要替换解析器实现,不用动拦截逻辑。

2. 核心细节解析与实操要点

设计的思路定了,落地的时候有些细节必须抠清楚。这部分直接决定框架好不好用、会不会线上翻车。

2.1 注解怎么定:参数越少越好用

最早我在注解里设计了五六个别名参数,比如deptAlias、userAlias、roleAlias,实操下来发现很蠢。因为业务开发不会去记那么多字段,问一句“这注解里面到底填什么”就卡住了。

后来我换成极简方案:注解里只有一个type参数,用来声明数据权限的类型:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DataScope { /** * 数据权限类型 * ALL: 全部数据 * DEPT_ALL: 本部门及以下 * DEPT: 本部门 * SELF: 仅本人 */ ScopeType type() default ScopeType.DEPT_ALL; /** * 参与过滤的字段别名,默认是当前业务表的别名 */ String fieldAlias() default ""; }

fieldAlias只在特殊场景下需要,比如你的查询主表不是用户表,而是project表时,需要指定p.id这样子的完整列名,其余情况走默认就行。用最少的参数解决绝大多数问题,方便业务方抄作业。

2.2 数据范围的定义与属性表设计

权限类型的定义需要考虑真实业务的复杂性。“全部数据”和“仅本人”都好理解,难办的是中间地带的“本部门及以下”。举个实例:一个集团平台,某大区的负责人既要看自己部门的数据,还要看下属几个子部门的数据。单纯通过数据字典配置很难支撑这种层级关系。

我这边在设计上参考了比较经典的“数据权限范围字典表”,核心字段包括:

  • 权限标识(ALL/DEPT_ALL/DEPT/SELF);
  • 数据范围编码;
  • SQL拼接模板;
  • 适用范围描述。

同一个用户可能同时出现在部门层级的不同位置,所以权限范围的匹配不只是一对一,还需要结合用户在组织机构中的位置来做递归匹配。

好在这套逻辑在工作流、OA系统、进销存系统里被验证过无数次,踩的坑也摆在那里。我把它的实现拆成两步:

  1. 根据用户ID查出所在部门ID;
  2. 根据部门ID递归查出子部门ID集合。

子部门集合会作为“本部门及以下”的过滤条件拼进SQL。为了性能考虑,第三步我建议直接把子部门ID集合做缓存,部门层级变动后手动刷新缓存。否则每次都递归查一遍部门表,组织架构稍微复杂一点就能把数据库打爆。

2.3 ThreadLocal 上下文传递的正确姿势

很多小白把ThreadLocal理解成“一个变量到处传”,这里必须正本清源:ThreadLocal是“当前线程的局部变量”。如果处理不当,线上出现的数据串线问题,十有八九就出在这一步。

小技巧:在拦截器的preHandle中初始化ThreadLocal,在afterCompletion中必须清理。无论业务方法是否抛异常,afterCompletion都会被执行,这一点比finally还好用,因为异常不会被吞掉。

@Component public class DataScopeInterceptor implements HandlerInterceptor { private final DataScopeContext context; public DataScopeInterceptor(DataScopeContext context) { this.context = context; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod handlerMethod)) { return true; } DataScope dataScope = handlerMethod.getMethodAnnotation(DataScope.class); if (dataScope == null) { return true; } CurrentUser user = currentUser(); DataScopeContext.DataPermission permission = dataScopeService.resolve(user, dataScope.type()); context.set(permission); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { context.clear(); } }

事实上有相当多的业务系统使用线程池异步处理子任务,例如一个线程把数据导出拆成多个子线程。你以为子线程的权限上下文还在吗?不在。因为ThreadLocal是线程局部的,子线程根本拿不到父线程的副本。

解决方式不复杂——如果你的耗时可接受,就把权限上下文显式传进去;如果不行,使用TransmittableThreadLocal这类增强方案。这一点我建议在框架设计阶段就确定好,不然后续要给所有异步代码擦屁股。

2.4 SQL拼接:这块最容易写崩

数据权限最终的表现形式是SQL条件拼接。我最开始自测的时候,直接拼了个and p.dept_id in (...)进去,但MyBatis的XML里使用${}和#{}完全不是一回事。

#{}是预编译参数占位符,对应?,无法拼接一整段条件;${}是字符串直接替换,有注入风险,但有时候必须用它来拼接动态条件片段。

所以我在框架里采用了占位符替换的思路:让业务SQL里显式留一个${dataScope}占位符,然后由框架统一替换成最终的SQL条件片段。

<select id="selectProjectList" resultType="Project"> select p.id, p.name, p.owner_id, p.dept_id, p.project_amount from project p where p.deleted = 0 ${dataScope} order by p.id desc </select>

拦截器解析完用户权限后,把${dataScope}替换成比如:

and p.dept_id in (101, 102, 103)

注意这套写法有前提:占位符必须由框架统一注入,业务代码不能直接用外部参数去拼${}。否则就是自己把自己弄成注入漏洞。框架内部在替换前需要做严格的字段白名单校验,只允许由解析模块生成的合法条件片段。

3. 实操过程与核心环节实现

现在进入手把手环节。我维护的这套框架示例项目比较简单,但五脏俱全:用户、部门、角色、项目四张表,外加数据权限配置表。场景设计是一个项目管理系统,不同角色看到的项目列表范围不同。

3.1 项目结构与依赖准备

我用的是Maven构建的Spring MVC 5 + MyBatis 3 项目,最外层的依赖就是标准的Spring MVC和MyBatis。如果你用Spring Boot,可以平移到spring-boot-starter-web,CRUD写法、拦截器注册方式大同小异。

核心依赖清单:

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.4</version> </dependency>

目录结构:

src/main/java/com/example/framework/ ├── annotation/DataScope.java ├── context/DataScopeContext.java ├── interceptor/DataScopeInterceptor.java ├── service/DataScopeService.java └── service/impl/DataScopeServiceImpl.java src/main/java/com/example/business/ ├── controller/ProjectController.java ├── service/ProjectService.java └── mapper/ProjectMapper.xml

3.2 数据权限配置表

数据库里除业务表外,需要一张数据权限配置表,用来存储权限类型和SQL片段的映射关系。坦白讲,完全可以在代码里硬编码,但做成表的好处是将来运营可以改,不需要重新发版。

CREATE TABLE sys_data_scope ( id BIGINT PRIMARY KEY, scope_type VARCHAR(32) NOT NULL COMMENT '权限类型: ALL/DEPT_ALL/DEPT/SELF', scope_name VARCHAR(64), sql_template TEXT COMMENT '数据权限SQL片段模板,形如: and p.dept_id in (...)', status TINYINT DEFAULT 1, create_time DATETIME ); INSERT INTO sys_data_scope (scope_type, scope_name, sql_template) VALUES ('ALL', '全部数据', ''), ('DEPT_ALL', '本部门及以下', 'and p.dept_id in (${deptIds})'), ('DEPT', '本部门', 'and p.dept_id = ${deptId}'), ('SELF', '仅本人', 'and p.owner_id = ${userId}');

这里面有两点要注意:

第一,${deptIds}是模板参数,不是外部SQL注入。框架解析时会把当前用户的部门ID集合填充进去,集合来自权限解析服务,不是来自请求参数。

第二,模板里使用的是项目表的别名p,这个是约定好的,业务方必须保证查询SQL的主表别名和模板里的别名一致。不一致的话,需要在@DataScope(fieldAlias = "x")里指定。

3.3 核心服务实现

权限解析服务是整个框架的大脑。它的职责就是输入一个当前用户,输出一个具体的SQL条件片段。

@Service public class DataScopeServiceImpl implements DataScopeService { @Resource private SysUserMapper sysUserMapper; @Resource private SysDeptMapper sysDeptMapper; @Resource private SysDataScopeMapper sysDataScopeMapper; @Override public DataScopeContext.DataPermission resolve(CurrentUser currentUser, ScopeType scopeType) { String template = sysDataScopeMapper.selectTemplateByType(scopeType.name()); if (template == null || template.isEmpty()) { return DataScopeContext.DataPermission.empty(); } Map<String, Object> params = new HashMap<>(); params.put("userId", currentUser.getUserId()); if (scopeType == ScopeType.DEPT_ALL) { List<Long> deptIds = getChildDeptIds(currentUser.getDeptId()); deptIds.add(currentUser.getDeptId()); params.put("deptIds", deptIds.stream() .map(String::valueOf) .collect(Collectors.joining(","))); } else if (scopeType == ScopeType.DEPT) { params.put("deptId", currentUser.getDeptId()); } // 基于简单模板的字符串替换 String finalSql = template.replace("${deptIds}", String.valueOf(params.get("deptIds"))) .replace("${deptId}", String.valueOf(params.get("deptId"))) .replace("${userId}", String.valueOf(params.get("userId"))); return new DataScopeContext.DataPermission(finalSql); } }

快速说明几个设计决策:

  • 使用模板替换而不是代码拼接,好处是权限字段改动只动表,不动代码。
  • getChildDeptIds方法递归查子部门,这里必须做缓存和递归深度的控制,防止部门层级太深导致性能问题。
  • 如果是SELF类型,只填充userId就能完成过滤。

3.4 Controller 使用示例

Controller层就是这篇文章标题里说的“使用示例”,代码少到令人舒适:

@RestController @RequestMapping("/project") public class ProjectController { @Resource private ProjectService projectService; @GetMapping("/list") @DataScope(type = ScopeType.DEPT_ALL) public Result<PageResult<ProjectVO>> list(ProjectQuery query, PageQuery pageQuery) { return Result.ok(projectService.pageList(query, pageQuery)); } }

projectService.pageList内部调用Mapper查询:

<select id="selectProjectPage" resultType="com.example.business.vo.ProjectVO"> select p.id, p.name, p.owner_id, p.dept_id, p.project_amount from project p where p.deleted = 0 ${dataScope} order by p.id desc </select>

框架在请求进入时,已经把${dataScope}替换成了合法性很高的一段条件。效果如下:

  • 超管角色:${dataScope}为空,全表可见;
  • 部门经理:and p.dept_id in (101, 102, 103),本部门及下级可见;
  • 普通员工:and p.owner_id = 10086,只能看到自己的项目。

上面这个流程跑通之后,整个功能链就闭合了:注解 → 拦截器 → 权限解析服务 → SQL占位符 → 业务查询。

3.5 拦截器注册与Spring MVC配置

DataScopeInterceptor写好了还不算完,还必须注册到Spring MVC的拦截器链上。注意,Java Config和XML配置方式我强烈建议搞清楚一种就够。

XML方式:

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/logout"/> <bean class="com.example.framework.interceptor.DataScopeInterceptor"/> </mvc:interceptor> </mvc:interceptors>

Java Config方式:

@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private DataScopeInterceptor dataScopeInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(dataScopeInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login", "/logout"); } }

拦截器顺序很重要。如果你同时有登录拦截器(校验token)、数据权限拦截器、审计日志拦截器,那数据权限拦截器必须排在登录拦截器之后。因为数据权限拦截器需要从用户上下文拿当前用户信息,用户还没确认就谈权限,全是空谈。

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

这部分才是实操的精华。网上很多框架介绍都写得天花乱坠,但一运行就是各种问题。我把在这个框架上实际调试时遇到的典型问题记录如下,按频率排序。

4.1 同线程数据串线,A用户看到了B用户的数据

这个坑我一年要提醒上百次。原因是Tomcat线程池复用了线程,上一次请求在ThreadLocal里残留的数据没有清掉。新请求进来时,preHandle还没执行,异步业务可能先读了上下文,拿到就是上一次请求的脏数据。

解决方案在框架设计时已经强制必须处理:

@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { DataScopeContext.clear(); }

另外我需要强调一个容易被忽视的细节:千万不能在postHandle里清理。postHandle只在正常返回时执行,异常发生时可能跳过,使用afterCompletion才是保险的。

如果进程里有用到线程池,推荐考虑TransmittableThreadLocal这种支持值传递的增强型线程变量。美团分享过一个Java线程池问题排查的经典案例,本质就是这里提到的场景。

4.2 框架注解不生效,拦截不到Controller方法

出现这个问题的常见原因有三个:

  1. 拦截器注册的路径不匹配,/api/**写了但Controller实际在/rest/**下;
  2. 注解放在Controller上,而拦截器获取的是HandlerMethod,这没问题,但如果注解放在的类是被代理类,方法注解可能拿不到;
  3. 在Spring Boot 2.6之后,路径匹配策略发生了变化,导致拦截器路径失效。

最直接的排查方式:在preHandle里打一下日志,输出handler是不是HandlerMethod的类型,以及当前请求URI。先确认拦截器有没有生效,再确认方法上有没有注解,两步就能定位。

4.3 多表关联时权限字段别名总对不上

有一类数据权限涉及多表join,比如项目关联客户表,权限在客户维度上,但查询的别名是p。最早版本的框架只允许一个默认别名,导致这种场景直接垮掉。

解决方案是在注解上加了一个fieldAlias属性,用完整列名描述过滤字段:

@DataScope(type = ScopeType.DEPT_ALL, fieldAlias = "c.dept_id") public Result<...> listByCustomer(ProjectQuery query) { }

再打开SQL模板后,权限解析会把模板里的p.统一替换成c.,或者更优雅的方案:框架提供一个字段映射表,业务方只需要把列名映射好,权限条件自动基于映射后的字段生成。

排查这个问题时记住一个口诀:先看SQL,再看别名,先跑native,再去跑框架。确认原生SQL执行无误后,再检查框架生成的SQL片段是否和原生SQL冲突。

4.4 员工列表出现了子部门的用户,但部门查不到

这个问题的根源是部门数据不完整,子部门表数据缺失,或者用户的dept_id没有正确落到部门表的主键上。

出现这种跨部门漏数据的情况,多数是递归查询子部门的时候,递归没有闭合。例如有个部门的PID指向了自己,递归条件就永远退不出来。

在实现递归查询时要设置两层保护:

public List<Long> getChildDeptIds(Long deptId) { List<Long> result = new ArrayList<>(); Set<Long> visited = new HashSet<>(); Deque<Long> queue = new LinkedList<>(); queue.offer(deptId); while (!queue.isEmpty()) { Long id = queue.poll(); if (!visited.add(id)) { continue; // 防止环 } List<Long> children = deptMapper.selectChildIds(id); result.addAll(children); queue.addAll(children); } return result; }

有了visited防止循环引用,同时在SQL层面对status=1的部门做过滤,避免停用的部门还挂在组织架构树下面。

4.5 高频缓存与权限实时性的取舍

数据权限有一个天然的矛盾:缓存能提升性能,但权限变化可能不能及时生效。比如把用户从A部门调到B部门,如果权限数据缓存不刷新,用户还能看到原来部门的数据,这是管理事故。

我的做法是采用“短缓存+主动失效”双保险:

  • 部门层级缓存时长控制在30秒到1分钟;
  • 用户组织和角色发生变更时,主动调用缓存刷新接口,清除相关key。

这种方案对权限系统来说是最安全稳妥的。千万不要把权限数据缓存一整天,权限场景优先保证准确性,性能优化排在后头。运气不好真出了问题,背锅的还是写代码的人。

4.6 数据权限和分页的配合问题

分页是多条SQL同时执行的,MyBatis分页插件在计算总条数时会改写SQL。如果框架只是简单地把${dataScope}占位符替换成条件,理论上分页插件会拿到完整的SQL再处理,这个没问题。

但有一种坑:PageHelper会先执行count查询,再执行page查询。如果count查询中包含了数据权限表中用到的${}替换字段,而Mapper参数没有传递该字段,就会报错。MyBatis的${}是从Mapper接口传参中取的,框架做不到把全局变量注入Mapper参数。

所以更稳妥的做法是:在MyBatis层面定义动态SQL片段:

<sql id="dataScope"> ${dataScope} </sql>

然后业务SQL引入这段。这里${dataScope}是来自上下文或参数,不是框架占位符替换。我在实际生产中更推荐这种,因为能被分页插件正确处理,排查也容易。

不过这段是框架使用者约定俗成的内容,最终用哪种方式需要看业务场景。我建议保存一种主推方式,避免团队里有人用占位符,有人用参数传递,风格分裂。

常见问题速查表

现象原因解决方案
用户能看到其他人数据ThreadLocal未清理确保在afterCompletion清空上下文
数据权限注解不生效拦截器路径配错或未注册先打印handler,确认HandlerMethod上的注解
SQL报“无效的列名”权限字段别名与业务SQL别名不一致检查fieldAlias,保证alias一致
部门数据查不全递归查询部门时存在环或数据缺失引入访问集合,限制递归深度
分页数据异常count和page两次SQL竞争条件不一致将生成的SQL条件固化到XML片段中
缓存导致权限滞后权限缓存时间过长短缓存+主动失效

排查通用顺序:先看preHandle日志,再看生成的SQL片段,再审ThreadLocal上下文,最后才盯着返回结果去猜。按这个顺序,大多数问题十分钟内就能定到位。

5. 实际操作里的一点感受

框架从设计到示例落地,前后大概用了两周时间。真正让框架变得可用的,不是算法有多高明,而是把“脏活”全部揽到底:注解解析、上下文传递、SQL片段生成、异常处理。业务模块反而像没发生过事情一样只管自己的业务逻辑。

最初我实际使用这个示例时,最大的感受是“代码清爽了很多”。以前每个接口里都要传一堆部门ID、用户ID进去,Mapper.xml里也全是if判断权限的标签,现在全部收敛到框架层。新写一个列表接口,只要加上一个注解,SQL模板里放一个占位符,效果立刻就出来。

默认实现可能不适合你的业务,比如有些系统的部门有独立法人实体,有些系统的用户组织关系不在部门维度,而在“分组”维度。你需要做的不是推翻框架重写,而是扩展resolve方法:返回你自己的SQL条件片段,只要能把权限范围表达成合法SQL,框架其余部分不用动。

另外一个使用体会可以分享:如果哪天框架嵌入到老旧系统里,各种蛋疼的权限字段已经满天飞了,就不要试图通过一个注解统一掉所有接口。先让新接口使用框架,老接口慢慢迁移,每迁移一个验证一个,比一次大拆大改稳得多。代码重构这件事,没人是靠一次性推倒重来赢下来的。

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

SQL注入攻防实战:从原理到协议层绕过的纵深防御

1. SQL注入为什么至今仍是头号Web威胁坐在告警大屏前盯着那条红色的SQL注入规则告警&#xff0c;我一度以为是WAF误报。点进去一看&#xff0c;请求路径是 /api/order/query&#xff0c;参数里带着一串经过变形编码的负载&#xff0c;还原之后是典型的联合查询痕迹。那一刻我意…

作者头像 李华
网站建设 2026/10/13 3:58:23

DHCP中继实验全流程:从DORA抓包到跨网段地址分配

把DHCP实验完整跑一遍&#xff0c;最值钱的部分不是记住了几条配置命令&#xff0c;而是把“它到底在链路里做了什么”这件事彻底搞清楚。这篇文章是我在模拟器里从零搭出一套DHCP中继环境的完整记录&#xff0c;覆盖地址池规划、服务器配置、跨网段获取、抓包验证和故障排查&a…

作者头像 李华
网站建设 2026/10/13 3:57:28

Spring Security中BCryptPasswordEncoder密码加密与验证实战

1. 从一个登录接口的“慢”说起&#xff1a;为什么密码加密不能随便选很多刚接触后端开发的朋友&#xff0c;第一次写用户注册登录模块时&#xff0c;最容易踩的坑就是把密码明文存进数据库&#xff0c;或者用MD5、SHA-1这类哈希算法“意思一下”。我见过不少项目&#xff0c;用…

作者头像 李华
网站建设 2026/10/13 3:57:28

Unity鱼眼Shader实战:从FOV翻车到UV重映射的完整实现

1. 从一次“翻车”的广角镜头说起做Unity有些年头了&#xff0c;但真正让我对Shader产生“敬畏”的&#xff0c;还是第一次尝试给项目里的监控摄像头做广角效果。当时的需求很明确&#xff1a;一个固定位置的摄像头&#xff0c;要能拍到比普通透视相机更宽的视野&#xff0c;最…

作者头像 李华
网站建设 2026/10/13 3:57:06

React Native开发OpenHarmony应用:文件上传与断点续传方案详解

最近有个做跨端应用的朋友问我&#xff1a;团队现在主力技术栈是React Native&#xff0c;想把应用跑到OpenHarmony设备上&#xff0c;别的功能都好说&#xff0c;唯独文件上传这块有点棘手&#xff0c;尤其是大文件、弱网环境下的断点续传。他说网上资料太散&#xff0c;要么是…

作者头像 李华
网站建设 2026/10/13 3:55:18

手写排序算法全解析:从基础到工程实战的选型指南

1. 这年头为什么还要手写排序算法先把话说在前头&#xff1a;日常开发里&#xff0c;我几乎不会自己写排序&#xff0c;直接用语言内置的sort()就完事了。但"不会写排序"和"不懂排序"是两码事&#xff0c;很多面试题、性能问题排查、甚至数据结构设计&…

作者头像 李华