0%

[toc]

Spoofing 仿冒—>认证#

搞一个克隆的手机号, 接入到公用wifi中看违反视频干违法之事,然后另一个人背锅了 ( 外部交互方)—— 手机短信验证码接入, 或者ssl证书双向认证

公用wifi被我替换成了仿冒的同名假wifi, 其他手机就误接进来了,我就能偷偷知道谁在下小黄片了 (处理过程), 或者是搞了个假的淘宝,让别人点进来—— ssl双向认证

Tampering 篡改—>完整性#

服务器的监听特性被纂改?(处理过程)—— 校验端口正确,配置文件mac对比,mac存到服务端

直接再数据库里修改你的个人信息或者金钱(存储)——acl权限控制

某app偷偷修改你的host,把你想发给华为的数据发送到黑客的服务器上了。(数据流) —— acl权限认证,host不可更改,或者文件mac对比

Repudiation 抵赖,否认做过的事—>防抵赖#

有人用root登录后干了rm -rf 这事, 却说自己没做过,你查不到我(外部交互方) ——审计日志

修改日志记录函数(例如替换jar包之类的), 日志故意不记录自己的名字 ( 处理过程)——认证

直接去存储的地方修改日志信息(存储过程, 只有日志会存盘才有可能)——认证

Imformation Disclosure 信息泄露—>机密性#

从app的应用启动目录找到一个二进制文件,可以解密处密钥(处理过程)——加密

入侵数据库里。可以找到明文口令(存储过程)——加密

明文口令直接加在http中, 然后被人直接抓包拿到口令了(数据流)——加密

Denial of Service 拒绝服务—>可用性#

服务器承载不住请求量,内存不足或者cpu爆满(处理过程) ——负载均衡

疯狂执行写操作,然后磁盘满了(存储过程)——缓存

故意拦截中间链路,导致数据流中断(数据流)——过滤

Elevation of privilege 权限提升—>授权#

在处理过程中, 用户权限比实际的高(处理过程)——权限最小化\沙箱

Privacy隐私—>脱敏#

外部交互方: 没有经过用户同意,就收集隐私—— 应该加勾选项告知用户

数据存储: 直接把用户数据存盘却没有加密或者脱敏—— 数据脱敏、匿名化

[toc]

概念#

  • 谁和谁相互关联, 问你最终每个集体或者最多集体有多少人, 或者有多少个集体,用并查集即可。

  • 区间合并问题直接上并查集,别整更新左区间,更新右区间这种玩意,很容易错。

核心思路:#

  1. partent[k] 指k的父亲节点是什么
  2. 每次getParent(k)时, 要通过递归, 将其父节点更新成最上面的父节点,实现并查集的压缩,大大减少了复杂度
  3. union时, 则随意选一方进行连接
  4. 如何涉及集合中的数量问题,尽量别在union中更新,容易写错, 建议最后全部遍历一边,得到集合中的数量或者价值总数。
  5. 如果一定要union更新,注意顺序,p[p1]] = p2的话,说明p1的父亲变成了p2,那么应该是p2作为被加方,即sum[p2] += sum[p1],反过来了。且注意在getParent中不需要更新val

代码模板#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   long[] sums;
int[] parent;

void union(int n1, int n2) {
int p1 = getParent(n1);
int p2 = getParent(n2);
parent[p1] = p2;
sums[p2] += sums[p1];
}

int getParent(int num) {
int p = parent[num];
if (p == num) {
return p;
} else {
p = getParent(p);
}
parent[num] = p;
return p;
}

相关题目#

6159. 删除操作后的最大子段和 - 力扣(LeetCode)

1661012250432

本期总结:#

  1. 涉及边界问题一定要自己检查看是否写错了,花1分钟检查总好过避免WA浪费5分钟

  2. 取余问题一定要注意负数的情况,必须要用加上Mod值直到大于0才能去取余

  3. 区间合并问题直接上并查集,别整更新左区间,更新右区间这种玩意,很容易错。


6156. 得到 K 个黑块的最少涂色次数 - 力扣(LeetCode)

很简单,滑动窗口,而且因为数量够小,直接暴力解决即可,不用做滑动窗口的特殊处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int minimumRecolors(String blocks, int k) {
int min = Integer.MAX_VALUE;
for (int i = 0; i < blocks.length();i++) {
int count = 0;
int j = 0;
for (j = i; j < i+k && j < blocks.length();j++) {
if (blocks.charAt(j) == 'W') {
count++;
}
}
if (j != i+k) {
continue;
}
min = Math.min(count, min);
}
return min;
}
}

6157. 二进制字符串重新安排顺序需要的时间 - 力扣(LeetCode)

1661012329860

1661012381790

首先花了几分钟,推断出来最多变化n次(因为每处理一次至少前缀1会增加1个)

那么直接暴力O(n2)即可

但是因为边界处理写急了导致出错,我应该自己提交前再检查一下的

下面是错误的地方:

1661012470616

i+1==s.length()了,怎么可能添加11, 最多添加1个1才对。

正确答案:

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
class Solution {
public int secondsToRemoveOccurrences(String s) {

int res = 0;
while (true) {
StringBuilder sb = new StringBuilder();
int count = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '0' && (i+1!=s.length() && s.charAt(i + 1) == '1')) {
sb.append("10");
i++;
count++;
} else if (s.charAt(i) == '0' && (i+1 == s.length() || s.charAt(i + 1) == '0')) {
sb.append("0");
} else if (s.charAt(i) == '1' && (i+1 != s.length() && s.charAt(i + 1) == '1')) {
sb.append("11");
i++;
} else {
sb.append("1");
}
}
s = sb.toString();
if (count == 0) {
return res;
}
res++;
}
}
}

6158. 字母移位 II - 力扣(LeetCode)

1661012520347

1661012525774

做过这类题,和那种任务流的题目类似

直接记录每个位置有多少起点,多少终点,根据情况进行加或者减。

