开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

提升 RTC 音频体验 - 从搞懂硬件开始

发表于 2021-11-29

前言

RTC(实时音视频通信)技术的快速发展,助力了直播、短视频等互动娱乐形式的普及;在全球疫情持续蔓延的态势下,云会议需求呈现爆发式增长,进一步推动了 RTC 行业的快速发展。为了给客户提供稳定可靠的服务,网络系统方面需要不断提升频道连通率,降低会议过程中的断流率,增强抗弱网能力;视频方面需要提升视频清晰度,降低视频卡顿率等,音频方面在追求端到端 MOS 的同时,也要重点关注音频 3A 算法的效果,这些都是各厂家必须修炼的 “内功”,也是最终沉淀下来的核心竞争力。本文将重点阐述硬件设备采集的音频质量对 RTC 端到端音频体验的重要性。

采集质量不佳,会有什么影响?

在 RTC 架构中,端到端的音频信号处理流程大致如下图,上行分别经过了音频信号的采集,音频 3A(AEC: 回声消除、ANS: 自适应降噪和 AGC: 自动增益控制)和编码;下行分别经过丢包恢复,解码,混音和播放。

image.png
端到端的音频信号处理流程

不难看出,音频信号经过模数转换,再经过设备集成的音频信号处理芯片,最后才传递给 RTC SDK。由于硬件厂商的不同,音频采集解决方案参差不齐,因此采集到的音频质量的好坏直接影响着 3A 算法拿到的生产资料的可用性,同时也决定这最终用户接收到音频信号质量的上限。根据实际工作中遇到的音频问题,因为设备采集引起的问题基本可以归纳为如下几类:

image.png

举几个例子:

(1)采集异常

采集异常主要体现在频谱 “模糊”,严重的会导致无法听懂语义,影响正常交流。如下语谱图。
image.png

另外,采集异常后,播放的信号被麦克风采集后也会表现出异常,从而引起严重的非线性失真,影响回声消除效果,如下图。
image.png

(2) 采集抖动

常见的就是采集丢数据,听感上会听到有很多高频噪点(下图为上图中噪点放大后的局部图),严重的会影响 AEC 算法中对延时估计准确性和远近端非因果问题,严重的会导致漏回声。
image.png

image.png

(3)爆音和音量小问题

采集爆音问题主要发生在 PC,也是 PC 端设备最应该避免的问题,影响较大,除了截顶导致的频谱失真之外,严重的非线性失真会影响回声消除效果。爆音问题需要 AGC 算法通过自适应调节 PC 端模拟增益以及麦克风加强解决。
image.png

(4)频谱缺失

频谱缺失主要是硬件回调的音频采样率与实际的频谱分布不一致,即使编码器给到很高的编码码率,听感上也没有高音质的效果,如下图,采集信号采样率为 48kHz,但是频谱上限却只有 8k。
image.png

改善采集音质,硬件层面我们能做什么?

具备 RTC 能力的硬件设备早已渗透我们生活的方方面面,常见的如移动端手机和 PC,现在甚至连儿童电话手表,天猫精灵以及各种高端的指纹密码锁等设备都支持了 RTC。然而,设备的多样性直接决定这采集能力的差异性,抛开声学元器件设计差异这一因素,就 Android 端而言,芯片和软件系统的差异使得同一品牌的手机,也没办法用同一种配置适配所有型号的手机。

另外,现在绝大多数的移动端设备都自带硬件音频信号处理(后称硬件 3A)能力,不同芯片效果方面也是千差万别的同时,更严重的是经过硬件处理的音频信号频谱往往会有缺失,如开启硬件 3A 后回调到 RTC SDK 的音频信号频谱上限仅支持到 8k,相当于 16kHz 采样的音频信号,尤其在娱乐方面根本无法满足我们对高音质的追求。因此,做好硬件层的适配工作,是保障 RTC 高质量音频体验的基础。

Android 端

(1)需要搞清楚 javaaudioclass 和 opensles 这两种模式的差异,以及各自需要适配的参数,掌握关闭硬件 3A 的配置。

(2)采集抖动或音频音量异常,可以试试更改请求的采样率,通常设置的 48k 采样不会适用于所有的 android 设备。

Windows 端

(1)当前很多 Windows 设备会在屏幕顶端内置麦克风阵列,提供音频增强功能,开启方式如下图。这个功能默认屏幕正前方夹角区域为拾音区域,通过麦克风阵列技术可以有效的增强拾音区域内发言人语音,“隔离” 拾音区域以外的 “噪声”,其主要的弊端就在于开启此功能后仅支持 8k 频谱,且各厂家增强算法存在差异,效果也参差不齐。因此,软件需要具备能够 bypass 硬件自带音频增强功能的能力,为高音质做保障。

image.png
Windows 设备自带的双麦阵列(图片来源于网络)

image.png
音频设置中的增强功能开关

image.png
开启音频增强后,带来的频谱缺失

(2)音量方面,PC 端设备都支持模拟增益调节,大多数带有阵列的 Windows 设备都有额外的麦克风加强(如下图)。软件算法层面(3A 中的 AGC)需要具备自适应调节他们的能力,保障音频采集音量的平稳以控制采集底噪水平。初值设置或自适应调节不当都会导致音量小和爆音等问题,严重的会影响回声消除和降噪的效果,带来影响可用性的风险。

image.png
模拟增益与麦克风加强

苹果设备

(1)ios 端适配工作较少,需要熟悉关闭硬件 3A 的配置,因为 ios 设备自带的硬件 3A 频谱也只能支持到 10k-12k。

(2)Mac 笔记本设备比较简单,仅提供了模拟增益调节。但是有一点需要注意,RTC 在支持双声道播放时,由于麦克风会与某个扬声器在同一侧,导致播放音频时附近的麦克风采集爆音问题,一般只能优化软件 AEC 算法解决。

总结

当 48k 高音质成了刚需,为了保障采集环节的高质量,一方面需要投入时间去掌握 Android 参数适配的规律,同时市面上出现的越来越多的定制化的 android 设备(手表,智能音箱等),也必不可少的需要先确定好配置参数;另一方面关闭硬件设备自带的音频处理功能,启用 RTC 自带的纯软 3A 算法也是一种趋势,前提是要优化好软件 3A 算法整体效果以及控制好功耗,这也是客户评测各厂家之间音频体验的必测项,也是各厂家的核心竞争力之一。

「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。公众号后台回复【技术】可加入阿里云视频云产品技术交流群,和业内大咖一起探讨音视频技术,获取更多行业最新信息。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

安全经典JWT算法漏洞

发表于 2021-11-29

1、什么是JWT?

JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用secret(HMAC算法)或使用“RSA或ECDSA的公用/私有key pair密钥对”对JWT进行签名。

尽管可以对JWT进行加密以提供双方之间的secrecy保密性,但我们将重点关注signed tokens已签名的令牌。signed tokens已签名的令牌可以验证其中包含的claims声明的integrity完整性,而encrypted tokens加密的令牌则将这些other parties其他方的claims声明隐藏。当使用“公钥/私钥对”对令牌进行签名时,signature also certifies签名还证明只有持有私钥的一方才是对其进行签名的一方。

摘自官网

2、JWT能做什么?

1、授权

这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

2、信息交换

JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以可以确保发件人是本人。此外,由于签名是使用标头和有效负载计算的,因此还可以验证内容是否遭到篡改。

3、基于session认证所显露的问题

1、开销

每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

2、扩展性

用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求必须还要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力,这也意味着限制了应用的扩展能力。

3、CSRF

因为是基于cookie来进行用户识别的,所以cookie如果被截获,用户就会很容易受到CSRF的攻击。

【一>所有资源获取<一】
1、200份很多已经买不到的绝版电子书
2、30G安全大厂内部的视频资料
3、100份src文档
4、常见安全面试题
5、ctf大赛经典题目解析
6、全套工具包
7、应急响应笔记

JWT简介

4、JWT的认证流程

首先,前端通过web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。

后端核对用户名和密码成功后,形成一个JWT Token。

后端将JWT字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage中,退出登录时前端删除保存的JWT即可。

前端在每次请求时将JWT放入HTTP Header中的Authorization字段。

后端校验前端传来的JWT的有效性。

验证通过后,后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

image.png

5、JWT的结构

5.1、令牌组成:header.payload.signature

1、标头(Header)

2、有效载荷(Payload)

3、签名(Signature)

5.2、Header

标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256(默认,HS256)或RSA(RS256)。它会使用Base64编码组成JWT结构的第一部分。

注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子的,它并不是一种加密过程。

类似这样:

