[Spring Cloud] (6)gateway整体加解密

news2024/12/25 9:05:49

文章目录

  • 简述
  • 整体效果
  • 后端
    • 增加配置
      • nacos增加配置
      • GlobalConfig
    • 添加请求整体解密拦截器DecryptionFilter
    • 添加响应整体解密拦截器EncryptionFilter
  • 前端
    • 请求拦截器
      • 添加整体加密逻辑
      • 请求头中添加sessionId
    • 响应拦截器
      • 添加整体解密逻辑

简述

本文网关gateway,微服务,vue已开源到gitee
杉极简/gateway网关阶段学习

在经历前面5章的铺垫下,终于进入到了最重要的环节——拦截器功能实现,此时我们需要看的东西就比较集中而且简单了。

在Java分布式系统中,使用Spring Cloud Gateway进行整体的加解密处理可以起到以下几个关键作用:

  1. 安全性增强:通过加密客户端与服务端之间传输的数据,可以防止敏感信息在网络传输过程中被截获和窃取。即使数据包被拦截,没有相应的密钥也无法解读数据内容,从而保护了用户隐私和系统安全。
  2. 数据完整性校验:在数据传输过程中,除了加密保护数据外,还可以通过签名机制来确保数据的完整性。签名可以验证数据在传输过程中是否被篡改,增强了数据的可靠性。
  3. 减轻后端服务负担:通过在网关层统一进行加解密处理,后端微服务不需要再集成额外的安全模块来进行数据的加解密工作,从而降低了后端服务的复杂性和资源消耗。
  4. 统一的安全管理:网关作为所有请求的入口点,可以实现统一的安全管理策略。所有的加解密操作按照统一的规则进行,便于管理和维护,同时方便对安全策略进行升级和维护。
  5. 提高开发效率:开发者可以专注于业务逻辑的实现,而不必关注数据传输的安全细节,提高了开发效率和项目的迭代速度。
  6. 跨服务数据保护:在微服务架构中,服务间可能需要相互调用和数据交换。网关加解密确保了即使在内部服务间传输的数据也是安全的,防止了潜在的内部安全风险。
  7. 应对合规性要求:某些行业或地区的法律法规要求对用户数据进行加密处理,使用网关加解密可以更好地符合这些合规性要求,避免法律风险。
  8. 灵活的策略调整:网关层的加解密策略可以根据业务需求灵活调整,比如可以针对不同的请求路径或请求方法应用不同的加密算法和密钥,而不需要修改每个微服务的代码。
  9. 减少敏感信息泄露风险:对于某些对数据安全性要求极高的应用场景,如金融交易、个人隐私信息等,网关加解密可以有效减少敏感信息泄露的风险。
  10. 提高系统的可扩展性:随着业务的发展,系统的安全需求可能会变化。在网关层实现加解密可以方便地根据新的安全需求进行扩展和更新,而不影响现有的业务流程和后端服务。

Spring Cloud Gateway的加解密功能为分布式系统提供了一个安全、高效、灵活的数据传输解决方案,有助于提升系统的安全性和开发效率。

整体效果

像不需要加密的接口,请求参数与响应数据都是未加密的
image.png
image.png
像数据接口,发送的参数是加密数据,接收到的都是是加密数据。但是经过前后端的拦截器处理之后,显示出的依然是正常数据
image.png
image.png

后端

增加配置

nacos增加配置

  # 整体对称加解密
  aes: true
  # 整体非对称加解密
  rsa: true

GlobalConfig

    /**
     * 整体对称加解密
     */
    private boolean aes;

    /**
     * 整体非对称加解密
     */
    private boolean rsa;

image.png

添加请求整体解密拦截器DecryptionFilter

package com.fir.gateway.filter.request;

import com.alibaba.fastjson.JSONObject;
import com.fir.gateway.config.GlobalConfig;
import com.fir.gateway.config.exception.CustomException;
import com.fir.gateway.config.result.AjaxStatus;
import com.fir.gateway.dto.ConnectDTO;
import com.fir.gateway.utils.AESUtils;
import com.fir.gateway.utils.RSAUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Set;