但是对于移位的处理有点问题

1
2
3
4
5
for (int i = 0; i < s.length();i++) {
count += startPlaces[i];
cs[i] = (char)((s.charAt(i) -'a' + count)%26 + 'a');
count += endPlaces[i];
}

看似很对, 却忘记了 负数取余也是负数。。

对负数取余应当通过加上26的倍数让他大于0才对。于是改成下面这样得到移位后的字符:

1
2
3
4
5
6
7
8
9
char getMod(char c, int count) {
int num = c - 'a' + count;
if (num < 0) {
int mul = num / 26;
num += (mul+2)*26;
}
num %= 26;
return (char)(num + 'a');
}

结果还是错

因为我忘记了num是负数的时候,num/26也是负数。。应该要加一个Math.abs

正确做法如下getMod:

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
class Solution {
char getMod(char c, int count) {
int num = c - 'a' + count;
if (num < 0) {
int mul = Math.abs(num / 26);
num += (mul+1)*26;
}
num %= 26;
return (char)(num + 'a');
}

public String shiftingLetters(String s, int[][] shifts) {
int[] startPlaces = new int[s.length()+1];
int[] endPlaces = new int[s.length()+1];
for (int i = 0; i < shifts.length;i++) {
int[] shift = shifts[i];
startPlaces[shift[0]] += (shift[2] == 1 ? 1: -1);
endPlaces[shift[1]] += (shift[2] == 0 ? 1: -1);
}

int count = 0;
char[] cs = new char[s.length()];
for (int i = 0; i < s.length();i++) {
count += startPlaces[i];
cs[i] = getMod(s.charAt(i), count);
count += endPlaces[i];
}
return new String(cs);
}

}

6159. 删除操作后的最大子段和 - 力扣(LeetCode)

1661012916961

1661012929643

这题又是做过类似的题,删点的题目可以反过来变成加点。

即从后往前逐步遍历,新增,逐步合并区间

结果还是用了错误的方式去更新,想着直接更新左右两个索引的sum值即可

实际上你要更新的应该是左边索引的最左点, 右边索引的最右点才对,而且每次合并都要修改左右的最左点或者最右点,非常麻烦,如下中间那4个if, 绕来绕去,注定要搞一次WA才能知道问题,非常不对劲:

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
class Solution {
public long[] maximumSegmentSum(int[] nums, int[] removeQueries) {
long[] sums = new long[nums.length];
long[] results = new long[nums.length];
int[] lefts = new int[nums.length];
int[] rights = new int[nums.length];
long maxNum = Long.MIN_VALUE;
for (int i = removeQueries.length-1;i>=1;i--) {
int index = removeQueries[i];
long leftSum = index-1 >= 0 ? sums[index-1] :0;
long rightSum = index + 1 < nums.length ? sums[index+1] : 0;

long sum = leftSum + rightSum + nums[index];
lefts[index] = rights[index] = index;
sums[index] = sum;

if (leftSum > 0) {
lefts[index] = lefts[index-1];
}
if (rightSum > 0) {
rights[index] = rights[index+1];
}
if (leftSum > 0) {
rights[lefts[index]] = rights[index];
sums[lefts[index]] = sum;
}
if (rightSum > 0) {
lefts[rights[index]] = lefts[index];
sums[rights[index]] = sum;
}
maxNum = Math.max(maxNum, sum);
results[i-1] = maxNum;
}
return results;
}
}

这种合并的题目就别取巧了, 直接并查集走起

注意并查集合并时,涉及val的合并, 在getParent中不需要更新val

在union中,注意顺序,p[p1]] = p2的话,说明p1的父亲变成了p2,那么应该是p2作为被加方,即sum[p2] += sum[p1],反过来了

另外注意要先取变量,不要直接基于函数去搞,下面这样是错误的!因为第一步之后已经发生变成了:

1
2
3
4
void union(int n1, int n2) {
parent[getParent(n1)] = getParent(n2);
sums[getParent(n2)] += sums[getParent(n1)];
}

应该是:

1
2
3
4
5
6
void union(int n1, int n2) {
int p1 = getParent(n1);
int p2 = getParent(n2);
parent[p1] = p2;
sums[p2] += sums[p1];
}

并查集正确做法:

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
class Solution {
long[] sums;
int[] parent;
public long[] maximumSegmentSum(int[] nums, int[] removeQueries) {
long[] results = new long[nums.length];
sums = new long[nums.length];
long maxNum = Long.MIN_VALUE;
parent = new int[nums.length];
for (int i = 0; i < parent.length;i++) {
parent[i] = i;
}
for (int i = removeQueries.length-1;i>=1;i--) {
int index = removeQueries[i];

long leftSum = index-1 >= 0 ? sums[getParent(index-1)] :0;
long rightSum = index + 1 < nums.length ? sums[getParent(index+1)] : 0;

sums[index] = nums[index];
if (leftSum > 0) {
union(index-1, index);
}
if (rightSum > 0) {
union(index, index+1);
}
maxNum = Math.max(maxNum, sums[getParent(index)]);
results[i-1] = maxNum;
}
return results;
}

void union(int n1, int n2) {
int p1 = getParent(n1);
int p2 = getParent(n2);
parent[p1] = p2;
sums[p2] += sums[p1];
}

int getParent(int num) {
int p = parent[num];
if (p == num) {
return p;
} else {
p = getParent(p);
}
parent[num] = p;
return p;
}
}

[toc]

HTTPS防护常见问题#

HTTPS原理详解

Q: 讲一下HTTPS双向认证的过程?#

A:

  1. 服务端发送证书, 证书中包含网站信息、CA签名、网站公钥

  2. 客户端校验CA签名,确认是CA所签且来源方正是该网站。

  3. 客户端生成会话密钥

  4. 客户端用网站公钥对会话密钥进行加密,生成非对称的会话密钥密文

  5. 客户端发送会话密钥密文给服务端

  6. 服务端用私钥解密, 得到明文会话密钥。

  7. 二者使用会话密钥进行加密传输

    在这里插入图片描述