1
2
3
4
json复制代码{
"alg": "HS256", // 加密算法
"typ": "JWT" // 类型
}

5.3、Payload

令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64编码组成JWT结构的第二部分

标准中注册的声明(建议但是不强制使用):

1、iss:jwt签发者

2、sub:jwt所面向的用户

3、aud:接收jwt的一方

4、exp:jwt的过期时间,这个过期时间必须要大于签发时间

5、nbf:定义在什么时间之前,该jwt都是不可用的

6、iat:jwt的签发时间

7、jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

类似这样:

1
2
3
4
5
json复制代码{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

5.4、Signature

前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息。Signature需要使用编码后的Header和Payload以及我们提供的一个密钥,然后使用Header中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过

如:HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), ‘secret’);

测试环境

在jwt.io/网站中收录有各类语言的…

Auth0实现的java-jwt:“maven: com.auth0 / java-jwt / 3.3.0”

Brian Campbell实现的jose4j:“maven: org.bitbucket.b_c / jose4j / 0.6.3”

connect2id实现的nimbus-jose-jwt:“maven: com.nimbusds / nimbus-jose-jwt / 5.7”

Les Haziewood实现的jjwt:“maven: io.jsonwebtoken / jjwt-root / 0.11.1”

Inversoft实现的prime-jwt:“maven: io.fusionauth / fusionauth-jwt / 3.5.0”

Vertx实现的vertx-auth-jwt:“maven: io.vertx / vertx-auth-jwt / 3.5.1”

image.png

本文只做简略介绍,每种JWT库的具体实现不同,各自也有优缺点。有兴趣的同学可以研究下,这里贴上一位大佬的测试环境,这些全部囊括其中:

https://github.com/monkeyk/MyOIDC/

黑盒测试

为了方便,这里直接用WebGoat靶场来做测试

直接利用WebGoat的Java源码来启动靶场,是比较麻烦的,因为对jdk的版本要求比较高。

利用docker来搭建WebGoat,依次输入命令:

1
2
3
4
5
6
bash复制代码docker search webgoat
docker pull webgoat/webgoat-8.0:v8.1.0
docker pull webgoat/webwolf:v8.1.0
docker pull webgoat/goatandwolf:v8.1.0
docker images
docker run -d -p 8888:8888 -p 8080:8080 -p 9090:9090 webgoat/goatandwolf:v8.1.0

启动后,访问:

http://192.168.189.128:8080/WebGoat/start.mvc#lesson/JWT.lesson/3

image.png

就是这个投票功能,切换用户得到token:

image.png

点击回收站图标重置投票,提示

Not a valid JWT token, please try again

image.png

对应数据包:

image.png

可知,只有管理员才可以重置投票

修改token中的前两部分(“.”号分割),分别进行Base64解码:

“alg”的值改为NONE,“admin”的值改为true

image.png

image.png

拼接修改后的两段Base64编码后,重新发包:

image.png

报错了,去除“=”号:

image.png

还是报错,再把第三段直接删掉,注意保留“.”号:

image.png

可成功重置投票。

代码审计

网上大多数文章都是只描述了黑盒测试的步骤,少有此漏洞的代码层面的讲解,接下来利用调试,来深入了解下此漏洞的原理。

先来看WebGoat靶场中,此漏洞的代码片段:

生成access_token,对应的接口为/JWT/votings/login

image.png

校验access_token,对应的接口为/JWT/votings

image.png

这里用到的JWT库,为上边提到的jjwt,根据pom文件来查看依赖:

1
2
3
4
5
6
7
xml复制代码<!-- jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
<scope>test</scope>
</dependency>

我们这里直接利用SpringBoot来搭建一个简易的测试环境,方便调试。

具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
java复制代码package com.example.demo;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.TextCodec;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;

@RestController
public class test {
public static final String JWT_PASSWORD = TextCodec.BASE64.encode("victory");
private static String validUsers = "zzz";

@GetMapping("/login")
public void login(@RequestParam("user") String user, HttpServletResponse response) {
if (validUsers.contains(user)) {
Claims claims = Jwts.claims().setIssuedAt(Date.from(Instant.now().plus(Duration.ofDays(10))));
claims.put("user", user);
String token = Jwts.builder()
.setClaims(claims)
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD)
.compact();
Cookie cookie = new Cookie("access_token", token);
response.addCookie(cookie);
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
} else {
Cookie cookie = new Cookie("access_token", "");
response.addCookie(cookie);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
}
}

@GetMapping("/verify")
@ResponseBody
public String getVotes(@CookieValue(value = "access_token", required = false) String accessToken) {
if (StringUtils.isEmpty(accessToken)) {
return "no login";
} else {
try {
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
Claims claims = (Claims) jwt.getBody();
String user = (String) claims.get("user");
if ("zzz".equals(user)) {
return "zzz";
}
if ("admin".equals(user)) {
return "admin";
}
} catch (Exception e) {
return e.toString();
}
}
return "login";
}
}

先正常请求,生成access_token:

访问

http://127.0.0.1:8080/login?user=zzz

获取access_token

再访问

http://127.0.0.1:8080/verify

断点位置在验签解析处:

1
ini复制代码Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);

image.png

跟进Jwts.parser()

image.png

来看看DefaultJwtParser的构造方法:

public DefaultJwtParser() {

// 来看官方对于clock的阐述:

// github.com/jwtk/jjwt#j…
// Custom Clock Support
// If the above setAllowedClockSkewSeconds isn’t sufficient for your needs, the timestamps created during parsing for timestamp comparisons can be obtained via a custom time source. Call the JwtParserBuilder’s setClock method with an implementation of the io.jsonwebtoken.Clock interface.

For example:
// 如果上述设置允许的时钟倾斜秒不足以满足您的需要,则可以通过自定义时间源获得自定义时间戳。使用io.jsonwebtoken.Clock接口的实现调用JwtParserBuilder’s setClock方法。例如:

1
2
3
4
5
ini复制代码// Clock clock = new MyClock();
// Jwts.parserBuilder().setClock(myClock)
this.clock = DefaultClock.INSTANCE;
this.allowedClockSkewMillis = 0L;
}

image.png

回到

1
ini复制代码Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);

image.png

这个JWT_PASSWORD在上方的定义:

1
arduino复制代码public static final String JWT_PASSWORD = TextCodec.BASE64.encode("victory");

接着跟进

\io\jsonwebtoken\impl\DefaultJwtParser.class#setSigningKey()

image.png

这个 Assert.hasText() 只是校验了下是否为String:

image.png

接着这行:

this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes);

image.png

这就是为什么刚才要将Key进行Base64编码

给到DefaultJwtParser.keyBytes:

image.png

然后返回这个DefaultJwtParser对象:

image.png

回到:

image.png

继续跟进DefaultJwtParser#parse方法,首先判断String字符串:

image.png

然后初始化Header、Payload和Digest(摘要):

image.png

接着就是分隔符个数delimiterCount:

image.png

接着下面的for循环,会将验签的整段token转为char数组:

image.png

var7为token的char数组,var8为此数组中的字符个数。

接着看下这段for循环:

1
2
3
4
ini复制代码for(int var9 = 0; var9 < var8; ++var9) {
char c = var7[var9];
// 以“.”号来分割
if (c == '.') {

// 先保存分割的这段字符

CharSequence tokenSeq = Strings.clean(sb);

// token分别为前段:

1
2
vbscript复制代码"eyJhbGciOiJIUzUxMiJ9"、"eyJpYXQiOjE2MzY1NTIxODMsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoienp6In0"
String token = tokenSeq != null ? tokenSeq.toString() : null;

// 根据delimiterCount来判断是Header还是Payload,存到对应的field

1
2
3
4
5
ini复制代码if (delimiterCount == 0) {
base64UrlEncodedHeader = token;
} else if (delimiterCount == 1) {
base64UrlEncodedPayload = token;
}

// 每次遇到“.”号都将delimiterCount加一,然后清空StringBuilder对象

1
2
3
ini复制代码++delimiterCount;
sb.setLength(0);
} else {

// 将此char字符放入StringBuilder对象
// 结束此for循环时,StringBuilder对象存放着第三段:

1
2
3
4
go复制代码"pntCuTlybllQYsg4BHtgNEQrEmheFalhhv6VEU_CFZ18MP8uvVBCLYK0RjAkIZpyF7KLlBhYzdhN20i8zdMU3A"
sb.append(c);
}
}

接着往下:

image.png

如果分隔符数量不是2,则JWT格式有误,抛出异常。

接着,将刚才筛选出来的第三段给到Digest摘要:

image.png

接着来看这个if判断:

1
2
3
4
scss复制代码// 如果base64UrlEncodedHeader不为null
if (base64UrlEncodedHeader != null) {
// Base64解码base64UrlEncodedHeader
payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);

// 读取Header的内容,给到Map键值对

Map<String, Object> m = this.readValue(payload);

// 这里是关键分支,根据base64UrlEncodedDigest是否为空,不同走向

1
2
3
4
5
ini复制代码if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m);
} else {
header = new DefaultHeader(m);
}

image.png

可以看到,默认的“alg”为HS512。

现在,更换成POC试下:

1
ini复制代码access_token=eyJhbGciOiJub25lIn0.eyJpYXQiOjE2MzY1NTIxODMsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiYWRtaW4ifQ.

image.png

对应修改的前两段Base64编码:

“alg”改为了NONE:

image.png

“user”改为了admin:

image.png

再根据断点,快速回到我们刚才的位置:

image.png

由于这个if判断:

1
2
3
4
scss复制代码// 如果base64UrlEncodedHeader不为null
if (base64UrlEncodedHeader != null) {
// Base64解码base64UrlEncodedHeader
payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);

// 读取Header的内容,给到Map键值对

Map<String, Object> m = this.readValue(payload);

// 这里是关键分支,根据base64UrlEncodedDigest是否为空,不同走向

1
2
3
4
5
ini复制代码if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m);
} else {
header = new DefaultHeader(m);
}

我们已经将第三段删除掉了,base64UrlEncodedDigest为null,所以会走到else分支:

header = new DefaultHeader(m);

来看DefaultHeader的构造方法:

1
2
3
4
arduino复制代码\io\jsonwebtoken\impl\DefaultHeader.class
public DefaultHeader(Map<String, Object> map) {
super(map);
}

再来看super:

1
2
3
4
5
arduino复制代码\io\jsonwebtoken\impl\JwtMap.class
public JwtMap(Map<String, Object> map) {
Assert.notNull(map, "Map argument cannot be null.");
this.map = map;
}

所以,实例化的DefaultHeader对象给到header:

image.png

接着往下:

image.png

跟进

1
csharp复制代码\io\jsonwebtoken\impl\compression\DefaultCompressionCodecResolver.class#resolveCompressionCodec()

image.png

接着跟进此类的getAlgorithmFromHeader方法:

image.png

分别来看这两行:

1
2
css复制代码Assert.notNull(header, "header cannot be null.");
return header.getCompressionAlgorithm();

先来看Assert.notNull(header, "header cannot be null.");

Assert,断言

就是断定某一个实际的值是否为自己预期想得到的,如果不一样就抛出异常。

这里的断言,是jjwt库自实现的,跟进下这个notNull方法:

1
csharp复制代码\io\jsonwebtoken\lang\Assert.class#notNull()

image.png

判断传入的Object对象是否为null。

再来看return header.getCompressionAlgorithm();

先来执行下:

image.png

返回null

具体跟进看下

1
csharp复制代码\io\jsonwebtoken\impl\DefaultHeader.class#getCompressionAlgorithm()

image.png

这里判断是否有“zip”或“calg”字段,而我们的是“alg”({“alg”:”none”}),快速运行来试一下:

image.png

返回”none”,而源代码这里,返回的是null。

回到

1
csharp复制代码\io\jsonwebtoken\impl\compression\DefaultCompressionCodecResolver.class#resolveCompressionCodec()

image.png

接着往下就返回null了:

image.png

回到

\io\jsonwebtoken\impl\DefaultJwtParser.class#parse()

image.png

返回的null给到compressionCodec,接着往下:

image.png

compressionCodec为null,走else分支:

image.png

这里就是将刚才存到Payload的第二段Base64编码字符进行Base64解码,保存到payload。

处理后的结果:

image.png

1
json复制代码payload赋值为{"iat":1636552183,"admin":"false","user":"admin"}

接着往下:

image.png

看下这个Claims:

\io\jsonwebtoken\Claims.class

image.png

对应到Payload标准中注册的声明(建议但是不强制使用):

iss:jwt签发者

sub:jwt所面向的用户

aud:接收jwt的一方

exp:jwt的过期时间,这个过期时间必须要大于签发时间

nbf:定义在什么时间之前,该jwt都是不可用的

iat:jwt的签发时间

jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

接着看这个if:

image.png

payload的格式符合要求,可以进入if体:

image.png

读取payload,新组一个Map对象:

image.png

接着利用DefaultClaims的构造方法,得到标准Claims:

image.png

DefaultClaims实例对象给到claims:

image.png

接着往下:

image.png

由于我们的POC中,删除了第三段:

1
ini复制代码access_token=eyJhbGciOiJub25lIn0.eyJpYXQiOjE2MzY1NTIxODMsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiYWRtaW4ifQ.

所以,不进入这个if体。

接着往下:

image.png

这里的this.allowedClockSkewMillis默认为0L,所以allowSkew为false

接着,如果claims不为null,进入if体,校验有效期,这里显然不为null:

image.png

image.png

先获取当前时间,然后调用DefaultClaims的getExpiration方法获取过期异常:

image.png

传入“exp”调用DefaultClaims的get方法:

image.png

再跟进JwtMap的get方法:

image.png

回顾下

exp:jwt的过期时间,这个过期时间必须要大于签发时间

这里找不到“exp”,直接返回null到DefaultJwtParser的parse方法:

image.png

跳过这个if判断,继续往下:

image.png

跟进看看:

image.png

跟上边类似,这次取的是“nbf”

回顾下

nbf:定义在什么时间之前,该jwt都是不可用的

也是返回null:

image.png

继续往下:

image.png

从方法名字可看出,校验期望Claims,跟进看下:

image.png

默认为空的,所以直接return了:

image.png

再次回到:

image.png

1
2
3
4
5
csharp复制代码if (base64UrlEncodedDigest != null) {
return new DefaultJws((JwsHeader)header, body, base64UrlEncodedDigest);
} else {
return new DefaultJwt((Header)header, body);
}

关键分支,Digest被我们删掉了

return一个新的DefaultJwt对象:

image.png

DefaultJwt的构造方法:

1
2
3
4
5
6
ini复制代码public DefaultJwt(Header header, B body) {
this.header = header;
this.body = body;
}
再次回到
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);

image.png

看下返回的Jwt实例对象:

image.png

接着往下:

image.png

跟进

\io\jsonwebtoken\impl\DefaultJwt.class#getBody()

image.png

可以看到,直接返回了传入的Payload部分,给到DefaultClaims实例对象claims:

image.png

完事,user被覆盖了:

image.png

回想下,到现在为止,都没有看到判断“alg”的分支,那我们不修改第一部分的内容试下:

image.png

好吧,只要删除了第三部分就可以成功。

结语

本篇文章只是针对了JWT一个比较老的验签漏洞,做一个分析。要学习JWT框架,涉及的知识还是挺多的,JWT支持各种对称和非对称算法,JWT的JWE和JWS分别对应加密/解密和签名/验签,学习过程还是十分有趣的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【力扣-贪心】7、划分字母区间(763) 763 划分字母

发表于 2021-11-29

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

763. 划分字母区间

题目描述

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:

1
2
3
4
5
6
arduino复制代码输入: S = "ababcbacadefegdehijhklij"
输出: [9,7,8]
解释:
划分结果为 "ababcbaca", "defegde", "hijhklij"。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。

提示:

  • S的长度在[1, 500]之间。
  • S只包含小写字母 'a' 到 'z' 。

解析

题述中同一个字母只能出现在同一个片段中,所以可以求出每个片段中字符出现的最远位置,以这个最远位置作为分界点来进行分段。

  • 步骤
    • 定义数组vector<int> pos记录每个字符出现的最远位置
      • 这里不考虑大写字母,所以数组的大小可以指定为 26
      • 对数组进行初始化 ,值全部初始化为 0
    • 分割片段
      • 每个片段区间使用 [left,right]来记录长度
      • 分割点的确定
        • 如果字符出现的最远位置等于当前遍历到的索引,就将最远位置作为分割点
        • 更新本片段的右边界 right,更新下一个片段的左边界 left
      • 记录区间的长度并加入到结果集中

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
c++复制代码class Solution
{
public:
vector<int> partitionLabels(string s)
{
// 题述中只考虑小写字母,
// 所以可以定义一个长度为26的数组记录每个字符出现的最远位置
// 这里 a的下标对应为0,z的下标对应25
int pos[26] = {0};

// 遍历字符串中的字符,记录每个字符出现的最远位置
for (int i = 0; i < s.size(); i++)
{
pos[s[i] - 'a'] = i;
}

// 结果集
vector<int> result;
// 记录区间的左边界
int left = 0;
// 记录区间的右边界
int right = 0;

// 遍历字符串
for (int i = 0; i < s.size(); i++)
{
// 找到字符出现的最远位置,作为本片段区间的右边界
right = max(right, pos[s[i] - 'a']);
if (i == right)
{

// 更新下一个分割片段区间的的左边界
left = i + 1;
// 记录每个片段的长度
result.push_back(right - left + 1);
}
}

return result;
}
};

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