;


/**
 * 请求整理解密-请求拦截器
 *
 * @author fir
 */
@Slf4j
@Component
public class DecryptionFilter implements Ordered, GlobalFilter {


    /**
     * 网关参数配置
     */
    @Resource
    private GlobalConfig globalConfig;


    @Resource
    private RedisTemplate<String, Object> redisTemplate;


    @Override
    public int getOrder() {
        return -280;
    }


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("整体解密:start");
        ServerHttpRequest req = exchange.getRequest();
        String method = req.getMethodValue();

        boolean rsa = globalConfig.isRsa();
        boolean aes = globalConfig.isAes();
        if (rsa || aes) {
            // 白名单路由判断
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getPath().toString();
            List<String> whiteUrls = globalConfig.getWhiteUrls();

            if(!whiteUrls.contains(path)){
                ServerHttpRequest builder = req.mutate().build();
                if (HttpMethod.GET.matches(method)) {
                    log.info("当前请求参数为: {}", req.getQueryParams());
                    builder = changeGet(exchange, builder);
                } else if (HttpMethod.POST.matches(method)) {
                    log.info("当前请求参数为: {}", req.getQueryParams());
                    builder = changeGet(exchange, builder);
                }
                exchange = exchange.mutate().request(builder).build();
                log.info("整体解密:true");
            }else {
                log.info("整体解密:true,白名单");
            }
        }else {
            log.info("整体解密:true,验证已关闭");
        }
        return chain.filter(exchange);
    }


    /**
     * 获取请求参数等信息进行过滤处理
     *
     * @param exchange          请求
     * @param serverHttpRequest 请求
     * @return 处理结束的参数
     */
    @SneakyThrows
    private ServerHttpRequest changeGet(ServerWebExchange exchange, ServerHttpRequest serverHttpRequest) {
        String session = exchange.getRequest().getHeaders().getFirst("s");

        if (session == null) {
            throw new CustomException(AjaxStatus.SESSION_INVALID);
        }

        JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(session);
        if (jsonObject == null) {
            throw new CustomException(AjaxStatus.SESSION_EXPIRE);
        }
        ConnectDTO connectDTO = jsonObject.toJavaObject(ConnectDTO.class);
        String privateKey = connectDTO.getPrivateKey();
        String secretKey = connectDTO.getSecretKey();


        // 获取原参数
        URI uri = serverHttpRequest.getURI();
        String originalQuery = uri.getRawQuery();
        String decodedQuery = null;
        if(StringUtils.isNotBlank(originalQuery)){
            decodedQuery = URLDecoder.decode(originalQuery, "UTF-8");
        }

        // 更改参数
        MultiValueMap<String, String> newQueryParams = new LinkedMultiValueMap<>();
        if (StringUtils.isNotBlank(originalQuery) && org.springframework.util.StringUtils.hasText(decodedQuery)) {
            // 修改请求参数,String[] array只能处理前端特定加密 {data:加密内容的形式}, 传递到后端,会变更为 data=加密内容。
            // 除此以外的所有方式不能通过本方法进行解密
            String[] array = decodedQuery.split("=");
            if (array.length > 1) {
                decodedQuery = array[1];

                if (decodedQuery != null) {
                    boolean rsa = globalConfig.isRsa();
                    boolean aes = globalConfig.isAes();

                    if (rsa) {
                        // 对数据进行非对称解密
                        originalQuery = RSAUtils.decryptSection(decodedQuery, privateKey);
                    }
                    if (aes) {
                        // 对数据进行对称解密
                        originalQuery = AESUtils.decrypt(originalQuery, secretKey);
                    }
                }

                Map<String, Object> dataMap = JSONObject.parseObject(originalQuery, Map.class);
                if (dataMap != null) {
                    Set<String> strings = dataMap.keySet();
                    for (String key : strings) {
                        String encodedString = URLEncoder.encode(dataMap.get(key).toString(), StandardCharsets.UTF_8.toString());
                        newQueryParams.add(key, encodedString);
                    }
                }
            }

        }

        // 替换查询参数
        URI newUri = UriComponentsBuilder.fromUri(uri)
                .query(null)
                .queryParams(newQueryParams)
                .build(true)
                .toUri();

        ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
        // 将解密后的参数重新设置到请求中

        uri = request.getURI();
        log.info("更改后的当前请求参数为: {}", uri.getRawQuery());
        return request;
    }
}

