实战:手写一个通用Web层鉴权注解,解决水平权限漏洞

实战:手写一个通用Web层鉴权注解,解决水平权限漏洞

实战:手写一个通用Web层鉴权注解,解决水平权限漏洞

🌺The Begin🌺点点关注,收藏不迷路🌺

一、背景:一次渗透测试引发的改造

前段时间公司做渗透测试,我们系统暴露了一个典型的安全漏洞——水平权限漏洞

简单来说,就是用户A可以看到不属于他所在公司的数据。比如:用户A登录系统后,修改URL中的公司ID参数,就能查看到B公司的业务数据。

这个问题在行业内其实很常见,核心原因是Web层缺少水平鉴权。我们的系统运行好几年了,接口越来越多,但鉴权这块一直没好好做。

漏洞等级被定为高危,修复工作立刻提上议程。

二、需求分析:如何高效修复

面对几十个Controller、几百个接口,我的修复方案必须满足:

  1. 接入简单:开发人员加个注解就能搞定,不用写重复代码
  2. 灵活通用:能处理各种奇葩的入参结构(直接参数、对象属性、集合嵌套等)
  3. 兼容老代码:不能影响现有逻辑,老的接口不加注解就保持原样
  4. 可扩展:后续可能增加角色鉴权、垂直鉴权等

三、业务模型:用户-公司授权关系

先看下我们的权限模型:

has

has

User

string

userName

PK

string

nickName

UserCompany

string

userName

FK

long

companyId

FK

Company

long

companyId

PK

string

companyName

规则很简单:

  • 一个用户可以被授权访问多个公司的数据
  • 一个公司可以有多个授权用户
  • 用户只能查看他有权限的公司的数据

四、整体架构设计

鉴权注解的核心流程:

权限平台

请求处理流程

HTTP请求

Spring MVC

是否有@UserPermission?

直接执行业务方法

进入AOP切面

从Request获取用户信息

是否是Admin?

从入参提取鉴权对象

调用权限平台接口

是否有权限?

抛出权限异常

用户-公司关系服务

五、代码实现:一步一步来

5.1 注解定义

首先定义注解,通过属性来描述"要从哪里取鉴权信息":

packagecom.example.auth.annotation;importjava.lang.annotation.*;/** * 用户权限注解 * 用在Controller方法或类上,进行水平权限校验 */@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceUserPermission{/** * 鉴权对象类型:单个公司还是多个公司 */AuthObjectTypeobjectType()defaultAuthObjectType.COMPANY;/** * 鉴权值类型:描述如何从入参中提取值 */AuthValueTypevalueType()defaultAuthValueType.RAW;/** * 参数索引,当valueType不是RAW时,指定从第几个参数取值 */intindex()default0;/** * 参数名称,支持多级,如 "companyInfo.companyId" */StringparamName()default"companyId";/** * 是否忽略鉴权,用于覆盖类上的注解 */booleanignore()defaultfalse;}

两个枚举的定义:

