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

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


  • 首页

  • 归档

  • 搜索

fastapi微服务系列(2)-之GRPC的intercep

发表于 2021-11-16

对于一个框架来说,通常具备有所谓的中间件,有时候也可以说是拦截器,其实和钩子差不多的概念。

那grpc也不例外。但是使用python如何应用到我们的拦截器的呐? 拦截器又可以做哪些事情呢?

1:grpc的拦截器可以做啥?

本身拦截器的概念和我们的中间件类似,所以类似fastapi中我们的中间件能做,拦截器都可以做:

  • 身份验证
  • 日志请求记录
  • 全局上下文的信息处理等
  • 多个拦截器和多个中间件遵循的请求规则都是洋葱模型
  • 拦截器必须有返回值,返回是响应报文体

PS:而且相对GRPC来说不止于我们的服务端有钩子,客户端也有钩子(拦截器),和我们的httpx库提供的类似的钩子函数差不多!

PS:拦截器可以作用再客户端和服务端:客户端拦截器和服务端拦截器

2:grpc的拦截器分类

  • 一元拦截器(UnaryServerInterceptor)-客户端中
  • 流式拦截器(StreamClientInterceptor)- 客户端中
  • python中的服务端是实现ServerInterceptor

image.png

3:在python实现grpc拦截器

查看服务传递的拦截器参数说明:

image.png

3.1 服务端的自带拦截器

主要注意点:

  • 拦截器传入是一个实例化的对象
  • 拦截器列表的传入,可以是元组也可以是列表
  • 多拦截器的形式遵循洋葱模型

服务端拦截器需要实现拦截器的抽象方法:

image.png

完整服务端示例代码:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
python复制代码from concurrent import futures
import time
import grpc
import hello_pb2
import hello_pb2_grpc
import signal


# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(hello_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 返回是我们的定义的响应体的对象
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))

def SayHelloAgain(self, request, context):
# 返回是我们的定义的响应体的对象

# # 设置异常状态码
# context.set_code(grpc.StatusCode.PERMISSION_DENIED)
# context.set_details("你没有这个访问的权限")
# raise context

# 接收请求头的信息
print("接收到的请求头元数据信息", context.invocation_metadata())
# 设置响应报文头信息
context.set_trailing_metadata((('name', '223232'), ('sex', '23232')))
# 三种的压缩机制处理
# NoCompression = _compression.NoCompression
# Deflate = _compression.Deflate
# Gzip = _compression.Gzip
# 局部的数据进行压缩
context.set_compression(grpc.Compression.Gzip)
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))


class MyUnaryServerInterceptor1(grpc.ServerInterceptor):

def intercept_service(self,continuation, handler_call_details):
print("我是拦截器1号:开始----1")
respn = continuation(handler_call_details)
print("我是拦截器1号:结束----2",respn)
return respn

class MyUnaryServerInterceptor2(grpc.ServerInterceptor):

def intercept_service(self,continuation, handler_call_details):
print("我是拦截器2号:开始----1")
respn = continuation(handler_call_details)
print("我是拦截器2号:结束----2",respn)
return respn

def serve():

# 实例化一个rpc服务,使用线程池的方式启动我们的服务
# 服务一些参数信息的配置
options = [
('grpc.max_send_message_length', 60 * 1024 * 1024), # 限制发送的最大的数据大小
('grpc.max_receive_message_length', 60 * 1024 * 1024), # 限制接收的最大的数据的大小
]
# 三种的压缩机制处理
# NoCompression = _compression.NoCompression
# Deflate = _compression.Deflate
# Gzip = _compression.Gzip
# 配置服务启动全局的数据传输的压缩机制
compression = grpc.Compression.Gzip
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10),
options=options,
compression=compression,
interceptors=[MyUnaryServerInterceptor1(),MyUnaryServerInterceptor2()])
# 添加我们服务
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
# 开始启动的服务
server.start()

def stop_serve(signum, frame):
print("进程结束了!!!!")
# sys.exit(0)
raise KeyboardInterrupt

# 注销相关的信号
# SIGINT 对应windos下的 ctrl+c的命令
# SIGTERM 对应的linux下的kill命令
signal.signal(signal.SIGINT, stop_serve)
# signal.signal(signal.SIGTERM, stop_serve)

# wait_for_termination --主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
server.wait_for_termination()


if __name__ == '__main__':
serve()

关键的配置地方是:

image.png
此时使用我们的客户端请求服务端,服务端会输出一下的信息:

1
2
3
4
5
python复制代码我是拦截器1号:开始----1
我是拦截器2号:开始----1
我是拦截器2号:结束----2 RpcMethodHandler(request_streaming=False, response_streaming=False, request_deserializer=<built-in method FromString of GeneratedProtocolMessageType object at 0x00000175D2988558>, response_serializer=<method 'SerializeToString' of 'google.protobuf.pyext._message.CMessage' objects>, unary_unary=<bound method Greeter.SayHelloAgain of <__main__.Greeter object at 0x00000175D46167B8>>, unary_stream=None, stream_unary=None, stream_stream=None)
我是拦截器1号:结束----2 RpcMethodHandler(request_streaming=False, response_streaming=False, request_deserializer=<built-in method FromString of GeneratedProtocolMessageType object at 0x00000175D2988558>, response_serializer=<method 'SerializeToString' of 'google.protobuf.pyext._message.CMessage' objects>, unary_unary=<bound method Greeter.SayHelloAgain of <__main__.Greeter object at 0x00000175D46167B8>>, unary_stream=None, stream_unary=None, stream_stream=None)
接收到的请求头元数据信息 (_Metadatum(key='mesasge', value='1010'), _Metadatum(key='error', value='No Error'), _Metadatum(key='user-agent', value='grpc-python/1.41.1 grpc-c/19.0.0 (windows; chttp2)'))

3.2 客户端的自带拦截器

客户端拦截器的需要实现类和服务端的不一样:

image.png

且当我们的使用客户端拦截器的时候,主要链接到我们的RPC的时候的方式也有所改变:

image.png

完整客户端示例代码:

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
67
68
69
70
71
python复制代码import grpc
import hello_pb2
import hello_pb2_grpc


class ClientServerInterceptor1(grpc.UnaryUnaryClientInterceptor):
def intercept_unary_unary(self, continuation, client_call_details, request):
print("客户端的拦截器1:---开始1")
resp = continuation(client_call_details, request)
print("客户端的拦截器1:---结束2", resp)
return resp

class ClientServerInterceptor2(grpc.UnaryUnaryClientInterceptor):
def intercept_unary_unary(self, continuation, client_call_details, request):
print("客户端的拦截器2:---开始1")
resp = continuation(client_call_details, request)
print("客户端的拦截器2:---结束2", resp)
return resp

def run():
# 连接 rpc 服务器
options = [
('grpc.max_send_message_length', 100 * 1024 * 1024),
('grpc.max_receive_message_length', 100 * 1024 * 1024),
('grpc.enable_retries', 1),
('grpc.service_config',
'{ "retryPolicy":{ "maxAttempts": 4, "initialBackoff": "0.1s", "maxBackoff": "1s", "backoffMutiplier": 2, "retryableStatusCodes": [ "UNAVAILABLE" ] } }')
]

# 三种的压缩机制处理
# NoCompression = _compression.NoCompression
# Deflate = _compression.Deflate
# Gzip = _compression.Gzip
# 配置服务启动全局的数据传输的压缩机制
compression = grpc.Compression.Gzip
# with grpc.insecure_channel(target='localhost:50051',
# options=options,
# compression=compression
# ) as channel:


with grpc.insecure_channel(target='localhost:50051',
options=options,
compression=compression
) as channel:
# 通过通道服务一个服务intercept_channel
interceptor_channel = grpc.intercept_channel(channel, ClientServerInterceptor1(),ClientServerInterceptor2())
stub = hello_pb2_grpc.GreeterStub(interceptor_channel)
# 生成请求我们的服务的函数的时候,需要传递的参数体,它放在hello_pb2里面-请求体为:hello_pb2.HelloRequest对象
try:

reest_header = (
('mesasge', '1010'),
('error', 'No Error')
)

response, callbask = stub.SayHelloAgain.with_call(request=hello_pb2.HelloRequest(name='欢迎下次光临'),
# 设置请求的超时处理
timeout=5,
# 设置请求的头的信息
metadata=reest_header,
)
print("SayHelloAgain函数调用结果的返回: " + response.message)
print("SayHelloAgain函数调用结果的返回---响应报文头信息: ", callbask.trailing_metadata())
except grpc._channel._InactiveRpcError as e:
print(e.code())
print(e.details())


if __name__ == '__main__':
run()

4:grpc拦截器上下文传递

我们的都知道作为中间件的话,一般某些业务场景下是有些使用承载请求上下文的传递的任务滴,然是自带的拦截器,似乎完全没有对应的

1
vbscript复制代码request, context

相关的引入传递,如果我们的需要传递上下文的时候呢?这就无法实现了!!!!

要实现具有上下文的传递拦截器的话使用第三方库来实现:

1
复制代码 pip install grpc-interceptor

这个库还字典的相关的测试:

1
css复制代码$ pip install grpc-interceptor[testing]

4.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
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
python复制代码from concurrent import futures
import time
import grpc
import hello_pb2
import hello_pb2_grpc
import signal
from typing import Any,Callable

# 实现 proto文件中定义的 GreeterServicer的接口
class Greeter(hello_pb2_grpc.GreeterServicer):
# 实现 proto 文件中定义的 rpc 调用
def SayHello(self, request, context):
# 返回是我们的定义的响应体的对象
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))

def SayHelloAgain(self, request, context):
# 返回是我们的定义的响应体的对象

# # 设置异常状态码
# context.set_code(grpc.StatusCode.PERMISSION_DENIED)
# context.set_details("你没有这个访问的权限")
# raise context

# 接收请求头的信息
print("接收到的请求头元数据信息", context.invocation_metadata())
# 设置响应报文头信息
context.set_trailing_metadata((('name', '223232'), ('sex', '23232')))
# 三种的压缩机制处理
# NoCompression = _compression.NoCompression
# Deflate = _compression.Deflate
# Gzip = _compression.Gzip
# 局部的数据进行压缩
context.set_compression(grpc.Compression.Gzip)
return hello_pb2.HelloReply(message='hello {msg}'.format(msg=request.name))

from grpc_interceptor import ServerInterceptor
from grpc_interceptor.exceptions import GrpcException
from grpc_interceptor.exceptions import NotFound
class MyUnaryServerInterceptor1(ServerInterceptor):

def intercept(
self,
method: Callable,
request: Any,
context: grpc.ServicerContext,
method_name: str,
) -> Any:

rsep = None
try:
print("我是拦截器1号:开始----1")
rsep= method(request, context)
except GrpcException as e:
context.set_code(e.status_code)
context.set_details(e.details)
raise
finally:
print("我是拦截器1号:结束----2",rsep)
return rsep

class MyUnaryServerInterceptor2(ServerInterceptor):

def intercept(
self,
method: Callable,
request: Any,
context: grpc.ServicerContext,
method_name: str,
) -> Any:

rsep = None
try:
print("我是拦截器2号:开始----1")
rsep= method(request, context)
except GrpcException as e:
context.set_code(e.status_code)
context.set_details(e.details)
raise
finally:
print("我是拦截器2号:结束----2",rsep)
return rsep

def serve():

# 实例化一个rpc服务,使用线程池的方式启动我们的服务
# 服务一些参数信息的配置
options = [
('grpc.max_send_message_length', 60 * 1024 * 1024), # 限制发送的最大的数据大小
('grpc.max_receive_message_length', 60 * 1024 * 1024), # 限制接收的最大的数据的大小
]
# 三种的压缩机制处理
# NoCompression = _compression.NoCompression
# Deflate = _compression.Deflate
# Gzip = _compression.Gzip
# 配置服务启动全局的数据传输的压缩机制
compression = grpc.Compression.Gzip
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10),
options=options,
compression=compression,
interceptors=[MyUnaryServerInterceptor1(),MyUnaryServerInterceptor2()])
# 添加我们服务
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 配置启动的端口
server.add_insecure_port('[::]:50051')
# 开始启动的服务
server.start()

def stop_serve(signum, frame):
print("进程结束了!!!!")
# sys.exit(0)
raise KeyboardInterrupt

# 注销相关的信号
# SIGINT 对应windos下的 ctrl+c的命令
# SIGTERM 对应的linux下的kill命令
signal.signal(signal.SIGINT, stop_serve)
# signal.signal(signal.SIGTERM, stop_serve)

# wait_for_termination --主要是为了目标启动后主进程直接的结束!需要一个循环的方式进行进行进程运行
server.wait_for_termination()


if __name__ == '__main__':
serve()

通过上面的方式,我们就可以对应我们的上下文请求做相关的处理了!这个和我们的web框架的中间件几乎是接近类似了!

4.2 简单分析第三方库简单源码