添加响应整体解密拦截器EncryptionFilter

package com.fir.gateway.filter.request;

import com.alibaba.fastjson.JSONObject;
import com.fir.gateway.config.GlobalConfig;
import com.fir.gateway.config.exception.CustomException;
import com.fir.gateway.config.result.AjaxStatus;
import com.fir.gateway.dto.ConnectDTO;
import com.fir.gateway.utils.AESUtils;
import com.fir.gateway.utils.RSAUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Set;

;


/**
 * 请求整理解密-请求拦截器
 *
 * @author fir
 */
@Slf4j
@Component
public class DecryptionFilter implements Ordered, GlobalFilter {


    /**
     * 网关参数配置
     */
    @Resource
    private GlobalConfig globalConfig;


    @Resource
    private RedisTemplate<String, Object> redisTemplate;


    @Override
    public int getOrder() {
        return -280;
    }


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("整体解密:start");
        ServerHttpRequest req = exchange.getRequest();
        String method = req.getMethodValue();

        boolean rsa = globalConfig.isRsa();
        boolean aes = globalConfig.isAes();
        if (rsa || aes) {
            // 白名单路由判断
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getPath().toString();
            List<String> whiteUrls = globalConfig.getWhiteUrls();

            if(!whiteUrls.contains(path)){
                ServerHttpRequest builder = req.mutate().build();
                if (HttpMethod.GET.matches(method)) {
                    log.info("当前请求参数为: {}", req.getQueryParams());
                    builder = changeGet(exchange, builder);
                } else if (HttpMethod.POST.matches(method)) {
                    log.info("当前请求参数为: {}", req.getQueryParams());
                    builder = changeGet(exchange, builder);
                }
                exchange = exchange.mutate().request(builder).build();
                log.info("整体解密:true");
            }else {
                log.info("整体解密:true,白名单");
            }
        }else {
            log.info("整体解密:true,验证已关闭");
        }
        return chain.filter(exchange);
    }


    /**
     * 获取请求参数等信息进行过滤处理
     *
     * @param exchange          请求
     * @param serverHttpRequest 请求
     * @return 处理结束的参数
     */
    @SneakyThrows
    private ServerHttpRequest changeGet(ServerWebExchange exchange, ServerHttpRequest serverHttpRequest) {
        String session = exchange.getRequest().getHeaders().getFirst("s");

        if (session == null) {
            throw new CustomException(AjaxStatus.SESSION_INVALID);
        }

        JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(session);
        if (jsonObject == null) {
            throw new CustomException(AjaxStatus.SESSION_EXPIRE);
        }
        ConnectDTO connectDTO = jsonObject.toJavaObject(ConnectDTO.class);
        String privateKey = connectDTO.getPrivateKey();
        String secretKey = connectDTO.getSecretKey();


        // 获取原参数
        URI uri = serverHttpRequest.getURI();
        String originalQuery = uri.getRawQuery();
        String decodedQuery = null;
        if(StringUtils.isNotBlank(originalQuery)){
            decodedQuery = URLDecoder.decode(originalQuery, "UTF-8");
        }

        // 更改参数
        MultiValueMap<String, String> newQueryParams = new LinkedMultiValueMap<>();
        if (StringUtils.isNotBlank(originalQuery) && org.springframework.util.StringUtils.hasText(decodedQuery)) {
            // 修改请求参数,String[] array只能处理前端特定加密 {data:加密内容的形式}, 传递到后端,会变更为 data=加密内容。
            // 除此以外的所有方式不能通过本方法进行解密
            String[] array = decodedQuery.split("=");
            if (array.length > 1) {
                decodedQuery = array[1];

                if (decodedQuery != null) {
                    boolean rsa = globalConfig.isRsa();
                    boolean aes = globalConfig.isAes();

                    if (rsa) {
                        // 对数据进行非对称解密
                        originalQuery = RSAUtils.decryptSection(decodedQuery, privateKey);
                    }
                    if (aes) {
                        // 对数据进行对称解密
                        originalQuery = AESUtils.decrypt(originalQuery, secretKey);
                    }
                }

                Map<String, Object> dataMap = JSONObject.parseObject(originalQuery, Map.class);
                if (dataMap != null) {
                    Set<String> strings = dataMap.keySet();
                    for (String key : strings) {
                        String encodedString = URLEncoder.encode(dataMap.get(key).toString(), StandardCharsets.UTF_8.toString());
                        newQueryParams.add(key, encodedString);
                    }
                }
            }

        }

        // 替换查询参数
        URI newUri = UriComponentsBuilder.fromUri(uri)
                .query(null)
                .queryParams(newQueryParams)
                .build(true)
                .toUri();

        ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
        // 将解密后的参数重新设置到请求中

        uri = request.getURI();
        log.info("更改后的当前请求参数为: {}", uri.getRawQuery());
        return request;
    }
}