wrk 虽小但强! 前言 修改 httpd 的最大连接数

发表于 2021-11-29

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」。

前言

客户端采用wrk压测工具,服务端安装httpd服务。具体的httpd配置详见历史文章:# 关于Apache HTTP Serve你应该知道的!使用wrk对服务端进行压测,检测wrk的压测性能。

修改 httpd 的最大连接数

我们测试的后端服务采用httpd,为了能够承载更多的客户端连接,我们需要对httpd的默认配置进行修改。

直接将如下代码加到 httpd 的配置文件中

1
2
3
4
5
6
7
8
xml复制代码<IfModule mpm_prefork_module>
                StartServers                     10
                MinSpareServers                   5
                MaxSpareServers                  10
                ServerLimit                    5500
                MaxClients                     5000
                MaxRequestsPerChild               0
</IfModule>

wrk

关于wrk

wrk是一款简单的HTTP压测工具,托管在Github上,github.com/wg/wrk,当然国内的码云Gitee上也有其资源,gitee.com/why168/wrk。

wrk 的一个很好的特性就是能用很少的线程压出很大的并发量。原因是它使用了一些操作系统特定的高性能 io 机制, 比如 select, epoll, kqueue 等。其实它是复用了 redis 的 ae 异步事件驱动框架。确切的说 ae 事件驱动框架并不是 redis 发明的,它来至于 Tcl的解释器 jim,这个小巧高效的框架,因为被 redis 采用而更多的被大家所熟知。

依赖

1
2
3
arduino复制代码# wrk 依赖gcc
# yum install gcc
# apt-get  install  build-essential

安装

1
2
3
4
5
6
7
8
bash复制代码# clone wrk 源码到测试机
git clone https://gitee.com/why168/wrk.git

# 编译
cd wrk
make

# 编译完后当前路径会生成wrk可执行文件

测试前解除客户端和服务端的限制

sysctl.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码# ===向/etc/sysctl.conf中增加以下几行配置===
fs.file-max = 1048576
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_mem = 524288 1048576 1572864
net.ipv4.tcp_rmem = 4096 4096 8388608
net.ipv4.tcp_wmem = 4096 4096 8388608

# TCP连接复用
net.ipv4.tcp_tw_reuse = 1
# TCP连接快速回收
net.ipv4.tcp_tw_recycle = 1

kernel.pid_max=1048576
kernel.threads-max=1048576
vm.max_map_count=1048576

端口限制放开

1
2
3
4
5
bash复制代码# 打开端口范围
echo 1024 65000 > /proc/sys/net/ipv4/ip_local_port_range

# 修改端口释放等待时间(15-30s,默认为60s)
echo 15 > /proc/sys/net/ipv4/tcp_fin_timeout

文件句柄放开

使用命令:ulimit -n 1048576

或:修改配置文件

1
2
3
4
5
bash复制代码cat /etc/security/limits.conf

# End of file
* soft nofile 1048576
* hard nofile 1048576

执行测试

1
2
3
4
5
6
7
8
9
10
11
bash复制代码➜  ok git:(master) ./wrk -t12 -c100 -d30s http://www.baidu.com
Running 30s test @ http://www.baidu.com
  12 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    15.24ms    6.29ms 178.82ms   96.92%
    Req/Sec   471.00    119.40   595.00     86.44%
  30000 requests in 30.10s, 446.41MB read
  Socket errors: connect 0, read 88161, write 0, timeout 0
Requests/sec:    996.72
Transfer/sec:     14.83MB
➜  ok git:(master)

12 线程 100 个连接(并发)。QPS:996.72。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot 中的 AOP,到底是 JDK 动态代

发表于 2021-11-29

周六发了一个抽奖送书的活动,不过抽奖的人不多,中奖率蛮高的,小伙伴们可以去试试运气:

  • 指令重排序?代码不按写的顺序执行吗?送书啦!

好啦,开始今天的正文。

大家都知道,AOP 底层是动态代理,而 Java 中的动态代理有两种实现方式:

  • 基于 JDK 的动态代理
  • 基于 Cglib 的动态代理

这两者最大的区别在于基于 JDK 的动态代理需要被代理的对象有接口,而基于 Cglib 的动态代理并不需要被代理对象有接口。

那么小伙伴们不禁要问,Spring 中的 AOP 是怎么实现的?是基于 JDK 的动态代理还是基于 Cglib 的动态代理?

  1. Spring

先来说结论,Spring 中的动态代理,具体用哪种,分情况:

  • 如果代理对象有接口,就用 JDK 动态代理,否则就是 Cglib 动态代理。
  • 如果代理对象没有接口,那么就直接是 Cglib 动态代理。

来看看这段来自官方文档的说辞:

可以看到,即使在最新版的 Spring 中,依然是如上策略不变。即能用 JDK 做动态代理就用 JDK,不能用 JDK 做动态代理就用 Cglib,即首选 JDK 做动态代理。

  1. Spring Boot

Spring Boot 和 Spring 一脉相承,那么在动态代理这个问题上是否也是相同的策略呢?抱歉,这个还真不一样。

Spring Boot 中对这个问题的处理,以 Spring Boot2.0 为节点,前后不一样。

在 Spring Boot2.0 之前,关于 Aop 的自动化配置代码是这样的(Spring Boot 1.5.22.RELEASE):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Configuration
@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
matchIfMissing = true)
public static class JdkDynamicAutoProxyConfiguration {

}

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = false)
public static class CglibAutoProxyConfiguration {

}

}

可以看到,这个自动化配置主要是在讨论 application.properties 配置文件中的 spring.aop.proxy-target-class 属性的值。

具体起作用的是 @ConditionalOnProperty 注解,关于这个注解中的几个属性,松哥也来稍微说下:

  • prefix:配置文件的前缀。
  • name:配置文件的名字,和 prefix 共同组成配置的 key。
  • having:期待配置的值,如果实际的配置和 having 的值相同,则这个配置就会生效,否则不生效。
  • matchIfMissing:如果开发者没有在 application.properties 中进行配置,那么这个配置类是否生效。

基于如上介绍,我们很容易看出:

  • 如果开发者设置了 spring.aop.proxy-target-class 为 false,则使用 JDK 代理。
  • 如果开发者设置了 spring.aop.proxy-target-class 为 true,则使用 Cglib 代理。
  • 如果开发者一开始就没配置 spring.aop.proxy-target-class 属性,则使用 JDK 代理。

这是 Spring Boot 2.0 之前的情况。

再来看看 Spring Boot 2.0(含)之后的情况(Spring Boot 2.0.0.RELEASE):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Configuration
@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class,
AnnotatedElement.class })
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = false)
public static class JdkDynamicAutoProxyConfiguration {

}

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
public static class CglibAutoProxyConfiguration {

}

}

可以看到,大部分配置都是一样的,有一个地方不太相同,那就是 matchIfMissing 属性的值。可以看到,从 Spring Boot2.0 开始,如果用户什么都没有配置,那么默认情况下使用的是 Cglib 代理。

  1. 实践

最后我们写一个简单的例子验证一下我们的想法。

首先创建一个 Spring Boot 项目(本案例使用最新版 Spring Boot,即默认使用 Cglib 代理),加入三个依赖即可,如下:

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

接下来我们创建一个 IUserService 接口,如下:

1
2
3
java复制代码public interface IUserService {
void hello();
}

然后我们再来创建一个该接口的实现类:

1
2
3
4
5
6
7
java复制代码@Service
public class UserServiceImpl implements IUserService {
@Override
public void hello() {

}
}

方法不用实现。

再来一个简单的切面:

1
2
3
4
5
6
7
8
9
java复制代码@EnableAspectJAutoProxy
@Aspect
@Component
public class LogAspect {
@Before("execution(* org.javaboy.demo.UserServiceImpl.*(..))")
public void before(JoinPoint jp) {
System.out.println("jp.getSignature().getName() = " + jp.getSignature().getName());
}
}

最后再来一个简单的测试方法,注入 IUserService 实例:

1
2
3
4
5
6
7
8
9
java复制代码@RestController
public class HelloController {
@Autowired
IUserService iUserService;
@GetMapping("/hello")
public void hello() {
iUserService.hello();
}
}