Q: 为什么不使用非对称密钥加密 传输数据, 而要用对称密钥这样绕一大圈?#

A:
对称算法的加解密性能比非对称算法快很多, 所以非对称只用于会话密钥的传输即可, 只需要一次。


Q: 为什么数据开始传输之后不需要再做验签, 难道者中间的数据就不会再被人篡改或者替换吗?#

A:
不会,有3个原因保证:

  1. 会话密钥是会话级别, 动态生成的,只有这一次连接才会用到。因此以前废弃的会话密钥不用担心被人拿去利用。
  2. 建立连接并传递会话密钥之前,已经通过验签确认过对方身份是可信的。
  3. 没有任何第三方知道会话密钥是什么。因此第三方攻击人无法用正确的会话密钥加密数据,即无法做到伪造会话密钥来欺骗接收者的目的。

Q: CA根证书可能被造假吗?#

例如黑客修改了用户机器中的CA证书,导致CA的公钥被替换了, 后面访问了黑客所在的网址时,就会验签成功。

A:
如果真的能修改的话,那么是可行的。
但是操作系统基本会内置著名CA的公钥,除非黑客已经能直接触碰你的操作系统底层了,否则基本办不到。


Q: 黑客把这个服务端返回的证书, 保存下来,然后下次别人访问黑客的钓鱼网站时, 黑客把这个证书原封不动发回去,会通过验证吗?#

A:
客户端会验证成功,
但是客户端做公钥时
于是对数据进行对称+非对称加密
重点来了
钓鱼网站接收到的数据,他没法用,他没有正规网站的私钥! 因为客户端用的是正规网站签发的公钥才可以。
黑客做中间人两边相互拦截,把假的签发过的公钥+证书发给客户端
客户端验签过程就会发现端倪, 发现他这个证书不对劲, 不是自己这边持有的CA机构正常签发出来证书。

Q: MD5 在双向认证过程中有什么用?#

A:
对网址申请信息进行摘要, 避免签名内容过长。同时也避免了逆向推导原网址信息的过程。

Q: 什么是证书链?#

A:

  • 一个证书或证书链的拆封操作,是为了
    从中获得一个公钥。可示为X1p?X1<>,这为一个中缀操作,其左操作数为一个认证机构的公钥,
    右操作数则为该认证机构所颁发的一个证书。如果能正确解开,输出结果为用户的公钥

  • 证书链验证的要求是,路径中每个证书从最终实体到根证书
    都是有效的,并且每个证书都要正确地对应发行该证书的权威可信任性CA。操作表达式为 Ap?A<>B<>,
    指出该操作使用A的公钥,从B的证书中获得B的公钥Bp,然后再通过 Bp来解封C的证书。操作的最终结果

说人话就是下游证书解出来是上游父证书的密文公钥,然后继续解

理解证书和证书链

Token机制#

Q: 如何设计token?#

A:
token进行加密处理,将请求 URL、时间戳、加密后的token、 三者进行合并加盐签名,因为盐值是保密的,所以其他人只是得到token的话,无法进行正确的签名,后端验证请求的签名值来判断请求是否有效。


Q: token泄露怎么办?#

A:
每次请求可以将token和请求的对应的ip地址或设备id保存起来,放到数据库或者redis缓存中,如果后面请求token对应的ip地址或设备id不一样,则视为非法请求

HTTPS保护。

应用程序里用到的时候再解密。 如果支持TEE的话可以在TEE里进行解密。

理解证书和证书链

云服务AKSK#

[toc]
log4j 远程代码漏洞问题被大范围曝光后已经有一段时间了。

很多人只能看到一个“弹出一个计算器”的演示, 于是内心想着“哦,就是执行任意代码,启动个计算器” , 却对这个漏洞的原理不甚了解。
image.png

而对于java开发应用不是非常深的同学来讲, jndi、rmi更是很陌生的名词。
这里会以不断提问的方式,逐步推进这个问题的解答, 一步步揭开这个漏洞的本质,并给出对这个漏洞的思考。


Q: log4j里的”${}“符号是什么?有什么用?#

A:
可以通过$ { }的方式, 打印一些特殊的值到日志中。
例如$ {hostName} 就可以打印主机名
$ {java:vm} 打印jvm信息
$ {thread:threadName}就可以打印线程名

当你把这个值作为日志的参数, 就会打印出来这些值而非原参数名字。

可以理解为log4j的功能更强大了,不需要自己写java代码来打印这些信息,直接用一个字符串就能搞定这些打印。

上面这些都是要实现对应的Lookup类才能做的,即要么log4j内置,要么我们自己新增。


Q: 上面这个打印本机信息的是漏洞的原因吗?看起来好象可以在机器里执行奇怪的命令?或者查看文件路径?#

A:
不是的。
上面这些lookup,都是事先定义好的一些loopup字符, 并不能做任意的事情!
而且就算你发了这些${java.vm}啥的, 也只能在服务端打印和收集,你作为攻击者,是收集不到这些信息的

真正的原因,是因为log4j 支持的 ${jndi:xxxx}, 即支持jndi进行lookup来寻找对象并打印。


Q: 什么是JNDI?#

A:
Java Naming and Directory Interface(JAVA命名和目录接口)

简单说就是可以通过JNDI, 在java环境中用一个名字, 去lookup寻找一个东西使用。

例如可以直接在自己的Java环境中配置一个数据库连接,名字叫“java:MySqlDS”
然后别的java进程通过jndi 去查找”java:MysqlDs“, 接着就会得到一个数据库连接。
这样如果1个机器有多个进程,都要用同一个连接, 完全可以修改整个java环境的jndi数据库对象,然后其他进程就能同时生效了。

1
2
3
4
5
6
7
8
9
10
11
12
Connection conn=null; 