前端

请求拦截器

添加整体加密逻辑

// 请求整体加密
if (AESKey) {
  const secretKey = this.get("secretKey");
  let date = JSON.stringify(request.params);
  date = this.encryptAES(date, secretKey);
  request.params = {"data": date};
}
if (AESKey && RSAKey) {
  const serverPublicKey = this.get("serverPublicKey");
  let date = request.params.data;
  date = this.rsaEncrypt(date, serverPublicKey);
  request.params = {"data": date};
} else if (RSAKey) {
  const serverPublicKey = this.get("serverPublicKey");
  let date = JSON.stringify(request.params);
  date = this.rsaEncrypt(date, serverPublicKey);
  request.params = {"data": date};
}

image.png

请求头中添加sessionId

            let s = this.get("sessionId")
            // 请求中增加会话信息
            if (s) {
                request.headers.s = s;
            }

image.png

响应拦截器

添加整体解密逻辑

修改一下内容:

return securityUtils.gatewayResponse(response);

image.png

securityUtils.js中添加响应处理函数

/**
 * gateway网关验证信息处理(响应头)
 */
gatewayResponse(response) {
  let key = true;

  // 放置业务逻辑代码
  // response是服务器端返回来的数据信息,与Promise获得数据一致
  let data = response.data
  // config包含请求信息
  let config = response.config

  // 判断 data 是否为对象
  if (typeof data === 'object' && data !== null) {
    // 判断 data 是否匹配特定格式
    if (
      Object.prototype.hasOwnProperty.call(data, 'msg') &&
      Object.prototype.hasOwnProperty.call(data, 'code') &&
      typeof data.msg === 'string' &&
      typeof data.code === 'number'
    ) {
      // 数据匹配特定格式
      if (data.code === 401) {
        sessionStorage.clear()

      }
      return data;
    }
  }


  // 获取当前请求的url
  let url = config.url
  whiteList.find(function (value) {
    if (value === url) {
      key = false;
    }
  });


  // 对非白名单数据进行整体解密处理
  if (key) {
    // 获取加密密钥,并传入解密组件进行解密
    if (RSAKey) {
      const privateKey = this.get("privateKey")
      data = this.rsaDecrypt(data, privateKey)
    }
    if (AESKey) {
      let securityKey = this.get("secretKey")
      data = this.decryptAES(data, securityKey)
    }

    if (data != null && data !== "") {
      data = JSON.parse(data);
    }else {
      data =  new Promise(() => {});
    }


  }

  return data;
},