进入这个第三方库的源码内部的,其实发现它自己也是实现了

1
复制代码grpc.ServerInterceptor

然后对它进一步进行了抽象一层

  • 第一步其实和我们自带的实现一样,先是获取返回的下一个带处理的handle
1
ini复制代码next_handler = continuation(handler_call_details)

然后对返回这个next_handler进行是那种类型的的拦截器:

1
2
3
4
diff复制代码- unary_unary
- unary_stream
- stream_unary
- stream_stream
  • 判断完成是哪里蕾西的拦截器之后返回
1
复制代码handler_factory, next_handler_method

然后调用的是最终返回是handler_factory的对象

  • handler_factory的对象需要的参数有:
+ invoke\_intercept\_method 拦截器的方法
+ request\_deserializer 请求的系列化
+ response\_serializer 响应的系列化
  • 而我们的invoke_intercept_method 拦截器的方法获取则需要
+ 传入定义的一个
1
2
makefile复制代码request: Any,
context: grpc.ServicerContext,
  • 然后返回是我们的最终需要实现的方法!我去晕了~

4.3 补充说明handler_call_details

如果我们的单纯只是需要获取到RPC请求里面的提交请求头元数据的,我们可以使用它读取:

1
bash复制代码print("handler call details: ", handler_call_details.invocation_metadata)

它本身是一个:

1
复制代码grpc._server._HandlerCallDetails的类型

总结

以上仅仅是个人结合自己的实际需求,做学习的实践笔记!如有笔误!欢迎批评指正!感谢各位大佬!

结尾

END

简书:www.jianshu.com/u/d6960089b…

掘金:juejin.cn/user/296393…

公众号:微信搜【小儿来一壶枸杞酒泡茶】

小钟同学 | 文 【欢迎一起学习交流】| QQ:308711822

本文转载自: 掘金

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

Raft算法 Raft 算法概览 leader electi

发表于 2021-11-16

raft是工程上使用较为广泛的强一致性、去中心化、高可用的分布式协议。在这里强调了是在工程上,因为在学术理论界,最耀眼的还是大名鼎鼎的Paxos。但Paxos是:少数真正理解的人觉得简单,尚未理解的人觉得很难,大多数人都是一知半解。

raft是一个共识算法(consensus algorithm),所谓共识,就是多个节点对某个事情达成一致的看法,即使是在部分节点故障、网络延时、网络分割的情况下。这些年最为火热的加密货币(比特币、区块链)就需要共识算法,而在分布式系统中,共识算法更多用于提高系统的容错性,比如分布式存储中的复制集(replication)。raft协议就是一种leader-based的共识算法,与之相应的是leaderless的共识算法。

Raft 算法概览

Raft算法的头号目标就是容易理解(UnderStandable)。当然,Raft增强了可理解性,在性能、可靠性、可用性方面是不输于Paxos的。

为了达到易于理解的目标,raft做了很多努力,其中最主要是两件事情:

  • 问题分解
  • 状态简化

raft会先选举出leader,leader完全负责replicated log的管理。leader负责接受所有客户端更新请求,然后复制到follower节点,并在“安全”的时候执行这些请求。如果leader故障,followes会重新选举出新的leader。

这就涉及到raft最新的两个子问题: leader election和log replication

leader election

raft协议中,一个节点任一时刻处于以下三个状态之一:

  • leader
  • follower
  • candidate

下图可以很直观的看到这三个状态的区别
1089769-20181216202049306-1194425087.png

所有节点在启动的时候都是follower状态;在一段时间内如果没有收到来自leader的心跳,从follower切换到candidate,发起选举;如果收到majority的造成票(含自己的一票)则切换到leader状态;如果发现其他节点比自己更新,则主动切换到follower。

总之,系统中最多只有一个leader,如果在一段时间里发现没有leader,则大家通过选举-投票选出leader。leader会不停的给follower发心跳消息,表明自己的存活状态。如果leader故障,那么follower会转换成candidate,重新选出leader。

term 任期

我们知道leader是大家投票选举出来的,每个leader工作一段时间,然后选出新的leader继续负责。每一届新的履职期称之为一届任期,在raft协议中,也是这样的,对应的术语叫term。

1089769-20181216202155162-452543292.png

term(任期)以选举(election)开始,然后就是一段或长或短的稳定工作期(normal Operation)。从上图可以看到,任期是递增的,这就充当了逻辑时钟的作用;另外,term 3展示了一种情况,就是说没有选举出leader就结束了,然后会发起新的选举。

选举过程

我们知道如果follower在 election timeout 内没有收到来自leader的心跳,(也许此时还没有选出leader,大家都在等;也许leader挂了;也许只是leader与该follower之间网络故障),则会主动发起选举。步骤如下:

  • 增加节点本地的 current term ,切换到candidate状态
  • 投自己一票
  • 并行给其他节点发送 RequestVote RPCs
  • 等待其他节点的回复

在这个过程中,根据来自其他节点的消息,可能出现三种结果

  1. 收到majority的投票(含自己的一票),则赢得选举,成为leader
  2. 被告知别人已当选,那么自行切换到follower
  3. 一段时间内没有收到majority投票,则保持candidate状态,重新发出选举

第一种情况,赢得了选举之后,新的leader会立刻给所有节点发消息,广而告之,避免其余节点触发新的选举。在这里,先回到投票者的视角,投票者如何决定是否给一个选举请求投票呢,有以下约束:

  • 在任一任期内,单个节点最多只能投一票
  • 候选人知道的信息不能比自己的少
  • first-come-first-served 先来先得

第二种情况,比如有三个节点A B C。A B同时发起选举,而A的选举消息先到达C,C给A投了一票,当B的消息到达C时,已经不能满足上面提到的第一个约束,即C不会给B投票,而A和B显然都不会给对方投票。A胜出之后,会给B,C发心跳消息,节点B发现节点A的term不低于自己的term,知道有已经有Leader了,于是转换成follower。

第三种情况,当存在双数节点的时候,可能出现没有任何节点获得majority投票,比如下图这种情况:

1089769-20181216202546810-1327167758.png

如果出现平票的情况,那么就延长了系统不可用的时间(没有leader是不能处理客户端写请求的),因此raft引入了randomized election timeouts来尽量避免平票情况。同时,leader-based 共识算法中,节点的数目都是奇数个,尽量保证majority的出现。

log Replication

当有了leader,系统应该进入对外工作期了。客户端的一切请求来发送到leader,leader来调度这些并发请求的顺序,并且保证leader与followers状态的一致性。raft中的做法是,将这些请求以及执行顺序告知followers。leader和followers以相同的顺序来执行这些请求,保证状态一致。

Replicated state machines

共识算法的实现一般是基于复制状态机(Replicated state machines)。

简单来说:相同的初识状态 + 相同的输入 = 相同的结束状态。replicated log 具有持久化、保序的特点,是大多数分布式系统的基石。

因此,可以这么说,在raft中,leader将客户端请求(command)封装到一个个log entry,将这些log entries复制(replicate)到所有follower节点,然后大家按相同顺序应用(apply)log entry中的command,则状态肯定是一致的。

下图形象展示了这种log-based replicated state machine

1089769-20181216202234422-28123572.png

请求完成流程

当系统(leader)收到一个来自客户端的写请求,到返回给客户端,整个过程从leader的视角来看会经历以下步骤:

  • leader 追加log Entry
  • leader 将log Entry通过RPC并行的分发给follower
  • leader 等待大多数节点复制成功的响应
  • leader 将entry应用到状态机上
  • leader 对客户端进行响应
  • leader 通知follower应用日志

可以看到日志的提交过程有点类似两阶段提交(2PC),不过与2PC的区别在于,leader只需要大多数(majority)节点的回复即可,这样只要超过一半节点处于工作状态则系统就是可用的。

那么日志在每个节点上是什么样子的呢

1089769-20181216202309906-1698663454.png

每个log entry都存储着一条用于状态机的指令,同时保存从leader收到该entry时的 term 号以及一个 index 指明自己在log中的位置。

Raft的日志机制提供两个保证,统称为Log Matching Property:

  • 不同机器的日志中如果有两个entry有相同的偏移和term号,那么它们存储相同的指令。
  • 如果不同机器上的日志中有两个相同偏移和term号的entry,那么日志中这个entry之前的所有entry保持一致。

这个一致性检查以一种类似归纳法的方式进行:初始状态大家都没有日志,不需要进行Log Matching Property检查,但是无论何时followers只要日志要追加都要进行此项检查。因此,只要AppendEntries返回成功,leader就知道这个follower的日志一定和自己的完全一样。

在正常情形下,leader和follower的日志肯定是一致的,所以AppendEntries一致性检查从不失败。然而,如果leader crash,那么它们的日志很可能出现不一致。这种不一致会随着leader或者followers的crash变得非常复杂。下图展示了所有日志不一致的情形:

1089769-20181216202408734-1760694063.png

如上图(a)(b)followers可能丢失日志,(c)(d)有多余的日志,或者是(e)(f)跨越多个terms的又丢失又多余。在Raft里,leader强制followers和自己的日志严格一致,这意味着followers的日志很可能被leader的新推送日志所覆盖。

leader为了强制它人与自己一致,势必要先找出自己和follower之间存在分歧的点,然后令followers删掉那个分歧点之后的日志,再将自己在那个点之后的日志同步给followers。这个实现也是通过AppendEntries RPCs的一致性检查来做的。leader会把发给每一个follower的新日志的偏移nextIndex也告诉followers。当新leader刚开始服务时,它把所有follower的nextIndex都初始化为它最新的log entry的偏移+1(如上图中的11)。如果一个follower的日志和leader的不一致,AppendEntries RPC会失败,leader就减小nextIndex然后重试,直到找到分歧点,剩下的就好办了,移除冲突日志entries,同步自己的。

safety

之前了解了Raft如何选主和如何进行日志复制,然而这些还不足以保证不同节点能执行严格一致的指令序列,需要额外的一些安全机制。比如,一个follower可能在当前leader commit日志时不可用,然而过会它又被选举成了新leader,这样这个新leader可能会用新的entries覆盖掉刚才那些已经committed的entries。结果不同的复制状态机可能会执行不同的指令序列,产生不一致的状况。这里Raft增加了一个可以确保新leader一定包含任何之前commited entries的选举机制。

  • 选举限制

一个candidate为了选举成功必须联系大多数节点,假设它们的集合叫A,而一个entry如果能commit必然存储在大多数节点,这意味着对于每一个已经committed的entry,A集合中必然有一个节点持有它。如果这个candidate的Log不比A中任何一个节点旧才有机会被选举为leader,所以这个candidate如果要成为leader一定已经持有了所有commited entries。

  • 提交早期terms的entries

1089769-20181216202438174-260853001.png

某个leader选举成功之后,不会直接提交前任leader时期的日志,而是通过提交当前任期的日志的时候“顺手”把之前的日志也提交了,具体怎么实现了,在log matching部分有详细介绍。那么问题来了,如果leader被选举后没有收到客户端的请求呢,论文中有提到,在任期开始的时候发立即尝试复制、提交一条空的log。

因此,在上图中,不会出现(C)时刻的情况,即term4任期的leader s1不会复制term2的日志到s3。而是如同(e)描述的情况,通过复制-提交 term4的日志顺便提交term2的日志。如果term4的日志提交成功,那么term2的日志也一定提交成功,此时即使s1crash,s5也不会重新当选。

  • 调解过期leader

在Raft中有可能同一时刻不只一个server是leader。一个leader突然与集群中其他servers失去连接,导致新leader被选出,这时刚才的老leader又恢复连接,此时集群中就有了两个leader。这个老leader很可能继续为客户端服务,试图去复制entries给集群中的其它servers。但是Raft的term机制粉碎了这个老leader试图造成任何不一致的行为。每一个RPC servers都要交换它们的当前term号,新leader一旦被选举出来,肯定有一个大多数群体包含了最新的term号,老leader的RPC都必须要联系一个大多数群体,它必然会发现自己的term号过期,从而主动让贤,退变为follower状态。

然而有可能是老leader commit一个entry后失去连接,这时新leader必然有那个commit的entry,只是新leader可能还没commit它,这时新leader会在初次服务客户端前先把这个entry再commit一次,followers如果已经commit过直接返回成功,没commit就commit后返回成功而已,不会造成不一致。

Follower 或者candidate crash

Followers和candidate的crash比起leader来说处理要简单很多,它们的处理流程是相同的,如果某个follower或者candidate crash了,那么未来发往它的RequestVote和AppendEntries RPCs 都会失败,Raft采取的策略就是不停的重发,如果crash的机器恢复就会执行成功。另外,如果server crash是在完成一个RPC但在回复之前,那么在它恢复之后仍然会收到相同的RPC(让它重试一次),Raft的所有RPC都是幂等操作,如果follower已经有了某个entry,但是leader又让它复制,follower直接忽略即可。

集群扩容

13282795-5854b8814c503c71.webp