// Context就是jdni的类
Context ctx = new InitialContext();
// jndi关键方法,通过loopup找一个对象
Object datasourceRef = ctx.lookup("java:MySqlDS");
//引用数据源
DataSource ds = (Datasource) datasourceRef;
conn = ds.getConnection();
......
c.close();

除了数据库连接, 他还支持loopup找dns, 可以弄一个dnsContext然后寻找”sun.com“对应的dns对象
使用JNDI进行高级DNS查询

这样log4j里就可以通过 ${jndi:dns:huaweicloud.com}来获取当前机器中huaweicloud.com对应的域名对象进行打印,来确认网络请求失败时,是否是dns获取有问题。

这也就是log4j为啥要引入jndi的原因,可以更方便地获取一些可打印的对象进行日志统计。

然而, jndi还支持通过RMI/LDAP+url字符串, 来寻找并获取一个远程对象
这个寻找远程对象的操作,就是此次漏洞的核心问题所在。


这里只讲RMI。LDAP类似,就不再论述。

Q: RMI是什么?#

A:
RMI, Remote Method Invocation。
具体含义:

  • 远程服务器实现具体的Java方法并提供接口
  • 客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法

在RMI中,实际上就是返回了一个stub(桩)调用对象给客户端, 然后客户都用这个stub对象去做远程调用。

这样客户端就不用关心背后网络怎么写的
甚至不用知道对方服务是什么端口或者ip
因此也不需要写sokect的一堆方法搞半天了,也避免了总是修改访问的url啥的。
具体过程如下:
image.png

  1. Server端监听一个端口,这个端口是JVM随机选择的;
  2. Client端并不知道Server远程对象的通信地址和端口,但是Stub中包含了这些信息,并封装了底层网络操作;
  3. Client端可以调用Stub上的方法;
  4. Stub连接到Server端监听的通信端口并提交参数;
  5. 远程Server端上执行具体的方法,并返回结果给Stub;
  6. Stub返回执行结果给Client端,从Client看来就好像是Stub在本地执行了这个方法一样;

Q:RMI客户端不需要关心服务端的监听端口? 那客户端从哪里拿到stub对象呢?总不可能凭空生成吧#

A:
服务端那边可以启动一个 RMI注册中心服务RMIRegistry, 端口设置为统一的1099, ip也是固定的。

然后当客户端希望拿到某个服务例如订单服务order的stub对象时, 就用”order“这个名字到RMI注册中心上去请求这个stub
这样的话, 客户端只需要知道RMI注册中心即可, 不需要知道其他服务的ip、端口,非常节省管理成本。

服务端代码长这样:

1
2
3
4
5
6
// 建立一个订单服务通信桩
OrderServerStub stub = new OrderServerStub();
// 启动一个RMI注册中心, 端口为1099
LocateRegistry.createRegistry(1099);
// 把OrderServer这个桩,注册到rmi://0.0.0.0:1099/order这个url上
Naming.bind("rmi://0.0.0.0:1099/order", stub);

客户端的代码长这样,可以看到一个loopup就把这个桩找过来了。
然后就能直接调用stub里的queryOrder方法查询订单了!

1
2
3
Registry registry = LocateRegistry.getRegistry("kingx_kali_host",1099);
OrderServerStub stub = (OrderServerStub) registry.lookup("hello");
stub.queryOrder("aaa");

image.png


Q: 那JNDI和RMI又是什么关系?怎么就联系到一起了#

A:
上面的代码里, 可以看到RMI需要自己写一段Java代码执行。
如果以后你不用RMI来存这个通信对象了,而是用LDAP之类的,咋办?难道代码都要重新写然后部署一份吗?

而如果能用JNDI的方式,通过一个小小的字符串,就能拿到,那就简单了。
那么当我需要切换通信对象的获取方式时, 切换JDNI里的设置即可。

而RMI正好实现了JNDI的spi接口,以至于能支持用JNDI+ 字符串去获取对象
image.png

这里贴一下SPI的概念:

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制

  • 说人话,spi就是框架方提供一个interface接口,然后只要有人在服务的class发现路径下写一个实现类,就能在代码里直接用上。

而log4j里,正好就支持用${jndi:rmi:x.x.x.x:1099/path}的方式进行RMI对象的获取。

log4j开发者可能本意只是方便用jndi获取各种java容器内置对象,没想到忽略了rmi的获取方式。

这就导致了 我们的服务可能会访问 黑客部署的RMI服务, 获取到一个不可信的远程调用对象。


Q: 但是刚才提到,我们只会通过RMI去拿到一个stub,stub里的内容仅仅是通过特定的ip+port去做发送, 代码是固定的,再怎么恶意的命令, 也只会在RMI注册中心即黑客的服务器上执行, 怎么就在我这边触发了攻击?#

而且这个stub对象的class文件在我们服务器本地并没有, 难道不会报classNotFind异常吗?

A:
某个讲RMI注入的文章里这样说道:

RMI服务端除了直接绑定远程对象之外,还可以通过References引用类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。
绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

  • 说人话,就是RMI允许客户端的java环境中没有这个stub对象
  • RMI服务端(那个1099端口的服务)他会返回给你一个factory(序列化传过来), 让你调用这个factory做转换。而这个可被序列化生成的factory就是问题的根本原因。

整个利用流程如下:

  1. 目标代码中调用了InitialContext.lookup(URI),且URI为用户可控;
  2. 攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;
  3. 攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;
  4. 目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例;
  5. 攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果;

Q: 那么log4j-core 2.15版本又是怎么改的呢?#

A:
限定jndi使用的协议, 禁止在jndi中用ldap、rmi去调用一些远端的服务。


思考#

说实话,这个漏洞影响之所以这么大, 就是因为原理太过简单, 随便发一段rmi注册中心的demo和客户端调用demo给别人,他就能复现,甚至用这个方式去攻击。

