【Sa-Token】SpringBoot 整合 Sa-Token 快速实现 API 接口签名安全校验

在涉及跨系统接口调用时,我们容易碰到以下安全问题:

  • 请求身份被伪造
  • 请求参数被篡改
  • 请求被抓包,然后重放攻击

sa-token api-sign 模块将帮你轻松解决以上难题。(此插件是内嵌到 sa-token-core 核心包中的模块,开发者无需再次引入其它依赖,插件直接可用)

假设我们有如下业务需求:

用户在 A 系统参与活动成功后,活动奖励以余额的形式下发到 B 系统。

1. 初始方案:直接裸奔

在不考虑安全问题的情况下,我们很容易完成这个需求:

1、在 B 系统开放一个接口

@RestController
@RequestMapping("/sign")
public class SignController {@PostMapping("/addMoney")public String addMoney(Long userId, Long money) {// TODO 处理业务...return "ADD SUCCESS";}}

2、在 A 系统使用 http 工具类调用这个接口

@RestController
@RequestMapping("/activity")
public class ActivityController {@PostMapping("/join")public String join() {// 参加完活动后,发送余额Long userId = 1L;Long money = 100L;Map<String, Object> params = new HashMap<>();params.put("userId", userId);params.put("money", money);String url = "http://localhost:8079/sign/addMoney";String result = HttpUtil.post(url, params);return "join";}}

上述代码简单的完成了需求,但是很明显它有一个安全问题:
B 系统开放的接口不仅可以被 A 系统调用,还可以被其它任何人调用,甚至别人可以本地跑一个 for 循环调用这个接口,为自己无限充值金额

2. 方案升级:增加 secretKey 校验

为防止 B 系统开放的接口被陌生人任意调用,我们增加一个 secretKey 参数

@PostMapping("/addMoney")
public String addMoney(Long userId, Long money, String secretKey) {// 校验 secretKeyif (!check(secretKey)) {throw new RuntimeException("无效 secretKey,无法响应请求");}// TODO 处理业务...return "ADD SUCCESS";
}

由于 A 系统是我们 “自己人”,所以它可以拿着 secretKey 进行合法请求:

@PostMapping("/join")
public String join() {// 参加完活动后,发送余额Map<String, Object> params = new HashMap<>();params.put("userId", userId);params.put("money", money);params.put("secretKey", "×××××××××××");String url = "http://localhost:8079/sign/addMoney";String result = HttpUtil.post(url, params);return "join";
}

现在,即使 B 系统的接口被暴露了,也不会被陌生人任意调用了,安全性得到了一定的保证,但是仍然存在一些问题:

  1. 如果请求被抓包,secretKey 就会泄露,因为每次请求都在 url 中明文传输了 secretKey 参数。
  2. 如果请求被抓包,请求的其它参数就可以被任意修改,例如可以将 money 参数修改为 9999999,B系统无法确定参数是否被修改过。

3.方案再升级:使用摘要算法生成参数签名

首先,在 A 系统不要直接发起请求,而是先计算一个 sign 参数:

@PostMapping("/join")
public String join() {// 参加完活动后,发送余额Long userId = 1L;Long money = 100L;String secretKey = "×××××××××××";// 计算 signString sign = md5("money=" + money + "&userId=" + userId + "&key=" + secretKey);Map<String, Object> params = new HashMap<>();params.put("userId", userId);params.put("money", money);params.put("sign", sign);String url = "http://localhost:8079/sign/addMoney";String result = HttpUtil.post(url, params);return "join";
}

注意:此处计算签名时,需要将所有参数按照字典顺序依次排列(key除外,挂在最后面)

然后在 B 系统接收请求时,使用同样的算法、同样的秘钥,生成 sign 字符串,与参数中 sign 值进行比较:

@PostMapping("/addMoney")
public String addMoney(Long userId, Long money, String sign) {// 在 B 系统,使用同样的算法、同样的密钥,计算出 sign2,与传入的 sign 进行比对String sign2 = md5("money=" + money + "&userId=" + userId + "&key=" + secretKey);if (!sign2.equals(sign)) {return "无效 sign,无法响应请求";}// TODO 处理业务...return "ADD SUCCESS";
}

因为 sign 的值是由 userId、money、secretKey 三个参数共同决定的,所以只要有一个参数不一致,就会造成最终生成 sign 也是不一致的,所以,根据比对结果:

  • 如果 sign 一致,说明这是个合法请求。
  • 如果 sign 不一致,说明发起请求的客户端秘钥不正确,或者请求参数被篡改过,是个不合法请求。

此方案优点:

  • 不在 url 中直接传递 secretKey 参数了,避免了泄露风险。
  • 由于 sign 参数的限制,请求中的参数也不可被篡改,B 系统可放心的使用这些参数。

此方案仍然存在以下缺陷:

  • 被抓包后,请求可以被无限重放,B 系统无法判断请求是真正来自于 A 系统发出的,还是被抓包后重放的。
@PostMapping("/join")
public String join() {// 参加完活动后,发送余额Long userId = 1L;Long money = 100L;String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串String secretKey = "×××××××××××";// 计算 signString sign = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey);Map<String, Object> params = new HashMap<>();params.put("userId", userId);params.put("money", money);params.put("nonce", nonce);params.put("sign", sign);String url = "http://localhost:8079/sign/addMoney";String result = HttpUtil.post(url, params);return "join";
}

4. 方案再再升级:追加 nonce 随机字符串

首先,在 A 系统发起调用前,追加一个 nonce 参数,一起参与到签名中:

public String join() {// 参加完活动后,发送余额Long userId = 1L;Long money = 100L;String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串String secretKey = "×××××××××××";// 计算 signString sign = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey);Map<String, Object> params = new HashMap<>();params.put("userId", userId);params.put("money", money);params.put("nonce", nonce);params.put("sign", sign);String url = "http://localhost:8079/sign/addMoney";String result = HttpUtil.post(url, params);return "join";
}

然后在 B 系统接收请求时,也把 nonce 参数加进去生成 sign 字符串,进行比较:

public String addMoney(Long userId, Long money, String nonce,String sign) {// 检查此 nonce 是否已被使用过了if (Objects.nonNull(CacheUtil.get("nonce_" + nonce))) {return "此 nonce 已被使用过了,请求无效";}// 在 B 系统,使用同样的算法、同样的密钥,计算出 sign2,与传入的 sign 进行比对String sign2 = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey);if (!sign2.equals(sign)) {return "无效 sign,无法响应请求";}// 存入缓存CacheUtil.set("nonce_" + nonce, "1");// TODO 处理业务...return "ADD SUCCESS";
}

代码分析:

  1. 为方便理解,我们先看第 3 步:此处在校验签名成功后,将 nonce 随机字符串记入缓存中。
  2. 再看第 1 步:每次请求进来,先查看一下缓存中是否已经记录了这个随机字符串,如果是,则立即返回:无效请求。

这两步的组合,保证了一个 nonce 随机字符串只能被使用一次,如果请求被抓包后重放,是无法通过 nonce 校验的。

至此,问题似乎已被解决了 …… 吗?

别急,我们还有一个问题没有考虑:这个 nonce 在字符串在缓存应该被保存多久呢?