扩容最大的挑战就是保证一致性很困难,因为扩容不是原子的,有可能集群中一部分机器用老配置信息,另一部分用新配置信息,开始只有Server1-3,这时候我们添加2个节点,但是配置变更不可能在所有节点上同时生效。如果在箭头所在的时间点刚好触发选举,server1 和server 2的保存的配置还是只有3个节点的集群,所以server1可以通过自己和server 2的选票成为Leader。而Server5通过3、4、5的选票也超过半数,也可以成为Leader。造成存在2个Leader的错误。

Raft的解决方案

Raft协议通过在新旧配置变更中间添加过渡阶段来处理这个问题,称之为联合共识(joint consensus)阶段。在联合共识阶段,集群会做如下的约束:

  • 日志条目被复制给集群中新、老配置的所有节点
  • 新加入节点和原有节点都可以成为 leader
  • 达成一致(针对选举和提交)需要分别在两种配置上都超过半数

满足了上面的条件,集群就可以在切换的同时仍然对外提供服务。下面用具体场景来看下Raft的解决方案。

添加节点

当前有一个3个节点的集群,现在要往集群中添加2个节点。Raft通过发送日志的方式来发送配置变更日志指令。

发送联合共识日志

第一阶段,Leader首先向集群中的节点发送一条新的日志,日志内的指令就是配置变更。这条日志跟普通的日志一样复制给Follower节点,但是提交后不会对状态机的数据有任何改动,因为它不是数据操作指令。当前的状态如下图:

13282795-603f2cbe86332523.webp

上图中C(o+n)的日志就是配置变更日志,告诉其它几点集群需要进入old和new共存的联合共识阶段。跟处理普通的日志条目不一样,节点在收到C(o+n)的日志后,立马就进入联合共识阶段,而不需要等到Leader提交这条日志。也就是说,上图中S1,S4和S5进入联合共识阶段,而S2,S3因为还没收到日志,所以还处于旧配置阶段。

按照前面讲的,进入联合共识阶段后,Raft要求任何决议的达成必须在新老配置中都达成半数。对于上图中的S1来说,因为它已经处于联合共识阶段,所以如果它要将配置变更的日志提交,必须在老的集群(S1,S2,S3)中超过半数,在新的5个节点的集群中也要超过半数。上图中日志已经复制到3个节点,满足新集群中超过半数的要求,但是在老的3个节点的集群中未超过半数,所以这条日志现在还不能提交。

提交联合共识日志

13282795-703d77bd222c7d7f.webp

如上图,进入第二阶段,Leader提交了C(o+n)日志,现在超过半数节点都运行在联合共识阶段,即使Leader崩溃,新选出Leader的集群也仍然会运行在联合共识阶段。这时,Leader会复制一条新集群生效(图中的C(new))的日志到集群中,告诉Follower节点现在可以切换到新的集群模式下运行了。跟C(o+n)一样,Follower也不需要等到Leader提交C(new),只要一收到日志,可以立刻切换到新集群。这里有一点需要注意,在C(o+n)提交和复制C(new)日志的这一阶段,集群仍然是正常对外提供服务的,就是说在C(new)日志发出之前,客服端发给Leader的指令可以正常提交,只是提交的条件是日志被复制到新老两个集群的超过半数节点。

提交新集群日志

13282795-51e96be8da3bc35c.webp

第三阶段,在开始复制C(new)日志后,Leader已经运行于新的集群中,所以只要C(new)被复制到不少于3个节点,就可以提交了。之后整个集群的扩容就完成了。

扩容安全性

  1. 在上面的阶段1,如果Leader崩溃了,会不会出现多个Leader?

Raft协议中规定,对于配置变更的日志,不需要等到提交,各个节点只要收到了就按新的配置运行。在Phase 1阶段,如果S1崩溃了,S2和S3处于旧配置阶段,假设它们中的S2变成候选人,收到S3的选票即超过半数,当选Leader。S4和S5处于联合共识阶段,假设S4变成候选人,那么它必须同时在新旧两个集群超过半数。假设现在S1崩溃后迅速重启并加入集群,那么在新集群中,S4可以收到S1和S5的选票,超过半数;在旧集群中,收到S1的选票,但是无法收到S2或者S3的选票,因为在同一轮选举中,S2和S3已经把票投给了S2。所以,S4无法赢得旧集群的选举,不会出现两个Leader的情况。
2. 在阶段1,如果Leader崩溃,新当选的Leader会继续提交C(o+n)吗?

答案是不一定。如果是S2当选Leader,因为它没有C(o+n)的日志,按照上篇文章讲到的日志复制规范,S4和S5上的C(o+n)会被删除,所以不会提交。客户端必须重新发送配置变更指令。如果是S4当选Leader,会继续复制C(o+n)的日志,配置变更流程会继续进行。这两种情况都是符合Raft规范的。
3. 在阶段2Leader崩溃了,已经提交的C(o+n)会受到影响吗?

不会。Raft协议承诺已提交的日志永久生效。S1在Phase 2崩溃,在老的集群节点中,因为S2有最新的日志,所以只有S2可以当选新的Leader。在新加到集群的节点中,无论是S4还是S5成为新的Leader,都会继续复制和提交C(o+n)

可用性

Raft在集群配置变更时,为了提高可用性,还有以下两个问题需要解决:

  1. 新节点同步数据期间无法提交新日志,造成集群短时间不可用?

Raft规定在新节点同步数据期间,可以以没有投票权的方式加入进来(这里类似于ZooKeeper中的Observer节点),比如一共5个节点,有2个节点在同步数据阶段,那Leader可以按3个节点的集群来组织投票。等到数据同步完成后,再正式按正常节点来处理。
2. 被删除的服务器在Leader进入C-new阶段后将不再收到心跳,会触发选举使Leader失效?

为了避免这个问题,Raft规定,当节点确认当前领导人存在时,服务器会忽略请求投票的 RPCs。确认领导人存在的方法就是,收到投票RPC的时间距离上次收到Leader心跳或日志的时间还不到最小选举超时时间,那可以拒绝这个请求。

本文转载自: 掘金

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

Nginx中间件渗透总结

发表于 2021-11-16

简介

image.png

Nginx(engine x)是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务器Nginx是由伊戈尔开发,因为它的稳定性、丰富的功能集、实例配置文件和低系统资源的消耗而闻名。

Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like协议下发行,其特点是占用内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好

中国大陆使用 nginx 的网站用户有:百度、京东、新浪、网易、腾讯、淘宝

Nginx适用于高并发、可以做负载均衡服务器和HTTP服务器、具有代码特点、可以作为代理服务器

image.png

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

Nginx可以用作以下

  1. 静态服务器

首先,Nginx是一个HTTP服务器,可以将服务器上的静态文件(HTML、图片等)通过HTTP协议展现给客户端

Nginx是一款轻量级的Webserver/反向代理server以及电子邮件代理server。并在一个BSD-like协议下发行,特点是占用内存小,并发能力强,Nginx相较于Apache/lighttpd具有占用内存少,稳定性高等优势,并且依靠并发能力强,丰富的模块库以及友好灵活的配置而闻名

Nginx本身也是一个静态资源的服务器当只有静态资源的时候,就可以使用Nginx来做服务器,同时现在也很流行动静分离,可以通过Nginx来实现动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来动静资源做好了拆分以后,我们可以根据静态资源的特点将其作为缓存操作,这就是网站静态化处理的核心思路

  1. 反向代理服务器

客户端本来可以直接通过HTTP协议访问某网站应用服务器,网站管理员可以在中间加上一个Nginx,客户端请求Nginx,Nginx请求应用服务器,然后将结果返回客户端,此时Nginx就是反向代理服务器

用户A向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理将推断向何处(原始server)转交请求,并将获得的内容返回给client。而用户A始终认为他访问的是原始server而不是nginx。因为防火墙作用,仅仅同意nginx进出,防火墙和反向代理的共同作用保护了院子内的资源—原始server

image.png

如果服务器可以直接HTTP访问,为什么要在中间加一个反向代理,下面的负载均衡、虚拟主机等都基于反向代理实现,当然反向代理的功能也不仅仅是这些

  1. 负载均衡

当网站访问量非常大,网站站长开心赚钱的同时,也摊上事了。因为网站越来越慢,一台服务器已经不够用了。于是将同一个应用部署在多台服务器上,将大量用户的请求分配给多台机器处理。同时带来的好处是,其中一台服务器万一挂了,只要还有其他服务器正常运行,就不会影响用户使用Nginx可以通过反向代理来实现负载均衡

负载均衡的意思就是分摊到多个操作单元上进行执行,例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等等,从而共同完成工作任务,简单而言就是当有2台或以上服务器时,根据规则随机的将请求分发到指定的服务器上处理,负载均衡一般都需要同时配置反向代理,通过反向代理跳转到负载均衡,而Nginx目前支持自带3种负载均衡策略,还有2种常用的第三方策略

  1. 虚拟主机

有的网站访问量大,需要负载均衡。然而不是所有的网站都如此出色,有的网站由于访问量太小,需要节省成本,将多个网站部署在同一台服务器上

例如将www.aaa.com和www.bbb.com两个网站部署在同一台服务器上,两个域名解析到同一个IP地址,但是用户通过两个域名却可以打开两个完全不同的网站,互不影响,就像访问两个服务器一样,所以叫两个虚拟主机

  1. 正向代理

正向代理是一个位于用户A和原始server之间的代理server ,为了从原始server取得内容,用户A向代理server发送一个请求并指定目标(原始server)。然后代理server向原始server转交请求并将获得的内容返回给用户,用户必须进行一些特别的设置才能使用正向代理

用途:在防火墙内的局域网用户提供访问Internet的途径。还能够使用缓冲特性降低网络使用率

从安全角度讲:

正向代理同意用户通过它访问任意站点而且隐藏用户自身,因此你必须采取安全措施以确保仅为经过授权的用户提供服务。

正反向代理对外都是透明的。访问者并不知道自己访问的是一个代理。

  1. Nginx vs Apache

image.png

image.png

Nginx环境

image.png

image.png

Nginx渗透

文件解析漏洞

漏洞简介:

​ 对于任意文件名,在后面添加 /xxx.php(xxx为任意字符)后,即可将文件作为php解析

漏洞范围:

​ 该漏洞是nginx配置所导致,与版本无关。

漏洞复现:

image.png

1.jpg后面加上 /xxx.php ,会将 1.jpg 以PHP解析

注意下图中的访问链接

image.png

image.png

该漏洞是Nginx配置所导致,与Nginx版本无关,下面是常见的漏洞配置:

image.png

1
2
3
4
5
6
7
8
9
10
ini复制代码server { 
location ~ \.php$ {
root /work/www/test;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME
$document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_pass unix:/tmp/php-fpm.sock;
}
}

当攻击者访问 /1.jpg/xxx.php 时,Nginx将查看URL,看到它以 .php 结尾,并将路径传递给 PHP fastcgi 处理程序.Nginx传给php的路径为 C:/phpStrudy/WWW/1.jpg/xxx.php ,在phpinf中可以查看 _SERVER[“ORIG_SCRIPT_FILENAME”] 得到

image.png

PHP根据URL映射,在服务器上寻找xxx.php文件,但是xxx.php不存在,又由于 cgi.fix_pathinfo 默认是开启的,因此PHP会继续检查路径中存在的文件,并将多余的部分当作 PATH_INFO 。接着PHP在文件系统中找到.jpg文件,而后以PHP的形式执行.jpg的内容,并将 /xxx.php 存储在 PATH_INFO 后丢弃,因此我们在phpinfo中的$_SERVER[‘PATH_INFO’] 看到的值为空(no value),如果继续添加后缀则蠢顺势显示后缀

image.png

最后整理一下思路:cgi.fix_pathinfo

php的一个选项:cgi.fix_pathinfo,该选项默认开启,值为1,用于修理路径的

例如:当php遇到文件路径 /yxc.jpg/xxx.php/yxc.sec 时,若 /yxc.jpg/xxx.php/yxc.sec 不存在,则会去掉最后的 /yxc.sec ,然后判断 /yxc.jpg/xxx.php 是否存在,若存在则将 /yxc.jpg/xxx.php 当作文件 /yxc.jpg/xxx.php/yxc.sec ,若 /yxc.jpg/xxx.php 仍不存在,则继续去掉 xxx.php ,以此类推

修复建议:

  1. 配置cgi.fix_pathinfo(php.ini中)为0,并重启php-cgi程序
  2. 如果需要使用到cgi.fix_pathinfo这个特性(例如:wordpress),那么可以禁止上传目录的执行脚本权限或将上传存储的内容与网站分离,即站库分离
  3. 高版本PHP提供了security.limit_extensions 这个配置参数,设置 security.limit_extensions = .php

目录遍历

Nginx 的目录遍历与 Apache一样,属于配置方面的问题,错误的配置可导致目录遍历与源码泄露

先去www目录下随便新建一个文件夹,然后进行访问

image.png