为什么log4j的设计者当时没有考虑到呢?
很大概率可能是因为jndi的spi机制扩展性太强。
也许最初,jndi只支持dns、数据库driver等对象的命名获取

但是后来随着版本更新, JNDP通过SPI机制, 支持了RMI、LDAP等实现, 而这个是log4j开发者当时没考虑到的。

换句话说, 这是java高可扩展性和安全性的一次冲突, 因此JNDI的调用方式, 未来应该会被更加谨慎地使用了。


参考文章:
https://y4er.com/post/attack-java-jndi-rmi-ldap-1/

https://kingx.me/Exploit-Java-Deserialization-with-RMI.html

313. 超级丑数 - 力扣(LeetCode)

1660927304363

这个好难啊

之前做264的时候,我是直接用bfs+优先队列做的

但这一题里n很大,质因数也很大, 所以bfs要么超时要么内存溢出,这是我写的超时bfs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int nthSuperUglyNumber(int n, int[] primes) {
Queue<Long> queue = new PriorityQueue<>();
queue.offer(1L);
Set<Long> vis = new HashSet<>();
int index = 0;
while(!queue.isEmpty()) {
long num = queue.poll();
index++;
if (index == n) {
return (int)num;
}
for (int prim : primes) {
long newNum = prim * num;
if (vis.contains(newNum)) {
continue;
}
vis.add(newNum);
queue.offer(newNum);
}
}
return -1;
}
}

这题我是只能看了答案才知道怎么做,甚至看答案还研究了半天,好累。

前置题目: 264. 丑数 II - 力扣(LeetCode) , 里面的质因数只有[2,3,5],以这个为例

相当于对于1~x个丑数而言,想要求第x+1个丑数

首先知道下面这个概念:

  • 当第8个丑数被2乘了之后,下一个可以尝试被2乘的且尽可能小的,一定是第9个丑数,不可能跳级到第10个丑数,即乘2的动作,应当按顺序发生在之前已经算出来的丑数上。

  • 3和5都同理

  • 且每个丑数(注意是丑数而不是非丑数)都可以被2、3、5乘一次

那我就维护2、3、5的三个指针,每次从 各指针位置丑数 * (2或3或5) 选出最小值, 然后更新指针即可。

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
class Solution {
public int nthSuperUglyNumber(int n, int[] primes) {
long[] dp = new long[n+1];
dp[1] = 1;
int[] pIndex = new int[primes.length];
Arrays.fill(pIndex, 1);
for (int i = 2;i<=n;i++) {
long min = Integer.MAX_VALUE;
List<Integer> selectPis = new ArrayList<>();
// 找到最小值以及符合的指针
for (int pi = 0;pi< primes.length;pi++) {
if (dp[pIndex[pi]] * primes[pi] < min) {
min = dp[pIndex[pi]] * primes[pi];
selectPis.clear();
selectPis.add(pi);
} else if (dp[pIndex[pi]] * primes[pi] == min) {
selectPis.add(pi);
}
}
dp[i] = min;
// 也可以先得到最小值,再逐个排除
for (int selectPi : selectPis) {
pIndex[selectPi]++;
}
}
return (int)dp[n];
}
}

面试题 02.04. 分割链表 - 力扣(LeetCode)

1660927840571

设置2个头节点,按x的值区分处理即可

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode partition(ListNode head, int x) {
ListNode h1 = new ListNode(0);
ListNode h2 = new ListNode(0);
ListNode node1 = h1,node2 = h2;
ListNode node = head;
while(node != null) {
ListNode nextNode = node.next;
if (node.val < x) {
node1.next = node;
node1 = node1.next;
} else {
node2.next = node;
node2 = node2.next;
}
node = nextNode;
}
node1.next = h2.next;
node2.next = null;
return h1.next;
}
}

面试题 08.09. 括号 - 力扣(LeetCode)

1660927848000

直接dfs即可,不会有重复,无需记忆化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public List<String> generateParenthesis(int n) {
dfs(0, n, n, new StringBuilder());
return results;
}

List<String> results = new ArrayList<>();
void dfs(int leftInStackCount, int leftRealCount, int rightRealCount, StringBuilder sb) {
if (rightRealCount == 0) {
results.add(sb.toString());
return;
}
if (leftInStackCount > 0) {
sb.append(")");
dfs(leftInStackCount-1, leftRealCount, rightRealCount - 1, sb);
sb.deleteCharAt(sb.length()-1);
}
if (leftRealCount > 0) {
sb.append("(");
dfs(leftInStackCount+1, leftRealCount-1, rightRealCount, sb);
sb.deleteCharAt(sb.length()-1);
}
}
}

[toc]

1、 单机架构#

server、文件、mysql都在同一台物理服务器上。
用于前期研发测试、或者访问量较小的内部管理页网站等。

2、 集群架构(水平扩充)#

这里的集群,是指各节点服务的处理能力完全一致,单纯提升业务的水平处理能力。

集群架构需要做以下重点改造:

  1. 业务处理服务器 server 无状态化+集群化, 部署多台对等节点。
  2. 设置负载均衡调度服务,用于专门的负载均衡,例如ngnix自带负载均衡设置
  3. 负载均衡调度服务也要可靠, 可利用公共的公网流量接入组件(apig、elb、slb)来负责做负载均衡服务的dns轮询。
  4. 缓存使用设置分布式redis
  5. mysql数据库做到主从分离。
  6. 静态文件下放到cdn节点, 由ISP互联网服务提供商进行下发,提升响应速度。

3、 垂直拆分#

Q: 业务逻辑全部耦合在一个服务中有什么缺点?#

A:某个无关紧要的业务,可能因为请求量突然过大,导致影响了所有业务的使用 。 例如ths的cs里同时包含了用户埋点数据采集转发的业务逻辑, 如果这块流量突然过大, 可能导致正常行情数据都无法使用。


大型电商的拆分逻辑举例#