image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1643628.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VsCode插件 -- Power Mode

一、安装插件 1. 首先在扩展市场里搜索 Power Mode 插件&#xff0c;如下图 二、配置插件 设置 点击小齿轮 打上勾 就可以了 第二种设置方法 1. 安装完成之后&#xff0c;使用快捷键 Ctrl Shift P 打开命令面板&#xff0c;在命令行中输入 settings.json &#xff0c; 选择首…

扩展学习|结合故事的力量和数字的力量:混合方法研究和混合研究综述

文献来源&#xff1a;Pluye, Pierre, and Quan Nha Hong. "Combining the power of stories and the power of numbers: mixed methods research and mixed studies reviews." Annual review of public health 35 (2014): 29-45. 文献获取&#xff1a;链接&#xff1…

【机器视觉】yolo-world-opencvsharp-.net4.8 C# 窗体应用程序

这段代码是基于 OpenCvSharp, OpenVinoSharp 和 .NET Framework 4.8 的 Windows Forms 应用程序。其主要目的是加载和编译机器学习模型&#xff0c;对输入数据进行推理&#xff0c;并显示结果。 下面是该程序的主要功能和方法的详细总结&#xff1a; 初始化 OpenVINO 运行时核心…

【微服务】配置管理

Nacos配置管理 配置管理配置共享配置热更新 配置管理 将微服务集群中常用&#xff0c;经常变化的配置都写到一个独立的配置文件微服务中进行统一管理 配置共享 在Nacos的界面当中进行配置管理&#xff0c;在配置列表中添加配置 比如各个服务中的jdbc的连接配置&#xff1a; …

【AI工具声音克隆】——OpenVoice一键部署modelScope一键使用

一、声音/音色克隆简介 声音或音色克隆的原理实现步骤主要基于深度学习技术&#xff0c;特别是语音合成和生成模型。以下是声音/音色克隆的大致实现步骤&#xff1a; 数据收集&#xff1a; 收集语音数据&#xff0c;作为模型的训练样本。数据应尽可能多样化&#xff0c;包括不…

基于stm32的USB虚拟U盘+FATFS+W25Q64

基于stm32的USB虚拟U盘FATFSW25Q64 本文目标&#xff1a;基于stm32的USB虚拟U盘FATFSW25Q64 按照本文的描述&#xff0c;简单跑通USB的MSC类来进行简单交互。 先决条件&#xff1a;拥有C语言基础&#xff0c;装有编译和集成的开发环境&#xff0c;比如&#xff1a;Keil uVis…

如果还有机会再跟芒格共度一天,巴菲特想做什么?

这是芒格因离世而缺席的第一年&#xff0c;但他的身影却无处不在。问答环节&#xff0c;巴菲特会不小心脱口而出“查理&#xff0c;该你回答了”&#xff0c;他也称&#xff0c;与芒格在一起的时间比独处要快乐。 可以说&#xff0c;虽然99岁的芒格因离世而缺席了2024年伯克希…

记录几种排序算法

十种常见排序算法可以分类两大类别&#xff1a;比较类排序和非比较类排序。 常见的快速排序、归并排序、堆排序以及冒泡排序等都属于比较类排序算法。比较类排序是通过比较来决定元素间的相对次序&#xff0c;其时间复杂度不能突破 O(nlogn)。在冒泡排序之类的排序中&…

Python基础详解一

一&#xff0c;print打印 print("hello word") print(hello word) 双引号和单引号都可以 二&#xff0c;数据类型 Python中常用的有6种值的类型 输出类型信息 print(type(11)) print(type("22")) print(type(22.2)) <class int> <class str&…

Mybatis进阶2

Mybatis进阶1-CSDN博客 Mybatis入门-CSDN博客 Mybatis入门2-CSDN博客 我们接下来要学习Mybatis的高级查询 我们先在数据库中准备我们需要的数据表 teacher表 课程表&#xff1a;与教师表是一对多的关系&#xff0c;所以有一个外键字段 学生表 由于学生表和课程表是多对多的…