修改 C:\phpstudy\nginx\conf\nginx.conf,在下面标示位置中添加 autoindex on ;

image.png

重启nginx,再次访问即可

image.png

修复建议:on改为off即可

空字节任意代码执行漏洞

影响版本:

​ nginx 0.5.*

​ nginx 0.6.*

​ nginx 0.7 <- 0.7.65

​ nginx 0.8 <- 0.8.37

Nginx在遇到%00空字节时与后端FastCGI处理不一致,导致可以在图片中嵌入PHP代码然后通过访问 xxx.jpg%00.php 来执行其中的代码

复现环境:

​ nginx 0.7.65+php 5.3.2

在nginx-0.7.65/html/目录下创建 1.jpg,内容为:

1
php复制代码<?php phpinfo();?>

访问1.jpg,无法访问,所以在URL中输入 1.jpg..php

然后抓包,hex选项下,.的hex编码为2e,将第一个2e改为00

image.png

image.png

成功绕过

该漏洞不受 cgi.fix_pathinfo 影响,当其为0时,依然解析

高版本不存在该漏洞

CRLF注入漏洞

漏洞产生:

Nginx会将 $uri 进行编码,导致传入 %0a%0d 即可引入换行符,造成CRLF注入漏洞。

错误的配置文件原本的目的是为了让http的请求跳转到https上的,意思就是配置实现了强制跳转的功能,当用户访问nginx服务器时,由于此配置的存在会被强制跳转到以https协议访问之前访问的链接

image.png

  1. 配置中的 url是我们可以控制的,这样我们就可以在url 是我们可以控制的,这样我们就可以在 url是我们可以控制的,这样我们就可以在url 处填入CRLF ,然后对服务器进行访问实现头部注入
  2. 服务器会返回一个302跳转给用户

漏洞危害:

​ 劫持合法用户会话,利用管理员身份进行恶意操作,篡改页面内容、进一步渗透网站

​ 利用CRLF injection设置一个 SESSION ,造成一个 “会话固定漏洞”

原理:

​ CRLF是“回车 + 换行”(\r\n)的简称。在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF(使用payload %0a%0d%0a%0d进行测试)来取出HTTP内容并显示出来。所以,一旦我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie(www.xx.com%0a%0d%0a%0dSet-cookie:JSPSESSID%3Dxxx)或者HTML代码(http://www.xx.com…<img src=1 onerror=alert(“xss”)>),所以CRLF Injection又叫HTTP Response Splitting,简称HRS

Nginx会将$uri进行解码,导致传入%0a%0d即科引入换行符,造成CRLF注入漏洞

uri跳转HTTPS,uri跳转HTTPS,uri跳转HTTPS,uri就会产生%0a%0d换行符,换行符就一定存在CRLF注入漏洞

漏洞复现:

​ cd vulhub/nginx/insecure-configuration

​ docker-compose up -d

​ [http://127.0.0.1/%0ASet-cookie:JSPSESSID%3D36](http://127.0.0.1/ Set-cookie:JSPSESSID%3D36)

image.png

CRLF + XSS 配合:

1
perl复制代码%0D%0A%0D%0A%3Cimg%20src=1%20onerror=alert(/xss/)%3E

image.png

使用旧版浏览器即可弹窗

CRLF深入:

攻击者操作:

  1. 攻击者打开一个网站,然后服务器会回复他一个session id。比如SID=abcdefg。Attack把这个id记下了
  2. Attack给被攻击者发送一个电子邮件,他假装抽奖或者推销,诱导攻击者点击链接:http://unsafe/?SID=abcdefg,SID后面是Attack自己的session id
  3. 被攻击者被吸引后,点击了http://unsafe/?SID=abcdefg,像往常一样,输入了自己的账号和口令从而登录到银行网站
  4. 因为服务器的session id不改变,现在被攻击者点击后,他就拥有了被攻击者的身份,就可以为所欲为了

文件名逻辑漏洞(CVE-2013-4547)

影响版本:

​ nginx 0.8.41 - 1.4.3 / 1.5.0 - 1.5.7

​ 复现时需要文件名的后面存在空格,而windows是不允许存在此类文件的,因此复现采用vulhub

漏洞复现:

image.png

cd vulhub/nginx/CVE-2013-4547

docker-compose build

docker-compose up -d

docker ps

image.png

上传1.jpg

内容为:

1
php复制代码<?php phpinfo();?>

image.png

抓包在后缀名后面加个空格,成功绕过

修改名称为 2.jpg…php ,在hex编码下将jpg后面的两个2e改为20,00,

image.png

成功绕过

原理:

image.png

修复建议:

​ 升级版本

整数溢出 CVE-2017-7529

漏洞描述:

​ 在nginx的range filter 中存在整数溢出漏洞,可以通过带有特殊构造的range的HTTP头的恶意请求引发这个整数溢出漏洞,并导致信息泄露。

影响版本:

​ Nginx 0.5.6 - 1.13.2

危害程度:

​ 低

本文转载自: 掘金

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

Python matplotlib 绘制等高线图 前言 1

发表于 2021-11-16

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

前言

我们在往期对matplotlib.pyplot()方法学习,到现在我们已经会绘制折线图、柱状图、散点等常规的图表啦(往期的内容如下,大家可以方便查看往期内容)

  • matplotlib 模块概述:对matplotlib 模块常用方法进行汇总
  • matplotlib 模块底层原理:对matplotlib 模块脚本层、美工层及后端层如果工作进行学习
  • matplotlib 绘制折线图:折线图相关属性进行汇总
  • matplotlib 绘制柱状图:对柱状图相关属性进行汇总
  • matplotlib 绘制直方图:直方图相关属性进行汇总
  • matplotlib 绘制散点图:对散点图相关属性进行汇总

在matplotlib.pyplot 中除了可以绘制常规图表如折线、柱状、散点等,还可以绘制常用在地理上的平面展示地型的等高线图

等高线图.png

本期,我们将详细学习matplotlib 绘制等高线图相关属性的学习,let’s go~

  1. 等高线图概述

  • 什么是等高线图?

+ 等高线图又称为水平图,通过2D形式展示3D图像的图表
+ 等高线图又称为等高地线图,将地表高度相同的点连成一个环线展示到平面曲线上
+ 等高线图又称为Z切片图,因变量Z与自变量X,Y变化而变化
+ 等高线图可以分为首曲线、计曲线、间曲线与助曲线
  • 等高线图常用场景

+ 等高线图常用在展示某地地形情况
+ 等高线图也可以计算当地山地高低情况
+ 等高线图常用于地质、地理勘察绘制而成
+ 等高线图也可以用于绘制圆形、椭圆形等数学公式展示
  • 绘制等高线图步骤

1. 导入matplotlib.pyplot模块
2. 准备数据,可以使用numpy/pandas整理数据
3. 调用pyplot.contour()或者pyplot.contourf()绘制等高线
  • 案例展示

等高线图绘制需要借助很多高中所学的三角函数、指数函数等公式,我们本期案例使用等高线方法汇总圆

+ 案例数据准备


    - np.arrage()准备一系列连续的数据
    - np.meshgrid()将数据转换成矩阵
1
2
3
4
5
6
7
8
python复制代码import numpy as np
# 定义一组连续的数据

x_value = np.arange(-5,5,0.1)
y_value = np.arange(-5,5,0.1)

# 转换成矩阵数据
x,y = np.meshgrid(x_value,y_value)
+ 绘制等高线
1
2
3
4
5
6
7
8
9
10
11
12
python复制代码
import matplotlib.pyplot as plt

plt.contour(x,y,z)

plt.title("Display Contour")
plt.xlabel("x(m)")
plt.ylabel("y(m)")

plt.show()

plt.show()
![image.png](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/3fe88f208930c515aaca066621489cc31b122ec8b551477dc3c45ef378693874)
  1. 等高线图属性

  • 设置等高线颜色

+ 关键字:colors
+ 取值范围:
    - 表示颜色的英文单词:如红色"red"
    - 表示颜色单词的简称如:红色"r",黄色"y"
    - RGB格式:十六进制格式如"#88c999";(r,g,b)元组形式
    - 也可以传入颜色列表
  • 设置等高线透明度:

+ 关键字:alpha
+ 默认为1
+ 取值范围为:0~1
  • 设置等高线颜色级别

+ 关键字:cmap
+ colors和cmap两个关键字不能同时提供
+ 取值为:注册的颜色表明
    - 形式如:"颜色表\_r"
    - 常用的有:'Accent', 'Accent\_r', 'Blues', 'Blues\_r', 'BrBG', 'BrBG\_r', 'BuGn', 'BuGn\_r', 'BuPu', 'BuPu\_r', 'CMRmap', 'CMRmap\_r', 'Dark2', 'Dark2\_r', 'GnBu', 'GnBu\_r', 'Greens'
  • 设置等高线宽度

+ 关键字:linewidths
+ 默认为等高线宽度为1.5
+ 取值可以float类型或者列表
  • 设置等高线样式

+ 关键字:linestyles
+ 默认值为:solid
+ 取值可选:{*None*, 'solid', 'dashed', 'dashdot', 'dotted'}
+ linestyles为None且线条为单色时,负轮廓的线条会设置成dashed
  • 我们对上一节的等高线图添加一些属性
+ 线条为红色,线条宽度逐渐增大,线条样式为dashed,透明度设置为0.5



1
2
3
python复制代码 plt.contour(x,y,z,colors="r",
linestyles="dashed",
linewidths=np.arange(0.5,4,0.5),alpha=0.5)
![image.png](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/271235f125ebad42eb1e25800f03ed59b806f163175cb9624e83a2022fa444d5) + 传入colors列表
1
2
3
python复制代码plt.contour(x,y,z,
colors=('r','green','blue',(1,1,0),"#afeeee","0.5"),
linewidths=np.arange(0.5,4,0.5))
![image.png](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/9ab7c2b8abef28dcce02bc637e703793359c5fa9ab3e138d245956dbbf5bd016) + 为等高线图,设置cmap为红色系
1
2
3
4
5
6
7
python复制代码z = np.exp(-x**2-y**2)
z1 = np.exp(-(x-1)**2-(y-1)**2)
Z = (z-z1)*2

plt.contour(x,y,Z,
cmap='afmhot_r',
linewidths=np.arange(0.5,4,0.5))
![image.png](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/22f42e56c6db5d2b93cedb3c8b74563dd66038d7ff7bc96b12df6959aa9245ff)
  1. 显示轮廓标签

我们查看等高线图时,轮廓标签会辅助我们更好的查看图表。添加轮廓标签,我们需要借助clabe

  • pyplot.contour()绘制等高线方法,会返回QuadContourset
  • QuadContourset 包含level列表数据
  • 使用pyplot.clabel()接受level列表数据标注在等高线上
1
2
3
4
5
6
7
8
9
10
11
python复制代码
x_value = np.arange(-3,3,0.025)
y_value = np.arange(-3,3,0.025)

x,y = np.meshgrid(x_value,y_value)

z = (1-x**2+y**5)*np.exp(-x**2-y**2)

cs = plt.contour(x,y,z,cmap="Blues_r",linewidths=np.arange(0.5,4,0.5))

plt.clabel(cs,fontsize=9,inline=True)

image.png

  1. 填充颜色

通常在等高线图中,不同区域填充不一样的颜色,帮助我们查看图表时更好地理解

  • 使用pyplot.contourf()对比同区域轮廓进行填充颜色
1
2
3
4
5
6
7
python复制代码z = (1-x**2+y**5)*np.exp(-x**2-y**2)

cs = plt.contour(x,y,z,10,colors="b",linewidths=0.5)

plt.clabel(cs,fontsize=12,inline=True)

plt.contourf(x,y,z,10,cmap="Blues_r",alpha=0.75)

image.png

  1. 添加颜色条说明

我们可以借助pyplot.colorbar()方法来添加颜色条说明

1
2
3
4
5
6
7
8
9
10
11
python复制代码z = (x**2+y**5)*np.exp(-x**2-y**2)
z1 = np.exp(-(x-1)**2-(y-1)**2)
Z = (z-z1)*2

cs = plt.contour(x,y,Z,10,colors="black",linewidths=0.5)

plt.clabel(cs,fontsize=12,inline=True)

plt.contourf(x,y,Z,10,cmap="afmhot_r",alpha=0.5)

plt.colorbar(shrink=0.8)

image.png

总结

本期,对matplotlib.pyplot 绘制等高线方法contour和contourf相关属性的学习。在绘制等高线图时,我们需要对三角函数、指数函数、正余弦函数等知识有一点了解,才能绘制出想要的图表

学习本节过程中,高中的数学知识都还给老师,摸摸头,头发怎么又掉了😱

以上是本期内容,欢迎大佬们点赞评论,下期见~

本文转载自: 掘金

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

linux命令入门之必备宝典

发表于 2021-11-16

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

常用命令汇总

1.mkdir make diretory 创建一个新的目录(空目录)

1
2
csharp复制代码mkdir /data    <--在根目录下面创建一个data空目录
-p <--实现创建多级目录

2.ls list 列表文件或目录信息

1
2
3
4
5
csharp复制代码ls /data                <-- 查看data目录下面的数据信息
ls -l /data <-- -l 参数表示查看文件目录详细信息
ls -d /data <-- 只查看当前指定的目录信息 (d是diretory目录的意思)
ls -dl /data <-- 只查看当前制定的目录详细信息
ls -a <-- 显示目录中所有隐藏信息

3.cd change diretory 切换当前所在路径信息

1
2
csharp复制代码cd /data                  <--切换根目录下面的data目录中
cd .. <--返回上一级目录

4.pwd print working diretort 显示当前所在路径信息

5.touch 创建文件 修改文件时间信息

1
2
csharp复制代码touch   fu.txt            <--相对路径方式创建文件
touch /data/fu.txt <--绝对路径方式创建文件

6.vi /vim 编辑文件内容命令

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
csharp复制代码进入命令模式/编辑模式
vi /vim fu.txt
编辑模式,需要保存时
先按esc ---> 再按:wq!,即可保存并退出编辑模式
编辑模式,不保存直接退出
先按esc ---> 再按:q,即可退出编辑模式

编辑模式常用参数介绍
i <---进入到插入编辑模式 从光标所在位置进行编译

I <---从当前行的行首进行编辑
G/shift+g 快速切换到文件最后一行
gg 返回到文件第一行
3gg 快速切换到文件第三行
dd <--- 剪切一行内容
3dd <--- 剪切三行内容
yy <--- 复制一行内容
3yy <--- 复制三行内容
p <--- 表示剪切或者复制的内容
3p <--- 表示粘贴三次
u <--- 进行编辑还原操作(前提在没有保存的前提下)
o <--- 从当前光标所在行的下一行进行编辑
O <--- 从当前光标所在行的上一行进行编辑
wq <--- 保存文件内容并退出编辑模式
q <--- 不保存退出
wq! <--- 强制保存文件内容并退出编辑模式
q! <--- 强制退出不保存
:set nu(number) 显示行号设置
:set nonu(no number) 取消行号显示设置
:set list 显示问价中的结尾符号
vi <--- windows系统自带的记事本功能
vim <--- nodepad++ emeditor sublime text (专业的文本编译软件)

7.echo 将输入的内容显示在屏幕上

1
2
3
csharp复制代码echo "hello world" >/data/fu.txt  ---  将编写的内容放入一个文件中
-n <--- 表示输出的信息结束,不会进行换行处理
-e <--- 识别正则符号 识别一些特殊符号 /n(换行) /t(空格)

8.cat <— 查看显示文件内容信息

1
2
3
4
5
6
7
csharp复制代码cat fu.txt         <--- 查看fu.txt文件中的内容
cat >fu.txt<<EOF
01.fu
02.fu
EOF <--- 一次性编辑多行内容信息
-n <--- 显示文件的行号内容
-A <--- 显示文件中的一些特殊标记符号($)

9.cp 复制文件或备份文件信息

1
2
csharp复制代码-r    ---  递归复制目录数据信息
-a --- 归档参数,包含了-r -d -p参数的作用

10.mv 移动或剪切数据命令

  1. rm 移除数据信息(目录或文件)

1
2
csharp复制代码-r     --- 递归删除目录中的数据信息
-f --- 强制删除数据信息,不要再出现询问提示信息
  1. find 查找数据信息命令 == everthing软件类似

1
2
3
4
csharp复制代码-type  f        --指定查找文件类型 f表示文件 d表示目录
-name 数据名称 -- 指定查找的数据名称
-exec --将find命令找出的结果交给-exec后面的命令进行处理
说明: 不指定查找路径,表示从当前路径查找数据

13.which 查找命令文件绝对路径信息

14.mount <— 挂载设备文件命令

1
csharp复制代码 mount    设备文件信息   挂载目录(挂载点)

15.tree <— 查看目录结构信息命令

1
csharp复制代码-L       <--- 查看一级目录层级

16.grpe <— 表示过滤文件数据信息命令(awk三剑客老三)

1
2
3
4
5
6
7
csharp复制代码-v         表示排除匹配到的文件信息
-A 表示等价于after意思,取出过滤内容之后几行信息
-B 表示等价于before意思,取出过滤内容之前几行信息
-C 表示center意思,取出过滤内容上下几行信息
-I 表示搜索过滤的内容不区分大小写
-E 可以识别扩展正则信息
-0 可以显示命令匹配的过程(只显示匹配的信息,按行显示)

sed <— 表示对文件中的行进行处理操作(三剑客老二)

1
2
3
4
5
6
7
8
9
csharp复制代码               可以编辑修改文件
-p 表示显示搜索出来的信息内容
-n 取消默认操作
-d 排除指定要过滤出来的字符信息
-r 读取扩展正则表达式的方式
sed -n '20,30p' 文件信息 <-- 表示取出指定文件20到30行的内容
-i 替换一个文件中内容信息
-s 搜索到要替换的文件信息
-g 全局搜索要替换的文件信息

awk <– 表示多文件中的列进行处理操作(三剑客老大)

1
2
csharp复制代码MR==20    指定所取出的行号信息
-F 指定分割符

17.head <– 显示文件前几行信息内容,默认显示前10行信息

1
csharp复制代码head -5  == head -n5 显示文件前5行内容

18.tail <– 显示文件后几行信息内容,默认显示后10行信息

1
csharp复制代码tail -5  == tail -n5 显示文件后5行内容

19.alias <– 显示或设置别名功能

1
2
csharp复制代码-p         <-- 查看别名信息
修改配置 <-- vim ~/.bashrc 或 vim /etc/bashrc

20.source <– 加载系统配置文件的 /etc/profile

21.seq <– 显示数字序列信息

1
csharp复制代码seq 30      <--  显示出1到30行数字信息

22.unmae <– 查看系统信息

1
2
3
csharp复制代码-r           <--  查看内核信息
-m <-- 查看系统架构信息
-a <-- 查看系统所有信息

23.su - <– 表示切换用户身份命令

1
2
csharp复制代码su           <--从root切换用户,切换后用户的路径还在root用户路径下
su - <-- 默认不指定切换用户信息,表示直接切换为root身份

24.mount <– 挂载命令

1
csharp复制代码mount  要挂载什么  挂载到什么位置(目录-挂载点)

25.umount <– 卸载命令

1
csharp复制代码umount       挂载点信息(门)
  1. df <– 查看磁盘使用情况:查看设备挂载情况

1
csharp复制代码-h           <--  以人类可读的方式显示输出信息

27.date <– 显示或修改时间信息

1
2
3
csharp复制代码date -s  要修改的时间
date输出格式
%F %T man date

28.xarge <– 将等到信息内容按行显示输出,默认利用空格做为分隔符

1
2
3
csharp复制代码-n2         <-- 指定一行只显示2个字符信息
-i <-- 将得到的信息按行放入到命令后面的{}中
-I <-- 将得到的信息复制给参数构面的{},然后在用相应的命令调用{}

29.init <– 设置系统运行级别

1
csharp复制代码 init  4
  1. runlevel <– 查看当前系统运行级别,以及查看系统修改前运行级别

31.chkconfig <– 查看或者配置系统服务是否会自启动

1
2
csharp复制代码chkconfig     [--level 运行级别] 服务名称 on/off
chkconfig -- list 服务名称 <-- 只查看指定服务的运行级别状态

32.ps -ef <– 查看系统中的进程信息

33.wc <– 统计文件系统命令

1
csharp复制代码wc  -l       <-- 统计文件行数
  1. tar <– 压缩和解压数据信息命令

1
2
3
csharp复制代码-z      采用gzip方式进行压缩
-j 采用bzip方式进行压缩
-c 创建压缩包信息

35.cut <– 切割一行字符串信息

1
2
3
4
csharp复制代码-d ""   <-- 指定利用什么字符进行切割列信息
-f3,5 <-- 取出切割后的第几列信息
逗号分隔数字信息时,表示取出第三列和第五列
-f3-5 <-- 短横线分割数字信息时,表示取出第三列到第五列

36.rpm <– 主要用管理系统软件包

1
2
3
csharp复制代码-q       <-- 查询软件包命令
-a <-- 整个系统进行查询指定的软件包是否存在
-l <-- 列出软件包中安装的数据信息

37.yum <– 安装软件包命令

1
2
3
csharp复制代码reinstall  <-- 进行重复安装
install <-- 安装软件参数
-y <-- 不要提示确认信息

38.sz -y/rz -y <– 上传和下载数据文件命令

39.lsof <– 查看系统中的文件是否被相应进程调用

1
2
3
csharp复制代码[root@fu ~]# lsof |head -2
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
init 1 root cwd DIR 8,3 4096 2 /

40.du <– 查看文件或目录占用了多少磁盘空间

41.stat <– 查看文件属性信息(事件信息 文件大小 权限)

42.tr <– 一个替换字符信息的命令(类似sed)

43.ntpdate <– 同步时间命令

1
csharp复制代码ntpdate      <-- 时间服务器域名信息

44.du <– 查看文件或目录占用了多少磁盘空间

45.less/more 查看文件信息

46.stat <– 查看文件属性信息(时间信息 文件大小 权限)

46.tr <– 一个替换字符信息的命令(类似sed)

1
csharp复制代码    按照单个字符逐一替换

监控系统信息的命令

1
2
3
4
5
csharp复制代码01. uptime         查看负载信息情况命令
02. top 查看系统运行情况(实时监控)
03. w 查看系统用户登录信息
04. lscpu 查看cpu信息命令
05. free -m 查看内存信息

常用命令帮助方法

1
2
3
4
5
6
csharp复制代码man           manual 查看命令手册信息,获取命令使用方法
man touch <--- 查看touch命令的使用方法
/-r <--- 进行搜索指定参数信息
n <--- 向下搜索
N <--- 向上搜索
q <--- 退出man手册模式

常用快捷方式

1
2
3
4
5
6
7
8
csharp复制代码ctrl + l(clear)      <--- 表示清屏操作
ctrl + d <--- 表示退出/断开当前连接或者当前登录状态(用户退出当前登录状态)
ctrl + c <--- 表示操作终端或终止
tab <--- 补全命令快捷方式
按一下,表示把命令尽可能补全
按两下(连续),表示将所有相类似的命令都显示出来
方向键 上 下 <---- 调取已经输入过的历史命令信息
ctrl + r <----进入到命令行搜索模式,可以搜索历史命令

常用符号信息

将一个内容信息放入到指定文件中
<< 打开一个文件后,将指定信息放入到文件中

扩展常用符号详解:

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
csharp复制代码
> 或 1 标准输出(正确的内容)重定向符号(慎用)
会覆盖原有文件内容,进行添加新的信息
>>或 1>> 标准输出追加重定向符号
把前面输出的东西输入到后边的文件中,不会清除文件原有内容,只是追加到文件最后一行

< 标准输入重定向
<< 标准是输入追加重定向

* 匹配所有信息 (正则表达式)

| 管道符号 管道前面的命令输出结果 交给管道后面的命令进行处理

`` 反引号 先执行反引号里面的命令,将结果交给外面的命令进行处理

$() 先执行$()里面的命令,将结果交给外面的命令进行处理

.. <--- 表示上一级目录信息
cd .. 返回到上级目录
cd /../../ 返回到上上上级目录

. <--- 表示当前目录信息
以点开始的文件都是隐藏文件
! <--- 取反符号
# <--- 在一行的信息开头出现,表示注释掉配置功能
\ <--- 还原字符信息本来意思 转译符号
1
2
3
4
5
6
csharp复制代码/var/log/messages      <-- 系统默认的日志信息记录文件
/var/log/secure <-- 用户登录信息记录文件
/etc/fstab <-- 开机自动挂载列表,开机设备对应接口
/etc/hosts <-- 用于已知域名主机名获取ip地址与域名对应关系
/etc/profile <-- 设置环境变量或别名信息 source==./etc/profile
/etc/spool/cron/root <-- 定时任务的配置文件

企业案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码方法一:
echo /空文件 >/data/fu.txt
#用来清空文件内容比较多的文件
方法二:
ca /空文件 >/data/fu.txt
#利用错误命令来清空内容较多的文件
方法三:
>/data/fu.txt
#利用空命令来清除内容比较多的文件
2>: 错误重定向
把错误信息输入到后面的文件夹中,会删除文件夹原有内容
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
csharp复制代码01. 文件已经存在错误
[root@shhaioldboy02-LNB data]# mkdir /data
mkdir: cannot create directory `/data': File exists

02.命令不能被找到
[root@shhaioldboy02-LNB data]# mkdi
-bash: mkdi: command not found
03.没有你要找的文件或者目录
[root@shhaioldboy02-LNB ~]# cat /data/alex.txt
cat: /data/alex.txt: No such file or directory
[root@shhaioldboy02-LNB ~]# cd /data01
-bash: cd: /data01: No such file or directory

04.vim/vi命令只能操作文件,不能编辑目录
[root@shhaioldboy02-LNB ~]# vim /data
"/data" is a directory
05. 编辑的文件所在的目录必须要存在
[root@shhaioldboy02-LNB ~]# vim /data1/oldboy.txt ---- 目录data1是不存在的
"/data1/oldboy.txt" E212: Can't open file for writing

06.head和tail命令结合用$()[root@shhaioldboy02-LNB ~]# tail -11 $(head -30 /root/data/ett.txt|tail -11)
tail: option used in invalid context -- 1
[root@shhaioldboy02-LNB ~]# tail -11 `head -30 /root/data/ett.txt|tail -11`
tail: option used in invalid context -- 1

07. 当前登录系统的用户权限不够,操作被拒绝
[oldboy@shhaioldboy02-LNB ~]$ mv /etc/profile /tmp/
mv: cannot move `/etc/profile' to `/tmp/profile': Permission denied

本文转载自: 掘金

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

Java 常见流对象——文件字节流(缓存)

发表于 2021-11-16

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

加缓存之 byte[]

前文说到,要改进 FileInputStream 的读写效率。所以,需要增加一个“袋子”。而这里就可以用,byte[] 来充当袋子的作用。

  • 在 while 的前面加缓存,缓冲区的长度一定是 2 的整数幂,一般用 1024:
1
Java复制代码byte[] buff = new byte[1024];
  • while 里面微调:
1
2
3
4
Java复制代码while ((temp = file.read(buff))!=-1){
System.out.print(temp+" ");
fos.write(buff,0,temp);
}

首先,是 .read() 方法里多了一个缓存区参数,源码如下图:
image.png

其次,是 .write() 方法里的参数需要调整,源码如下图:
image.png
这里我试了一下,图片里的两种写法都是 ok 的,都可以跑出结果。

老师(尚学堂300集Java 后半部分换了另一个老师😭)用了一个较大的图片,然后用肉眼观察这个复制过程,控制台的红色矩形消失的时长。

我心想这样做还非得找个大图片吗?不行,我太懒了。而且,程序员当然要有理性的 量化思维 来算算,这种方式到底比前一种 好在哪里、好了多少。

所以那就让我们用高淇老师之前对比过 StringBuilder 和 StringBuffer 效率的计时方法(String 和 StringBuilder 效率对比),来进行一个简单的计时对比。

把上次的代码分别包装到:noneBuffer 函数 和 withBuffer 函数 里。然后前后记内存、计时,再相减。

上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Java复制代码public static void main(String[] args) {

long num_start = Runtime.getRuntime().freeMemory(); // now memory
long time_start = System.currentTimeMillis(); // now time
noneBuffer();
long num_end = Runtime.getRuntime().freeMemory();
long time_end = System.currentTimeMillis();
System.out.println("noneBuffer 占用内存: " + (num_start-num_end));
System.out.println("noneBuffer 占用时间: " + (time_end-time_start));

System.out.println("-------------------------------\n");
long num_start2 = Runtime.getRuntime().freeMemory(); // now memory
long time_start2 = System.currentTimeMillis(); // now time
withBuffer();
long num_end2 = Runtime.getRuntime().freeMemory();
long time_end2 = System.currentTimeMillis();
System.out.println("withBuffer 占用内存: " + (num_start2-num_end2));
System.out.println("withBuffer 占用时间: " + (time_end2-time_start2));

}

运行结果:

image.png

我在 while 里还是保留了 打印 temp 的操作。所以从运行结果的图片也可以很明显的看出来,两次读的时候,每次的 temp 是不一样的。具体对比情况见下表:

比较对象 noneBuffer withBuffer
temp 挨个字节 1024 一包一起
占用内存 2602560 0
占用时间 174 2

可以发现,有缓存区(withBuffer)的情况下比不加缓存区的效率简直完爆,占用的内存和时间都是个位数!

加缓存之 available()

前面那种方法,我们的 buffer 长度是写死的。现在,试试另一种可以相对「智能」地读到这个是什么。也就是 file.available(),源码如下:
image.png
可以看到,返回的是一个在不堵的情况下允许一次性读取的最大长度,但是不堵的情况也有可能会变得堵,比如在网络差的情况下读大文件就会卡。

我已经迫不及待地好奇,这个和上一个技术哪家强了。在稍微更改代码之后,得到以下结果:
iamge.png

withAvailble 函数 的核心代码:

1
2
3
4
5
6
7
8
9
10
11
Java复制代码file = new FileInputStream("/Users/fang/Images/题目.png");
fos = new FileOutputStream("/Users/fang/Images/题目 output3.png");
int temp = 0;
// 创建一个缓冲区,提高读写效率
System.out.println("file.available():" + file.available());
byte[] buff = new byte[file.available()];
file.read(buff);
fos.write(buff);

// 从内存调一下
fos.flush();

下面这条语句,在计时的时候注释掉了。这句是因为我比较好奇,读出来的是什么:
image.png

带这条语句的运行结果:
image.png
发现这个值和前面 1024 的总和一样!可不是嘛,图片是一样的图片,字节的总长度当然一样啦。

下一次再试试缓冲流的威力 ~

本文转载自: 掘金

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

SQL-相关子查询和非相关子查询

发表于 2021-11-16

SQL子查询可以分为相关子查询和非相关子查询两类。

假设Books表如下:

类编号 图书名 出版社 价格

2 c#高级应用 圣通出版 23.00

2 Jsp开发应用 机械出版社 45.00

3 高等数学 济南出版社 25.00

3 疯狂英语 清华大学出版社 32.00

非相关子查询的执行不依赖与外部的查询。

执行过程:

(1)执行子查询,其结果不被显示,而是传递给外部查询,作为外部查询的条件使用。

(2)执行外部查询,并显示整个结果。  

非相关子查询一般可以分为:返回单值的子查询和返回一个列表的子查询,

举例说明:

1.返回单值: 查询所有价格高于平均价格的图书名,作者,出版社和价格。

1
2
3
4
5
6
7
sql复制代码SElECT 图书名,作者,出版社,价格
FROM Books
WHERE 价格 >
(
SELECT AVG(价格)
FROM Books
)

2.返回值列表:查询所有借阅图书的读者信息

1
2
3
4
5
6
7
sql复制代码SElECT *
FROM Readers
WHERE 读者编号 IN
(
SELECT 读者编号
FROM [Borrow History]
)

相关子查询的执行依赖于外部查询。多数情况下是子查询的WHERE子句中引用了外部查询的表。

执行过程:

(1)从外层查询中取出一个元组,将元组相关列的值传给内层查询。

(2)执行内层查询,得到子查询操作的值。

(3)外查询根据子查询返回的结果或结果集得到满足条件的行。

(4)然后外层查询取出下一个元组重复做步骤1-3,直到外层的元组全部处理完毕。   

举例说明:

查询Booka表中大于该类图书价格平均值的图书信息SElECT 图书名,出版社,类编号,价格

1
2
3
4
5
6
7
vbnet复制代码SELECT FROM Books As a
WHERE 价格 >
(
SELECT AVG(价格)
FROM Books AS b
WHERE a.类编号=b.类编号
)

与前面介绍过的子查询不同,相关子查询无法独立于外部查询而得到解决。该子查询需要一个“类编号”的值。而这个值是个变量,随SQLSever检索Books表中的不同行而改变。下面详细说明该查询执行过程:

先将Books表中的第一条记录的“类编号”的值“2”代入子查询中,子查询变为:

SELECT AVG(价格)

FROM Books AS b

WHERE b.类编号=2

子查询的结果为该类图书的平均价格,所以外部查询变为:

SElECT 图书名,出版社,类编号,价格

FROM Books As a

WHERE 价格 > 34

如果WHERE条件为True,则第一条结果包括在结果集中,则否不包括。对Books表中的所有行运行相同的过程,最后形成的结果集及最后返回结果。

总结

非相关子查询是独立于外部查询的子查询,子查询总共执行一次,执行完毕后将值传递给外部查询。

相关子查询的执行依赖于外部查询的数据,外部查询执行一行,子查询就执行一次。

故非相关子查询比相关子查询效率高。

来源: blog.csdn.net/shiyong1949…

本文转载自: 掘金

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

SpringBoot集成OAuth20的四种授权方式

发表于 2021-11-16

背景

OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。 OAuth2.0 是OAuth协议的延续版本,但不向后兼容 OAuth 1.0 ,即完全废止了 OAuth1.0 。很多大公司,国外的如Google,Netflix,Microsoft等,国内的像ByteDance,Alibaba,Tencent等都提供了OAuth认证服务(开放平台),这些都足以说明OAuth标准逐渐成为开放资源授权的标准。

该代码仓库主要结合 SpringBoot 、 SpringSecurity 、 OAuth2.0 等技术实现了 OAuth2.0 的四种授权方式的Demo,内容如下:

  • OAuth2. 0四种授权方式(客户端信息存储在内存)
    • 授权码模式,源码在master分支:github.com/heartsuit/d…
    • 简化模式:源码在implicit分支:github.com/heartsuit/d…
    • 密码模式:源码在password分支:github.com/heartsuit/d…
    • 客户端模式:源码在client分支:github.com/heartsuit/d…
  • OAuth2. 0客户端信息存储在MySQL数据库,access_token存储在内存、MySQL、Redis,源码路径:github.com/heartsuit/d…
  • OAuth2. 0客户端信息存储在MySQL数据库,access_token存储在JWT,源码路径:github.com/heartsuit/d…

四种授权方式

模式 名称 是否支持refresh_token 是否需要结合浏览器测试
authorization_code 授权码模式 支持 是
implicit 简化模式 不支持 是
password 密码模式 支持 否
client_credentials 客户端模式 不支持 否

authorization_code 授权码模式 需结合浏览器测试

GET http://localhost:9000/oauth/authorize?client_id=client&response_type=code

  1. 获取授权码,会回调至目标地址

http://localhost:9000/oauth/authorize?client_id=client&response_type=code

  1. 认证之后,点击允许,获取授到权码
  2. 通过授权码向授权服务获取令牌:access_token

http://client:secret@localhost:9000/oauth/token

将上一步获取的授权码作为x-www-form-urlencoded的一个参数:

grant_type:authorization_code
code:wgs8XF

获取到access_token

1
2
3
4
5
6
json复制代码{
"access_token": "d21a37e3-d423-4419-bb41-5712e23aee6b",
"token_type": "bearer",
"expires_in": 43200,
"scope": "app"
}
  1. 后续对资源服务的请求,附带access_token即可

http://localhost:8000/private/hi?access_token=d69bc334-3d5c-4c86-972c-11826f44c4af

2021-01-11-RequestCode.png
2021-01-11-TokenCode

源码在master分支:github.com/heartsuit/d…

implicit 简化模式 不支持refresh token 需结合浏览器测试

GET http://localhost:9000/oauth/authorize?grant_type=implicit&response_type=token&scope=read&client_id=client0&client_secret=secret0

2021-01-11-RequestImplicit.png
2021-01-11-TokenImplicit.png

源码在implicit分支:github.com/heartsuit/d…

password 密码模式

POST http://localhost:9000/oauth/token?username=admin&password=123456&grant_type=password&scope=read&client_id=client1&client_secret=secret1
2021-01-11-RequestPassword.png
2021-01-11-TokenPassword.png

源码在password分支:github.com/heartsuit/d…

client_credentials 客户端模式 不支持refresh token

POST http://localhost:9000/oauth/token?grant_type=client_credentials&scope=read&client_id=client2&client_secret=secret2

2021-01-11-RequestClient.png
2021-01-11-TokenClient.png

源码在client分支:github.com/heartsuit/d…

四种方式的授权流程

以下流程来自:tools.ietf.org/html/rfc674…

authorization_code 授权码模式

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
scss复制代码 +----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)

Note: The lines illustrating steps (A), (B), and (C) are broken into
two parts as they pass through the user-agent.

1
less复制代码                 Figure 3: Authorization Code Flow

The flow illustrated in Figure 3 includes the following steps:

(A) The client initiates the flow by directing the resource owner’s user-agent to the authorization endpoint. The client includes its client identifier, requested scope, local state, and a redirection URI to which the authorization server will send the user-agent back once access is granted (or denied).

(B) The authorization server authenticates the resource owner (via the user-agent) and establishes whether the resource owner grants or denies the client’s access request.

(C) Assuming the resource owner grants access, the authorization server redirects the user-agent back to the client using the redirection URI provided earlier (in the request or during client registration). The redirection URI includes an authorization code and any local state provided by the client earlier.

(D) The client requests an access token from the authorization server’s token endpoint by including the authorization code received in the previous step. When making the request, the client authenticates with the authorization server. The client includes the redirection URI used to obtain the authorization code for verification.

(E) The authorization server authenticates the client, validates the authorization code, and ensures that the redirection URI received matches the URI used to redirect the client in step (C). If valid, the authorization server responds back with an access token and, optionally, a refresh token.

implicit 简化模式

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
sql复制代码 +----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI --->| |
| User- | | Authorization |
| Agent -|----(B)-- User authenticates -->| Server |
| | | |
| |<---(C)--- Redirection URI ----<| |
| | with Access Token +---------------+
| | in Fragment
| | +---------------+
| |----(D)--- Redirection URI ---->| Web-Hosted |
| | without Fragment | Client |
| | | Resource |
| (F) |<---(E)------- Script ---------<| |
| | +---------------+
+-|--------+
| |
(A) (G) Access Token
| |
^ v
+---------+
| |
| Client |
| |
+---------+

Note: The lines illustrating steps (A) and (B) are broken into two
parts as they pass through the user-agent.

1
css复制代码                   Figure 4: Implicit Grant Flow

The flow illustrated in Figure 4 includes the following steps:

(A) The client initiates the flow by directing the resource owner’s user-agent to the authorization endpoint. The client includes its client identifier, requested scope, local state, and a redirection URI to which the authorization server will send the user-agent back once access is granted (or denied).

(B) The authorization server authenticates the resource owner (via the user-agent) and establishes whether the resource owner grants or denies the client’s access request.

(C) Assuming the resource owner grants access, the authorization server redirects the user-agent back to the client using the redirection URI provided earlier. The redirection URI includes the access token in the URI fragment.

(D) The user-agent follows the redirection instructions by making a request to the web-hosted client resource (which does not include the fragment per [RFC2616]). The user-agent retains the fragment information locally.

(E) The web-hosted client resource returns a web page (typically an HTML document with an embedded script) capable of accessing the full redirection URI including the fragment retained by the user-agent, and extracting the access token (and other parameters) contained in the fragment.

(F) The user-agent executes the script provided by the web-hosted client resource locally, which extracts the access token.

(G) The user-agent passes the access token to the client.

password 密码模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码 +----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(A) Password Credentials
|
v
+---------+ +---------------+
| |>--(B)---- Resource Owner ------->| |
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(C)---- Access Token ---------<| |
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
Figure 5: Resource Owner Password Credentials Flow

The flow illustrated in Figure 5 includes the following steps:

(A) The resource owner provides the client with its username and password.

(B) The client requests an access token from the authorization server’s token endpoint by including the credentials received from the resource owner. When making the request, the client authenticates with the authorization server.

(C) The authorization server authenticates the client and validates the resource owner credentials, and if valid, issues an access token.

client_credentials 客户端模式

1
2
3
4
5
6
7
8
arduino复制代码 +---------+                                  +---------------+
| | | |
| |>--(A)- Client Authentication --->| Authorization |
| Client | | Server |
| |<--(B)---- Access Token ---------<| |
| | | |
+---------+ +---------------+
Figure 6: Client Credentials Flow

The flow illustrated in Figure 6 includes the following steps:

(A) The client authenticates with the authorization server and requests an access token from the token endpoint.

(B) The authorization server authenticates the client, and if valid, issues an access token.

Reference

  • oauth.net/2/
  • tools.ietf.org/html/rfc674…
  • www.ruanyifeng.com/blog/2019/0…

If you have any questions or any bugs are found, please feel free to contact me.

Your comments and suggestions are welcome!

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

本文转载自: 掘金

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

14-SpringSecurity:前后端分离项目中用户名与

发表于 2021-11-16

背景

登录认证几乎是所有互联网应用的必备功能,传统的用户名-密码认证方式依然流行,如何避免用户名、密码这类敏感信息在认证过程中被嗅探、破解?
2021-09-02-Chart.jpg
这里将传统的用户名、密码明文传输方式改为采用 RSA 的非对称加密算法密文传输,即使认证请求被网络抓包,只要私钥安全,则认证流程中的用户信息相对安全;

  1. 一般是生成RSA的密钥对之后,公钥存储在前端或后端(登录时每次请求后端返回公钥)进行加密,私钥存储在后端用于解密;
  2. 曾在实际的应用中看到过动态生成密钥对的做法,即公钥-私钥都是动态生成,每次请求都不一样,这与固定公钥-私钥的做法相比,性能上损耗较大,而在安全性上的收益并没有增加多少;因此这里采用固定密钥对的方式进行演示。

生成RSA密钥对

主要涉及三条命令:

1
2
3
4
5
6
7
8
bash复制代码# 生成RSA私钥
genrsa -out rsa_private_key.pem 1024

# 把RSA私钥转换成PKCS8格式
pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt

# 生成RSA公钥
rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
  • Windows操作系统: Win10

下载安装 OpenSSL :slproweb.com/products/Wi…

打开 openssl.exe 所在目录,我这里是: D:\Program Files\OpenSSL-Win64\bin ,运行exe,执行上述三行命令实现 RSA 密钥对生成:

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
bash复制代码OpenSSL> genrsa -out rsa_private_key.pem 1024
Generating RSA private key, 1024 bit long modulus (2 primes)
.....................................+++++
................+++++
e is 65537 (0x010001)

OpenSSL> pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL/KFpxZ2ZJq4/f8
1oM2LX/aX1llPL6SlFbk5pBw1ESuQDVrcA8T4grdrFoEY6T2mNQAMiuzRKfYkS1l
Qx1C+L0HruqOPhFwDL7rxrDQU+8g/trCv+DQoMAbIcteqgxLQrvMZs1OuJrK0XpG
p4Ca7Wxfuk8HUynjQ9fhXIjWzWTjAgMBAAECgYBMUAARNFszPF77RNqiGQOftOdt
ra+u8KofrTLk1FBSB7e6ycYr6bBuvGeg5dA0Sn7jFDTiWJF/69dQZdN/qC9Kb0OV
jRtXDCSMHe1oRlvDr8tZKn9h9UljJHXrIapXJi5Z1eNQ3DW8ltgJbx/DpQrsSTYJ
JiWWpwfb6e+ub09JEQJBAOt+DAxec2h1Gq43Fc/fJ6hUmVl0VI0d5WkeVHezhutE
gYj29gkHkQin5VIMbXtutB/083vUm+Fxqc5EXdxzYIsCQQDQfb+gNZgBzeNhF/j5
IdqW68PpSOmWj2z9sVvAktSS9VzTt46haBvnjzIbES+uzJXoW0LI0H1zDlbvbtRV
HQAJAkEAz+kQMBdvowjIzok5y7ZEqBxQ66aGQ7TiZ2Vsw+YPt0VbbBZF8IDqro61
KzRnsLNzekdkdK6oFWmptr+rcse2swJARN10QSfSqK3n7/cqHqgm+nivgku6FCgV
uQovI0Gcg1oWKjxUGU45AVhUFYqstFERJumV+pybAzj2UCnMarykeQJAAkXb5Z7A
sb7wmLCDMoyfzJCn54k1VDEvGVcrn4SiME53wEyGnrYkyg8R84hO7rHLOnwz0PtZ
iLWuHpqd2OovmA==
-----END PRIVATE KEY-----

OpenSSL> rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
writing RSA key
  • Linux操作系统:CentOS7

同样,执行上述三行命令实现 RSA 密钥对生成:
2021-09-02-RSALinux.jpgNote:

  1. 后续编码实现时,使用Windows上生成的秘钥进行演示;
  2. 公钥、私钥用的是下图中红色椭圆标注出来的内容。

2021-09-02-GererateRSA.jpg

1
2
3
bash复制代码公钥:MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/yhacWdmSauP3/NaDNi1/2l9ZZTy+kpRW5OaQcNRErkA1a3APE+IK3axaBGOk9pjUADIrs0Sn2JEtZUMdQvi9B67qjj4RcAy+68aw0FPvIP7awr/g0KDAGyHLXqoMS0K7zGbNTriaytF6RqeAmu1sX7pPB1Mp40PX4VyI1s1k4wIDAQAB

私钥:MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL/KFpxZ2ZJq4/f81oM2LX/aX1llPL6SlFbk5pBw1ESuQDVrcA8T4grdrFoEY6T2mNQAMiuzRKfYkS1lQx1C+L0HruqOPhFwDL7rxrDQU+8g/trCv+DQoMAbIcteqgxLQrvMZs1OuJrK0XpGp4Ca7Wxfuk8HUynjQ9fhXIjWzWTjAgMBAAECgYBMUAARNFszPF77RNqiGQOftOdtra+u8KofrTLk1FBSB7e6ycYr6bBuvGeg5dA0Sn7jFDTiWJF/69dQZdN/qC9Kb0OVjRtXDCSMHe1oRlvDr8tZKn9h9UljJHXrIapXJi5Z1eNQ3DW8ltgJbx/DpQrsSTYJJiWWpwfb6e+ub09JEQJBAOt+DAxec2h1Gq43Fc/fJ6hUmVl0VI0d5WkeVHezhutEgYj29gkHkQin5VIMbXtutB/083vUm+Fxqc5EXdxzYIsCQQDQfb+gNZgBzeNhF/j5IdqW68PpSOmWj2z9sVvAktSS9VzTt46haBvnjzIbES+uzJXoW0LI0H1zDlbvbtRVHQAJAkEAz+kQMBdvowjIzok5y7ZEqBxQ66aGQ7TiZ2Vsw+YPt0VbbBZF8IDqro61KzRnsLNzekdkdK6oFWmptr+rcse2swJARN10QSfSqK3n7/cqHqgm+nivgku6FCgVuQovI0Gcg1oWKjxUGU45AVhUFYqstFERJumV+pybAzj2UCnMarykeQJAAkXb5Z7Asb7wmLCDMoyfzJCn54k1VDEvGVcrn4SiME53wEyGnrYkyg8R84hO7rHLOnwz0PtZiLWuHpqd2OovmA==

后端服务

基于 SpringBoot , SpringSecurity 实现用户认证功能。

项目依赖

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

SpringSecurity配置

注意放行认证接口,否则报错:403。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/auth/login").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// turn off csrf, or will be 403 forbidden
.csrf().disable();
}
}