首页、用户、搜索、广告、购物、订单、商品、收益结算
2bf084dadd4bdf645c9643d71b5a8d76e11a7855


Q: 业务拆分得越细越好吗, 会有什么问题?#

A:
维护成本会很大,并且数据库改造等都需要跟上,有时候要考虑工期进度、以及必要性等。

4、服务化架构#

什么是服务化?#

Service-Oriented Architecture SOA架构

面向服务的架构#

可以理解为将原本的 controller- service- dao 这个过程再做拆分, 抽出公共的数据或者业务提供服务, 提供给其他服务使用。

从mvc上理解时,可以是将dao层独立成一个服务, 其他服务不再需要自己重复写dao代码,而是直接调数据获取接口即可。 这样数据获取的逻辑只要改单个服务即可,不需要多个服务同时修改。

代价就是服务间存在大量依赖关系,需要引入服务治理能力。
如何通俗易懂地解释什么是SOA


Q: 集群和分布式的区别是什么?#

A:
集群是把多台服务器集中在一起, 目的是实现同一个业务。
分布式是把不同的业务分布在不同的地方, 目的是实现不同的业务。

分布式中的每个节点都可以构成一个集群
但是集群却不一定是分布式的。


Q: 微服务和分布式的区别是什么?#

A:
微服务是“架构设计”方式,分布式是“系统部署”方式,两者概念不同

微服务是指很小的服务,可以小到只完成一个功能,这个服务可以单独部署运行,不同服务之间通过rpc调用。

分布式是指服务部署在不同的机器上,一个服务可以提供一个或多个功能,服务之间也是通过rpc来交互或者是webservice来交互的。

两者的关系是,系统应用部署在“超过一台”服务器或虚拟机上,且各分开部署的部分彼此通过各种通讯协议交互信息,就可算作分布式部署
生产环境下的微服务肯定是分布式部署的,分布式部署的应用不一定是微服务架构的,比如集群部署,它是把相同应用复制到不同服务器上,但是逻辑功能上还是单体应用。

简答来说

  • 微服务——拆出多种业务,不管几台机器
  • 集群——1种业务,拆到多台机器
  • 分布式—— x种业务,拆到1台以上,且业务之间有关联。 分布式不一定是微服务,集群也不一定是分布式, 但是分布式一定满足集群,因为机器数量肯定大于1。

Q: APIG网关接入层的作用是什么?#

A:

  • 当业务拆分复杂时, 可通过APIG网关确定转发逻辑, 不用维护webServer的公共逻辑。
  • 灰度发布(配置白名单或者正则,将部分用户请求导入到灰度环境)
  • 流控
  • 安全防护

避免webServer做过多的安全修复和改造,通过网关保证外部输入安全。


[toc]
讲解HTTPS认证原理的文章非常多,也算是做web开发的基础知识了。但是这类文章看过去都有一个特点——知识点超级多,很乱。

证书、签名、公钥、私钥、哈希、CA证书、网站证书、对称非对称加解密……一堆概念夹杂在一起,导致很多人对这块只能说个所以然,却无法做到完全理解。

这里我就用 从签发证书到数据加密交互,按流程完整解释, 并在其中穿插图片和问题,来完整解释这个原理。


一、创业前的资质申请——证书签发#

某天,我做了一个网站, 如果直接开放给所有人访问,那么就属于无证经营, 每个访问我网站的人,都会收到浏览器如下的警告。
在这里插入图片描述
为了解决这个问题, 我需要去工商局也就是证书颁发机构CA( Certificate Authority),去获取一个证书,来证明我这个网站是安全的。

CA机构要求我提供身份证 、 店铺信息等一系列申请信息,整合成一个server.req的文件, 提交给他。
在这里插入图片描述

CA机构会有专门的人进行审查,确认我的资质、合法性。审查通过后,他们就开始了证书签发工作。

  1. 首先, 他们觉得我的网站信息太长了,不好用来做检查或者对比,于是用一个 哈希算法(可以是sha256),将其变成一个固定长度的字符串, 这个字符串被他们叫做 摘要(Digest)

  2. 接着,他们请出了CA机构中的最高领导,将这个摘要加上领导名字 重新在纸上写了一遍,然而写出来时却是一串看不懂的线条。 这是领导独有的写法,除了他自己,任何人都无法模仿。而且普通人也根本看不懂。
    在这里插入图片描述

    这个领导的名字叫做 CA私钥
    而这个看似瞎写的过程, 叫做私钥签名,即对摘要重写并暗含了自己的名字,只是一般人根本看不懂罢了。

  3. 接着他们把我的网站信息、CA签名 打包成了一个证书,颁发给我,叫我好好保管, 如果有顾客问我,我就可以把这个证书拿出来给他们看。

我疑惑地问道: “你这个签名写得乱七八糟,谁也看不懂啊,顾客咋确认我这个是合法的证书?”
CA证书的机构笑了笑,说:“你只管提供就好了,顾客们自有办法”。

最终整个颁发的过程如下所示:
在这里插入图片描述

二、究极谨慎的顾客——证书验证#

那天,我游荡在街头,无意发现一个我很感兴趣的店,我进入了店内,却发现这家店是新开的。

如果我不确认这家店的合法性,那么很有可能进入一家黑店,被宰或者被犯罪都有可能。

此刻我必须确认这个店家的证书是CA机构签发的,还是他自己造假的证书。

而我正好认识一位和CA私钥一起出生长大的兄弟 CA公钥(公私钥都是同时生成的), 虽然无法模仿领导的笔迹,但是能够勉强看懂。

CA公钥确认了签名是CA私钥所写,而且能够正确解读出签名中的摘要信息。
他还将其与经营证书上所有文字生成的新摘要进行比对, 发现确实一致,因此他认定这是合法的证书。

上面这个过程,就叫做验签, 如下所示
在这里插入图片描述
可是,一般能解读出签名是CA私钥所写就够了, 为什么还要比对一下摘要信息呢?