DBUEG 运行一下,就可以看到 IUserService 是通过 Cglib 来代理的。

如果我们想用 JDK 来代理,那么只需要在 application.properties 中添加如下配置即可:

1
properties复制代码spring.aop.proxy-target-class=false

添加完成后,重新 DEBUG,如下图:

可以看到,已经使用了 JDK 动态代理了。

如果用的是 Spring Boot 1.5.22.RELEASE 这个版本,那么即使不在 application.properties 中添加配置,默认也是 JDK 代理,这个我就不测试了,小伙伴们可以自己来试试。

  1. 小结

总结一下:

  1. Spring 中的 AOP,有接口就用 JDK 动态代理,没有接口就用 Cglib 动态代理。
  2. Spring Boot 中的 AOP,2.0 之前和 Spring 一样;2.0 之后首选 Cglib 动态代理,如果用户想要使用 JDK 动态代理,需要自己手动配置。

just this。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

spark调优(四):瘦身任务主体 1 起因 2优化开始

发表于 2021-11-29

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

大家好,我是怀瑾握瑜,一只大数据萌新,家有两只吞金兽,嘉与嘉,上能code下能teach的全能奶爸

如果您喜欢我的文章,可以[关注⭐]+[点赞👍]+[评论📃],您的三连是我前进的动力,期待与您共同成长~


  1. 起因

刚接触大数据的时候,第一个接手的项目,好家伙,上来就打成一个zip包,200多M,每次打包需要等半天,每次提交azakaban看着缓慢的进度条,痛苦万分,每次给别人发包也是等进度条等的花都谢了。

2.优化开始

等到可以重构工程了,这个问题必须上手解决。

2.1 拆包

把工程重新架构一下,按功能区分项目,共同的东西单独一个子工程。

1
2
3
4
5
复制代码project
|-XXX-api
|-xxx-common
|-xxx-context
|-xxx-业务

2.2 提出脚本公共设置参数

原始代码中每个任务参数不固定,顺序不固定,并且配置文件位置也不固定,给排查问题造成很大困扰,优化后所有的参数必须统一格式,所有通用的信息都可以外部指定。

1
css复制代码spark入参:主方法,flow,数据库,运行日期,配置文件,队列,版本号,结束日期

尤其是把数据库作为外部参数切必输,给后续多业务多版本运行提供的基础支撑。

版本号更多是为了后续优化做规范管理。

2.3从jar包中抽离静态文件到HDFS上

原本工程非常巨大,最大的原因就是引用的jar包都在本地,而且每次提交任务都需要拉取这些文件,也是浪费时间,所以这边从jar包中非子工程的jar包都放到hdfs上面,让任务本体大幅度减少,并且设定了版本号,方便多版本同时运行时互不冲突。

1
2
bash复制代码/bin/spark-submit
-jars hdfs:///xx/java/lib/${version}/*.jar

这样优化后,主任务包从200M被直接砍到10M,上传azkaban也是眨眼的事。


结束语

如果您喜欢我的文章,可以[关注⭐]+[点赞👍]+[评论📃],您的三连是我前进的动力,期待与您共同成长~

可关注公众号【怀瑾握瑜的嘉与嘉】,获取资源下载方式

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

解读如何安全快速建立IT治理环境 背景 云治理中心定位 云治

发表于 2021-11-29

简介:云计算经过十多年的发展,从基础的IAAS,大数据,到各种的PaaS有丰富的产品和生态,非常有效地助力了业务增长和技术创新,并提高了业务的效率。最直观的感受是过去需要几天到一个月的资源交付,现在只需要秒级就可以实现。

视频解读:yqh.aliyun.com/live/cloud\…

背景

云计算经过十多年的发展,从基础的IAAS,大数据,到各种的PaaS有丰富的产品和生态,非常有效地助力了业务增长和技术创新,并提高了业务的效率。最直观的感受是过去需要几天到一个月的资源交付,现在只需要秒级就可以实现。

但在获得云的高效的同时,我们也会发现很多企业因为缺少统一的管理治理规划会遇到以下这些问题:

  • 第一类是身份风险。例如出现风险操作没有办法追溯到责任人,或把AK写在代码里面,不小心泄露出去,导致IT资产被黑客控制,又或是员工离职后不能及时收回权限,员工进行恶意操作等这些都是身份领域可能会遇到的风险。
  • 第二类是成本失控。常见的问题是企业上云之初没有管控,多员工进行云资源无限制的购买,造成成本失控。或企业资源从属于多个账号,因检测困难造成资源闲置、无法复用的问题。
  • 第三类是管理挑战。例如没有好的规划,运维人员因为业务的需要随意申请一些网络,导致出现网段的冲突。又例如没有标准化的规范,导致只能够人肉运维,无法自动化,稳定性受到挑战,整体运维效率低。
  • 第四类是合规上的风险。由于国家的监管要求会越来越严格,做等保合规的时候,很多企业才发现其实自己有很多漏洞。那这些漏洞其实是上云之初没有做好合理的规划,没有设置安全基线导致的。

那企业如何尽量避免这些风险,从而高效快速的进行云落地呢?

上述这些问题表面上看起来分散,但是在实践过程中,是否统一规划治理会对企业上云效率带来较大的影响。阿里云在服务众多企业客户过程中,总结发现企业客户上云存在以下两种类型:

  • 一类是治理优先型企业,例如较成熟的跨国企业,由于IT 管理方面已有较成熟的经验和体系。所以在上云之前,就会向阿里云提出非常准确的 IT 管理需求,把网络合规安全、财务和运维等基础的治理框架在业务上云前搭建好,之后在上云的过程中就可避免上述这些IT治理的问题,可快速的交付资源,更快的享受云的高效便捷,实现云价值的最大化。
  • 另外一类是业务优先型企业,例如互联网企业,由于处于业务增长期,更加看重业务的敏捷性。如果在上云的初期没有做统一的治理规划,在业务上云的过程中,问题就会逐渐暴露出来,比如身份泄露,网络地址冲突等,这时就需要投入大量的人力物力不断的修补这些问题,影响业务云上交付的效率。另外,在修补的过程中,如果没有长远的考量,只是临时制定方案去解决问题,可能会为未来留下更大的隐患,整体的上云曲线会更加漫长。

从以上两类客户的分析可以发现,无论客户是业务优先还是治理优先的方式上云,都需要从上云初期有统一的治理管理规划,才能够让企业在云上的IT管理更加顺畅。

那这个治理管理的规划是否有方法,如何在企业中落地?云治理中心就是我们实施落地的重要产品。

云治理中心定位

云治理中心是为企业提供统一的云资源管理治理的平台。一方面云治理中心提供友好的向导,可以降低学习门槛,一站式快速搭建LandingZone上云框架。

另外一方面云治理中心提供了对治理情况的持续观测跟踪,当企业的业务、合规要求发生变化的时候,便于维护和更新,保障云上环境始终能够符合企业的需求。

云治理中心的核心功能

具体来说,云治理中心具备以下这些核心能力:

  • 第一个是帮助企业分析当前的治理现状,一般操作系统都有一个root或者admin管理员账号。但在阿里云上,我们建议客户使用多账号的管理结构,需要创建一个最高权限云账号,称为master账号,它可以管理整个企业的云资源。这个账号的安全要求非常高,因此如何决策非常关键。对于初次上云的企业,云治理中心可以把当前的空白账号设定为管理员账号。对于已经在云上开展业务的企业,云治理中心可以分析当前的账号情况,帮助客户决策是否需要优化,或者要创建一个新的管理账号。
  • 第二个能力是自动化配置多账号环境,多账号是landingzone上云框架的基础,云治理中心可以帮助客户规划当前的多账号结构,包括商业关系,资源目录,和必要的职能账号,如日志、共享服务账号等。
  • 第三个能力是设置合规基线,很多客户有合规的需要,但是不知道应该如何设定,哪些是必要的合规规则。云治理中心会给企业推荐可用的合规规则,主要利用阿里云的配置审计的能力和管控策略的能力,这些规则策略会自动应用到企业下的所有账号,不需要客户对每个账号都进行配置,能够保障企业中所有的云账号都受到监管,从而降低业务风险。
  • 第四个能力是正在开发的账号创建能力,称为账号工厂。在最佳实践中,我们建议每个独立的业务单元都创建一个账号进行管理,方便结算、资源和权限的隔离。但是一个新的账号要受到企业的监管并且需要预先设定企业的合规配置,比如安全组、标签、用户角色等,是比较复杂的过程。通过云治理中心的账号工厂,可以很便捷的创建一致的合规云账号,快速交付给业务团队使用。对于业务团队而言,他们拿到这样的账号,不需要过多关心安全、网络和资源的合规权限,只需要专注业务的需求创建云资源 ,把业务迁移上云即可。
  • 第五个功能是可持续治理,通过云治理中心监控企业中所有账号资源是否合规,包括企业资源目录是否被改动,是否有私自创建的权限,是否有哪个账号不符合基线要求出现了风险,哪个账号有欠费等。另外在云治理中心可以提升资源跨账号的可观测性,管理员能够观测到企业所有资源的分布情况和变化趋势。