用户信息配置

为了集中焦点在本篇的用户名-密码加密传输上,避免引入其他复杂性,这里采用内存型用户信息来演示,关于从数据库中获取用户信息,可参考6-SpringSecurity:数据库存储用户信息。

1
2
3
4
5
6
7
java复制代码@Component
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return User.withUsername("dev").password(new BCryptPasswordEncoder().encode("123")).authorities("p1", "p2").build();
}
}

认证接口

这里将私钥配置在 applicaiton.yml 中。

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
java复制代码@RestController
@RequestMapping("auth")
@Slf4j
public class LoginController {
@Value("${rsa.private_key}")
private String privateKey;

private final AuthenticationManagerBuilder authenticationManagerBuilder;

public LoginController(AuthenticationManagerBuilder authenticationManagerBuilder) {
this.authenticationManagerBuilder = authenticationManagerBuilder;
}

@PostMapping("/login")
public String login(@RequestBody FormUser formUser, HttpServletRequest request) {
log.info("formUser encrypted: {}", formUser);

// 用户信息RSA私钥解密,方法一:自定义工具类:RSAEncrypt
// String username = RSAEncrypt.decrypt(formUser.getUsername(), privateKey);
// String password = RSAEncrypt.decrypt(formUser.getPassword(), privateKey);
// log.info("Userinfo decrypted: {}, {}", username, password);

// 用户信息RSA私钥解密,方法二:使用hutool中的工具类进行解密
RSA rsa = new RSA(privateKey, null);
String username = new String(rsa.decrypt(formUser.getUsername(), KeyType.PrivateKey));
String password = new String(rsa.decrypt(formUser.getPassword(), KeyType.PrivateKey));
log.info("Userinfo decrypted: {}, {}", username, password);

// 核验用户名密码
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("authentication: {}", authentication);

return SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString();
}
}