  • 保存 15 分钟?那抓包的人只需要等待 15 分钟,你的 nonce 记录在缓存中消失,请求就可以被重放了。
  • 那保存 24 小时?保存一周?保存半个月?好像无论保存多久,都无法从根本上解决这个问题。

你可能会想到,那我永久保存吧。这样确实能解决问题,但显然服务器承载不了这么做,即使再微小的数据量,在时间的累加下,也总一天会超出服务器能够承载的上限。

5. 方案再再再升级:追加 timestamp 时间戳

我们可以再追加一个 timestamp 时间戳参数,将请求的有效性限定在一个有限时间范围内,例如 15分钟。

首先,在 A 系统追加 timestamp 参数:

public String join() {// 参加完活动后,发送余额Long userId = 1L;Long money = 100L;Long timestamp = System.currentTimeMillis();String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串String secretKey = "×××××××××××";// 计算 signString sign = md5("money=" + money + "&nonce=" + nonce + "&timestamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey);Map<String, Object> params = new HashMap<>();params.put("userId", userId);params.put("money", money);params.put("nonce", nonce);params.put("timestamp", timestamp);params.put("sign", sign);String url = "http://localhost:8079/sign/addMoney";String result = HttpUtil.post(url, params);return "join";
}

在 B 系统检测这个 timestamp 是否超出了允许的范围

public String addMoney(Long userId, Long money, Long timestamp, String nonce,String sign) {// 1、检查 timestamp 是否超出允许的范围(此处假定最大允许15分钟差距)long timestampDisparity = System.currentTimeMillis() - timestamp; // 实际的时间差if(timestampDisparity > 1000 * 60 * 15) {return "timestamp 时间差超出允许的范围,请求无效";}// 检查此 nonce 是否已被使用过了if (Objects.nonNull(CacheUtil.get("nonce_" + nonce))) {return "此 nonce 已被使用过了,请求无效";}// 在 B 系统,使用同样的算法、同样的密钥,计算出 sign2,与传入的 sign 进行比对String sign2 = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey);if (!sign2.equals(sign)) {return "无效 sign,无法响应请求";}// 将 nonce 记入缓存,ttl 有效期和 allowDisparity 允许时间差一致 CacheUtil.set("nonce_" + nonce, "1", 1000 * 60 * 15);// TODO 处理业务...return "ADD SUCCESS";
}

至此,抓包者:

  • 如果在 15 分钟内重放攻击,nonce 参数不答应:缓存中可以查出 nonce 值,直接拒绝响应请求。
  • 如果在 15 分钟后重放攻击,timestamp 参数不答应:超出了允许的 timestamp 时间差,直接拒绝响应请求。

6. 服务器的时钟差异造成安全问题

以上的代码,均假设 A 系统服务器与 B 系统服务器的时钟一致,才可以正常完成安全校验,但在实际的开发场景中,有些服务器会存在时钟不准确的问题。

假设 A 服务器与 B 服务器的时钟差异为 10 分钟,即:在 A 服务器为 8:00 的时候,B 服务器为 7:50。

  1. A 系统发起请求,其生成的时间戳也是代表 8:00。
  2. B 系统接受到请求后,完成业务处理,此时 nonce 的 ttl 为 15分钟,到期时间为 7:50 + 15分 = 8:05。
  3. 8.05 后,nonce 缓存消失,抓包者重放请求攻击:
    • timestamp 校验通过:因为时间戳差距仅有 8.05 - 8.00 = 5分钟,小于 15 分钟,校验通过。
    • -nonce 校验通过:因为此时 nonce 缓存已经消失,可以通过校验。
    • sign 校验通过:因为这本来就是由 A 系统构建的一个合法签名。
    • 攻击完成。

要解决上述问题,有两种方案:

  • 方案一:修改服务器时钟,使两个服务器时钟保持一致。
  • 方案二:在代码层面兼容时钟不一致的场景。

要采用方案一的同学可自行搜索一下同步时钟的方法,在此暂不赘述,此处详细阐述一下方案二。

我们只需简单修改一下,B 系统校验参数的代码即可:

public String addMoney(Long userId, Long money, Long timestamp, String nonce,String sign) {// 1、检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值)long timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp);if(timestampDisparity > 1000 * 60 * 15) {return "timestamp 时间差超出允许的范围,请求无效";}// 检查此 nonce 是否已被使用过了if (Objects.nonNull(CacheUtil.get("nonce_" + nonce))) {return "此 nonce 已被使用过了,请求无效";}// 在 B 系统,使用同样的算法、同样的密钥,计算出 sign2,与传入的 sign 进行比对String sign2 = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey);if (!sign2.equals(sign)) {return "无效 sign,无法响应请求";}// 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )CacheUtil.set("nonce_" + nonce, "1", (1000 * 60 * 15) * 2;// TODO 处理业务...return "ADD SUCCESS";
}

8. 使用 Sa-Token 框架完成 API 参数签名

接下来步入正题,使用 Sa-Token 内置的 sign 模块,方便的完成 API 签名创建、校验等步骤:

  • 不限制请求的参数数量,方便组织业务需求代码。
  • 自动补全 nonce、timestamp 参数,省时省力。
  • 自动构建签名,并序列化参数为字符串。
  • 一句代码完成 nonce、timestamp、sign 的校验,防伪造请求调用、防参数篡改、防重放攻击。

8.1 引入依赖

api-sign 模块已内嵌到核心包,只需要引入 sa-token 本身依赖即可:(请求发起端和接收端都需要引入)

<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot-starter</artifactId><version>1.35.0.RC</version>
</dependency>

8.2 配置密钥

请求发起端和接收端需要配置一个相同的秘钥,在 application.yml 中配置:

sa-token: sign:# API 接口签名秘钥 (随便乱摁几个字母即可)secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor

8.3 请求发起端构建签名

public String join() {// 参加完活动后,发送余额Long userId = 1L;Long money = 100L;Map<String, Object> params = new HashMap<>();params.put("userId", userId);params.put("money", money);SaSignUtil.addSignParamsAndJoin(params);String url = "http://localhost:8079/sign/addMoney";return HttpUtil.post(url, params);
}

8.4 请求接受端校验签名

public String addMoney(Long userId, Long money) {// 1、校验请求中的签名SaSignUtil.checkRequest(SaHolder.getRequest());// 2、校验通过,处理业务System.out.println("userId=" + userId);System.out.println("money=" + money);return "ADD SUCCESS";
}

如上代码便可简单方便的完成 API 接口参数签名校验,当请求端的秘钥不对,或者请求参数被篡改、请求被重放时,均无法通过 SaSignUtil.checkRequest 校验

8.5 原理分析

8.5.1 构建签名

SaSignUtil#addSignParamsAndJoin(params);

public static String addSignParamsAndJoin(Map<String, Object> paramsMap) {return SaManager.getSaSignTemplate().addSignParamsAndJoin(paramsMap);
}

会调用 SaSignTemplate 类中的方法

SaSignTemplate#addSignParamsAndJoin() 方法

public String addSignParamsAndJoin(Map<String, Object> paramsMap) {// 1.添加参数:timestamp、nonce、signparamsMap = this.addSignParams(paramsMap);// 2.将 map 使用 & 转化为Stringreturn this.joinParams(paramsMap);
}

这个方法有两个逻辑:

  1. 添加参数:timestamp、nonce、sign
  2. 将 map 使用 & 转化为String

SaSignTemplate#addSignParams() 方法

public Map<String, Object> addSignParams(Map<String, Object> paramsMap) {paramsMap.put(timestamp, String.valueOf(System.currentTimeMillis()));paramsMap.put(nonce, SaFoxUtil.getRandomString(32));paramsMap.put(sign, this.createSign(paramsMap));return paramsMap;
}

SaSignTemplate#createSign() 方法:生成签名

public String createSign(Map<String, ?> paramsMap) {String secretKey = this.getSecretKey();SaSignException.throwByNull(secretKey, "参与参数签名的秘钥不可为空", 12201);if (((Map)paramsMap).containsKey(sign)) {paramsMap = new TreeMap((Map)paramsMap);((Map)paramsMap).remove(sign);}// 按照数据字典进行排序,并将 map 使用 & 转化为StringString paramsStr = this.joinParamsDictSort((Map)paramsMap);String fullStr = paramsStr + "&" + key + "=" + secretKey;// md5return this.abstractStr(fullStr);
}public String abstractStr(String fullStr) {return SaSecureUtil.md5(fullStr);
}

这个方法有两个逻辑:

  1. 按照数据字典进行排序,并将 map 使用 & 转化为String
  2. 使用 md5 摘要算法

8.5.2 验证签名

SaSignUtil.checkRequest(SaHolder.getRequest());

public static void checkRequest(SaRequest request) {SaManager.getSaSignTemplate().checkRequest(request);
}

还是会调用 SaSignTemplate 类中的方法

SaSignTemplate#checkParamMap() 方法:校验请求参数

public void checkRequest(SaRequest request) {this.checkParamMap(request.getParamMap());
}public void checkParamMap(Map<String, String> paramMap) {String timestampValue = (String)paramMap.get(timestamp);String nonceValue = (String)paramMap.get(nonce);String signValue = (String)paramMap.get(sign);// 1.校验时间戳this.checkTimestamp(Long.parseLong(timestampValue));// 2.校验随机数if (this.getSignConfigOrGlobal().getIsCheckNonce()) {this.checkNonce(nonceValue);}// 3.校验签名this.checkSign(paramMap, signValue);
}

这个方法有三个逻辑:

  1. 校验时间戳:判断是否在时间差范围内
  2. 校验随机数:判断此随机数是否已使用
  3. 校验签名:判断原签名和现在生成的签名是否一致

SaSignTemplate#checkNonce() 方法:校验随机数

public void checkNonce(String nonce) {if (SaFoxUtil.isEmpty(nonce)) {throw new SaSignException("nonce 为空,无效");} else {String key = this.splicingNonceSaveKey(nonce);if (SaManager.getSaTokenDao().get(key) != null) {throw new SaSignException("此 nonce 已被使用过,不可重复使用:" + nonce);} else {SaManager.getSaTokenDao().set(key, nonce, this.getSignConfigOrGlobal().getSaveNonceExpire() * 2L + 2L);}}
}

SaToken 存储

SaTokenDao 是存储接口,默认实现是用的是 SaTokenDaoDefaultImplSaTokenDaoDefaultImpl 存储数据,主要是通过 ConcurrentHashMap 存放在本地内存中。

SaManager#getSaTokenDao() 方法:

public static SaTokenDao getSaTokenDao() {if (saTokenDao == null) {Class var0 = SaManager.class;synchronized(SaManager.class) {if (saTokenDao == null) {setSaTokenDaoMethod(new SaTokenDaoDefaultImpl());}}}return saTokenDao;
}

SaTokenDaoDefaultImpl

public class SaTokenDaoDefaultImpl implements SaTokenDao {// 数据集合 public Map<String, Object> dataMap = new ConcurrentHashMap();// 过期时间集合 (单位: 毫秒) , 记录所有key的到期时间 [注意不是剩余存活时间] public Map<String, Long> expireMap = new ConcurrentHashMap();public Thread refreshThread;public volatile boolean refreshFlag;public SaTokenDaoDefaultImpl() {// 定时清理过期数据this.initRefreshThread();}public String get(String key) {this.clearKeyByTimeout(key);return (String)this.dataMap.get(key);}public void set(String key, String value, long timeout) {if (timeout != 0L && timeout > -2L) {this.dataMap.put(key, value);this.expireMap.put(key, timeout == -1L ? -1L : System.currentTimeMillis() + timeout * 1000L);}}public void initRefreshThread() {if (SaManager.getConfig().getDataRefreshPeriod() > 0) {this.refreshFlag = true;this.refreshThread = new Thread(() -> {while(true) {try {try {if (!this.refreshFlag) {return;}this.refreshDataMap();} catch (Exception var2) {var2.printStackTrace();}int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod();if (dataRefreshPeriod <= 0) {dataRefreshPeriod = 1;}Thread.sleep((long)dataRefreshPeriod * 1000L);} catch (Exception var3) {var3.printStackTrace();}}});this.refreshThread.start();}}
}

如果仅仅存放在本地内存中,涉及到多个项目,可能数据无法共享。

引入仓库 sa-token-dao-redis-jackson

<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-redis-jackson</artifactId><version>1.35.0.RC</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>

SaTokenDaoRedisJackson 使用 Redis 作为存储数据的地方

SaBeanInject#setSaTokenDaoSaBeanInject 是自动配置的。当系统中存在 SaTokenDao的 Bean 实例,则设置SaTokenDao 实例

public class SaBeanInject {@Autowired(required = false)public void setSaTokenDao(SaTokenDao saTokenDao) {SaManager.setSaTokenDao(saTokenDao);}
}

参考:
API 接口参数签名
【开源项目】使用Sa-Token框架完成API参数签名

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

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

相关文章

MFC学习之2048小游戏程序源码

2048游戏的开发原理相对简单&#xff0c;它基于一个4x4的方格&#xff0c;通过控制数字方块的移动来合成相同的数字方块&#xff0c;并生成新的数字方块。 具体实现过程如下&#xff1a; 确定需求&#xff1a;首先需要明确游戏的功能需求&#xff0c;如产生随机数字方块、控制…

MYSQL执行一条SELECT语句的具体流程

昨天CSDN突然抽风 我一个ctrlz把整篇文章给撤掉了还不能复原 直接心态崩了不想写了 不过这部分果然还是很重要,还是写出来吧 流程图 这里面总共有两层结构Server层 储存引擎 Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现&#xff0c;主要包…

WebSocket理解

WebSocket理解 WebSocket定义与HTTP关系相同点:不同点&#xff1a;联系总体过程 HTTP问题长轮询Ajax轮询 WebSocket特点 WebSocket 定义 本质上是TCP的协议 持久化的协议 实现了浏览器和服务器的全双工通信&#xff0c;能更好的节省服务器资源和带宽 与HTTP关系 相同点: 基于…

接口测试 react+unittest+flask 接口自动化测试平台

目录 1 前言 2 框架 2-1 框架简介 2-2 框架介绍 2-3 框架结构 3 平台 3-1 平台组件图 1 新建用例 2 生成测试任务 3 执行并查看测试报告 3-2 用例管理 3-2-1 用例设计 3-3 任务管理 3-3-1 创建任务 3-3-2 执行任务 3-3-3 测试报告 3-3-4 邮件通知 1 前言 构建…

【力扣算法12】之 11. 盛最多水的容器 python

文章目录 问题描述示例1示例2提示 思路分析代码分析完整代码详细分析运行效果截图调用示例运行结果完结 问题描述 给定一个长度为 n 的整数数组 height 。有n条垂线&#xff0c;第i条线的两个端点是(i, 0)和(i, height[i])。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构…

自然语言处理从入门到应用——LangChain:快速入门-[安装与环境配置]

分类目录&#xff1a;《自然语言处理从入门到应用》总目录 在《自然语言处理从入门到应用——LangChain&#xff1a;快速入门》系列文章中我们会用最简练的语言与范例带领大家快速调试并上手LangChain&#xff0c;读者读完本系列的文章后&#xff0c;就会对LangChain有一个大致…

解决IDEA项目external libraries依赖包消失的问题

有时候电脑重启后&#xff0c;再打开IDEA上的项目时会出现external libraries目录下的依赖包都消失了的情况&#xff0c;只剩下了一个JDK的包 网上说可以通过刷新IDEA的缓存解决&#xff0c;但我试了没有效果&#xff0c;最后使用如下办法解决&#xff1a; 1.删除项目目录下的…

图论算法笔记

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 第12章 最短路径算法12-1 有权图的最短路径问题最短路径问题-路径规划单源最短路径带权图的最短路径和无权图的最短路径带权图的最短路径算法-Dijkstra算法 12-2 Di…

搭载率突破40%!智能数字座舱比拼,车企还有降本空间吗?

进入2023年&#xff0c;汽车行业的「降本」风潮&#xff0c;驱动产业链上下游开始思考智能化、电动化的投入产出。除了显性的硬件成本&#xff08;继续堆料&#xff0c;强调性价比&#xff0c;还是减配&#xff09;&#xff0c;软件及背后的开发成本&#xff0c;对于车企来说&a…

GitLab 入选 Forrester Wave™️ 集成软件交付平台,并获评唯一「领导者」!

越来越多企业意识到多工具链集成带来的低效和高成本问题&#xff0c;同时承受着可见性低、反馈不畅通、网络风险大等痛点&#xff0c;应用平台方法来交付软件的呼声越来越大。 极狐(GitLab) 很早就意识到了平台方法的价值&#xff0c;也坚信极狐GitLab 这种单一应用程序的 DevS…

学习vue2笔记

学习vue2笔记 文章目录 学习vue2笔记脚手架文件结构关于不同版本的Vuevue.config.js配置文件ref属性props配置项mixin(混入)插件scoped样式总结TodoList案例webStorage组件的自定义事件全局事件总线&#xff08;GlobalEventBus&#xff09;消息订阅与发布&#xff08;pubsub&am…

Hutool - HttpUtil发送GET、POST请求

文章目录 引入Hutool依赖GET发起get请求 POST准备工作创建实体 - 大创建实体 - 小 发起post请求 结语 引入Hutool依赖 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version> &l…

el-date-picker 日期时间进行限制,精确到时分秒

需求&#xff1a;用户只能选择当时时间或当前时间之前的时间&#xff0c;且精确到时分秒 实现效果&#xff1a;用户只能选择当前时间的时间&#xff0c;如果选择是当天之前的时间&#xff0c;时分秒不做限制&#xff0c;如果选择的是当天时间&#xff0c;就要判断时分秒&#…

【数据挖掘】时间序列教程【十】

5.4 通用卡尔曼滤波 上一节中描述的状态空间模型作为观测方程的更一般的公式 和状态方程 这里是一个p1 向量是一个k1 向量, 是一个pk 矩阵, 是kk 矩阵。我们可以想到的和 给定初始状态 和 &#xff0c;预测方程为&#xff08;类似于上面&#xff09; 并且更新方程是&#x…

华为Harmony应用开发初探

HarmonyOS是一款面向万物互联时代的、全新的分布式操作系统。在传统的单设备系统能力基础上,HarmonyOS提出了基于同一套系统能力、适配多种终端形态的分布式理念,能够支持手机、平板、智能穿戴、智慧屏、车机等多种终端设备,提供全场景(移动办公、运动健康、社交通信、媒体…

外包干了2个月,技术退步明显...

先说一下自己的情况&#xff0c;大专生&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试…

Linux进程(二)---进程的优先级和环境变量

我想在这先完成上一章的一个未说完的话题.最后一个我们讲到了僵尸进程&#xff0c;是指子进程已经结束&#xff0c;但是父进程还在运行没有来得及回收.此时这个子进程便是僵尸进程. 但是如果父进程运行完了&#xff0c;也没有回收就直接结束了&#xff0c;那这个子进程改由谁维…

用html+javascript打造公文一键排版系统3:获取参数设置、公文标题排版

我们用自定义函数setDocFmt()来实现对公文的排版。 一、获取公文参数值 要对公文进行排版&#xff0c;首先要读取公文“参数设置”区中的参数值。比如公文要求对公文标题的一般规定是&#xff1a;一般用2号小标宋体字&#xff0c;居中显示。标题与正文中间空一行。 这些是“参…

合宙Air724UG LuatOS-Air core API--zkw530z

zkw530z zkw530z快速定位 zkw530z.settime(year,month,day,hour,min,sec) 创建530Z秒定时间参数 参数 参数 类型 释义 取值 year number 年份 month number 月份 day number 日期 hour number 小时 min number 分钟 sec number 秒钟 返回值 无 例子 zkw530z.settime…

RestHighLevelClient集成ES 7.X

Maven依赖 依赖版本号和elasticsearch版本号对应起来 <dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-client</artifactId><version>7.17.6</version></dependency><!-- elasti…