云治理中心的场景

从场景上看,当企业遇到以下问题的时候,可以通过云治理中心进行统一的治理。

第一个是有大量的账号缺少统一管理。由于各个云账号分属各个业务线管理,企业无法获知到底有多少账号,这些账号管理不善可能导致企业数据的泄露。

第二个是企业的员工账号管理混乱。企业部分账号存在过大授权,离职员工账号没有统一回收,导致可能存在被恶意操作的风险。

第三个是企业需要符合内外部监管的要求,对日志进行统一归集,设定统一的合规规则。

开通

以上介绍了如何使用云治理中心搭建统一的IT治理环境,大家若感兴趣可以通过在阿里云官网搜索“云治理中心”开通试用。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

☆打卡算法☆LeetCode 68、文本左右对齐 算法解析

发表于 2021-11-29

这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战

推荐阅读

  • CSDN主页
  • GitHub开源地址
  • Unity3D插件分享
  • 简书地址
  • 我的个人博客
  • QQ群:1040082875

大家好,我是小魔龙,Unity3D软件工程师,VR、AR,虚拟仿真方向,不定时更新软件开发技巧,生活感悟,觉得有用记得一键三连哦。

一、题目

1、算法题目

“给定单词数组和一个长度maxWidth,重新排版单词,使其成为恰好有maxWWidth个字符,且左右对齐的文本。”

题目链接:

来源:力扣(LeetCode)

链接:68. 文本左右对齐 - 力扣(LeetCode) (leetcode-cn.com)

2、题目描述

给定一个单词数组和一个长度 maxWidth,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。

你应该使用“贪心算法”来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 ‘ ‘ 填充,使得每行恰好有 maxWidth 个字符。

要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。

文本的最后一行应为左对齐,且单词之间不插入额外的空格。

说明:

  • 单词是指由非空格字符组成的字符序列。
  • 每个单词的长度大于 0,小于等于 maxWidth。
  • 输入单词数组 words 至少包含一个单词。
1
2
3
4
5
6
7
8
9
10
makefile复制代码示例 1:
输入:
words = ["This", "is", "an", "example", "of", "text", "justification."]
maxWidth = 16
输出:
[
   "This    is    an",
   "example  of text",
   "justification.  "
]
1
2
3
4
5
6
7
8
9
10
11
12
makefile复制代码示例 2:
words = ["What","must","be","acknowledgment","shall","be"]
maxWidth = 16
输出:
[
  "What   must   be",
  "acknowledgment  ",
  "shall be        "
]
解释: 注意最后一行的格式应为 "shall be " 而不是 "shall be",
  因为最后一行应为左对齐,而不是左右两端对齐。
第二行同样为左对齐,这是因为这行只包含一个单词。

二、解题

1、思路分析

这个题根据题干描述的贪心算法,需要确定的是每一行放置多少个单词,从而确定单词之间的空格个数。

对于填充空格的情况可以分为三种:

  • 最后一行:单词左对齐,单词之间应只有一个空格,在行末补充空格
  • 不是最后一行且只有一个单词:该单词左对齐,在行末补充空格
  • 不是最后一行且不只一个单词:将空格均匀的分配在单词之间

2、代码实现

代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
csharp复制代码public class Solution {
public IList<string> FullJustify(string[] words, int maxWidth) {
IList<string> ans = new List<string>();
int right = 0, n = words.Length;
while (true) {
int left = right; // 当前行的第一个单词在 words 的位置
int sumLen = 0; // 统计这一行单词长度之和
// 循环确定当前行可以放多少单词,注意单词之间应至少有一个空格
while (right < n && sumLen + words[right].Length + right - left <= maxWidth) {
sumLen += words[right++].Length;
}

// 当前行是最后一行:单词左对齐,且单词之间应只有一个空格,在行末填充剩余空格
if (right == n) {
StringBuilder sb = Join(words, left, n, " ");
sb.Append(Blank(maxWidth - sb.Length));
ans.Add(sb.ToString());
return ans;
}

int numWords = right - left;
int numSpaces = maxWidth - sumLen;

// 当前行只有一个单词:该单词左对齐,在行末填充剩余空格
if (numWords == 1) {
StringBuilder sb = new StringBuilder(words[left]);
sb.Append(Blank(numSpaces));
ans.Add(sb.ToString());
continue;
}

// 当前行不只一个单词
int avgSpaces = numSpaces / (numWords - 1);
int extraSpaces = numSpaces % (numWords - 1);
StringBuilder curr = new StringBuilder();
curr.Append(Join(words, left, left + extraSpaces + 1, Blank(avgSpaces + 1))); // 拼接额外加一个空格的单词
curr.Append(Blank(avgSpaces));
curr.Append(Join(words, left + extraSpaces + 1, right, Blank(avgSpaces))); // 拼接其余单词
ans.Add(curr.ToString());
}
}

// Blank 返回长度为 n 的由空格组成的字符串
public string Blank(int n) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; ++i) {
sb.Append(' ');
}
return sb.ToString();
}

// Join 返回用 sep 拼接 [left, right) 范围内的 words 组成的字符串
public StringBuilder Join(string[] words, int left, int right, string sep) {
StringBuilder sb = new StringBuilder(words[left]);
for (int i = left + 1; i < right; ++i) {
sb.Append(sep);
sb.Append(words[i]);
}
return sb;
}
}

image.png

3、时间复杂度

时间复杂度 : O(m)

其中m是数组words中所有字符串的长度之和。

空间复杂度: O(m)

其中m是数组words中所有字符串的长度之和。

三、总结

先分词,再排版。

排版的时候做一个空格集合,然后动态添加。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

NDPQ(NDP+PQ),定义分布式数据库新方向

发表于 2021-11-29

摘要:云服务提供商构建新的云原生关系数据库系统,专门为云基础架构设计,通常采用将计算和存储分离到独立扩展的分布式层的设计。

本文分享自华为云社区《性能提升100倍!GaussDB(for MySQL)近数据处理(NDP)解锁查询新姿势》,作者: GaussDB 数据库。

业务增长对数据库吞吐量和响应能力提出新挑战

随着企业和政府机构将其应用程序迁移到云端,对基于云的数据库即服务(DBaaS)产品的需求也在迅速增长。传统上的DBaaS产品,是云服务提供商基于现有的数据库软件本身,将常规数据库部署在云端虚拟机上,使用的是本地或者云存储。这种方法易于实施,但是未能提供足够的性能和可扩展性,而且由于需要复制数据,存储成本也很高。

为了应对这些挑战,云服务提供商开始构建新的云原生关系数据库系统,专门为云基础架构设计,通常采用将计算和存储分离到独立扩展的分布式层的设计。这种方法具有多种优势,包括数据库存储的自动扩展、按使用付费功能、跨多个AZ部署的高可靠性以及故障快速切换和恢复。这些云原生设计还有助于减少只读副本的数据更新时延,并提高硬件共享和可扩展性。华为云数据库GaussDB(for MySQL),正是具备上述优势的一款云原生分布式数据库。

由于计算和存储节点通过网络通信,网络带宽和延迟往往成为瓶颈。为了克服这一挑战,GaussDB(forMySQL)通过从数据库节点中去除写页面的操作并将检查点操作向下推送到存储节点,以优化与写入相关的网络流量。GaussDB(for MySQL)数据库节点向存储节点发送REDO日志,而不是数据页。因为REDO日志(记录对数据页的修改)通常比修改的数据页小得多, 所以这种方法减少了网络流量。存储节点(也称为页面存储)能够根据REDO日志构建数据库页面,并可以响应数据库节点的请求,将页面返回到数据库节点。

在传统数据库中,SQL执行引擎从存储中获取数据,并执行包括投影、谓词计算和聚合在内的步骤。对于经常涉及大型表扫描的分析查询,SQL执行引擎必须从存储中读取大量数据页。当存储节点与计算分离,通过网络通讯时,大表扫描会转化为增加的网络流量。一个典型的例子是对一个非常大的表进行计数查询,查询对象表的所有页面必须从页面存储池(Page stores)发送到要计数的数据库节点,之后,数据库节点将丢弃这些页面中的大部分,因为缓冲区池不能装载这么多数据页,这是对网络带宽资源的浪费。华为云创新的NDP(NearData Processing,近数据处理,简称NDP)方案解决了这一问题。