packagecom.example.auth.annotation;/** * 鉴权对象类型 */publicenumAuthObjectType{COMPANY,// 单个公司COMPANIES// 多个公司}
packagecom.example.auth.annotation;/** * 鉴权值类型:定义如何从入参中提取值 */publicenumAuthValueType{RAW,// 原始参数,直接就是companyId或companyIdsOBJECT_FIELD,// 对象的属性,如 bo.companyIdCOLLECTION_FIELD,// 集合元素的属性,如 List<Bo> 取 Bo.companyIdNESTED_FIELD,// 嵌套属性,如 bo.companyInfo.companyIdCOLLECTION_NESTED// 集合中的嵌套属性,如 bo.companyList.companyId}

5.2 权限管理服务

封装调用外部权限平台的逻辑:

packagecom.example.auth.manager;importcom.example.auth.client.UserPermissionFeignClient;importcom.example.common.exception.BizException;importcom.example.common.util.UserUtil;importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.apache.commons.collections4.CollectionUtils;importorg.apache.commons.lang3.BooleanUtils;importorg.springframework.stereotype.Component;importjava.util.List;importjava.util.stream.Collectors;/** * 用户权限管理器 * 封装调用权限平台的逻辑 */@Slf4j@Component@RequiredArgsConstructorpublicclassUserPermissionManager{privatefinalUserPermissionFeignClient permissionClient;/** * 校验用户是否有指定公司的权限 */publicbooleancheckCompany(String userName,Long companyId){if(companyId ==null){thrownewBizException("公司ID不能为空");} log.debug("校验用户{}对公司{}的权限", userName, companyId);var result = permissionClient.checkCompany(userName, companyId);returncheckResult(result);}/** * 校验用户是否有所有指定公司的权限 */publicbooleancheckCompanies(String userName,List<Long> companyIds){if(CollectionUtils.isEmpty(companyIds)){thrownewBizException("公司ID列表不能为空");}// 先去重,减少调用次数List<Long> distinctIds = companyIds.stream().distinct().collect(Collectors.toList()); log.debug("校验用户{}对{}个公司的权限", userName, distinctIds.size());if(distinctIds.size()==1){// 单个公司走单条接口returncheckCompany(userName, distinctIds.get(0));}var result = permissionClient.checkCompanies(userName, distinctIds);returncheckResult(result);}privatebooleancheckResult(Result<Boolean> result){if(result ==null||!result.isSuccess()|| result.getData()==null){ log.error("调用权限平台失败: {}", result);thrownewBizException("权限校验服务异常");}returnBooleanUtils.isTrue(result.getData());}}

5.3 AOP切面:核心逻辑

这是最关键的代码,负责拦截请求、提取鉴权值、调用权限服务:

packagecom.example.auth.aspect;importcom.example.auth.annotation.UserPermission;importcom.example.auth.annotation.AuthObjectType;importcom.example.auth.annotation.AuthValueType;importcom.example.auth.manager.UserPermissionManager;importcom.example.auth.model.UserInfo;importcom.example.common.exception.BizException;importcom.example.common.util.UserUtil;importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.stereotype.Component;importorg.springframework.web.context.request.RequestContextHolder;importorg.springframework.web.context.request.ServletRequestAttributes;importjava.beans.PropertyDescriptor;importjava.lang.reflect.Method;importjava.util.Collection;importjava.util.List;importjava.util.stream.Collectors;/** * 用户权限切面 */@Slf4j@Aspect@Component@RequiredArgsConstructorpublicclassUserPermissionAspect{privatefinalUserPermissionManager permissionManager;/** * 切点:所有Controller下的public方法 */@Pointcut("execution(public * com.example.web.controller..*.*(..))")publicvoidcontrollerMethod(){}@Around("controllerMethod()")publicObjectcheckPermission(ProceedingJoinPoint joinPoint)throwsThrowable{// 1. 获取注解UserPermission annotation =getAnnotation(joinPoint);if(annotation ==null|| annotation.ignore()){// 没注解或忽略鉴权,直接放行return joinPoint.proceed();}// 2. 获取当前用户UserInfo currentUser =getCurrentUser();if(currentUser ==null){thrownewBizException("获取用户信息失败");}// 3. Admin直接放行if(UserUtil.isAdmin(currentUser.getUserName())){ log.debug("Admin用户放行");return joinPoint.proceed();}// 4. 从入参中提取鉴权值Object authValue =extractAuthValue(joinPoint, annotation);// 5. 校验权限boolean hasPermission =checkUserPermission( currentUser.getUserName(), authValue, annotation.objectType());if(!hasPermission){ log.warn("用户{}没有权限访问: {}", currentUser.getUserName(), authValue);thrownewBizException("您没有权限访问该数据");}// 6. 放行return joinPoint.proceed();}/** * 获取方法上的注解,优先取方法级,没有则取类级 */privateUserPermissiongetAnnotation(ProceedingJoinPoint joinPoint){MethodSignature signature =(MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();Class<?> targetClass = signature.getDeclaringType();// 方法上的注解优先级更高UserPermission methodAnn = method.getAnnotation(UserPermission.class);if(methodAnn !=null){return methodAnn;}return targetClass.getAnnotation(UserPermission.class);}/** * 从请求中获取当前用户 */privateUserInfogetCurrentUser(){ServletRequestAttributes attrs =(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();if(attrs ==null){returnnull;}return(UserInfo) attrs.getRequest().getAttribute("userInfo");}/** * 从方法参数中提取鉴权值(核心方法) */privateObjectextractAuthValue(ProceedingJoinPoint joinPoint,UserPermission annotation){MethodSignature signature =(MethodSignature) joinPoint.getSignature();String[] paramNames = signature.getParameterNames();Object[] args = joinPoint.getArgs();if(paramNames ==null|| paramNames.length ==0){thrownewBizException("方法没有参数,无法提取鉴权值");}AuthValueType valueType = annotation.valueType();// 场景1:原始参数if(valueType ==AuthValueType.RAW){returnextractRawParam(paramNames, args, annotation.paramName());}// 其他场景:需要从对象中取值int index = annotation.index();if(index <0|| index >= args.length){thrownewBizException("参数索引越界: "+ index);}Object target = args[index];if(target ==null){thrownewBizException("第"+ index +"个参数为null");}switch(valueType){caseOBJECT_FIELD:// 场景2:对象的属性,如 bo.companyIdreturngetFieldValue(target, annotation.paramName());caseCOLLECTION_FIELD:// 场景3:集合元素的属性,如 List<Bo> 取 Bo.companyIdreturngetCollectionFieldValues(target, annotation.paramName());caseNESTED_FIELD:// 场景4:嵌套属性,如 bo.companyInfo.companyIdreturngetNestedFieldValue(target, annotation.paramName());caseCOLLECTION_NESTED:// 场景5:集合中的嵌套属性,如 bo.companyList.companyIdreturngetCollectionNestedValues(target, annotation.paramName());default:thrownewBizException("不支持的取值类型: "+ valueType);}}/** * 提取原始参数 */privateObjectextractRawParam(String[] paramNames,Object[] args,String paramName){for(int i =0; i < paramNames.length; i++){if(paramName.equals(paramNames[i])){return args[i];}}thrownewBizException("未找到参数: "+ paramName);}/** * 通过反射获取对象属性值(使用getter方法) */privateObjectgetFieldValue(Object obj,String fieldName){try{PropertyDescriptor pd =newPropertyDescriptor(fieldName, obj.getClass());Method getter = pd.getReadMethod();if(getter ==null){thrownewBizException("属性 "+ fieldName +" 没有getter方法");}return getter.invoke(obj);}catch(Exception e){ log.error("获取属性值失败: {}", fieldName, e);thrownewBizException("解析参数失败: "+ fieldName);}}/** * 获取集合元素的属性值列表 */privateList<Object>getCollectionFieldValues(Object obj,String fieldName){if(!(obj instanceofCollection)){thrownewBizException("参数不是Collection类型");}Collection<?> collection =(Collection<?>) obj;return collection.stream().map(item ->getFieldValue(item, fieldName)).collect(Collectors.toList());}/** * 获取嵌套属性值,如 obj.field1.field2 */privateObjectgetNestedFieldValue(Object obj,String fieldPath){String[] fields = fieldPath.split("\\.");Object current = obj;for(String field : fields){if(current ==null){thrownewBizException("嵌套属性路径中有null值: "+ fieldPath);} current =getFieldValue(current, field);}return current;}/** * 获取集合中的嵌套属性值 */privateList<Object>getCollectionNestedValues(Object obj,String fieldPath){String[] parts = fieldPath.split("\\.");if(parts.length !=2){thrownewBizException("COLLECTION_NESTED类型需要两级路径,如: companyList.companyId");}// 第一级:获取集合属性Object collectionObj =getFieldValue(obj, parts[0]);if(!(collectionObj instanceofCollection)){thrownewBizException(parts[0]+"不是Collection类型");}// 第二级:遍历集合,获取每个元素的属性Collection<?> collection =(Collection<?>) collectionObj;return collection.stream().map(item ->getFieldValue(item, parts[1])).collect(Collectors.toList());}/** * 执行权限校验 */privatebooleancheckUserPermission(String userName,Object authValue,AuthObjectType objectType){if(objectType ==AuthObjectType.COMPANY){// 单个公司Long companyId =convertToLong(authValue);return permissionManager.checkCompany(userName, companyId);}else{// 多个公司List<Long> companyIds =convertToLongList(authValue);return permissionManager.checkCompanies(userName, companyIds);}}privateLongconvertToLong(Object value){if(value instanceofLong){return(Long) value;}if(value instanceofInteger){return((Integer) value).longValue();}if(value instanceofString){returnLong.parseLong((String) value);}thrownewBizException("无法转换为Long类型: "+ value);}@SuppressWarnings("unchecked")privateList<Long>convertToLongList(Object value){if(value instanceofCollection){return((Collection<?>) value).stream().map(this::convertToLong).collect(Collectors.toList());}thrownewBizException("无法转换为Long列表: "+ value);}}

六、使用示例

看看实际项目中怎么用这个注解:

6.1 场景1:最简单的用法

@RestController@RequestMapping("/api/app")publicclassAppController{/** * 直接参数:companyId就在参数列表里 */@GetMapping("/list")@UserPermissionpublicResult<List<AppInfo>>listApps(long companyId){// 直接使用companyId,注解自动取值returnResult.success(appService.listByCompany(companyId));}}

6.2 场景2:对象属性

@DatapublicclassAppQueryRequest{privateLong companyId;privateString appName;privateInteger pageNum;privateInteger pageSize;}@PostMapping("/query")@UserPermission( valueType =AuthValueType.OBJECT_FIELD, paramName ="companyId")publicResult<PageInfo<AppInfo>>queryApps(@RequestBodyAppQueryRequest request){// 从request.companyId取值returnResult.success(appService.queryPage(request));}

6.3 场景3:批量操作

@PostMapping("/batch/delete")@UserPermission( objectType =AuthObjectType.COMPANIES, valueType =AuthValueType.COLLECTION_FIELD, paramName ="companyId")publicResult<Void>batchDelete(@RequestBodyList<AppInfo> apps){// 从每个AppInfo对象中提取companyId,组成列表后校验// 确保用户对这些companyId都有权限 appService.batchDelete(apps);returnResult.success();}

6.4 场景4:嵌套属性

@DatapublicclassComplexRequest{privateCompanyInfo companyInfo;@DatapublicstaticclassCompanyInfo{privateLong companyId;}}@PostMapping("/complex")@UserPermission( valueType =AuthValueType.NESTED_FIELD, paramName ="companyInfo.companyId")publicResult<Object>complexOperation(@RequestBodyComplexRequest request){// 从request.companyInfo.companyId取值returnResult.success();}

6.5 场景5:类级别默认配置

@RestController@RequestMapping("/api/user")@UserPermission(valueType =AuthValueType.OBJECT_FIELD, paramName ="companyId")publicclassUserController{/** * 继承类上的注解 */@GetMapping("/list")publicResult<List<UserVO>>listUsers(@RequestParamLong companyId){returnResult.success(userService.listByCompany(companyId));}/** * 覆盖类上的注解 */@PostMapping("/batch/query")@UserPermission( objectType =AuthObjectType.COMPANIES, valueType =AuthValueType.COLLECTION_FIELD, paramName ="companyId")publicResult<List<UserVO>>batchQuery(@RequestBodyList<CompanyQuery> queries){returnResult.success(userService.batchQuery(queries));}/** * 忽略鉴权 */@GetMapping("/public/info")@UserPermission(ignore =true)publicResult<PublicInfo>getPublicInfo(){returnResult.success(userService.getPublicInfo());}}

七、遇到的坑和解决方案

坑1:参数名获取不到

问题:编译后参数名变成arg0、arg1,导致按名称取值失败。

解决:Maven编译插件添加-parameters参数:

<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><compilerArgs><arg>-parameters</arg></compilerArgs></configuration></plugin>

坑2:循环依赖

问题:AOP切面注入UserPermissionManagerUserPermissionManager又依赖FeignClient,FeignClient可能依赖AOP,形成循环。

解决:使用@Lazy注解延迟加载:

@Aspect@Component@RequiredArgsConstructorpublicclassUserPermissionAspect{@LazyprivatefinalUserPermissionManager permissionManager;}

坑3:集合参数去重

问题:批量接口传入的companyIds可能重复,重复调用权限平台浪费资源。

解决:在UserPermissionManager中先做去重:

List<Long> distinctIds = companyIds.stream().distinct().collect(Collectors.toList());

坑4:事务失效

问题:切面中抛出异常,但业务方法的事务没有回滚。

解决:确保异常在事务切面之后抛出,调整切面顺序:

@Order(1)// 数字越小越先执行publicclassUserPermissionAspect{// ...}

八、编译期校验:把问题扼杀在摇篮里

注解用起来很方便,但也很容易配错。比如COLLECTION_FIELD必须搭配COMPANIES使用,如果配成COMPANY,运行时就会出错。

我们可以用注解处理器在编译期就发现这些问题:

8.1 创建注解处理器

packagecom.example.auth.processor;importcom.example.auth.annotation.UserPermission;importcom.example.auth.annotation.AuthObjectType;importcom.example.auth.annotation.AuthValueType;importjavax.annotation.processing.*;importjavax.lang.model.SourceVersion;importjavax.lang.model.element.*;importjavax.lang.model.type.TypeMirror;importjavax.tools.Diagnostic;importjava.util.Set;/** * UserPermission注解处理器 * 编译期校验注解配置是否正确 */@SupportedAnnotationTypes("com.example.auth.annotation.UserPermission")@SupportedSourceVersion(SourceVersion.RELEASE_8)publicclassUserPermissionProcessorextendsAbstractProcessor{@Overridepublicbooleanprocess(Set<?extendsTypeElement> annotations,RoundEnvironment roundEnv){for(Element element : roundEnv.getElementsAnnotatedWith(UserPermission.class)){checkAnnotation(element);}returntrue;}privatevoidcheckAnnotation(Element element){UserPermission annotation = element.getAnnotation(UserPermission.class);// 规则1:COLLECTION_FIELD必须搭配COMPANIESif(annotation.valueType()==AuthValueType.COLLECTION_FIELD&& annotation.objectType()!=AuthObjectType.COMPANIES){ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,"当valueType=COLLECTION_FIELD时,objectType必须是COMPANIES", element );}// 规则2:COLLECTION_NESTED必须搭配COMPANIESif(annotation.valueType()==AuthValueType.COLLECTION_NESTED&& annotation.objectType()!=AuthObjectType.COMPANIES){ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,"当valueType=COLLECTION_NESTED时,objectType必须是COMPANIES", element );}// 规则3:index不能小于0if(annotation.index()<0){ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,"index不能小于0", element );}// 规则4:RAW类型时,paramName必须存在if(annotation.valueType()==AuthValueType.RAW&&(annotation.paramName()==null|| annotation.paramName().isEmpty())){ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,"RAW类型时,paramName不能为空", element );}}}

8.2 注册处理器

resources/META-INF/services/javax.annotation.processing.Processor文件中:

com.example.auth.processor.UserPermissionProcessor 

配置好后,如果写错注解,IDE会直接报红:

@UserPermission( valueType =AuthValueType.COLLECTION_FIELD,// 编译错误! objectType =AuthObjectType.COMPANY// 应该用COMPANIES)publicResult<?>badMethod(){// ...}

九、性能优化建议

随着接入的应用越来越多,有几个性能点需要注意:

9.1 缓存权限结果

权限关系相对稳定,可以加一层缓存:

@ComponentpublicclassCachedUserPermissionManagerextendsUserPermissionManager{privatefinalCache<String,Boolean> permissionCache =Caffeine.newBuilder().expireAfterWrite(5,TimeUnit.MINUTES).maximumSize(10000).build();@OverridepublicbooleancheckCompany(String userName,Long companyId){String key = userName +":"+ companyId;return permissionCache.get(key, k ->super.checkCompany(userName, companyId));}}

9.2 批量接口合并请求

对于checkCompanies接口,可以合并同一用户的多次请求:

// 使用异步批量处理publicCompletableFuture<Boolean>checkCompaniesAsync(String userName,List<Long> companyIds){// 合并请求,批量调用}

9.3 反射优化

反射获取属性值有一定开销,可以考虑缓存PropertyDescriptor:

@ComponentpublicclassFieldReader{privatefinalConcurrentMap<String,PropertyDescriptor> cache =newConcurrentHashMap<>();publicObjectreadField(Object obj,String fieldName){Class<?> clazz = obj.getClass();String key = clazz.getName()+"#"+ fieldName;PropertyDescriptor pd = cache.computeIfAbsent(key, k ->{try{returnnewPropertyDescriptor(fieldName, clazz);}catch(IntrospectionException e){thrownewRuntimeException(e);}});try{return pd.getReadMethod().invoke(obj);}catch(Exception e){thrownewRuntimeException(e);}}}

十、总结与展望

通过这次改造,我们实现了:

  1. ✅ 统一鉴权:所有接口都用同一套注解,规范统一
  2. ✅ 接入简单:开发人员只需要加注解,不用写重复代码
  3. ✅ 灵活通用:支持5种常见的取值场景,覆盖95%以上的接口
  4. ✅ 安全可靠:编译期校验+运行时检查,双重保障

目前这个注解已经在我们的核心业务上线,覆盖了200+接口。后续还可以扩展:

  • 支持角色鉴权:增加@RolePermission注解
  • 支持数据脱敏:结合注解实现字段级脱敏
  • 支持操作审计:自动记录谁在什么时间操作了什么数据
  • 做成Starter:封装成Spring Boot Starter,供其他项目复用

如果觉得文章有帮助,欢迎点赞收藏。有问题可以在评论区交流,我会尽量回复。

在这里插入图片描述

🌺The End🌺点点关注,收藏不迷路🌺

Read more

人工智能:深度学习中的卷积神经网络(CNN)实战应用

人工智能:深度学习中的卷积神经网络(CNN)实战应用

人工智能:深度学习中的卷积神经网络(CNN)实战应用 1.1 本章学习目标与重点 💡 学习目标:掌握卷积神经网络的核心原理、经典网络架构,以及在图像分类任务中的实战开发流程。 💡 学习重点:理解卷积层、池化层的工作机制,学会使用 TensorFlow 搭建 CNN 模型并完成训练与评估。 1.2 卷积神经网络核心原理 1.2.1 卷积层:提取图像局部特征 💡 卷积层是 CNN 的核心组件,其作用是通过卷积核对输入图像进行局部特征提取。 卷积核本质是一个小型的权重矩阵。它会按照设定的步长在图像上滑动。每滑动一次,卷积核就会与对应区域的像素值做内积运算,输出一个特征值。 这个过程可以捕捉图像的边缘、纹理等基础特征。 ⚠️ 注意:卷积核的数量决定了输出特征图的通道数,数量越多,提取的特征维度越丰富。 ① 定义一个 3×3 大小的卷积核,步长设为 1,填充方式为 SAME

By Ne0inhk
人工智能:自然语言处理在医疗领域的应用与实战

人工智能:自然语言处理在医疗领域的应用与实战

人工智能:自然语言处理在医疗领域的应用与实战 学习目标 💡 理解自然语言处理(NLP)在医疗领域的应用场景和重要性 💡 掌握医疗领域NLP应用的核心技术(如电子病历分析、疾病诊断辅助、药物相互作用检测) 💡 学会使用前沿模型(如BioBERT、ClinicalBERT)进行医疗文本分析 💡 理解医疗领域的特殊挑战(如医疗术语、数据隐私、法规要求) 💡 通过实战项目,开发一个电子病历文本分类应用 重点内容 * 医疗领域NLP应用的主要场景 * 核心技术(电子病历分析、疾病诊断辅助、药物相互作用检测) * 前沿模型(BioBERT、ClinicalBERT)在医疗领域的使用 * 医疗领域的特殊挑战 * 实战项目:电子病历文本分类应用开发 一、医疗领域NLP应用的主要场景 1.1 电子病历分析 1.1.1 电子病历分析的基本概念 电子病历(Electronic Health Records, EHR)是医疗领域的核心数据之一,包含了患者的基本信息、诊断记录、

By Ne0inhk
计算机专业在AI浪潮下的学习路径深度分析:从“代码写手”到“系统掌舵者”

计算机专业在AI浪潮下的学习路径深度分析:从“代码写手”到“系统掌舵者”

这篇文章会把三个问题掰开揉碎:为何学、学什么、怎么学。贴近真实的学习体验:会遇到的坑、会反复卡住的点、应该怎么借助 AI 但不被 AI 带偏、怎么把学习变成可被面试官验证的成果。最后还会给三个场景的超细行动方案:转行找开发工作 / 在校担心就业 / 用 AI 做产品副业。 目录 1. 2026-2027:编程范式真的在变什么 2. 为什么“系统能力”会变成护身符 3. AI 时代的能力金字塔:你该把力气花在哪里 4. 通用学习路径四阶修炼(带验收标准) 5. 场景一:转行找开发工作(前端 / 后端 / 数据 / AI 应用) 6. 场景二:在校生如何把四年过成“可雇佣的四年” 7. 场景三:用 AI

By Ne0inhk
uni-app x跨平台开发实战:鸿蒙HarmonyOS网络模块封装与轮播图实现

uni-app x跨平台开发实战:鸿蒙HarmonyOS网络模块封装与轮播图实现

在玩中学,直接上手实战是猫哥一贯的自学方法心得。假期期间实在无聊!我不睡懒觉、不看电影、也不刷手机、不玩游戏、也无处可去。那么我干嘛嘞?闲的都想看蚂蚁上树,无聊透顶,百无聊赖,感觉假期好没意思啊。做什么呢? 于是翻出来之前做过的“爱影家”影视app项目,找个跨多端的技术栈再玩一把。 我先后尝试了kuikly、flutter 、arkui-x等框架,结果…,额,这几个没少踩坑做不动了。真想向天问一下,跨平台框架开发哪家强?最后尝试了下uni-app x,这个还真不错,就选它了,用它来实现个跨多端的免费观影APP分享给大家。 本文内容介绍uni-app x框架的网络请求和组件复用,这是每个开发者必须掌握的技能。本文将通过 uni-app x 框架,结合uni-app x独有的 UTS 语言规范,实践如何构建规范的网络请求模块,并实现动态轮播图组件。我们选用的案例是影视类应用的首页轮播图实现,接口来源于真实的开放 API。 关于uniapp-x的介绍: 可以体验打包后的hello uni-app

By Ne0inhk