鸿蒙ArkTs开发,仿抖音个人中心header 下拉放大

如果是iOS 或者android 上实现&#xff0c;可以用Scollview 的contentOffset 来实现&#xff0c;然而在鸿蒙ets中该如何实现&#xff1f;废话不多说开始撸代码 第一步、实现一个header // 创建header&#xff0c;准备一张背景图片BuilderHeaderBuilder(){Column() {Row() {Ima…

算法入门<一>:C++各种排序算法详解及示例源码

1、排序算法 排序算法&#xff08;sorting algorithm&#xff09;用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用&#xff0c;因为有序数据通常能够被更高效地查找、分析和处理。 1.1 评价维度 运行效率&#xff1a;我们期望排序算法的时间复杂度尽量低&#xf…

C语言学习【C语言基本数据类型】

C语言学习【C语言基本数据类型】 整数溢出 /* 整数溢出 */ #include "stdio.h" /* Last Modified Time: 2024-05-05 17:53:49 */int main(void) {int i 2147483647;unsigned int j 4294967295;printf("%d %d %d\n", i, i1, i2);printf("%u %u %u\…

【数据结构初阶】直接插入排序

最近浅学了直接插入排序&#xff0c;写个博客做笔记&#xff01;笔记功能除外若能对读者老爷有所帮助最好不过了&#xff01; 直接插入排序是插入排序的一种&#xff0c;那么介绍直接插入排序之前先介绍一下常见的排序算法&#xff01; 目录 1.常见的排序算法 2.直接插入排…

500行代码实现贪吃蛇(1)

文章目录 目录1. Win32 API 介绍1.1 Win32 API1.2 控制台程序&#xff08;Console&#xff09;1.3 控制台屏幕上的坐标COORD1.4 [GetStdHandle](https://learn.microsoft.com/zh-cn/windows/console/getstdhandle)1.5 [GetConsoleCursorInfo](https://learn.microsoft.com/zh-c…

项目经理【人】原则

系列文章目录 【引论一】项目管理的意义 【引论二】项目管理的逻辑 【环境】概述 【环境】原则 【环境】任务 【环境】绩效 【人】概述 【人】原则 一、共创模式 1.1 共创模式 二、干系人的影响力强度和态度 2.1 干系人影响力 2.2 干系人态度 2.3 干系人管理 三、干系人权力…

Java17 --- SpringCloud之Gateway

目录 一、Gateway网关创建 1.1、创建微服务子工程9527及配置和依赖 1.1.1、pom依赖 1.1.2、yml配置 1.1.3、主启动类并测试入驻consul 二、实现路由映射 2.1、服务8001新增测试代码 2.2、修改9527服务yml配置文件 2.3、远程调用接口加gateway 2.3.1、新增80服务测…

【Android学习】简单的登录页面和业务逻辑实现

实现功能 1 登录页&#xff1a;密码登录和验证码登录 2 忘记密码页&#xff1a;修改密码 3 页面基础逻辑 java代码 基础页面 XML login_main.xml <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.and…

C++静态数组和C语言静态数组的区别( array,int a[])

目录 一、区别 1、越界读&#xff0c;检查不出来 2、越界写&#xff0c;抽查&#xff0c;可能检查不出来&#xff0c;有局限性 二、array缺点 一、区别 C语言的静态数组int a[]; 静态数组的越界检查不稳定的&#xff1a; 1、越界读&#xff0c;检查不出来 2、越界写&#x…

开发一款简易APP

希望打开APP后,显示当前时间..可能不实用,重在体验 安装Flutter 如果在arm架构的 Mac 电脑上进行开发&#xff0c;需要安装 Rosetta 2, 因为一些辅助工具需要&#xff0c;可通过手动运行下面的命令来安装&#xff1a; sudo softwareupdate --install-rosetta --agree-to-licens…