GaussDB(for MySQL)近数据处理(NDP)详解

NDP的设计思路是避免在分布式系统中移动数据,并让数据处理在其所存储的地方进行。在云原生数据库中,存储节点通常由大量性能强大的服务器组成,这些存储节点上的CPU资源经常利用率较低,这就为近数据处理(NDP)提供了一个绝佳的机会。

GaussDB(for MySQL)的NDP功能将选定的SQL操作下推到页面存储中,页面存储过滤掉不必要的数据,只将匹配的数据子集返回给数据库节点进一步处理。例如,要处理计数查询,页数据存储可以计数行,并将计数而不是实际数据页返回到数据库节点。这样就避免了大量的网络流量,使用此技术也提升了查询响应时间。

GaussDB(for MySQL)可以将三种SQL操作推送到页面存储:列投影、谓词计算和聚合。

  • 列投影:页面存储通过仅保留查询所需的列并丢弃其余列,从而减少行的长度。
  • 谓词计算:页数据存储仅保留满足谓词的行,并丢弃不满足谓词的行。
  • 聚合:页面根据查询中聚合函数的要求,将多行聚合到单行中,并丢弃原始行。

这三种SQL操作可以以任何组合出现在NDP中。例如,NDP操作可能仅包含列投影,也可能包含所有三个SQL操作。让我们看看一个示例SQL查询:

1
2
3
4
5
sql复制代码sele ctsum(salary)
from worker
where age< 40 and
join_date>= date ‘2010-01-01’ and
join_date< date ‘2010-01-01’ + interval ‘1’ year

​对于“worker”表中的每一行,页面存储计算谓词“age < 40 and join_date >=date‘2010-01-01’ and join_date < date ‘2010-01-01’ + interval ‘1’ year”。如果行不满足谓词,则将立即丢弃。如果该行满足谓词,则将其聚合到sum(salary)值中,并丢弃原始行。如果页数据存储无法聚合行(由于某些内部处理要求),它仍然可以从行投影三列(salary, age, and join_date),并生成更窄的行。此后,原始行将被丢弃。最后,将一个显著减少的数据集返回到数据库节点。

GaussDB (for MySQL)的 NDP特性架构如下图所示。数据库节点向页面存储发送NDP请求(请注意,通常有多个页面存储服务于每个数据库节点),为了降低IOPS(每秒IO数),将多个页面分组为一个NDP请求(批量页面读取请求),页面存储中的NDP运算符可以执行上述三种SQL操作,并将较小的数据集返回到数据库节点。数据库节点可以是主节点,也可以是只读副本节点,两者都支持NDP。

NDP中的批处理读取和并行处理

在云原生数据库系统中,即使数据库节点和页面存储通过高速RDMA网络连接,但与传统数据库中的本地存储相比,延迟仍然很高,通过降低网络IOPS和并行执行多个IO可以减少延迟带来的负面影响。在NDP功能中,我们实现了“批处理读取”的概念。这个想法是在B+树叶数据节点中向前看,并将相邻的叶数据节点分组到一个批处理请求中,而这些B+树叶数据节点是正在进行近数据处理的查询所需要的。批量读取是降低IOPS的一个绝佳方法。如果我们在每个请求中发送一个页面,那么IO的数量将等于页面的数量。如果我们将1000个页面分组到一个请求中,IO的数量将减少1000倍。

下图阐述了批处理读取的工作原理。数据库节点发送批量请求,SAL(存储抽象层)标识页面所在的页面存储,并将批处理读取拆分为多个子读取:每个页面存储一个子读取。然后,子读取将并行发送到页面存储。使用这种方法,可以同时使用多个页面存储来服务NDP请求。

页面存储接收包含多个页面的NDP请求,而这些页面之间没有依赖关系,因此可以使用NDP以任何顺序处理。这样既提供了灵活性,又使页面存储能够将页面分配给多个线程并行处理。

​GaussDB(for MySQL)使用增强的SQL优化器自动判定NDP是否可能对特定查询有利。如果有利,它将自动启用NDP,SQL优化器查看扫描大小等因素,以及SQL运算如果推送到页面存储,是否可以显著降低数据集大小。一般来说,NDP并不有利于小扫描,例如,当可以用索引减少要扫描的数据量时。

同时,NDP也有自己的资源诉求。在数据库节点中,NDP主要占用内存资源,因为它需要内存来保存NDP页面。在GaussDB(for MySQL)数据库节点中,NDP页面与常规页面共享相同的内存池(又名缓冲区池),没有专门为NDP保留的内存。这种方法的优点是,当系统中没有NDP时,整个缓冲池可用于常规处理。但是页面内存一旦被NDP操作占用,在NDP操作完成之前,不能被其他查询使用。这就是为什么必须控制分配的NDP页数,以避免常规页被剥夺内存。

NDPQ(NDP+PQ),释放查询极致性能,定义分布式数据库新方向

并行查询(PQ)是商业关系型数据库系统的事实标准,为分析工作负载提供高性能支持。PQ通常采用“leader-worker”设计,要处理的表被划分为非重叠的数据块,并把这些数据块分配给多个worker处理。每个worker都会产生中间结果,leader会累积这些结果并做进一步处理,以产生最终结果。PQ在数据库节点中提供并行性,利用多个CPU并发处理查询。华为云GaussDB(for MySQL)具备PQ特性,而且NDP和PQ可以协同工作,进一步提高查询性能。可以为PQ worker启用NDP。PQ worker执行的一些SQL操作可以推送到页面存储区,通过将NDP和PQ结合,我们在GaussDB系统的数据库节点、多个页面存储之间和一个页面存储内部这三层激活了并行处理的魔力,进一步提高查询性能。

如何启用NDP?

GaussDB(for MySQL)会自动判断NDP是否有助于查询,并为查询启用NDP。用户需要做的就是打开系统变量“ndp_mode”。ndp_mode可以为整个数据库打开,也可以仅为当前会话打开。要为整个数据库打开ndp_mode,请在“set”命令中添加“global”关键字,如下所示:

1
ini复制代码set[global] ndp_mode = on

​您可以使用“explain”查询以了解是否为查询启用了NDP。例如,以下是树格式的TPC-H查询14的解释输出。为LINEITEM表扫描启用了NDP,投影和谓词计算都会推送到数据页面存储区。此外,还为LINEITEM表扫描启用了PQ。

下面是另一个例子,在LINEITEM表上的计数查询,我们将此查询命名为Q002。谓词计算和聚合都会推送到页面存储区,PQ也已启用。

​下面我们通过在100GB的TPC-H数据库上运行多个查询,展示NDP和PQ如何提升查询效率。

测试环境:

  • 上海-1区域的华为云GuassDB (for MySQL)
  • CPU:16个,内存:64GB,缓冲池大小:20GB
  • 将join_buffer_size 和 sort_buffer_size增加到1MB,因为这两个缓冲区对于哈希连接和排序的性能很重要
  • PQ并发度设置为16

下图的y轴显示查询响应时间加速因子。加速因子定义:如果原始查询时间为100秒,而启用PQ后,查询时间变为50秒,则加速因子应为2。

从下面的测试结果可以看出, NDP+PQ将Q002加速了100多倍。

​NDP将数据库节点和存储节点解耦,这一特性将成为未来云原生数据库系统的一个标准。大型扫描在OLAP工作负载中很常见,NDP将大大提升此类操作的效率。

综上所述,NDP可以:

  • 减少网络带宽的使用量
  • 降低网络IOPS
  • 同时使用多个页数据存储来实现NDP并行处理
  • 提高需要大表扫描的SQL查询的性能
  • 降低数据库节点的CPU使用率,使数据库节点能够支持更多的OLTP工作负载

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

springboot开发,有这个包就够了!

发表于 2021-11-29

2021-12-25更新

  1. 本项目已上传中央仓库,欢迎使用
1
2
3
4
5
xml复制代码<dependency>
<groupId>io.github.chenxuancode</groupId>
<artifactId>base</artifactId>
<version>1.0.0-Release</version>
</dependency>
  1. 快速生成项目:执行如下命令,可快速生成包含本项目功能的springboot项目
1
shell复制代码mvn archetype:generate -DarchetypeGroupId=io.github.chenxuancode -DarchetypeArtifactId=archetype -DarchetypeVersion=1.0.0-Release -DgroupId=io.github.chenxuancode -DartifactId=demo -Dport=8888