因为签名公钥只能识别出这个字是私钥所写,但是私钥曾经为别人写过很多的签名。
万一造假者把签名换成了其他正规店铺里抄来的签名,那怎么办? 所以肯定得确认下这个签名的内容是不是和证书上所写的店铺是同一家。

上面的过程是单向认证,即客户端验证服务端。

如果商家担心碰到居心不良的顾客, 那么同样可以让顾客提供 类似签发过程的证书,以证明自己也是可信的顾客, 这就是双向认证。

三、超级而隐秘的交易#

上面的店家和顾客,通过证书验证,确认了各自的身份。接着便决定互相进行快递交易。
但因为路上不安全,经常有强盗或者小偷出没,如何保证快递不被盗取很重要。

顾客打算把钱(通信数据)放在一个密码箱里, 然后设置好密码,再快递过去。
这个密码叫做会话密钥,任何人拿到密码都能打开密码箱,所以也叫对称密钥

但是店家该怎么知道这个密码呢?

如果顾客直接把密码箱的密码快递过去, 密码在路上被小偷看到了, 那后面发出去的钱岂不是就能被小偷解开并拿到了?
在这里插入图片描述

这时候顾客想起来,当时拿到的商家经营证书里, 携带了一个叫 公钥的东西, 通过这个东西, 可以把 密码箱密码 变成另一串看不懂的数字, 只有商家自己能够解读这个数字。

于是顾客用公钥生成了一个 加密后的密码, 并成功送到了店家手中。

在这里插入图片描述

店家拿到后, 使用自己的私钥成功解读出了这个密码。
接着店家和顾客就 都把密码箱设置成密码,进行快递, 除了店家和顾客, 其他人都无法知道这个密码是多少, 劫匪们也就无从下手了。
在这里插入图片描述

四、其他关键问题#

上面过程中,还有几个关键问题,这里提出,可以先自己思考,再看答案:


Q: 为什么不使用非对称密钥加密 传输数据, 而要用对称密钥这样绕一大圈?#

A:
对称算法的加解密性能比非对称算法快很多, 所以非对称只用于会话密钥的传输即可, 只需要一次。


Q: 为什么数据开始传输之后不需要再做验签, 难道者中间的数据就不会再被人篡改或者替换吗?#

A:
不会,有3个原因保证:

  1. 会话密钥是会话级别, 动态生成的,只有这一次连接才会用到。因此以前废弃的会话密钥不用担心被人拿去利用。
  2. 建立连接并传递会话密钥之前,已经通过验签确认过对方身份是可信的。
  3. 没有任何第三方知道会话密钥是什么。因此第三方攻击人无法用正确的会话密钥加密数据,即无法做到伪造会话密钥来欺骗接收者的目的。

Q: CA根证书可能被造假吗?#

例如黑客修改了用户机器中的CA证书,导致CA的公钥被替换了, 后面访问了黑客所在的网址时,就会验签成功。

A:
如果真的能修改的话,那么是可行的。
但是操作系统基本会内置著名CA的公钥,除非黑客已经能直接触碰你的操作系统底层了,否则基本办不到。


Q: 黑客把这个服务端返回的证书, 保存下来,然后下次别人访问黑客的钓鱼网站时, 黑客把这个证书原封不动发回去,会通过验证吗?#

A:
客户端会验证成功,
但是客户端做公钥时
于是对数据进行对称+非对称加密
重点来了
钓鱼网站接收到的数据,他没法用,他没有正规网站的私钥! 因为客户端用的是正规网站签发的公钥才可以。
黑客做中间人两边相互拦截,把假的签发过的公钥+证书发给客户端
客户端验签过程就会发现端倪, 发现他这个证书不对劲, 不是自己这边持有的CA机构正常签发出来证书。


五、总结和完整大图#

上文的顾客就是客户端(也可以是浏览器), 店家就是服务端(网站), 工商局就是可信CA机构(或者某域内自己设置的颁发机构也行)。

而一些要点总结如下:

  • CA私钥用于签名, CA公钥用于验签
  • 要先生成摘要,再签名。目的是为了验签后确认来源是所签的服务端。
  • 服务端公钥用于加密, 服务端私钥用于解密。
  • 传输数据使用对称密钥进行加密和解密。

在这里插入图片描述

1636. 按照频率将数组升序排序 - 力扣(LeetCode)

1660838816939

先统计个数,然后按照个数进行排序

注意:

对于int[],不可以直接进行Arrays.sort(nums, (a,b)->a-b);

要么先转成Integer[]

要么用