自定义工具类进行解密

1
2
3
4
5
xml复制代码<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>
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
java复制代码public class RSAEncrypt {
/**
* RSA公钥加密
* @param str 待加密字符串
* @param publicKey 公钥
* @return 密文
*/
public static String encrypt(String str, String publicKey) {
try {
//base64编码的公钥
byte[] decoded = Base64.decodeBase64(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
//RSA加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
return Base64.encodeBase64String(cipher.doFinal(str.getBytes("UTF-8")));
} catch (Exception e) {
throw new RuntimeException(e);
}
}

/**
* RSA私钥解密
* @param str 已加密字符串
* @param privateKey 私钥
* @return 明文
*/
public static String decrypt(String str, String privateKey) {
try {
//64位解码加密后的字符串
byte[] inputByte = Base64.decodeBase64(str.getBytes("UTF-8"));
//base64编码的私钥
byte[] decoded = Base64.decodeBase64(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
//RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
return new String(cipher.doFinal(inputByte));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

使用hutool中的工具类进行解密

1
2
3
4
5
xml复制代码<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.0.6</version>
</dependency>

前端工程

基于 Vue3.0 , axios 实现极简登录页面。

Note:

  1. 前提需要有 Node.js 环境,可使用 nvm 进行 Node.js 的多版本管理;可参考heartsuit.blog.csdn.net/article/det…
  2. npm install <package>默认会在依赖安装完成后将其写入package.json,因此安装依赖的命令都未附加save参数。
1
2
bash复制代码$ node -v
v12.16.1

安装vue-cli并创建项目

1
2
3
bash复制代码npm install -g @vue/cli
vue --version
vue create hello-world

刚开始的 package.json 依赖是这样:

1
2
3
4
json复制代码  "dependencies": {
"core-js": "^3.6.5",
"vue": "^3.0.0"
},

集成Axios

  • 安装依赖
1
bash复制代码npm install axios

此时, package.json 的依赖变为:

1
2
3
4
5
json复制代码  "dependencies": {
"axios": "^0.21.1",
"core-js": "^3.6.5",
"vue": "^3.0.0"
},
  • 按需引入

在需要使用axios的组件中引入 import axios from "axios";

集成jsencrypt

此时, package.json 的依赖变为:

1
2
3
4
5
6
json复制代码  "dependencies": {
"axios": "^0.21.1",
"core-js": "^3.6.5",
"jsencrypt": "^3.2.1",
"vue": "^3.0.0"
},
  • 按需引入

在需要使用JSEncrypt的组件中引入 import JSEncrypt from "jsencrypt";

最终的前端登录组件代码

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
vue复制代码<template>
<div>
<span>用户名</span><input type="text" v-model="user.username" />
<span>密码</span><input type="text" v-model="user.password" />
<input type="submit" v-on:click="login" value="登录" />
</div>
</template>
<script>
import { defineComponent } from "vue";
import axios from "axios";
import JSEncrypt from "jsencrypt";

export default defineComponent({
name: "RSADemo",
setup() {},
data() {
return {
user: { username: "dev", password: 123 },
publicKey: `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/yhacWdmSauP3/NaDNi1/2l9Z
ZTy+kpRW5OaQcNRErkA1a3APE+IK3axaBGOk9pjUADIrs0Sn2JEtZUMdQvi9B67q
jj4RcAy+68aw0FPvIP7awr/g0KDAGyHLXqoMS0K7zGbNTriaytF6RqeAmu1sX7pP
B1Mp40PX4VyI1s1k4wIDAQAB`,
};
},
mounted() {
this.login();
},
methods: {
login: function () {
let userinfo = {
username: this.encrypt(this.user.username),
password: this.encrypt(this.user.password),
};

axios.post("http://localhost:8000/auth/login", userinfo).then(
function (res) {
if (res.status == 200) {
console.log(res.data);
} else {
console.error(res);
}
},
function (res) {
console.error(res);
}
);
},
encrypt: function (str) {
let jsEncrypt = new JSEncrypt();
// 设置加密公钥,一般通过后端接口获取,这里写在前端代码中
jsEncrypt.setPublicKey(this.publicKey);
let encrypted = jsEncrypt.encrypt(str.toString());
return encrypted;
},
},
});
</script>

RSA加密传输效果

2021-09-02-AuthEncrypted.jpg

2021-09-02-AuthDecrypted.jpg

可能遇到的问题

  • 开发环境跨域

方法一:通过开发环境(生产环境可通过Nginx实现)的代理服务进行请求转发,新建 vue.config.js 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8000/',
changeOrigin: true,
ws: true,
secure: true,
pathRewrite: {
'^/api': ''
}
}
}
}
};

方法二:因为后端服务是我们自己开发的,所以可以在后端进行CORS配置,允许跨域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").
allowedOriginPatterns("*").
allowedMethods("*").
allowedHeaders("*").
allowCredentials(true).
exposedHeaders(HttpHeaders.SET_COOKIE).maxAge(3600L);
}
};
}
}