Tip:

  • DgroupId修改为你的groupId,DartifactId修改为你项目的artifactId
  • 命令控制台会确认你项目的groupId、artifactId等信息,如无需修改回车确认即可
  1. 本项目代码已更新至github.com/chenxuancod…

last: 欢迎大家反馈建议!!! 码字不易,各位小可爱给个点赞关注star

1
2
3
4
5
6
7
8
9
10
11
12


-------------------------------------------------------------------------分割


**正文**


在成熟的项目开发中,都会由基础包提供一些项目通用性的功能组件,避免每个项目重复造轮子。本项目将springboot项目开发中常用的基础功能进行封装,目前本包具备**统一依赖管理**、**异常处理**、**响应报文包装**、**统一日志管理**、**敏感数据加解密**等功能。支持可插拔方式,只要引入依赖便具备上述功能。


下面讲讲如何实现

统一依赖管理

将一些常用的依赖,统一梳理到基础包中,可以方便后续对组件进行管理(升级或漏洞修复之类),基础包中的组件依赖原则是稳定以及最少依赖。目前base包括以下组件,基本满足springboot项目开发的基本功能。各项目基础包的依赖由base统一管理,只需要引入base模块即可,特性包由各项目自己引入。
目前的基础包已经集成了mybatis-plus swagger等常用的基础组件

组件名称 版本
spring-boot-starter-validation 2.3.12.RELEASE
spring-boot-starter-web 2.3.12.RELEASE
spring-boot-starter-test 2.3.12.RELEASE
spring-boot-starter-aop 2.3.12.RELEASE
mysql-connector-java 8.0.16
mybatis-plus 3.4.0
springfox-swagger2 2.8.0
springfox-swagger-ui 2.8.0
swagger-bootstrap-ui 1.8.5
lombok 1.18.20
hutool-all 5.7.14

异常处理

定义了统一全局异常处理器,鼓励不在业务代码中进行异常捕获, 将 dao、service、controller 层的所有异常全部抛出到上层. 减少try-catch对业务代码的侵入性
如果需要返回接口的指定错误提示信息,可以直接抛出自定义异常AiException

1
java复制代码throw new ApiException("两次密码输入不一致");

实现原理

使用@RestControllerAdvice开启全局异常的捕获,自定义一个方法使用ExceptionHandler注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Slf4j
@RestControllerAdvice
public class ExceptionControllerAdvice {

@ExceptionHandler(ApiException.class)
public ResultVO<String> apiExceptionHandler(ApiException e) {
log.error("接口请求异常:{}{}",e.getResultCode(),e.getMsg());
return new ResultVO<>(e.getResultCode(), e.getMsg());
}

@ExceptionHandler
public ResultVO unknownException(Exception e) {
log.error("发生了未知异常", e);
return new ResultVO<>(ResultCode.ERROR, "系统出现错误, 请联系网站管理员!");
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
}

}

其它未显式抛出的异常会自动被外层异常处理器识别为未知错误并返回前端

日志处理

在springboot项目中,通常使用logback组件进行日志管理。那么如果每一个服务自己写一份logback配置文件,势必会导致日志格式、日志路径五花八门,不好管理,所以日志处理交由基础包统一处理。
通常日志处理需要思考的几个点包括:日志如何打印、日志如何拆分管理、日志如何收集

出入参日志打印

在Controller方法上使用@WebLog便可实现请求响应报文的打印

1
2
3
4
5
6
7
java复制代码@PostMapping("/register")
@ApiOperation(value = "注册")
@WebLog
public String register(@RequestBody @Validated RegisterParam param) {
userService.register(param);
return "操作成功";
}

实现原理

定义日志切面LogAspect,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class LogAspect {

@Pointcut("@annotation(com.sleeper.common.base.annotate.WebLog)")
public void webLog() {}


@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("IP:{} Class Method:{}.{} Request Args: {}",request.getRemoteAddr(),joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), new Gson().toJson(joinPoint.getArgs()));
}


@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
log.info("Response Args : {} Time-Consuming : {} ms", new Gson().toJson(result),System.currentTimeMillis() - startTime);
return result;
}

}

WebLog注解

1
2
3
4
5
6
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface WebLog {

}

日志拆分保存

日志拆分保存通过logback进行配置,当前日志进行错误日志与普通日志的拆分,每种文件类型再按天进行拆分,当天超过200M的日志文件再以文件名中编号递增的形式进行拆分,具体规则如下
${LOG_ERROR_HOME}/${springAppName}-%d{yyyy-MM-dd}.%i.log ${LOG_INFO_HOME}/${springAppName}-%d{yyyy-MM-dd}.%i.log |

使用AsyncAppender异步输出的方式输出日志,完整的logback日志请查看:

链路追踪

目前链路追踪通过MDC实现,MDC是Slf4J类日志系统中实现分布式多线程日志数据传递的重要工具可利用MDC将一些运行时的上下文数据打印出来。关于MDC的介绍可以看看这篇juejin.cn/post/690122…

实现原理
通过拦截器对请求进行拦截,生成traceId并通过MDC put接口设置到THreadLocalMap中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String traceId = request.getHeader("traceId");
if (traceId == null) {
traceId = IdUtil.getSnowflake().nextIdStr();
}
MDC.put("traceId", traceId);
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
MDC.remove("TRACE_ID");
}

在logback-spring.xml中增加%X{traceId}

1
xml复制代码<property name="PATTERN" value="%red(%d{yyyy-MM-dd HH:mm:ss.SSS}) %X{traceId} %yellow(%-5level) %highlight([%t]) %boldMagenta([%C]).%green(%method[%L]): %m%n"/>

响应报文自动封装

通常接口都需要按照一定的结构返回,包括服务处理结果编码、编码对应的文本信息、返回值等,可以通过 @RestControllerAdvice对Controller进行增强实现响应报文的自动封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@RestControllerAdvice("com.sleeper")
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
// 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false
return !returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseWrap.class);
}

@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
// String类型不能直接包装,所以要进行些特别的处理
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 将数据包装在ResultVO里后,再转换为json字符串响应给前端
return objectMapper.writeValueAsString(new ResultVO<>(data));
} catch (JsonProcessingException e) {
throw new ApiException("返回String类型错误");
}
}
// 将原本的数据包装在ResultVO里
return new ResultVO<>(data);
}
}

对于不想自动封装结果的接口,使用注解 @NotResponseWrap在方法上标记即可

敏感数据加解密

有时候,在开发过程中需要对某一些数据如手机号、身份证号等数据在保存到数据库的时候进行加密处理,防止数据泄露。本基础包提供了@SensitiveData(作用于CLASS) @SensitiveField(作用于FEILD,以实现加解密操作,只需在数据实体对象上加上 @SensitiveData注解,在敏感字段上加上@SensitiveField便可实现敏感数据加解密操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Data
@SensitiveData
public class SysUser implements Serializable {

private static final long serialVersionUID = 1L;

@TableId(value = "id", type = IdType.AUTO)
private String id;

@SensitiveField
private String mobile;

}

实现原理

重写mybatis拦截器 ResultSetHandler.handleResultSets ParameterHandler.setParameters方法,在设置请求参数时识别注解并将字段进行AES加密,在获取结果集时识别注解并将字段进行AES解密.详情请看: github.com/chenxuancod…

优雅停机

没有优雅停机,服务器此时直接直接关闭(kill -9),那么就会导致当前正在容器内运行的业务直接失败,在某些特殊的场景下产生脏数据。开启优雅停机后,在web容器关闭时,web服务器将不再接收任何请求,并将等待活动请求完成的缓冲器。

实现原理
基础包使用的Spring Boot 2.3版本内置此功能,不需要再自行扩展容器线程池来处理,Jetty, Reactor Netty, Tomcat和 Undertow 以及反应式和基于 Servlet 的 web 应用程序都支持优雅停机功能,只需要配置server.shutdown=graceful.
本基础包默认开启优雅停机功能,通过SPI机制在服务中进行配置注入,并结合后续部署脚本以服务优雅停机的统一管理

1
2
3
4
5
6
7
8
9
10
11
scss复制代码@Configuration
public class ShutDownConfig {
@Autowired
ServerProperties serverProperties;
@Autowired
LifecycleProperties lifecycleProperties;
@Bean
public void setShutDownConfig() {
serverProperties.setShutdown(Shutdown.GRACEFUL);
lifecycleProperties.setTimeoutPerShutdownPhase(Duration.ofSeconds(20));
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…122123124…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%