1
2
Arrays.stream(nums).boxed().sorted((a,b)->
(counts[a] - counts[b) ).mapToInt(Integer::intValue).toArray()

来实现,先stream然后boxed最后还要mapToInt才能toArray

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {

public int[] frequencySort(int[] nums) {
int[] counts = new int[201];
for (int num : nums) {
counts[num + 100]++;
}
// int[]数组不可以直接自定义排序
// Arrays.sort(nums, (a,b)->a-b);
return Arrays.stream(nums).boxed().sorted((a,b)->
counts[a+100] != counts[b+100] ? (counts[a+100] - counts[b+100]) : (b-a)).mapToInt(Integer::intValue).toArray();
}
}

剑指 Offer 30. 包含min函数的栈 - 力扣(LeetCode)

1660838926999

维护2个数组即可,stack数组和min数组(毕竟往栈底看过去的最小值一定是确定的)

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
class MinStack {

/** initialize your data structure here. */
int[] min = new int[20001];
int[] stack = new int[20001];
int index = 0;
public MinStack() {

}

public void push(int x) {
min[index] = Math.min(x, index-1>=0 ? min[index-1] : Integer.MAX_VALUE);
stack[index++] = x;
}

public void pop() {
min[index-1] = Integer.MAX_VALUE;
index--;
}

public int top() {
return stack[index-1];
}

public int min() {
return min[index-1];
}
}

322. 零钱兑换 - 力扣(LeetCode)

1660838978323

时间很紧,先是相当了bfs并用记忆化处理,每次出队硬币个数最少的,类似于走迷宫了。

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
class Solution {
public int coinChange(int[] coins, int amount) {
Queue<int[]> queue = new PriorityQueue<>((a,b)->(a[0]-b[0]));

queue.offer(new int[]{0,0});
Set<Integer> vis = new HashSet<>();
vis.add(0);
while(!queue.isEmpty()) {
int count = queue.peek()[0];
int am = queue.poll()[1];
if (am == amount) {
return count;
}
for (int coin : coins) {
int newAmount = am + coin;
if (newAmount > amount) {
continue;
}
if (vis.contains(newAmount)) {
continue;
}
vis.add(newAmount);
queue.offer(new int[]{count+1, newAmount});
}
}
return -1;
}
}

1660839037340

性能太差了

看了答案想起来这个完全可以动态规划

因为amount的范围是0~104而非109,完全可以把amount作为一个dp状态处理, 且coin的数量也只有12个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int coinChange(int[] coins, int amount) {
long[] dp = new long[amount+1];

for (long a = 1; a <= amount;a++) {
dp[(int)a] = Integer.MAX_VALUE;
for (int coin : coins) {
if (a - coin < 0) {
continue;
}
dp[(int)a] = Math.min(dp[(int)a], dp[(int)a-coin] + 1);
}
}
return (int)dp[amount] == Integer.MAX_VALUE ? -1 : (int)dp[amount];
}
}

1660839089263

性能一下子就上去了

再做一下优化,根据数据范围,去除long和int的强转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];

for (int a = 1; a <= amount;a++) {
dp[a] = 10001;
for (int coin : coins) {
if (a < coin) {
continue;
}
dp[a] = Math.min(dp[a], dp[a-coin] + 1);
}
}
return dp[amount] == 10001 ? -1 : dp[amount];
}
}

1660839698405

提升到11ms了

另外有个很奇怪的,下面这个的性能始终慢2ms,我感觉明明过滤了很多不必要的coins循环才对

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
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
int minCoin = coins[0];
for (int i = 1; i < coins.length;i++) {
minCoin= Math.min(minCoin, coins[i]);
}

Arrays.sort(coins);
for (int a = 1; a <= amount;a++) {
dp[a] = 10001;
if (a < minCoin) {
continue;
}
for (int coin : coins) {
int lastCoinSum = a-coin;
if (lastCoinSum < 0) {
break;
}
dp[a] = Math.min(dp[a], dp[lastCoinSum] + 1);
}
}
return dp[amount] == 10001 ? -1 : dp[amount];
}
}

1660839823484

[toc]

Q:-client和-server的对比#

()启动较快
()性能和内存管理效率高(注意启动快和性能好不是一回事)
桌面应用一般使用(), 服务器一般使用()
A:
(-clien)启动较快
(-server)性能和内存管理效率高
桌面应用一般使用(-clien), 服务器一般使用(-server)


有4个跟内存相关的参数
-Xmn -Xms -Xmx -Xss


Q:用于配置java初始堆内存的是()#

A:
-Xms。
-X、memory、size ,内存大小


Q:用于配置java堆的最大值的是()#

A:
-Xmx。
-X、memory、max
最大内存
2583965d5747d12e17b63fad1a4480868801656b


Q:如果不设置,-Xms和-Xmx的大小分别默认是多少?#

A:
不设置的话,二者相等,默认是 物理内存/64(小于1G)


Q:用于配置新生代内存大小的最大值是:()#

你问我什么是新生代内存?
就是下面这个,1个E区加2个S区的这个内存大小

A:
-Xmn。
-X、memory、new
相类似的还有-XX:NewSize 和 -XX:MaxNewSize。


Q: 如何根据上面的参数计算老年代内存大小?#

A:
Xmx的值(堆最大值)- Xmn的值(新生代内存)


Q: 用于配置线程栈内存的是()? 替代的还有哪个参数?#

A:
-Xss。 另一个是-XX:ThreadStackSize
-Xss指 -X stack size


有下面3个和gc相关的参数
-Xnoclassgc -Xincgc -Xloggc:file
回答以下问题:

Q:可用于关闭针对类对象的gc功能的是()#

可用于关闭针对类对象的gc功能的是(-Xnoclassgc)

Q: 可用于减少gc的程序停顿时间的是()#

可用于减少gc的程序停顿时间的是(-Xincgc)

Q: 用于输出gc相关日志的是()#

用于输出gc相关日志的是(-Xloggc:file)


Q:-verbose 一般是用于什么的?#

A:
查询gc问题。

-verbose:class 输出jvm载入类的相关信息,当jvm报告说找不到类或者类冲突时可此进行诊断。
-verbose:gc 输出每次GC的相关情况,后面会有更详细的介绍。
-verbose:jni 输出native方法调用的相关情况,一般用于诊断jni调用错误信息。


Q: -XX:PermSize和-XX:MaxPermSize设置的是什么内存?#

A:
方法区的内存。就是最开始那个图里的这个

通过配置-XX:PermSize以及-XX:MaxPermSize来控制这块内存的大小,jvm在启动的时候会根据-XX:PermSize初始化分配一块连续的内存块,这样的话,如果-XX:PermSize设置过大,可能会很浪费。而Max如果设置小了,可能会omm。


Q:-XX:MetaspaceSize和-XX:MaxMetaspaceSize又是什么内存?#

A:
元数据区内存。 java8引入的,用于替代上面的perm区。
无论-XX:MetaspaceSize和-XX:MaxMetaspaceSize两个参数如何设置,随着类加载越来越多不断扩容调整,直到MetaspaceSize(如果没有配置就是默认20.8m)触发FGC,上限是-XX:MaxMetaspaceSize,默认是几乎无穷大