附:代码生成RSA密钥对

当然,除了使用Windows、Linux上的openssl工具生成密钥对之外,我们也可以使用代码来直接生成。

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.64</version>
</dependency>
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
java复制代码public class RSAEncrypt {
private static final KeyPair keyPair = genKeyPair() ;
private static org.bouncycastle.jce.provider.BouncyCastleProvider bouncyCastleProvider = null;

public static synchronized org.bouncycastle.jce.provider.BouncyCastleProvider getInstance() {
if (bouncyCastleProvider == null) {
bouncyCastleProvider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
}
return bouncyCastleProvider;
}

/**
* 随机生成密钥对
*/
public static KeyPair genKeyPair() {
try {
// Provider provider =new org.bouncycastle.jce.provider.BouncyCastleProvider();
// Security.addProvider(DEFAULT_PROVIDER);
SecureRandom random = new SecureRandom();
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", getInstance());
generator.initialize(1024,random);
return generator.generateKeyPair();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
/**
* 获取公钥字符串(base64字符串)
* @return
*/
public static String generateBase64PublicKey() {
PublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
return new String(Base64.encodeBase64(publicKey.getEncoded()));
}
/**
* 获取私钥字符串(base64字符串)
* @return
*/
public static String generateBase64PrivateKey() {
PrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 得到私钥字符串
return new String(Base64.encodeBase64((privateKey.getEncoded())));
}
...
}

Reference

  • Source Code: Github
  • blog.csdn.net/aexlinda/ar…

If you have any questions or any bugs are found, please feel free to contact me.

Your comments and suggestions are welcome!

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

本文转载自: 掘金

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

tkMybatis数据持久层框架入门

发表于 2021-11-16

gitee链接

Spring Boot版本:2.3.4.RELEASE

tkMybatis封装了底层SQL,使开发者在操作单表的情况下可以不考虑SQL的写法,通过Java函数接口操作数据库。

并且tkMybatis是基于Mybaits的,所以也能编写XML文件来执行复杂的SQL语句。

Maven依赖

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
xml复制代码<dependencies>
<!--springboot启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>

<!-- Spring-data-jpa依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>

<!--tkMybatis通用mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
</dependencies>

配置文件

application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
yml复制代码server:
port: 8888
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://myserverhost:3306/mytest?createDatabaseIfNotExist=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: admin
# 开启hibernate表自动更新
jpa:
hibernate:
ddl-auto: update
show-sql: true

配置文件中开启了hibernate自动建表方便测试,建表前检查下数据库有没有同名表,有则删除。

新建两个实体类

TbUser和TbProduct

1
2
3
4
5
6
7
8
9
markdown复制代码- src
- main
- java
- com
- cc
- model
- entity
TbProduct
TbUser

TbUser:

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
java复制代码package com.cc.model.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name = "tb_user")
public class TbUser implements Serializable {
@Id
@Column(name = "id", columnDefinition = "BIGINT(20) UNSIGNED NOT NULL auto_increment COMMENT '主键id'")
private Long id;

@Column(name = "username", columnDefinition = "VARCHAR(64) UNIQUE NOT NULL COMMENT '用户名,唯一'")
private String username;

@Column(name = "password", columnDefinition = "VARCHAR(64) NOT NULL COMMENT '密码'")
private String password;

private static final long serialVersionUID = 1L;

...
}

TbProduct:

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
java复制代码package com.cc.model.entity;

import java.math.BigDecimal;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Id;

@Entity
@Table(name = "tb_product")
public class TbProduct implements Serializable {
@Id
@Column(name = "id", columnDefinition = "BIGINT(20) UNSIGNED NOT NULL auto_increment COMMENT '主键id'")
private Long id;

@Column(name = "name", columnDefinition = "VARCHAR(64) UNIQUE NOT NULL COMMENT '用户名,唯一'")
private String name;

@Column(name = "price", columnDefinition = "DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '价格'")
private BigDecimal price;

@Column(name = "user_id", columnDefinition = "BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户id'")
private Long userId;

private static final long serialVersionUID = 1L;

...
}

创建操作user表和product表的dao类:

UserDao:

1
2
3
4
5
6
7
8
9
java复制代码package com.cc.dao;

import com.cc.model.entity.TbUser;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;

@Repository
public interface UserDao extends Mapper<TbUser> {
}

ProductDao:

1
2
3
4
5
6
7
8
9
java复制代码package com.cc.dao;

import com.cc.model.entity.TbProduct;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;

@Repository
public interface ProductDao extends Mapper<TbProduct> {
}

tkmybatis配置类

为了使dao类能被扫描到,还需要mybatis测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.cc.config;

import org.springframework.context.annotation.Configuration;
import tk.mybatis.spring.annotation.MapperScan;

/**
* MyBatis配置类
* @author cc
* @date 2021-07-09 14:38
*/
@Configuration
@MapperScan({"com.cc.dao"})
public class MyBatisConfig {
}

创建一个controller来测试

TestController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.cc.controller;

import com.cc.dao.ProductDao;
import com.cc.dao.UserDao;
import com.cc.model.entity.TbUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
private final UserDao userDao;
private final ProductDao productDao;

public TestController(UserDao userDao, ProductDao productDao) {
this.userDao = userDao;
this.productDao = productDao;
}
}

启动程序使其生效。

实战

实战的内容为:

  • 插入一个user数据
  • 更新一个user数据
  • 查询所有user数据
  • 查询指定名称的user
  • 插入几条product数据
  • 根据userId查询关联的所有product数据(连表查询)
  • 删除user数据

为了简单,接口的参数都是写死的。

插入一个user数据

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@GetMapping("/insertUser")
public void insertUser(@RequestBody TbUser user) {
user = new TbUser();
user.setId(1L);
user.setUsername("cc");
user.setPassword("1");

int r = userDao.insertSelective(user);
if (r <= 0) {
throw new RuntimeException("插入失败");
}
}

更新一个user数据

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@GetMapping("/updateUser")
public void updateUser(@RequestBody TbUser user) {
user = new TbUser();
user.setId(1L);
user.setUsername("cc");
user.setPassword("123456");

int r = userDao.updateByPrimaryKeySelective(user);
if (r <= 0) {
throw new RuntimeException("更新失败");
}
}

查询所有user数据

1
2
3
4
5
6
7
8
9
java复制代码@GetMapping("/selectAllUser")
public List<TbUser> selectAllUser(@RequestBody TbUser user) {
user = new TbUser();
user.setId(1L);
user.setUsername("cc");
user.setPassword("123456");

return userDao.selectAll();
}

查询指定id/名称的user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@GetMapping("/selectUserById")
public TbUser selectUserById(@RequestBody TbUser user) {
return userDao.selectByPrimaryKey(1L);
}

@GetMapping("/selectUserByUsername")
public TbUser selectUserByUsername(@RequestBody TbUser user) {
user = new TbUser();
user.setUsername("cc");

Example example = new Example(TbUser.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("username", user.getUsername());

return userDao.selectOneByExample(example);
}

插入几条product数据

1
2
3
4
5
6
7
8
9
java复制代码@GetMapping("/insertSomeProduct")
public void insertSomeProduct() {
for (long i = 0; i < 5; i++) {
TbProduct product = new TbProduct();
product.setId(i);
product.setName("test" + i);
productDao.insertSelective(product);
}
}

根据userId查询关联的所有product数据(连表查询)

因为dao只能操作单表,所以我们需要编写xml来实现连表操作。

首先要在配置文件文件中添加包扫描:

application.yml

1
2
3
4
5
6
java复制代码## dao.xml的路径配置
mybatis:
mapper-locations:
- classpath:dao/*.xml # resource下的dao
configuration:
map-underscore-to-camel-case: true # 下划线自动转驼峰

然后在resources下创建dao文件夹,并且新建product.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cc.dao.ProductDao">
<select id="selectByUserId" resultType="com.cc.model.entity.TbProduct">
SELECT
*
FROM
tb_product AS p
LEFT JOIN tb_user u ON u.id = p.user_id
WHERE
u.id = #{userId}
</select>
</mapper>

在ProductDao下新增函数:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.cc.dao;

import com.cc.model.entity.TbProduct;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;

import java.util.List;

@Repository
public interface ProductDao extends Mapper<TbProduct> {
List<TbProduct> selectByUserId(Long userId);
}

然后编写接口:

1
2
3
4
java复制代码@GetMapping("/selectProductByUserId")
public List<TbProduct> selectProductByUserId() {
return productDao.selectByUserId(1L);
}

删除user数据

1
2
3
4
5
6
7
java复制代码@GetMapping("/deleteUser")
public void deleteUser() {
int r = userDao.deleteByPrimaryKey(1L);
if (r <= 0) {
throw new RuntimeException("删除用户失败");
}
}

例子虽然不多,但是已经足够了解到,借助tkMybatis框架,我们操作单表的时候不用再写SQL语句,可以省略许多的代码,我们不用再为每一个实体类编写增删改查语句,仅仅只要新建一个映射实体类的Dao,不过几行通用代码而已。

还有当单表操作不满足于我们需求的时候,可以自行编写SQL语句并应用到XML下,真正做到按需开发,能省则省。

本文转载自: 掘金

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

1…324325326…956

开发者博客

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