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

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


  • 首页

  • 归档

  • 搜索

【SpringCloudGateway】自定义日志过滤器:获

发表于 2021-08-04

在接触了SpringCloudGateway时,需要给路由添加一个日志记录的过滤器,难点出现在获取RequestBody上。

在阅读源码时发现了全局过滤器AdaptCachedBodyGlobalFilter,可以将RequestBody缓存到exchange中。

它的执行优先级非常高(注释编号:A),利用这一点,我们可以让这个全局网关过滤器工作,缓存requestBody,然后在自定义过滤器中读取requestBody,进行日志记录:
先来看AdaptCachedBodyGlobalFilter的源码(版本:3.0.3.RELEASE):

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
java复制代码public class AdaptCachedBodyGlobalFilter implements GlobalFilter, Ordered, ApplicationListener<EnableBodyCachingEvent> {
private ConcurrentMap<String, Boolean> routesToCache = new ConcurrentHashMap<>();
@Override
public void onApplicationEvent(EnableBodyCachingEvent event) {
//编号C. 在接收到EnableBodyCachingEvent事件时 记录需要缓存requestBody的路由。
this.routesToCache.putIfAbsent(event.getRouteId(), true);
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// the cached ServerHttpRequest is used when the ServerWebExchange can not be
// mutated, for example, during a predicate where the body is read, but still
// needs to be cached.
// 如果在断言中requestBody已经被读过了,为什么cachedRequest不等于null时,也跳过了下面的缓存方法呢,
//可以看下ServerWebExchangeUtils的cacheRequestBody方法,发现只要能读取到cachedRequest,说明至少已经缓存过RequestBody了。
ServerHttpRequest cachedRequest =exchange.getAttributeOrDefault(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR,
null);
if (cachedRequest != null) {
exchange.getAttributes().remove(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR);
return chain.filter(exchange.mutate().request(cachedRequest).build());
}
// 获取exchange缓存中的 requestBody
DataBuffer body = exchange.getAttributeOrDefault(CACHED_REQUEST_BODY_ATTR, null);
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
// 编号B. 如果当前的路由id没有被保存到routesToCache话,则不做缓存
if (body != null || !this.routesToCache.containsKey(route.getId())) {
return chain.filter(exchange);
}
// 如果requestBody没有被缓存,并且路由被标记为需要缓存缓存的话,去缓存requestBody
return ServerWebExchangeUtils.cacheRequestBody(exchange, (serverHttpRequest) -> {
// don't mutate and build if same request object
if (serverHttpRequest == exchange.getRequest()) {
return chain.filter(exchange);
}
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
});
}

@Override
public int getOrder() {
//编号A. Order.HIGHEST_PRECEDENCE = Integer.MIN_VALUE,说明这个过滤器的执行优先级非常高。
return Ordered.HIGHEST_PRECEDENCE + 1000;
}
}

我们看到(注释编号:B)想要ServerWebExchangeUtils.cacheRequestBody(exchange,functions)方法执行(也就是缓存requestBody)的必要条件有两个:

  1. Databuffer没有被缓存到exchange的attributes对象中。
  2. 路由被标记为需要缓存,也就是this.routesToCache.containsKey(rouceId)方法必须返回true。

第一个条件不必多说,那么如何满足第二个条件呢?看到代码(注释编号:C),当本过滤器接收到事件EnableBodyCachingEvent时,会将路由ID,保存到this.routesToCache中。因此,只需要我们的自定义过滤器LogInfoGatewayFilter发送出EnableBodyCachingEvent事件,框架就会自动为我们缓存requestBody。因而在本过滤器被执行的时候,就不再需要自建ServerHttpRquest的装饰类了。

下一个问题:如何发送EnableBodyCachingEvent事件?

搜索源码发现,只有RetryGatewayFilterFactory重试过滤器工厂这个类发送了事件,其实也很好理解,当路由中设置了重试过滤器,最简便的方式就是事先缓存好请求数据。(这里不会深入RetryGatewayFilterFactory的源码 ,只需要我们从中了解如何发送RetryGatewayFilterFactory就可以了)

继续阅读代码:

1
2
3
4
5
6
7
8
9
java复制代码public GatewayFilter apply(String routeId, Repeat<ServerWebExchange> repeat, Retry<ServerWebExchange> retry) {
if (routeId != null && getPublisher() != null) {
// 发送事件,使缓存生效
getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId));
}
return (exchange, chain) -> {
trace("Entering retry-filter");
....
}

发现所有继承了AbstractGatewayFilterFactory<C>抽象过滤器网关的类都会一起继承方法getPublisher(),来获取事件推送器publier,用来发送EnableBodyCachingEvent事件。

接下来我们如何获取routeId?

同样也是参照RetryGatewayFilterFactory类,发现他的配置类RetryConfig实现了HasRouteId接口,当在RouteDefinitionRouteLocator在加载过滤器的时候,会将路由的id赋值到RetryConfig中。因此,可以模仿RetryConfig编写一个filter的配置类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public static class LogConfig implements HasRouteId {

private String routeId;

@Override
public String getRouteId() {
return routeId;
}

@Override
public void setRouteId(String routeId) {
this.routeId = routeId;
}
}

做好了以上的准备,自定义日志过滤器LogInfoGatewayFilterFactory在被加载时,就会将开启缓存的事件发送给AdaptCachedBodyGlobalFilter,将routeId缓存起来。AdaptCachedBodyGlobalFilter在网关接收到网络请求的时候,将requestBody缓存到exchange中。

日志过滤器工厂类的代码如下:

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复制代码/**
* 日志过滤器工厂类
* @author ZongZi
* @date 2021/8/3 3:35 下午
*/
@Component
public class LogInfoGatewayFilterFactory extends AbstractGatewayFilterFactory<LogInfoGatewayFilterFactory.LogConfig> {

private static final Log log = LogFactory.getLog(LogInfoGatewayFilterFactory.class);
public LogInfoGatewayFilterFactory() {
super(LogConfig.class);
}
@Override
public GatewayFilter apply(LogConfig logConfig) {
String routeId = logConfig.getRouteId();
if (routeId != null && getPublisher() != null) {
// 将routeId上报,这样AdaptCachedBodyGlobalFilter就可以缓存requestBody
getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId));
}
return new LogInfoGatewayFilter();
}

// 实现HasRouteId,让框架传递路由ID进来。
public static class LogConfig implements HasRouteId {
private String routeId;

@Override
public String getRouteId() {
return routeId;
}

@Override
public void setRouteId(String routeId) {
this.routeId = routeId;
}
}
}

日志网关过滤的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* 日志网关过滤器v1
* @author ZongZi
* @date 2021/8/2 2:17 下午
*/
public class LogInfoGatewayFilter implements GatewayFilter {

private static final Logger log = LoggerFactory.getLogger(LogInfoGatewayFilter.class);

private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBody";

private List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 获取被全局网关过滤器缓存起来的 requestBody
DataBuffer cachedRequestBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(cachedRequestBody.asByteBuffer());
String s = charBuffer.toString();
System.out.println(s)
return chain.filter(exchange);
}
}

过滤器配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
yml复制代码spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: found
uri: http://localhost:8081
predicates:
- Path=/user/**
filters:
- LogInfo

添加LogInfo的过滤器,在启动的时候,就可以输出RquestBody啦

注:以上的代码还只在本地环境中测试,至于在正式环境中表现如何,还需要经过实践考验。

本文转载自: 掘金

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

Java代码中,如何监控Mysql的binlog?

发表于 2021-08-04

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战


最近在工作中,遇到了这样一个业务场景,我们需要关注一个业务系统数据库中某几张表的数据,当数据发生新增或修改时,将它同步到另一个业务系统数据库中的表中。

一提到数据库的同步,估计大家第一时间想到的就是基于binlog的主从复制了,但是放在我们的场景中,还有几个问题:

  • 第一,并不是需要复制所有表的数据,复制对象只有少量的几张表
  • 第二,也是比较麻烦的,两个业务系统数据库表结构可能不一致。例如,要同步数据库1的A表中的某些字段到数据库2的B表中,在这一过程中,A表和B表的字段并不是完全相同

这样的话,我们只能通过代码的方式,首先获取到数据库1表中数据的变动,再通过手动映射的方式,插入到数据库2的表中。但是,获取变动数据的这一过程,还是离不开binlog,因此我们就需要在代码中对binlog进行一下监控。

先说结论,我们最终使用了一个开源工具mysql-binlog-connector-java,用来监控binlog变化并获取数据,获取数据后再手动插入到另一个库的表中,基于它来实现了数据表的同步。项目的git地址如下:

1
http复制代码https://github.com/shyiko/mysql-binlog-connector-java

在正式开始前,还是先简单介绍一下mysql的binlog,binlog是一个二进制文件,它保存在磁盘中,是用来记录数据库表结构变更、表数据修改的二进制日志。其实除了数据复制外,它还可以实现数据恢复、增量备份等功能。

启动项目前,首先需要确保mysql服务已经启用了binlog:

1
shell复制代码show variables like 'log_bin';

如果为值为OFF,表示没有启用,那么需要首先启用binlog,修改配置文件:

1
2
3
shell复制代码log_bin=mysql-bin
binlog-format=ROW
server-id=1

对参数做一个简要说明:

  • 在配置文件中加入了log_bin配置项后,表示启用了binlog
  • binlog-format是binlog的日志格式,支持三种类型,分别是STATEMENT、ROW、MIXED,我们在这里使用ROW模式
  • server-id用于标识一个sql语句是从哪一个server写入的,这里一定要进行设置,否则我们在后面的代码中会无法正常监听到事件

在更改完配置文件后,重启mysql服务。再次查看是否启用binlog,返回为ON,表示已经开启成功。

在Java项目中,首先引入maven坐标:

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.github.shyiko</groupId>
<artifactId>mysql-binlog-connector-java</artifactId>
<version>0.21.0</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
java复制代码public static void main(String[] args) {
BinaryLogClient client = new BinaryLogClient("127.0.0.1", 3306, "hydra", "123456");
client.setServerId(2);

client.registerEventListener(event -> {
EventData data = event.getData();
if (data instanceof TableMapEventData) {
System.out.println("Table:");
TableMapEventData tableMapEventData = (TableMapEventData) data;
System.out.println(tableMapEventData.getTableId()+": ["+tableMapEventData.getDatabase() + "-" + tableMapEventData.getTable()+"]");
}
if (data instanceof UpdateRowsEventData) {
System.out.println("Update:");
System.out.println(data.toString());
} else if (data instanceof WriteRowsEventData) {
System.out.println("Insert:");
System.out.println(data.toString());
} else if (data instanceof DeleteRowsEventData) {
System.out.println("Delete:");
System.out.println(data.toString());
}
});

try {
client.connect();
} catch (IOException e) {
e.printStackTrace();
}
}

首先,创建一个BinaryLogClient客户端对象,初始化时需要传入mysql的连接信息,创建完成后,给客户端注册一个监听器,来实现它对binlog的监听和解析。在监听器中,我们暂时只对4种类型的事件数据进行了处理,除了WriteRowsEventData、DeleteRowsEventData、UpdateRowsEventData对应增删改操作类型的事件数据外,还有一个TableMapEventData类型的数据,包含了表的对应关系,在后面的例子中再具体说明。

在这里,客户端监听到的是数据库级别的所有事件,并且可以监听到表的DML语句和DDL语句,所以我们只需要处理我们关心的事件数据就行,否则会收到大量的冗余数据。

启动程序,控制台输出:

1
2
text复制代码com.github.shyiko.mysql.binlog.BinaryLogClient openChannelToBinaryLogStream
信息: Connected to 127.0.0.1:3306 at mysql-bin.000002/1046 (sid:2, cid:10)

连接mysql的binlog成功,接下来,我们在数据库中插入一条数据,这里操作的数据库名字是tenant,表是dept:

1
sql复制代码insert into dept VALUES(8,"人力","","1");

这时,控制台就会打印监听到事件的数据:

1
2
3
4
5
6
text复制代码Table:
108: [tenant-dept]
Insert:
WriteRowsEventData{tableId=108, includedColumns={0, 1, 2, 3}, rows=[
[8, 人力, , 1]
]}

我们监听到的事件类型数据有两类,第一类是TableMapEventData,通过它可以获取操作的数据库名称、表名称以及表的id。之所以我们要监听这个事件,是因为之后监听的实际操作中返回数据中包含了表的id,而没有表名等信息,所以如果我们想知道具体的操作是在哪一张表的话,就要先维护一个id与表的对应关系。

第二个打印出来的监听事件数据是WriteRowsEventData,其中记录了insert语句作用的表,插入涉及到的列,以及实际插入的数据。另外,如果我们只需要对特定的一张或几张表进行处理的话,也可以提前设置表的名单,在这里根据表id到表名的映射关系,实现数据的过滤,

接下来,我们再执行一条update语句:

1
sql复制代码update dept set tenant_id=3 where id=8 or id=9

控制台输出:

1
2
3
4
5
6
7
text复制代码Table:
108: [tenant-dept]
Update:
UpdateRowsEventData{tableId=108, includedColumnsBeforeUpdate={0, 1, 2, 3}, includedColumns={0, 1, 2, 3}, rows=[
{before=[8, 人力, , 1], after=[8, 人力, , 3]},
{before=[9, 人力, , 1], after=[9, 人力, , 3]}
]}

在执行update语句时,可能会作用于多条数据,因此在实际修改的数据中,可能包含多行记录,这一点体现在上面的rows中,包含了id为8和9的两条数据。

最后,再执行一条delete语句:

1
sql复制代码delete from dept where tenant_id=3

控制台打印如下,rows中同样返回了生效的两条数据:

1
2
3
4
5
6
7
text复制代码Table:
108: [tenant-dept]
Delete:
DeleteRowsEventData{tableId=108, includedColumns={0, 1, 2, 3}, rows=[
[8, 人力, , 3],
[9, 人力, , 3]
]}

简单的使用原理介绍完成后,再回到我们原先的需求上,需要将一张表中新增或修改的数据同步到另一张表中,问题还有一个,就是如何将返回的数据对应到所在的列上。这时应该怎么实现呢?以update操作为例,我们要对提取的数据后进行一下处理,更改上面例子中的方法:

1
2
3
4
5
6
7
8
9
10
java复制代码if (data instanceof UpdateRowsEventData) {
System.out.println("Update:");
UpdateRowsEventData updateRowsEventData = (UpdateRowsEventData) data;
for (Map.Entry<Serializable[], Serializable[]> row : updateRowsEventData.getRows()) {
List<Serializable> entries = Arrays.asList(row.getValue());
System.out.println(entries);
JSONObject dataObject = getDataObject(entries);
System.out.println(dataObject);
}
}

在将data类型强制转换为UpdateRowsEventData后,可以使用getRows方法获取到更新的行数据,并且能够取到每一列的值。

之后,调用了一个自己实现的getDataObject方法,用它来实现数据到列的绑定过程:

1
2
3
4
5
6
7
8
9
java复制代码private static JSONObject getDataObject(List message) {
JSONObject resultObject = new JSONObject();
String format = "{\"id\":\"0\",\"dept_name\":\"1\",\"comment\":\"2\",\"tenant_id\":\"3\"}";
JSONObject json = JSON.parseObject(format);
for (String key : json.keySet()) {
resultObject.put(key, message.get(json.getInteger(key)));
}
return resultObject;
}

在format字符串中,提前维护了一个数据库表的字段顺序的字符串,标识了每个字段位于顺序中的第几个位置。通过上面这个函数,能够实现数据到列的填装过程,我们再执行一条update语句来查看一下结果:

1
sql复制代码update dept set tenant_id=3,comment="1" where id=8

控制台打印结果如下:

1
2
3
4
5
text复制代码Table:
108: [tenant-dept]
Update:
[8, 人力, 1, 3]
{"tenant_id":3,"dept_name":"人力","comment":"1","id":8}

可以看到,将修改后的这一条记录中的属性填装到了它对应的列中,之后我们再根据具体的业务逻辑中,就可以根据字段名取出数据,将数据同步到其他的表了。

最后

如果觉得对您有所帮助,小伙伴们可以点赞、转发一下~非常感谢

微信搜索:码农参上,来加个好友,点赞之交也可以哦~

公众号后台回复“面试”、“导图”、“架构”、“实战”,获得免费资料哦~

本文转载自: 掘金

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

快速上手 Rook,入门云原生存储编排

发表于 2021-08-04

Rook 是一个开源 cloud-native storage orchestrator(云原生存储编排器),为各种存储解决方案提供平台、框架和支持,以与云原生环境进行原生集成。

Rook 将存储软件转变为自我管理(self-managing)、自我扩展(self-scaling)和自我修复(self-healing)的存储服务。
它通过自动化部署(automating deployment)、引导(bootstrapping)、配置(configuration)、供应(provisioning)、
扩展(scaling)、升级(upgrading)、迁移(migration)、灾难恢复(disaster recovery)、监控(monitoring)和资源管理(resource management)来实现这一点。
Rook 使用底层云原生容器管理、调度和编排平台提供的设施来执行其职责。

Rook 利用扩展点深度集成到云原生环境中,并为调度、生命周期管理、资源管理、安全、监控和用户体验提供无缝体验。

Cassandra 快速入门

Cassandra 是一个高可用、容错、对等的 NoSQL 数据库,具有闪电般的性能和可调的一致性。它提供了无单点故障的大规模可扩展性。

Scylla 是在 C++ 中对 Cassandra 的接近硬件重写。
它采用无共享架构,可实现真正的线性扩展和主要硬件优化,从而实现超低延迟和极高吞吐量。它是 Cassandra 的直接替代品,并使用相同的接口,因此 Rook 也支持它。

前提条件

运行 Rook Cassandra operator 需要 Kubernetes 集群。
为了确保你有一个为 Rook 准备好的 Kubernetes 集群(Cassandra 不需要 flexvolume 插件)

部署 Cassandra Operator

首先使用以下命令部署 Rook Cassandra Operator:

1
2
3
console复制代码$ git clone --single-branch --branch v1.6.8 https://github.com/rook/rook.git
cd rook/cluster/examples/kubernetes/cassandra
kubectl apply -f operator.yaml

这将在命名空间 rook-cassandra-system 中安装 operator。您可以检查 operator 是否已启动并运行:

1
console复制代码kubectl -n rook-cassandra-system get pod

创建和初始化 Cassandra/Scylla 集群

现在 operator 正在运行,我们可以通过创建 clusters.cassandra.rook.io 资源的实例来创建 Cassandra/Scylla 集群的实例。
该资源的某些值是可配置的,因此请随意浏览 cluster.yaml 并根据自己的喜好调整设置。

当你准备创建一个 Cassandra 集群时,只需运行:

1
console复制代码kubectl create -f cluster.yaml

我们可以使用以下命令验证是否已创建代表我们新 Cassandra 集群的 Kubernetes 对象。
这很重要,因为它表明 Rook 已成功扩展 Kubernetes,使 Cassandra 集群成为 Kubernetes 云原生环境中的一等公民。

1
console复制代码kubectl -n rook-cassandra get clusters.cassandra.rook.io

要检查是否所有所需的成员都在运行,您应该从以下命令中看到与 cluster.yaml 中指定的成员数量相同的条目数:

1
console复制代码kubectl -n rook-cassandra get pod -l app=rook-cassandra

您还可以从其状态跟踪 Cassandra 集群的状态。要检查集群的当前状态,请运行:

1
console复制代码kubectl -n rook-cassandra describe clusters.cassandra.rook.io rook-cassandra

访问数据库

  • 从 kubectl:

要在新集群中获取 cqlsh shell:

1
2
console复制代码kubectl exec -n rook-cassandra -it rook-cassandra-east-1-east-1a-0 -- cqlsh
> DESCRIBE KEYSPACES;
  • 从 Pod 内部:

当你创建一个新的集群时,Rook 会自动为客户端创建一个服务来访问集群。服务的名称遵循约定<cluster-name>-client。您可以通过运行以下命令在集群中查看此服务:

1
console复制代码kubectl -n rook-cassandra describe service rook-cassandra-client

在 Kubernetes 集群中运行的 Pod 可以使用此服务连接到 Cassandra。
这是使用 Python Driver 的示例:

1
2
3
4
python复制代码from cassandra.cluster import Cluster

cluster = Cluster(['rook-cassandra-client.rook-cassandra.svc.cluster.local'])
session = cluster.connect()

Scale Up

operator 支持扩展机架(rack)以及添加新机架(rack)。要进行更改,您可以使用:

1
console复制代码kubectl edit clusters.cassandra.rook.io rook-cassandra
  • 要扩展一个 rack,请将 rack 的 Spec.Members 字段更改为所需值。
  • 要添加新 rack,请在 racks 列表中添加一个新 rack。请记住为新 rack 选择不同的 rack 名称。
  • 编辑并保存 yaml 后,请检查集群的状态和事件以获取有关正发生情况的信息:
1
console复制代码kubectl -n rook-cassandra describe clusters.cassandra.rook.io rook-cassandra

Scale Down

operator 支持按比例缩小 rack。要进行更改,您可以使用:

1
console复制代码kubectl edit clusters.cassandra.rook.io rook-cassandra
  • 要缩小一个 rack,请将 rack 的 Spec.Members 字段更改为所需值。
  • 编辑并保存 yaml 后,请检查集群的状态和事件以获取有关正发生情况的信息:
1
console复制代码kubectl -n rook-cassandra describe clusters.cassandra.rook.io rook-cassandra

Clean Up

要清理与此演练相关的所有资源,您可以运行以下命令。

注意:这将破坏您的数据库并删除其所有相关数据。

1
2
console复制代码kubectl delete -f cluster.yaml
kubectl delete -f operator.yaml

故障排除

如果集群没有出现,第一步是检查 operator 的日志:

1
console复制代码kubectl -n rook-cassandra-system logs -l app=rook-cassandra-operator

如果 operator 日志中一切正常,您还可以查看 Cassandra 实例之一的日志:

1
console复制代码kubectl -n rook-cassandra logs rook-cassandra-0

Cassandra 监控

要为 cassandra rack 启用 jmx_exporter,您应该在 CassandraCluster CRD 中为 rack 指定 jmxExporterConfigMapName 选项。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yaml复制代码apiVersion: cassandra.rook.io/v1alpha1
kind: Cluster
metadata:
name: my-cassandra
namespace: rook-cassandra
spec:
...
datacenter:
name: my-datacenter
racks:
- name: my-rack
members: 3
jmxExporterConfigMapName: jmx-exporter-settings
storage:
volumeClaimTemplates:
- metadata:
name: rook-cassandra-data
spec:
storageClassName: my-storage-class
resources:
requests:
storage: 200Gi

获取所有指标的简单 config map 示例:

1
2
3
4
5
6
7
8
9
10
yaml复制代码apiVersion: v1
kind: ConfigMap
metadata:
name: jmx-exporter-settings
namespace: rook-cassandra
data:
jmx_exporter_config.yaml: |
lowercaseOutputLabelNames: true
lowercaseOutputName: true
whitelistObjectNames: ["org.apache.cassandra.metrics:*"]

ConfigMap 的数据字段必须包含带有 jmx exporter 设置的 jmx_exporter_config.yaml key。

当 config map 更新时,Pod 没有自动重新加载机制。
configmap 更改后,您应该手动重新启动所有 rack pods:

1
2
3
4
bash复制代码NAMESPACE=<namespace>
CLUSTER=<cluster_name>
RACKS=$(kubectl get sts -n ${NAMESPACE} -l "cassandra.rook.io/cluster=${CLUSTER}")
echo ${RACKS} | xargs -n1 kubectl rollout restart -n ${NAMESPACE}

Ceph Storage 快速入门

本指南将引导您完成 Ceph 集群的基本设置,并使您能够使用集群中运行的其他 pod 中的块、对象和文件存储。

最低版本

Rook 支持 Kubernetes v1.11 或更高版本。

Important 如果您使用的是 K8s 1.15 或更早版本,则需要创建不同版本的 Rook CRD。
创建在示例清单的 pre-k8s-1.16 子文件夹中找到的 crds.yaml。

前提条件

为确保您拥有可用于 Rook 的 Kubernetes 集群。

为了配置 Ceph 存储集群,至少需要以下本地存储选项之一:

  • 原始设备(无分区或格式化文件系统)
    • 这需要在主机上安装 lvm2。
      为了避免这种依赖性,您可以在磁盘上创建一个完整的磁盘分区(见下文)
  • 原始分区(无格式化文件系统)
  • block 模式下存储类中可用的持久卷

您可以使用以下命令确认您的分区或设备是否已格式化文件系统。

1
console复制代码lsblk -f
1
2
3
4
5
6
7
8
> css复制代码NAME                  FSTYPE      LABEL UUID                                   MOUNTPOINT
> vda
> └─vda1 LVM2_member >eSO50t-GkUV-YKTH-WsGq-hNJY-eKNf-3i07IB
> ├─ubuntu--vg-root ext4 c2366f76-6e21-4f10-a8f3-6776212e2fe4 /
> └─ubuntu--vg-swap_1 swap 9492a3dc-ad75-47cd-9596-678e8cf17ff9 [SWAP]
> vdb
>
>

如果 FSTYPE 字段不为空,则在相应设备的顶部有一个文件系统。在这种情况下,您可以将 vdb 用于 Ceph,而不能使用 vda 及其分区。

TL;DR

如果幸运的话,可以使用以下 kubectl 命令和示例 yaml 文件创建一个简单的 Rook 集群。

1
2
3
4
console复制代码$ git clone --single-branch --branch v1.6.8 https://github.com/rook/rook.git
cd rook/cluster/examples/kubernetes/ceph
kubectl create -f crds.yaml -f common.yaml -f operator.yaml
kubectl create -f cluster.yaml

集群环境

Rook 文档侧重于在生产环境中启动 Rook。还提供了一些示例来放宽测试环境的一些设置。在本指南后面创建集群时,请考虑以下示例集群清单:

  • cluster.yaml: 在裸机上运行的生产集群的集群设置。至少需要三个工作节点。
  • cluster-on-pvc.yaml: 在动态云环境中运行的生产集群的集群设置。
  • cluster-test.yaml: 测试环境的集群设置,例如 minikube。

部署 Rook Operator

第一步是部署 Rook operator。检查您是否正在使用与您的 Rook 版本相对应的示例 yaml 文件。

1
2
3
4
5
console复制代码cd cluster/examples/kubernetes/ceph
kubectl create -f crds.yaml -f common.yaml -f operator.yaml

# verify the rook-ceph-operator is in the `Running` state before proceeding
kubectl -n rook-ceph get pod

在生产中启动 Operator 之前,您可能需要考虑一些设置:

  1. 如果您使用 kubernetes v1.15 或更早版本,则需要在此处创建 CRD,在 /cluster/examples/kubernetes/ceph/pre-k8s-1.16/crd.yaml。
    CustomResourceDefinition 的 apiextension v1beta1 版本在 Kubernetes v1.16 中已弃用。
  2. 考虑是否要启用默认禁用的某些 Rook 功能。有关这些和其他高级设置,请参阅 operator.yaml。
    1. 设备发现:如果启用了 ROOK_ENABLE_DISCOVERY_DAEMON 设置,Rook 将监视要配置的新设备,常用于裸机集群。
    2. Flex driver:Flex driver 已被弃用,取而代之的是 CSI driver,但仍可通过 ROOK_ENABLE_FLEX_DRIVER 设置启用。
    3. Node affinity and tolerations(节点关联和容忍度):默认情况下,CSI driver 将在集群中的任何节点上运行。 要配置 CSI driver affinity,可以使用多种设置。

创建 Rook Ceph 集群

现在 Rook operator 正在运行,我们可以创建 Ceph 集群。
为了使集群在重新启动后继续存在,请确保设置对主机有效的 dataDirHostPath 属性。

创建集群:

1
console复制代码kubectl create -f cluster.yaml

使用 kubectl 列出 rook-ceph 命名空间中的 pod。
一旦它们全部运行,您应该能够看到以下 pod。
osd pod 的数量将取决于集群中的节点数量和配置的设备数量。
如果没有修改上面的 cluster.yaml,预计每个节点会创建一个 OSD。
CSI、rook-ceph-agent(flex driver)和 rook-discover pod 也是可选的,具体取决于您的设置。

1
console复制代码kubectl -n rook-ceph get pod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> sql复制代码NAME                                                 READY   STATUS      RESTARTS   AGE
> csi-cephfsplugin-provisioner-d77bb49c6-n5tgs 5/5 Running 0 140s
> csi-cephfsplugin-provisioner-d77bb49c6-v9rvn 5/5 Running 0 140s
> csi-cephfsplugin-rthrp 3/3 Running 0 140s
> csi-rbdplugin-hbsm7 3/3 Running 0 140s
> csi-rbdplugin-provisioner-5b5cd64fd-nvk6c 6/6 Running 0 140s
> csi-rbdplugin-provisioner-5b5cd64fd-q7bxl 6/6 Running 0 140s
> rook-ceph-crashcollector-minikube-5b57b7c5d4-hfldl 1/1 Running 0 105s
> rook-ceph-mgr-a-64cd7cdf54-j8b5p 1/1 Running 0 77s
> rook-ceph-mon-a-694bb7987d-fp9w7 1/1 Running 0 105s
> rook-ceph-mon-b-856fdd5cb9-5h2qk 1/1 Running 0 94s
> rook-ceph-mon-c-57545897fc-j576h 1/1 Running 0 85s
> rook-ceph-operator-85f5b946bd-s8grz 1/1 Running 0 92m
> rook-ceph-osd-0-6bb747b6c5-lnvb6 1/1 Running 0 23s
> rook-ceph-osd-1-7f67f9646d-44p7v 1/1 Running 0 24s
> rook-ceph-osd-2-6cd4b776ff-v4d68 1/1 Running 0 25s
> rook-ceph-osd-prepare-node1-vx2rz 0/2 Completed 0 60s
> rook-ceph-osd-prepare-node2-ab3fd 0/2 Completed 0 60s
> rook-ceph-osd-prepare-node3-w4xyz 0/2 Completed 0 60s
>
>

要验证集群是否处于健康状态,请连接到 Rook toolbox 并运行 ceph status 命令。

  • 所有 mons 都应达到法定人数
  • mgr 应该是活跃的
  • 至少有一个 OSD 处于活动状态
  • 如果运行状况不是 HEALTH_OK,则应调查警告或错误
1
console复制代码ceph status
1
2
3
4
5
6
7
8
9
10
11
> yaml复制代码 cluster:
> id: a0452c76-30d9-4c1a-a948-5d8405f19a7c
> health: HEALTH_OK
>
> services:
> mon: 3 daemons, quorum a,b,c (age 3m)
> mgr: a(active, since 2m)
> osd: 3 osds: 3 up (since 1m), 3 in (since 1m)
> ...
>
>

Storage

有关 Rook 公开的三种存储类型的演练,请参阅以下指南:

  • Block:创建要由 Pod 使用的块(block)存储
  • Object:创建可在 Kubernetes 集群内部或外部访问的对象存储
  • Shared Filesystem:创建要在多个 pod 之间共享的文件系统

Ceph 仪表板

Ceph 有一个仪表板,您可以在其中查看集群的状态。

工具

我们创建了一个 toolbox 容器,其中包含用于调试和排除 Rook 集群故障的全套 Ceph 客户端。

监控

每个 Rook 集群都有一些内置的指标收集器(collectors)/导出器(exporters),用于使用 Prometheus 进行监控。

销毁

完成测试集群后,请参阅这些说明以清理集群。

网络文件系统 (NFS)

NFS 允许远程主机通过网络挂载文件系统并与这些文件系统交互,就像它们是在本地挂载一样。这使系统管理员能够将资源整合到网络上的中央服务器上。

前提条件

  1. 运行 Rook NFS operator 需要 Kubernetes 集群。
  2. 要暴露的卷,需要通过 PVC 附加到 NFS server pod。
    可以被附加(attached)和导出(exported)任何类型的 PVC,例如 Host Path、AWS Elastic Block Store、GCP Persistent Disk、CephFS、Ceph RBD 等。
    这些卷的限制(limitations)在它们由 NFS 共享时也适用。您可以在 Kubernetes docs 中进一步了解这些卷的详细信息和限制。
  3. NFS client packages 必须安装在 Kubernetes 可能运行挂载 NFS 的 pod 的所有节点上。在 CentOS 节点上安装 nfs-utils 或在 Ubuntu 节点上安装 nfs-common。

部署 NFS Operator

首先使用以下命令部署 Rook NFS operator:

1
2
3
4
console复制代码$ git clone --single-branch --branch v1.6.8 https://github.com/rook/rook.git
cd rook/cluster/examples/kubernetes/nfs
kubectl create -f common.yaml
kubectl create -f operator.yaml

您可以检查 operator 是否已启动并运行:

1
console复制代码kubectl -n rook-nfs-system get pod
1
2
3
4
> arduino复制代码NAME                                    READY   STATUS    RESTARTS   AGE
> rook-nfs-operator-879f5bf8b-gnwht 1/1 Running 0 29m
>
>

部署 NFS Admission Webhook (可选)

Admission webhooks 是 HTTP 回调,用于接收对 API 服务器的准入请求。
两种类型的 admission webhooks 是验证 admission webhook 和 mutating admission webhook。
NFS Operator 支持验证 admission webhook,它在存储到 etcd(持久化)之前验证发送到 API server 的 NFSServer 对象。

要在 NFS 上启用 admission webhook,例如验证 admission webhook,您需要执行以下操作:

首先,确保安装了 cert-manager。如果尚未安装,您可以按照 cert-manager 安装文档中的说明进行安装。
或者,您可以简单地运行以下单个命令:

1
console复制代码kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.15.1/cert-manager.yaml

这将轻松安装最新版本 (v0.15.1) 的 cert-manager。 完成后,确保 cert-manager 组件部署正确并处于 Running 状态:

1
console复制代码kubectl get -n cert-manager pod
1
2
3
4
5
6
> sql复制代码NAME                                      READY   STATUS    RESTARTS   AGE
> cert-manager-7747db9d88-jmw2f 1/1 Running 0 2m1s
> cert-manager-cainjector-87c85c6ff-dhtl8 1/1 Running 0 2m1s
> cert-manager-webhook-64dc9fff44-5g565 1/1 Running 0 2m1s
>
>

一旦 cert-manager 运行,您现在可以部署 NFS webhook:

1
console复制代码kubectl create -f webhook.yaml

验证 webhook 已启动并正在运行:

1
console复制代码kubectl -n rook-nfs-system get pod
1
2
3
4
5
> sql复制代码NAME                                    READY   STATUS    RESTARTS   AGE
> rook-nfs-operator-78d86bf969-k7lqp 1/1 Running 0 102s
> rook-nfs-webhook-74749cbd46-6jw2w 1/1 Running 0 102s
>
>

创建 Openshift 安全上下文约束(可选)

在 OpenShift 集群上,我们需要创建一些额外的安全上下文约束。如果您未在 OpenShift 中运行,则可以跳过此部分并转到下一部分。

要为 nfs-server pod 创建安全上下文约束,我们可以使用以下 yaml,它也可以在 /cluster/examples/kubernetes/nfs 下的 scc.yaml 中找到。

注意:旧版本的 OpenShift 可能需要 apiVersion: v1

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
yaml复制代码kind: SecurityContextConstraints
apiVersion: security.openshift.io/v1
metadata:
name: rook-nfs
allowHostDirVolumePlugin: true
allowHostIPC: false
allowHostNetwork: false
allowHostPID: false
allowHostPorts: false
allowPrivilegedContainer: false
allowedCapabilities:
- SYS_ADMIN
- DAC_READ_SEARCH
defaultAddCapabilities: null
fsGroup:
type: MustRunAs
priority: null
readOnlyRootFilesystem: false
requiredDropCapabilities:
- KILL
- MKNOD
- SYS_CHROOT
runAsUser:
type: RunAsAny
seLinuxContext:
type: MustRunAs
supplementalGroups:
type: RunAsAny
volumes:
- configMap
- downwardAPI
- emptyDir
- persistentVolumeClaim
- secret
users:
- system:serviceaccount:rook-nfs:rook-nfs-server

您可以使用以下命令创建 scc:

1
console复制代码oc create -f scc.yaml

创建 Pod 安全策略(推荐)

我们建议您也创建 Pod 安全策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yaml复制代码apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: rook-nfs-policy
spec:
privileged: true
fsGroup:
rule: RunAsAny
allowedCapabilities:
- DAC_READ_SEARCH
- SYS_RESOURCE
runAsUser:
rule: RunAsAny
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
volumes:
- configMap
- downwardAPI
- emptyDir
- persistentVolumeClaim
- secret
- hostPath

使用名称 psp.yaml 保存此文件并使用以下命令创建:

1
console复制代码kubectl create -f psp.yaml

创建和初始化 NFS 服务器

现在 operator 正在运行,我们可以通过创建 nfsservers.nfs.rook.io 资源的实例来创建 NFS 服务器的实例。
NFS server resource 的各种字段和选项可用于配置要导出的服务器及其卷。

在我们创建 NFS Server 之前,我们需要创建 ServiceAccount 和 RBAC 规则

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
yaml复制代码---
apiVersion: v1
kind: Namespace
metadata:
name: rook-nfs
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: rook-nfs-server
namespace: rook-nfs
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: rook-nfs-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get"]
- apiGroups: ["policy"]
resources: ["podsecuritypolicies"]
resourceNames: ["rook-nfs-policy"]
verbs: ["use"]
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups:
- nfs.rook.io
resources:
- "*"
verbs:
- "*"
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: rook-nfs-provisioner-runner
subjects:
- kind: ServiceAccount
name: rook-nfs-server
# replace with namespace where provisioner is deployed
namespace: rook-nfs
roleRef:
kind: ClusterRole
name: rook-nfs-provisioner-runner
apiGroup: rbac.authorization.k8s.io

使用名称 rbac.yaml 保存此文件并使用以下命令创建:

1
console复制代码kubectl create -f rbac.yaml

本指南有 3 个主要示例,用于演示使用 NFS 服务器导出卷(exporting volumes):

  1. 默认 StorageClass 示例
  2. XFS StorageClass 示例
  3. Rook Ceph volume 示例

默认 StorageClass 示例

第一个示例将逐步创建一个 NFS server 实例,该实例导出由您碰巧运行的环境的默认 StorageClass 支持的存储。
在某些环境中,这可能是主机路径(host path),在其他环境中,它可能是云提供商虚拟磁盘(cloud provider virtual disk)。
无论哪种方式,此示例都需要存在默认的 StorageClass。

首先将以下 NFS CRD 实例定义保存到名为 nfs.yaml 的文件中:

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
yaml复制代码---
# A default storageclass must be present
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-default-claim
namespace: rook-nfs
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
---
apiVersion: nfs.rook.io/v1alpha1
kind: NFSServer
metadata:
name: rook-nfs
namespace: rook-nfs
spec:
replicas: 1
exports:
- name: share1
server:
accessMode: ReadWrite
squash: "none"
# A Persistent Volume Claim must be created before creating NFS CRD instance.
persistentVolumeClaim:
claimName: nfs-default-claim
# A key/value list of annotations
annotations:
rook: nfs

保存了 nfs.yaml 文件后,现在创建 NFS server,如下所示:

1
console复制代码kubectl create -f nfs.yaml

XFS StorageClass 示例

Rook NFS 通过 xfs_quota 支持磁盘配额。因此,如果您需要为卷指定磁盘配额,则可以按照此示例进行操作。

在这个例子中,我们将使用一个带有 prjquota 选项的作为 xfs 挂载的底层卷。
在创建底层卷(underlying volume)之前,您需要使用 xfs 文件系统和 prjquota mountOptions 创建 StorageClass。
Kubernetes 的许多分布式存储提供商都支持 xfs 文件系统。
通常通过在 storageClass 参数中定义 fsType: xfs 或 fs: xfs。
但实际上如何指定 storage-class 文件系统类型取决于它自己的存储提供者。
您可以查看 kubernetes.io/docs/concep… 了解更多详情。

这是 GCE PD 和 AWS EBS 的示例 StorageClass

  • GCE PD
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard-xfs
parameters:
type: pd-standard
fsType: xfs
mountOptions:
- prjquota
provisioner: kubernetes.io/gce-pd
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
  • AWS EBS
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard-xfs
provisioner: kubernetes.io/aws-ebs
parameters:
type: io1
iopsPerGB: "10"
fsType: xfs
mountOptions:
- prjquota
reclaimPolicy: Delete
volumeBindingMode: Immediate

一旦您已经拥有带有 xfs 文件系统和 prjquota mountOptions 的 StorageClass,您就可以使用以下示例创建 NFS server 实例。

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
yaml复制代码---
# A storage class with name standard-xfs must be present.
# The storage class must be has xfs filesystem type and prjquota mountOptions.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-xfs-claim
namespace: rook-nfs
spec:
storageClassName: "standard-xfs"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: nfs.rook.io/v1alpha1
kind: NFSServer
metadata:
name: rook-nfs
namespace: rook-nfs
spec:
replicas: 1
exports:
- name: share1
server:
accessMode: ReadWrite
squash: "none"
# A Persistent Volume Claim must be created before creating NFS CRD instance.
persistentVolumeClaim:
claimName: nfs-xfs-claim
# A key/value list of annotations
annotations:
rook: nfs

将此 PVC 和 NFS Server 实例保存为 nfs-xfs.yaml 并使用以下命令创建。

1
console复制代码kubectl create -f nfs-xfs.yaml

Rook Ceph volume 示例

在这个替代示例中,我们将使用不同的基础卷(underlying volume)作为 NFS server 的 export。
这些步骤将引导我们导出 Ceph RBD block volume,以便客户端可以通过网络访问它。

在 Rook Ceph 集群启动并运行后,我们可以继续创建 NFS server。

将此 PVC 和 NFS 服务器实例保存为 nfs-ceph.yaml:

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
yaml复制代码---
# A rook ceph cluster must be running
# Create a rook ceph cluster using examples in rook/cluster/examples/kubernetes/ceph
# Refer to https://rook.io/docs/rook/master/ceph-quickstart.html for a quick rook cluster setup
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-ceph-claim
namespace: rook-nfs
spec:
storageClassName: rook-ceph-block
accessModes:
- ReadWriteMany
resources:
requests:
storage: 2Gi
---
apiVersion: nfs.rook.io/v1alpha1
kind: NFSServer
metadata:
name: rook-nfs
namespace: rook-nfs
spec:
replicas: 1
exports:
- name: share1
server:
accessMode: ReadWrite
squash: "none"
# A Persistent Volume Claim must be created before creating NFS CRD instance.
# Create a Ceph cluster for using this example
# Create a ceph PVC after creating the rook ceph cluster using ceph-pvc.yaml
persistentVolumeClaim:
claimName: nfs-ceph-claim
# A key/value list of annotations
annotations:
rook: nfs

创建您保存在 nfs-ceph.yaml 中的 NFS server 实例:

1
console复制代码kubectl create -f nfs-ceph.yaml

验证 NFS Server

我们可以使用以下命令验证是否已创建代表我们的新 NFS server 及其导出的 Kubernetes 对象。

1
console复制代码kubectl -n rook-nfs get nfsservers.nfs.rook.io
1
2
3
4
> sql复制代码NAME       AGE   STATE
> rook-nfs 32s Running
>
>

验证 NFS server pod 是否已启动并正在运行:

1
console复制代码kubectl -n rook-nfs get pod -l app=rook-nfs
1
2
3
4
> sql复制代码NAME         READY     STATUS    RESTARTS   AGE
> rook-nfs-0 1/1 Running 0 2m
>
>

如果 NFS server pod 处于 Running 状态,那么我们已经成功创建了一个暴露的 NFS 共享,客户端可以开始通过网络访问。

访问 Export

从 Rook 版本 v1.0 开始,Rook 支持 NFS 的动态配置(dynamic provisioning)。此示例将展示如何将动态配置功能用于 nfs。

部署 NFS Operator 和 NFSServer 实例后。必须创建类似于以下示例的 storageclass 来动态配置卷。

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
labels:
app: rook-nfs
name: rook-nfs-share1
parameters:
exportName: share1
nfsServerName: rook-nfs
nfsServerNamespace: rook-nfs
provisioner: nfs.rook.io/rook-nfs-provisioner
reclaimPolicy: Delete
volumeBindingMode: Immediate

您可以将其另存为文件,例如:名为 sc.yaml 然后使用以下命令创建 storageclass。

1
console复制代码kubectl create -f sc.yaml

注意:StorageClass 需要传递以下 3 个参数。

  1. exportName: 它告诉供应商(provisioner)使用哪个导出来供应卷。
  2. nfsServerName: 它是 NFSServer 实例的名称。
  3. nfsServerNamespace: NFSServer 实例运行所在的命名空间。

创建上述 storageclass 后,您可以创建引用 storageclass 的 PV claim,如下面给出的示例所示。

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rook-nfs-pv-claim
spec:
storageClassName: "rook-nfs-share1"
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Mi

您也可以将其保存为文件,例如:名为 pvc.yaml 然后使用以下命令创建 PV claim。

1
console复制代码kubectl create -f pvc.yaml

消费 Export

现在我们可以通过创建一个示例 web server app 来使用刚刚创建的 PV,
该应用程序使用上述 PersistentVolumeClaim 声明导出的卷。有 2 个 pod 构成此示例:

  1. 将读取和显示 NFS 共享内容的 Web server pod
  2. 将随机数据写入 NFS 共享的 writer pod,以便网站不断更新

从 cluster/examples/kubernetes/nfs 文件夹启动 busybox pod(writer)和 web server:

1
2
console复制代码kubectl create -f busybox-rc.yaml
kubectl create -f web-rc.yaml

让我们确认预期的 busybox writer pod 和 Web server pod 都已启动并处于 Running 状态:

1
console复制代码kubectl get pod -l app=nfs-demo

为了能够通过网络访问 Web server,让我们为它创建一个 service:

1
console复制代码kubectl create -f web-service.yaml

然后我们可以使用我们之前启动的 busybox writer pod 来检查 nginx 是否正确地提供数据。
在下面的 1-liner 命令中,我们使用 kubectl exec 在 busybox writer pod 中运行一个命令,
该命令使用 wget 检索 web server pod 托管的 web page。
随着 busybox writer pod 继续写入新的时间戳,我们应该会看到返回的输出也每大约 10 秒更新一次。

1
console复制代码$ echo; kubectl exec $(kubectl get pod -l app=nfs-demo,role=busybox -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://$(kubectl get services nfs-web -o jsonpath='{.spec.clusterIP}'); echo
1
2
3
4
> yaml复制代码Thu Oct 22 19:28:55 UTC 2015
> nfs-busybox-w3s4t
>
>

清理销毁

要清理与此演练相关的所有资源,您可以运行以下命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console复制代码kubectl delete -f web-service.yaml
kubectl delete -f web-rc.yaml
kubectl delete -f busybox-rc.yaml
kubectl delete -f pvc.yaml
kubectl delete -f pv.yaml
kubectl delete -f nfs.yaml
kubectl delete -f nfs-xfs.yaml
kubectl delete -f nfs-ceph.yaml
kubectl delete -f rbac.yaml
kubectl delete -f psp.yaml
kubectl delete -f scc.yaml # if deployed
kubectl delete -f operator.yaml
kubectl delete -f webhook.yaml # if deployed
kubectl delete -f common.yaml

故障排除

如果 NFS server pod 没有出现,第一步是检查 NFS operator 的日志:

1
console复制代码kubectl -n rook-nfs-system logs -l app=rook-nfs-operator

本文转载自: 掘金

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

springboot+security基于前后端分离的RSA

发表于 2021-08-04

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

📖摘要

拒绝学习的惰性很可怕

现在与以前不一样,资料多、途径广,在这中间夹杂的广告也非常多。这就让很多初学者很难找到自己要的知识,最后看到有人推荐相关学习资料立刻屏蔽、删除,但同时技术优秀的资料也不能让需要的人看见了。久而久之把更多的时间精力都放在游戏、娱乐、影音上,适当的放松是可以的,但往往沉迷以后就很难出来,因此需要做好一些可以让自己成长的计划,稍有克制。

今天分享下 —— springboot + security基于前后端分离的 RSA 密码加密登录流程,欢迎关注!

重点说明一下关于这个方式请求时可能会出现参数携带+号被置换成空格的骚操作,请移步这篇文章:RSA加密请求报错:javax.crypto.BadPaddingException: Decryption error


🌂RSA加密简介

RSA加密 是一种非对称加密。可以在不直接传递密钥的情况下,完成解密。这能够确保信息的安全性,避免了直接传递密钥所造成的被破解的风险。是由一对密钥来进行加解密的过程,分别称为公钥和私钥。两者之间有数学相关,该加密算法的原理就是对一极大整数做因数分解的困难性来保证安全性。通常个人保存私钥,公钥是公开的(可能同时多人持有)。


✨RSA加密

加密是为了安全性考虑,简单的说,加密是为了防止信息被泄露

场景:特工B要给特工A传递一条消息,内容为某一命令。

RSA的加密过程如下:
(1)A特工生成一对密钥(公钥和私钥),私钥不公开,A特工自己保留。公钥为公开的,任何人可以获取。
(2)A特工传递自己的公钥给B特工,B特工用A特工的公钥对消息进行加密。
(3)A特工接收到B特工加密的消息,利用A特工自己的私钥对消息进行解密。

在这个过程中,只有2次传递过程,第一次是A特工传递公钥给B特工,第二次是B特工传递加密消息给A特工,即使都被敌方截获,也没有危险性,因为只有A特工的私钥才能对消息进行解密,防止了消息内容的泄露。基于这一特性,我们可以在前后端登陆上进行密码加密。保证密码的安全性。

image.png


💖RSA密码加密(Java)实现

  • 登录功能,密码肯定不能以明文形式传输,所以前端传过来的密码就应该是RSA加密过后的密码。
  • 因为 RSA 是需要公钥和私钥的,公钥加密,私钥解密。那么就可以随机生成一个公钥私钥密钥对,然后将这个密钥对保存下来,不要泄露,将公钥给前端将密码加密,后端通过私钥解密。最终再使用加盐加密的方法将密码保存到数据库中
  • 下面是 RSA加密 的代码*
  • 实际工作中可以将公钥和私钥提前生成好,然后放到配置文件中去
  • 说一下思路,具体用法可以直接运行 main 主函数慢慢研究。

Base64:封装base64编码。用于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
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
java复制代码import com.fckj.fckjrestaurant.constant.Constant;
import com.fckj.fckjrestaurant.util.codec.Base64Utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;


/**
* @Description: RSA加解密
* @BelongsProject: fckj-restaurant
* @BelongsPackage: com.fckj.fckjrestaurant.util.RSA
* @Author: ChenYongJia
* @CreateTime: 2021-06-04 11:46
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Slf4j
public class RSAUtils {

/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;

/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;

/**
* 获取密钥对
*
* @return java.security.KeyPair
* @date 2021/6/7 15:32
* @author ChenYongJia
* @version 1.0
*/
public static KeyPair getKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance(Constant.ALGORITHM_NAME);
generator.initialize(1024);
return generator.generateKeyPair();
}

/**
* 获取私钥
*
* @param privateKey 私钥字符串
* @return java.security.PrivateKey
* @date 2021/6/7 15:32
* @author ChenYongJia
* @version 1.0
*/
public static PrivateKey getPrivateKey(String privateKey) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance(Constant.ALGORITHM_NAME);
byte[] decodedKey = Base64Utils.decoder(privateKey.getBytes());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
return keyFactory.generatePrivate(keySpec);
}

/**
* 获取公钥
*
* @param publicKey 公钥字符串
* @param publicKey
* @return java.security.PublicKey
* @date 2021/6/7 15:32
* @author ChenYongJia
* @version 1.0
*/
public static PublicKey getPublicKey(String publicKey) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance(Constant.ALGORITHM_NAME);
byte[] decodedKey = Base64Utils.decoder(publicKey.getBytes());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);
return keyFactory.generatePublic(keySpec);
}

/**
* RSA加密
*
* @param data 待加密数据
* @param publicKey 公钥
* @return java.lang.String
* @date 2021/6/7 15:32
* @author ChenYongJia
* @version 1.0
*/
public static String encrypt(String data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(Constant.ALGORITHM_NAME);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
int inputLen = data.getBytes().length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offset > 0) {
if (inputLen - offset > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data.getBytes(), offset, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data.getBytes(), offset, inputLen - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
// 获取加密内容使用base64进行编码,并以UTF-8为标准转化成字符串
// 加密后的字符串
return new String(Base64Utils.encoder(encryptedData));
}

/**
* RSA解密
*
* @param data 待解密数据
* @param privateKey 私钥
* @return java.lang.String
* @date 2021/6/7 15:33
* @author ChenYongJia
* @version 1.0
*/
public static String decrypt(String data, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(Constant.ALGORITHM_NAME);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] dataBytes = Base64.decodeBase64(data);
int inputLen = dataBytes.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
//对数据分段解密
while (inputLen - offset > 0) {
if (inputLen - offset > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(dataBytes, offset, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(dataBytes, offset, inputLen - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
// 解密后的内容
return new String(decryptedData, "UTF-8");
}

/**
* 签名
*
* @param data 待签名数据
* @param privateKey 私钥
* @return java.lang.String
* @date 2021/6/7 15:33
* @author ChenYongJia
* @version 1.0
*/
public static String sign(String data, PrivateKey privateKey) throws Exception {
byte[] keyBytes = privateKey.getEncoded();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(Constant.ALGORITHM_NAME);
PrivateKey key = keyFactory.generatePrivate(keySpec);
Signature signature = Signature.getInstance(Constant.MD5_RSA);
signature.initSign(key);
signature.update(data.getBytes());
return new String(Base64Utils.encoder(signature.sign()));
}

/**
* 验签
*
* @param srcData 原始字符串
* @param publicKey 公钥
* @param sign 签名
* @return boolean
* @date 2021/6/7 15:33
* @author ChenYongJia
* @version 1.0
*/
public static boolean verify(String srcData, PublicKey publicKey, String sign) throws Exception {
byte[] keyBytes = publicKey.getEncoded();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(Constant.ALGORITHM_NAME);
PublicKey key = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(Constant.MD5_RSA);
signature.initVerify(key);
signature.update(srcData.getBytes());
return signature.verify(Base64Utils.decoder(sign.getBytes()));
}

public static void main(String[] args) {
try {
// 生成密钥对
KeyPair keyPair = getKeyPair();
String privateKey = new String(Base64Utils.encoder(keyPair.getPrivate().getEncoded()));
String publicKey = new String(Base64Utils.encoder(keyPair.getPublic().getEncoded()));
log.info("私钥:" + privateKey);
log.info("公钥:" + publicKey);
// RSA加密
String data = "123456";
String encryptData = encrypt(data, getPublicKey(publicKey));
log.info("加密后内容:" + encryptData);
// RSA解密
String decryptData = decrypt(encryptData, getPrivateKey(privateKey));
log.info("解密后内容:" + decryptData);
// RSA签名
String sign = sign(data, getPrivateKey(privateKey));
// RSA验签
boolean result = verify(data, getPublicKey(publicKey), sign);
log.info("验签结果:" + result);
} catch (Exception e) {
e.printStackTrace();
log.error("RSA加解密异常");
}
}

}

✔Springboot 业务代码实现

逻辑:前端获取 publickey——公钥,用公钥对密码进行编码加密,后端获取密码,用相对应的私钥解密加密数据,与数据库比对密码是否一致

controller 层就不写了,主要看 service 层业务逻辑代码:生成密钥对,放到 redis里面存储,返回前端公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码 @Override
public String getPublicKey() {
try {
Object privateKey = redisUtil.get(Constant.RSA_PRIVATE_KEY);
Object publicKey = redisUtil.get(Constant.RSA_PUBLIC_KEY);
if (WyCheckUtil.isEmpty(publicKey) || WyCheckUtil.isEmpty(privateKey)) {
KeyPair keyPair = RSAUtils.getKeyPair();
privateKey = new String(Base64Utils.encoder(keyPair.getPrivate().getEncoded()));
publicKey = new String(Base64Utils.encoder(keyPair.getPublic().getEncoded()));
// 存入私钥
redisUtil.set(Constant.RSA_PRIVATE_KEY, privateKey);
// 存入公钥
redisUtil.set(Constant.RSA_PUBLIC_KEY, publicKey);
}
log.info("privateKey = {},publicKey = {}", privateKey, publicKey);
return publicKey.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

前端:基于iview和vue框架

1
2
3
4
5
6
javascript复制代码前端用crypto-js进行加密,
npm i jsencrypt,
然后页面头引入import JSEncrypt from 'jsencrypt';
const encrypt = new JSEncrypt();
encrypt.setPublicKey('你的公钥');
password = encrypt.encrypt(‘你的密码’);// 加密后的字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码getPublicKey().then(res => {
let password = this.form.password
let publicKey = res.data.data.data
console.log(publicKey)
const encrypt = new JSEncrypt()
encrypt.setPublicKey(publicKey)
password = encrypt.encrypt(password)
console.log(password)
this.$emit('on-success-valid', {
userName: this.form.userName,
password: password
})
})

登录验证密码时解密对比

1
2
3
4
5
6
7
8
java复制代码String inputDecryptData = "";
try {
Object privateKey = redisUtil.get(Constant.RSA_PRIVATE_KEY);
inputDecryptData = RSAUtils.decrypt(password, RSAUtils.getPrivateKey(privateKey.toString()));
} catch (Exception e) {
log.error("RSA加解密出现异常======>", e);
throw new BizException("RSA加解密出现异常");
}

先获取私钥,我是放到redis里面,然后用私钥解密加密数据,同事获取数据库用户对象,获取密码同样用私钥解密比对与用户输入的密码是否一致

至此,Springboot&security基于前后端分离的RSA密码加密登录流程就完成了


🎉最后

  • 更多参考精彩博文请看这里:《陈永佳的博客》
  • 喜欢博主的小伙伴可以加个关注、点个赞哦,持续更新嘿嘿!

本文转载自: 掘金

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

Redis基础(二)—— 安装与配置

发表于 2021-08-04

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

Redis 的配置

Redis的安装

1、环境安装,安装gcc:

​ Redis是C语言开发,安装Redis需要先将官网下载的源码进行编译,编译依赖gcc环境,如果没有gcc环境,需要安装gcc

​ ==首先要确保==root登录,其次就是Linux要能连网络

并进入到 ~ 目录下

1
shell复制代码[root@xxx ~]#

输入安装命令

1
2
3
4
shell复制代码yum -y install gcc automake autoconf libtool make

==== 查看gcc版本
gcc -v

ps: 运行yum时出现/var/run/yum.pid已被锁定,PID为xxxx的另一个程序正在运行的问题解决

1
shell复制代码rm-f/var/run/yum.pid

2、Redis安装

​ 官方下载:redis.io/download

1、安装

这里我安装的是 reids5.0+ 版本的,之前安装6.0出错了,所以还是用了5.0,实在不敢折腾了~

1
shell复制代码wget https://download.redis.io/releases/redis-5.0.7.tar.gz

2、把Redis放到路径/usr/local,解压 tar -zxvf redis-5.0.7.tar.gz

3、执行命令

命令1:

1
shell复制代码cd redis-5.0.7

命令2:

1
2
3
shell复制代码=== 编译
make
=== 成功标志

1597748831577

如果没有安装 gcc ,这里会报错

image-20210712164124640

命令3:

1
shell复制代码make PREFIX=/usr/local/redis install

==注意==:PREFIX必须大写、同时会自动为我们创建redis目录,并将结果安装此目录

命令4:

1
shell复制代码cd /usr/local/redis/bin

1597749294910

Redis 配置

从Redis安装包中,复制redis.conf文件到Redis的安装目录下(一般为 /usr/local/redis,与bin目录同级)

1
shell复制代码[root@xiaojian redis-5.0.7]# cp redis.conf /usr/local/redis
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
shell复制代码1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize no

3. 指定Redis监听端口,默认端口为6379,为什么选用6379作为默认端口,因为6379在手机按键上MERZ对
应的号码,而MERZ取自意大利歌女Alessia Merz的名字
port 6379
4. 绑定的主机地址,(一般注释掉,否则无法使用远程连接redis服务)
bind 127.0.0.1

8. 设置数据库的数量,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id
databases 16
9. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
save <seconds> <changes>
Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更
改。

11. 指定本地数据库文件名,默认值为dump.rdb
dbfilename dump.rdb

15. 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH <password>命令提
供密码,默认关闭
requirepass foobared

16. 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以
打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis
会关闭新的连接并向客户端返回max number of clients reached错误信息
maxclients 128

17. 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清
除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然
可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
maxmemory <bytes> // 建议:内存为1G时,范围 256-512

18. 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开
启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步
的,所以有的数据会在一段时间内只存在于内存中。默认为no
appendonly no

19. 指定更新日志文件名,默认为appendonly.aof
appendfilename appendonly.aof

Redis中的内存维护策略

redis作为优秀的中间缓存件,时常会存储大量的数据,即使采取了集群部署来动态扩容,也应该
即使的整理内存,维持系统性能。
在redis中有两种解决方案,

  • 一是为数据设置超时时间;
  • 二是采用LRU算法动态将不用的数据删除。内存管理的一种页面置换算法,对于在内存中但又不用
    的数据块(内存块)叫做LRU,操作系统会根据哪些数据属于LRU而将其移出内存而腾出空间来加载
    另外的数据。
+ 1.volatile-lru:设定超时时间的数据中,删除最不常使用的数据.
+ 2.allkeys-lru:查询所有的key中最近最不常使用的数据进行删除,**这是应用最广泛的策略.**
+ 3.volatile-random:在已经设定了超时的数据中随机删除.
+ 4.allkeys-random:查询所有的key,之后随机删除.
+ 5.volatile-ttl:查询全部设定超时时间的数据,之后排序,将马上将要过期的数据进行删除操作.
+ 6.noeviction:如果设置为该属性,则不会进行删除操作,如果内存溢出则报错返回.volatile-lfu:从所有配置了过 期时间的键中驱逐使用频率最少的键allkeys-lfu:从所有键中驱逐使用频率最少的键
[www.jianshu.com/p/c8aeb3eee…](https://www.jianshu.com/p/c8aeb3eee6bc)

自定义配置Redis

1
2
3
shell复制代码daemonize no 修改为 daemonize yes;守护进程,后台运行,除非手动kill进程
bind 127.0.0.1 注释掉,否则无法远程连接redis服务
requirepass 设置密码

Redis启动

在redis安装目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码# 服务端启动
./bin/redis-server ./redis.conf

# 客户端启动
# 本地客户端启动
./bin/redis-cli -a password
# 远程客户端启动
redis-cli -h host -p port -a password
- host: redis启动的服务器地址
- port: redis启动的端口
- password: 配置文件中,requirepass设置的密码

# 测试连接成功
127.0.0.1:6379> ping
PONG

Redis的关闭

第一种关闭方式:(断电、非正常关闭。容易数据丢失)

1
2
3
4
shell复制代码# 查询PID: 
ps -ef | grep -i redis
# 强行关闭进程
kill -9 PID

第二种关闭方式(正常关闭、数据保存)

1
2
3
4
5
shell复制代码# 关闭redis服务,通过客户端进行shutdown
./bin/redis-cli shutdown
# 有密码时,先登录,再shutdown
./bin/redis-cli -a password
127.0.0.1:6379> shutdown

测试性能

redis-sentinel 是一个压力测试工具 !

官方自带的性能测试工具,

序号 选项 描述 默认值
1 -h 指定服务器主机名 127.0.0.1
2 -p 指定服务器端口 6379
3 -s 指定服务器 socket
4 -c 指定并发连接数 50
5 -n 指定请求数 10000
6 -d 以字节的形式指定 SET/GET 值的数据大小 2
7 -k 1=keep alive 0=reconnect 1
8 -r SET/GET/INCR 使用随机 key, SADD 使用随机值
9 -P 通过管道传输 请求 1
10 -q 强制退出 redis。仅显示 query/sec 值
11 –csv 以 CSV 格式输出
12 -l 生成循环,永久执行测试
13 -t 仅运行以逗号分隔的测试命令列表。
14 -I Idle 模式。仅打开 N 个 idle 连接并等待。

测试一下:

1
2
bash复制代码# 测试:50个并发连接,10000个请求
[root@xiaojian bin]# ./redis-benchmark -h localhost -p 6379 -c 50 -n 10000

本文转载自: 掘金

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

Mybatis拦截器实现自动填充时间 需求描述 具体实现 作

发表于 2021-08-04

需求描述

每张表都存在创建时间,修改时间,每次新增,修改数据得时候,如何让数据库自动填充数据,而无需开发去手动设置上这个值呢? 通过官网文档发现,我们可以实现一个拦截器,在数据库层进行自动填充时间数据。

具体实现

第一步: 创建两个注解,分别作用于字段上面

image.png

image.png

第二步: 创建一个拦截器,实现Interceptor接口

image.png

注意事项

如果断点调试发现这个拦截器没有生效,看看springboot是否扫描到这个包

作用

  1. 减少累赘代码得开发,开发人员更专注于业务层得开发
  2. 解耦,无需关注时间得创建,一旦需求改变,只需要修改拦截器就行
  3. 可以作为底层适配,只需要添加注解就可以实现此功能

本文转载自: 掘金

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

SpringBoot+Shiro+Jwt实现登录认证——最干

发表于 2021-08-04

这是我参与8月更文挑战的第四天,活动详情查看:8月更文挑战

  1. 概述

1.1 SpringBoot

这个就没什么好说的了,能看到这个教程的,估计都是可以说精通了SpringBoot的使用

1.2 Shiro

一个安全框架,但不只是一个安全框架。它能实现多种多样的功能。并不只是局限在web层。在国内的市场份额占比高于SpringSecurity,是使用最多的安全框架

可以实现用户的认证和授权。比SpringSecurity要简单的多。

1.3 Jwt

我的理解就是可以进行客户端与服务端之间验证的一种技术,取代了之前使用Session来验证的不安全性

为什么不适用Session?

原理是,登录之后客户端和服务端各自保存一个相应的SessionId,每次客户端发起请求的时候就得携带这个SessionId来进行比对

  1. Session在用户请求量大的时候服务器开销太大了
  2. Session不利于搭建服务器的集群(也就是必须访问原本的那个服务器才能获取对应的SessionId)

它使用的是一种令牌技术

Jwt字符串分为三部分

  1. Header

存储两个变量

1. 秘钥(可以用来比对)
2. 算法(也就是下面将Header和payload加密成Signature)
  1. payload

存储很多东西,基础信息有如下几个

1. 签发人,也就是这个“令牌”归属于哪个用户。一般是`userId`
2. 创建时间,也就是这个令牌是什么时候创建的
3. 失效时间,也就是这个令牌什么时候失效
4. 唯一标识,一般可以使用算法生成一个唯一标识
  1. Signature

这个是上面两个经过Header中的算法加密生成的,用于比对信息,防止篡改Header和payload

然后将这三个部分的信息经过加密生成一个JwtToken的字符串,发送给客户端,客户端保存在本地。当客户端发起请求的时候携带这个到服务端(可以是在cookie,可以是在header,可以是在localStorage中),在服务端进行验证

好了,废话不多说了,下面开始实战,实战分为以下几个部分

  1. SpringBoot整合Shiro
  2. SpringBoot整合Jwt
  3. SpringBoot+Shiro+Jwt
1
2
3
4
5
6
7
8
9
10
xml复制代码        <dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
  1. SpringBoot整合Shiro

两种方式:

  1. 将ssm的整合的配置使用java代码方式在springBoot中写一遍
  2. 使用官方提供的start

2.1 使用start整合springBoot

pom.xml

1
2
3
4
5
6
xml复制代码<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
<!--注意不要写成shiro-spring-boot-starter-->

application.properties

1
2
3
4
ini复制代码shiro.loginUrl="xxx"
#认证不通过的页面
shiro.UnauthorizedUrl="xxx"
#授权不通过的跳转页面

创建ShiroConfig.java进行一些简单的配置

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
java复制代码@Configuration
public class SpringShiroConfig {
@Bean
public Realm customRealm() {
return new CustomRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
// 关闭 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
// 哪些请求可以匿名访问
chain.addPathDefinition("/login", "anon"); // 登录接口
chain.addPathDefinition("/notLogin", "anon"); // 未登录错误提示接口
chain.addPathDefinition("/403", "anon"); // 权限不足错误提示接口
// 除了以上的请求外,其它请求都需要登录
chain.addPathDefinition("/**", "authc");
return chain;
}
// Shiro 和 Spring AOP 整合时的特殊设置
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}

//还有关闭ShiroDao功能

创建自定义的Realm

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
csharp复制代码public class CustomRealm extends AuthorizingRealm {
private static final Set<String> tomRoleNameSet = new HashSet<>();
private static final Set<String> tomPermissionNameSet = new HashSet<>();
private static final Set<String> jerryRoleNameSet = new HashSet<>();
private static final Set<String> jerryPermissionNameSet = new HashSet<>();
static {
tomRoleNameSet.add("admin");
jerryRoleNameSet.add("user");
tomPermissionNameSet.add("user:insert");
tomPermissionNameSet.add("user:update");
tomPermissionNameSet.add("user:delete");
tomPermissionNameSet.add("user:query");
jerryPermissionNameSet.add("user:query");
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (username.equals("tom")) {
info.addRoles(tomRoleNameSet);
info.addStringPermissions(tomPermissionNameSet);
} else if (username.equals("jerry")) {
info.addRoles(jerryRoleNameSet);
info.addStringPermissions(jerryPermissionNameSet);
}
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
if (username == null)
throw new UnknownAccountException("用户名不能为空");
SimpleAuthenticationInfo info = null;
if (username.equals("tom"))
return new SimpleAuthenticationInfo("tom", "123", CustomRealm.class.getName());
else if (username.equals("jerry"))
return new SimpleAuthenticationInfo("jerry", "123", CustomRealm.class.getName());
else
return null;
}
}

2.2 不使用starter

1
2
3
4
5
6
xml复制代码<!-- 自动依赖导入 shiro-core 和 shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>

编写 Shiro 的配置类:ShiroConfig

将 Shiro 的配置信息(spring-shiro.xml 和 spring-web.xml)以 Java 代码配置的形式改写:

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
typescript复制代码@Configuration
public class ShiroConfig {
@Bean
public Realm realm() {
return new CustomRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shirFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());

shiroFilterFactoryBean.setLoginUrl("/loginPage");
shiroFilterFactoryBean.setUnauthorizedUrl("/403");

Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/loginPage", "anon");
filterChainDefinitionMap.put("/403", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/hello", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

return shiroFilterFactoryBean;
}
/* ################################################################# */
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制指定注解的底层实现使用 cglib 方案
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}

编写 Controller

与 Shiro 和 SSM 的整合一样。略

编写 Thymeleaf 页面

略

  1. SpringBoot整合Jwt

3.1 依赖

1
2
3
markdown复制代码1. springboot
2. java-jwt--核心依赖
3. jjwt--java版本的辅助帮助模块

3.2 代码

  1. 创建JwtUtil
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
java复制代码package cn.coderymy.utils;

import java.util.*;
import com.auth0.jwt.*;
import com.auth0.jwt.algorithms.Algorithm;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;

import java.util.*;


public class JwtUtil {

// 生成签名是所使用的秘钥
private final String base64EncodedSecretKey;

// 生成签名的时候所使用的加密算法
private final SignatureAlgorithm signatureAlgorithm;

public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
this.signatureAlgorithm = signatureAlgorithm;
}

/**
* 生成 JWT Token 字符串
*
* @param iss 签发人名称
* @param ttlMillis jwt 过期时间
* @param claims 额外添加到荷部分的信息。
* 例如可以添加用户名、用户ID、用户(加密前的)密码等信息
*/
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
if (claims == null) {
claims = new HashMap<>();
}

// 签发时间(iat):荷载部分的标准字段之一
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);

// 下面就是在为payload添加各种标准声明和私有声明了
JwtBuilder builder = Jwts.builder()
// 荷载部分的非标准字段/附加字段,一般写在标准的字段之前。
.setClaims(claims)
// JWT ID(jti):荷载部分的标准字段之一,JWT 的唯一性标识,虽不强求,但尽量确保其唯一性。
.setId(UUID.randomUUID().toString())
// 签发时间(iat):荷载部分的标准字段之一,代表这个 JWT 的生成时间。
.setIssuedAt(now)
// 签发人(iss):荷载部分的标准字段之一,代表这个 JWT 的所有者。通常是 username、userid 这样具有用户代表性的内容。
.setSubject(iss)
// 设置生成签名的算法和秘钥
.signWith(signatureAlgorithm, base64EncodedSecretKey);

if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
// 过期时间(exp):荷载部分的标准字段之一,代表这个 JWT 的有效期。
builder.setExpiration(exp);
}

return builder.compact();
}


/**
* JWT Token 由 头部 荷载部 和 签名部 三部分组成。签名部分是由加密算法生成,无法反向解密。
* 而 头部 和 荷载部分是由 Base64 编码算法生成,是可以反向反编码回原样的。
* 这也是为什么不要在 JWT Token 中放敏感数据的原因。
*
* @param jwtToken 加密后的token
* @return claims 返回荷载部分的键值对
*/
public Claims decode(String jwtToken) {

// 得到 DefaultJwtParser
return Jwts.parser()
// 设置签名的秘钥
.setSigningKey(base64EncodedSecretKey)
// 设置需要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}


/**
* 校验 token
* 在这里可以使用官方的校验,或,
* 自定义校验规则,例如在 token 中携带密码,进行加密处理后和数据库中的加密密码比较。
*
* @param jwtToken 被校验的 jwt Token
*/
public boolean isVerify(String jwtToken) {
Algorithm algorithm = null;

switch (signatureAlgorithm) {
case HS256:
algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
break;
default:
throw new RuntimeException("不支持该算法");
}

JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwtToken); // 校验不通过会抛出异常


/*
// 得到DefaultJwtParser
Claims claims = decode(jwtToken);

if (claims.get("password").equals(user.get("password"))) {
return true;
}
*/

return true;
}

public static void main(String[] args) {
JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);

Map<String, Object> map = new HashMap<>();
map.put("username", "tom");
map.put("password", "123456");
map.put("age", 20);

String jwtToken = util.encode("tom", 30000, map);

System.out.println(jwtToken);
/*
util.isVerify(jwtToken);
System.out.println("合法");
*/

util.decode(jwtToken).entrySet().forEach((entry) -> {
System.out.println(entry.getKey() + ": " + entry.getValue());
});
}
}

解析:

1. 在创建JwtUtil对象的时候需要传入几个数值
    1. 这个用户,用来生成秘钥
    2. 这个加密算法,用来加密生成jwt
2. 通过jwt数据获取用户信息的方法(decode())
3. 判断jwt是否存在或者过期的方法
4. 最后是测试方法
  1. 创建一个Controller
1. 登录的Controller
    1. 获取username和password,进行与数据库的校验,校验成功执行下一步,失败直接返回
    2. 使用创建JwtUtil对象,传入username和需要使用的加密算法
    3. 创建需要加在载荷中的一些基本信息的一个map对象
    4. 创建jwt数据,传入username,保存时间,以及基本信息的map对象
2. 校验Controller
    1. 获取前台传入的Jwt数据
    2. 使用`JWTUtil`中的`isVerify`进行该jwt数据有效的校验
  1. SpringBoot+Shiro+Jwt

  1. 由于需要对shiro的SecurityManager进行设置,所以不能使用shiro-spring-boot-starter进行与springboot的整合,只能使用spring-shiro
1
2
3
4
5
6
xml复制代码<!-- 自动依赖导入 shiro-core 和 shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
  1. 由于需要实现无状态的web,所以使用不到Shiro的Session功能,严谨点就是将其关闭
1
2
3
4
5
6
7
8
9
java复制代码public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {

@Override
public Subject createSubject(SubjectContext context) {
// 不创建 session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}

这样如果调用getSession()方法会抛出异常

4.1 流程

  1. 用户请求,不携带token,就在JwtFilter处抛出异常/返回没有登录,让它去登陆
  2. 用户请求,携带token,就到JwtFilter中获取jwt,封装成JwtToken对象。然后使用JwtRealm进行认证
  3. 在JwtRealm中进行认证判断这个token是否有效,也就是
1
markdown复制代码执行流程:1. 客户端发起请求,shiro的过滤器生效,判断是否是login或logout的请求<br/>    如果是就直接执行请求<br/>    如果不是就进入JwtFilter2. JwtFilter执行流程    1. 获取header是否有"Authorization"的键,有就获取,没有就抛出异常    2. 将获取的jwt字符串封装在创建的JwtToken中,使用subject执行login()方法进行校验。这个方法会调用创建的JwtRealm    3. 执行JwtRealm中的认证方法,使用`jwtUtil.isVerify(jwt)`判断是否登录过    4. 返回true就使基础执行下去

4.2 快速开始

0. JwtDeafultSubjectFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package cn.coderymy.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {

@Override
public Subject createSubject(SubjectContext context) {
// 不创建 session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}

1. 创建JwtUtil

这个一般是固定的写法,其中写了大量注释

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
java复制代码package cn.coderymy.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/*
* 总的来说,工具类中有三个方法
* 获取JwtToken,获取JwtToken中封装的信息,判断JwtToken是否存在
* 1. encode(),参数是=签发人,存在时间,一些其他的信息=。返回值是JwtToken对应的字符串
* 2. decode(),参数是=JwtToken=。返回值是荷载部分的键值对
* 3. isVerify(),参数是=JwtToken=。返回值是这个JwtToken是否存在
* */
public class JwtUtil {
//创建默认的秘钥和算法,供无参的构造方法使用
private static final String defaultbase64EncodedSecretKey = "badbabe";
private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;

public JwtUtil() {
this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
}

private final String base64EncodedSecretKey;
private final SignatureAlgorithm signatureAlgorithm;

public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
this.signatureAlgorithm = signatureAlgorithm;
}

/*
*这里就是产生jwt字符串的地方
* jwt字符串包括三个部分
* 1. header
* -当前字符串的类型,一般都是“JWT”
* -哪种算法加密,“HS256”或者其他的加密算法
* 所以一般都是固定的,没有什么变化
* 2. payload
* 一般有四个最常见的标准字段(下面有)
* iat:签发时间,也就是这个jwt什么时候生成的
* jti:JWT的唯一标识
* iss:签发人,一般都是username或者userId
* exp:过期时间
*
* */
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
//iss签发人,ttlMillis生存时间,claims是指还想要在jwt中存储的一些非隐私信息
if (claims == null) {
claims = new HashMap<>();
}
long nowMillis = System.currentTimeMillis();

JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())//2. 这个是JWT的唯一标识,一般设置成唯一的,这个方法可以生成唯一标识
.setIssuedAt(new Date(nowMillis))//1. 这个地方就是以毫秒为单位,换算当前系统时间生成的iat
.setSubject(iss)//3. 签发人,也就是JWT是给谁的(逻辑上一般都是username或者userId)
.signWith(signatureAlgorithm, base64EncodedSecretKey);//这个地方是生成jwt使用的算法和秘钥
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);//4. 过期时间,这个也是使用毫秒生成的,使用当前时间+前面传入的持续时间生成
builder.setExpiration(exp);
}
return builder.compact();
}

//相当于encode的方向,传入jwtToken生成对应的username和password等字段。Claim就是一个map
//也就是拿到荷载部分所有的键值对
public Claims decode(String jwtToken) {

// 得到 DefaultJwtParser
return Jwts.parser()
// 设置签名的秘钥
.setSigningKey(base64EncodedSecretKey)
// 设置需要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}

//判断jwtToken是否合法
public boolean isVerify(String jwtToken) {
//这个是官方的校验规则,这里只写了一个”校验算法“,可以自己加
Algorithm algorithm = null;
switch (signatureAlgorithm) {
case HS256:
algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
break;
default:
throw new RuntimeException("不支持该算法");
}
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwtToken); // 校验不通过会抛出异常
//判断合法的标准:1. 头部和荷载部分没有篡改过。2. 没有过期
return true;
}

public static void main(String[] args) {
JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
//以tom作为秘钥,以HS256加密
Map<String, Object> map = new HashMap<>();
map.put("username", "tom");
map.put("password", "123456");
map.put("age", 20);

String jwtToken = util.encode("tom", 30000, map);

System.out.println(jwtToken);
util.decode(jwtToken).entrySet().forEach((entry) -> {
System.out.println(entry.getKey() + ": " + entry.getValue());
});
}
}

2. 创建JwtFilter

也就是在Shiro的拦截器中多加一个,等下需要在配置文件中注册这个过滤器

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
java复制代码package cn.coderymy.filter;

import cn.coderymy.shiro.JwtToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.AccessControlFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/*
* 自定义一个Filter,用来拦截所有的请求判断是否携带Token
* isAccessAllowed()判断是否携带了有效的JwtToken
* onAccessDenied()是没有携带JwtToken的时候进行账号密码登录,登录成功允许访问,登录失败拒绝访问
* */
@Slf4j
public class JwtFilter extends AccessControlFilter {
/*
* 1. 返回true,shiro就直接允许访问url
* 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
* */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
log.warn("isAccessAllowed 方法被调用");
//这里先让它始终返回false来使用onAccessDenied()方法
return false;
}

/**
* 返回结果为true表明登录通过
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
log.warn("onAccessDenied 方法被调用");
//这个地方和前端约定,要求前端将jwtToken放在请求的Header部分

//所以以后发起请求的时候就需要在Header中放一个Authorization,值就是对应的Token
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
log.info("请求的 Header 中藏有 jwtToken {}", jwt);
JwtToken jwtToken = new JwtToken(jwt);
/*
* 下面就是固定写法
* */
try {
// 委托 realm 进行登录认证
//所以这个地方最终还是调用JwtRealm进行的认证
getSubject(servletRequest, servletResponse).login(jwtToken);
//也就是subject.login(token)
} catch (Exception e) {
e.printStackTrace();
onLoginFail(servletResponse);
//调用下面的方法向客户端返回错误信息
return false;
}

return true;
//执行方法中没有抛出异常就表示登录成功
}

//登录失败时默认返回 401 状态码
private void onLoginFail(ServletResponse response) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("login error");
}
}

3. 创建JwtToken

其中封装了需要传递的jwt字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码package cn.coderymy.shiro;

import org.apache.shiro.authc.AuthenticationToken;

//这个就类似UsernamePasswordToken
public class JwtToken implements AuthenticationToken {

private String jwt;

public JwtToken(String jwt) {
this.jwt = jwt;
}

@Override//类似是用户名
public Object getPrincipal() {
return jwt;
}

@Override//类似密码
public Object getCredentials() {
return jwt;
}
//返回的都是jwt
}

4. JwtRealm

创建判断jwt是否有效的认证方式的Realm

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
java复制代码package cn.coderymy.realm;

import cn.coderymy.shiro.JwtToken;
import cn.coderymy.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
@Slf4j
public class JwtRealm extends AuthorizingRealm {
/*
* 多重写一个support
* 标识这个Realm是专门用来验证JwtToken
* 不负责验证其他的token(UsernamePasswordToken)
* */
@Override
public boolean supports(AuthenticationToken token) {
//这个token就是从过滤器中传入的jwtToken
return token instanceof JwtToken;
}

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}

//认证
//这个token就是从过滤器中传入的jwtToken
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

String jwt = (String) token.getPrincipal();
if (jwt == null) {
throw new NullPointerException("jwtToken 不允许为空");
}
//判断
JwtUtil jwtUtil = new JwtUtil();
if (!jwtUtil.isVerify(jwt)) {
throw new UnknownAccountException();
}
//下面是验证这个user是否是真实存在的
String username = (String) jwtUtil.decode(jwt).get("username");//判断数据库中username是否存在
log.info("在使用token登录"+username);
return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
//这里返回的是类似账号密码的东西,但是jwtToken都是jwt字符串。还需要一个该Realm的类名

}

}

5. ShiroConfig

配置一些信息

  1. 因为不适用Session,所以为了防止会调用getSession()方法而产生错误,所以默认调用自定义的Subject方法
  2. 一些修改,关闭SHiroDao等
  3. 注册JwtFilter
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
java复制代码package cn.coderymy.config;

import cn.coderymy.filter.JwtFilter;
import cn.coderymy.realm.JwtRealm;
import cn.coderymy.shiro.JwtDefaultSubjectFactory;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SubjectFactory;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

//springBoot整合jwt实现认证有三个不一样的地方,对应下面abc
@Configuration
public class ShiroConfig {
/*
* a. 告诉shiro不要使用默认的DefaultSubject创建对象,因为不能创建Session
* */
@Bean
public SubjectFactory subjectFactory() {
return new JwtDefaultSubjectFactory();
}

@Bean
public Realm realm() {
return new JwtRealm();
}

@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
/*
* b
* */
// 关闭 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//禁止Subject的getSession方法
securityManager.setSubjectFactory(subjectFactory());
return securityManager;
}

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/unauthenticated");
shiroFilter.setUnauthorizedUrl("/unauthorized");
/*
* c. 添加jwt过滤器,并在下面注册
* 也就是将jwtFilter注册到shiro的Filter中
* 指定除了login和logout之外的请求都先经过jwtFilter
* */
Map<String, Filter> filterMap = new HashMap<>();
//这个地方其实另外两个filter可以不设置,默认就是
filterMap.put("anon", new AnonymousFilter());
filterMap.put("jwt", new JwtFilter());
filterMap.put("logout", new LogoutFilter());
shiroFilter.setFilters(filterMap);

// 拦截器
Map<String, String> filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/logout", "logout");
filterRuleMap.put("/**", "jwt");
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);

return shiroFilter;
}
}

6. 测试

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
java复制代码package cn.coderymy.controller;

import cn.coderymy.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.HashMap;
import java.util.Map;
@Slf4j
@Controller
public class LoginController {

@RequestMapping("/login")
public ResponseEntity<Map<String, String>> login(String username, String password) {
log.info("username:{},password:{}",username,password);
Map<String, String> map = new HashMap<>();
if (!"tom".equals(username) || !"123".equals(password)) {
map.put("msg", "用户名密码错误");
return ResponseEntity.ok(map);
}
JwtUtil jwtUtil = new JwtUtil();
Map<String, Object> chaim = new HashMap<>();
chaim.put("username", username);
String jwtToken = jwtUtil.encode(username, 5 * 60 * 1000, chaim);
map.put("msg", "登录成功");
map.put("token", jwtToken);
return ResponseEntity.ok(map);
}
@RequestMapping("/testdemo")
public ResponseEntity<String> testdemo() {
return ResponseEntity.ok("我爱蛋炒饭");
}

}

4.3 授权方面的信息

在JwtRealm中的授权部分,可以使用JwtUtil.decode(jwt).get("username")获取到username,使用username去数据库中查找到对应的权限,然后将权限赋值给这个用户就可以实现权限的认证了

本文转载自: 掘金

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

SpringBoot系列之邮件发送姿势介绍

发表于 2021-08-04

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

SpringBoot系列之邮件发送姿势介绍

邮件发送,在实际的项目开发中,可能用的不是特别多,如果没有特定的需求,相信也没有多少小伙伴会特意的去关注,那么如果现在我们希望针对项目做一个异常的报警系统,当出现异常的时候,可以向指定的小伙伴发送邮件提醒,那么让我们来实现这个功能,可以怎么办呢?

这里介绍一下如何使用SpringBoot封装好的MailSender来实现邮件发送

I. 项目环境

1. 项目依赖

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

开一个web服务用于测试

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<dependencies>
<!-- 邮件发送的核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- 适用于html模板形式的邮件发送,借助freemarker来实现html模板渲染 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies>

2. 配置

在开始之前,我们需要先准备一个用于发送邮件的账号,比如我这里使用163的邮箱来发送邮件,需要先到邮箱提供商哪里获取授权码,具体如何获取这个东西,不同的邮箱姿势有些不同,各位小伙伴根据自己的实际情况,搜索一下,相信很快就能get到

这里简单介绍下网易邮箱的获取方式

接下来设置发送邮件相关的配置信息,配置文件application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码spring:
#邮箱配置
mail:
host: smtp.163.com
from: xhhuiblog@163.com
# 使用自己的发送方用户名 + 授权码填充
username:
password:
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true

II. 邮件发送

接下来进入正题,我们将从简单基础的文本邮件发送开始,逐渐介绍如何添加附件,使用漂亮的html模板等

1. 简单文本邮件发送

我们这里直接使用JavaMailSender来发送一个基础的文本邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Service
public class MailDemo {
@Autowired
private JavaMailSender javaMailSender;

@Value("${spring.mail.from:xhhuiblog@163.com}")
private String from;

private void basicSend() {
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
//邮件发送人
simpleMailMessage.setFrom(from);
//邮件接收人,可以是多个,参数为可变参数
simpleMailMessage.setTo("bangzewu@126.com");
//邮件主题,也就是标题
simpleMailMessage.setSubject("SpringBoot测试邮件发送");
//邮件内容
simpleMailMessage.setText("简单的邮件正文");

javaMailSender.send(simpleMailMessage);
}
}
  • JavaMailSender: 直接作为一个Spring 的bean对象使用
  • SimpleMailMessage:简单的邮件对象,里面有一些邮件发送时,关联的基础信息
    • from: 发送方
    • replyTo: 邮件回复的收件人
    • to: 收件人
    • cc: 抄送
    • bcc: 密送
    • subject: 主题,也就是邮件标题
    • text: 邮件正文,文本格式
    • date: 邮件发送时间

2. html发送

对于简单的文本邮件发送,用上面的基本就够了,如果我们希望邮件的内容更美观一点的话,可以借助HTML来实现排版

区别于上面的SimpleMailMessage, 这里使用的是MimeMessage,来实现html内容发送

使用姿势与上面相比差不多,无非就是正文变成了html文本罢了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码/**
* 发送html
*/
public void sendHtml() throws MessagingException {
MimeMessage mimeMailMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMailMessage, true);
mimeMessageHelper.setFrom(from);
mimeMessageHelper.setTo("bangzewu@126.com");
mimeMessageHelper.setSubject("SpringBoot测试邮件发送");

//邮件内容
mimeMessageHelper.setText("<h1>Hello World</h1> <br/> " +
"<div> 欢迎点击 <a href=\"https://blog.hhui.top\">一灰灰博文地址</a><br/>" +
" <img width=\"200px\" height=\"200px\" src=\"https://blog.hhui.top/hexblog/imgs/info/wx.jpg\"/>" +
"</div>", true);

javaMailSender.send(mimeMailMessage);
}

重点注意

  • 注意上面的setText方法的第二个参数,必须有,且为true,否则会当成文本内容发送

3. 添加附件

邮件中添加附件,我们自己写邮件的时候可以直接选择附件上传,那么代码的实现方式又有什么区别呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码/**
* 发送附件
*/
public void sendWithFile() throws MessagingException, IOException {
MimeMessage mimeMailMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMailMessage, true);
mimeMessageHelper.setFrom(from);
mimeMessageHelper.setTo("bangzewu@126.com");
mimeMessageHelper.setSubject("SpringBoot测试邮件发送");

mimeMessageHelper.setText("<h1>Hello World</h1> <br/> " +
"<div> 欢迎点击 <a href=\"https://blog.hhui.top\">一灰灰博文地址</a><br/>" +
" <img width=\"200px\" height=\"200px\" src=\"https://blog.hhui.top/hexblog/imgs/info/wx.jpg\"/>" +
"</div>");

String url = "https://blog.hhui.top/hexblog/imgs/info/wx.jpg";
URL imgUrl = new URL(url);
mimeMessageHelper.addAttachment("img.jpg", imgUrl::openStream);

javaMailSender.send(mimeMailMessage);
}

注意上面的实现,与前面差别不大,关键点在于attachment附件,上面的实现是在附件中添加一个图片,为了简单起见,图片是直接从网络下载的,然后将Stream作为传参

4. Freemaker模板

上面的html发送,会发现需要我们自己来组装html正文,这个操作可能就不是很美好了,借助页面渲染引擎来实现邮件模板支持,可以说是一个比较常见的方案了,这里简单介绍下Freemaker的实现姿势,至于themlaf, beef或者jsp啥的,都没有太大的区别

首先写一个邮件模板 resources/template/mail.ftl

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
html复制代码<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot thymeleaf"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>邮件模板</title>
</head>
<style>
.title {
color: #c00;
font-weight: normal;
font-size: 2em;
}

.content {
color: darkblue;
font-size: 1.2em;
}

.sign {
color: lightgray;
font-size: 0.8em;
font-style: italic;
}
</style>
<body>

<div>
<div class="title">${title}</div>
<div class="content">${content}</div>
</div>
</body>
</html>

上面的模板中,定义了两个变量,一个title,一个content,这个就是我们需要替换的值

接下来是邮件发送实例

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
java复制代码import freemarker.template.Configuration;

@Autowired
private Configuration configuration;

/**
* freemarker 模板
*/
public void freeMakerTemplate() throws MessagingException, IOException, TemplateException {
MimeMessage mimeMailMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMailMessage, true);
mimeMessageHelper.setFrom(from);
mimeMessageHelper.setTo("bangzewu@126.com");
mimeMessageHelper.setSubject("SpringBoot测试邮件发送");

Map<String, Object> map = new HashMap<>();
map.put("title", "邮件标题");
map.put("content", "邮件正文");
String text = FreeMarkerTemplateUtils.processTemplateIntoString(configuration.getTemplate("mail.ftl"), map);
mimeMessageHelper.setText(text, true);

String url = "https://blog.hhui.top/hexblog/imgs/info/wx.jpg";
URL imgUrl = new URL(url);
mimeMessageHelper.addAttachment("img.jpg", imgUrl::openStream);

javaMailSender.send(mimeMailMessage);
}

注意上面的实现,关键点就利用FreeMarkerTemplateUtils来实现模板的渲染,输出html正文,因此如果想使用其他的模板渲染引擎,就是改这里即可

5. 测试与小结

最后简单的调用一下上面的实现,看下邮件是否可以发送成功

本篇博文介绍了一下如何发送邮件,并针对简单的文本邮件,html正文,附件等不同的给出了实例;整体看下来使用姿势不难,不过邮件的几个术语可以了解一下

  • to: 接收人,就是邮件发送的目标群众
  • cc: 抄送,一般来讲抄送的名单,只是让他感知到有这封邮件,属于周知对象
  • bcc: 密送,与上面两个不一样,接收人和抄送人不知道密送给谁了,这就是最大的区别,说实话这个玩意我从没用过

接下来一篇博文,将介绍一下如何将log日志与邮件发送关联起来,当出现异常的时候,邮件发送给开发者

III. 不能错过的源码和相关知识点

0. 项目

  • 工程:github.com/liuyueyi/sp…
  • 源码:github.com/liuyueyi/sp…

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰Blog个人博客 blog.hhui.top
  • 一灰灰Blog-Spring专题博客 spring.hhui.top

本文转载自: 掘金

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

基于SMS短信平台给手机发送短信 总结

发表于 2021-08-04

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

JAVA发送手机短信,我知道的有三种方式,恰逢项目需求,自己整理了基于SMS的短信发送,其他两种这里就说说一下

JAVA发送手机短信,我知道的有三种方式,恰逢项目需求,自己整理了基于SMS的短信发送,其他两种这里就说说一下

使用webservice接口发送手机短信,这个可以使用sina提供的webservice进行发送,但是需要进行注册

使用短信mao的方式进行短信的发送,这种方式应该是比较的常用,前提是需要购买硬件设备。

使用中国网建提供的SMS短信平台(申请账号地址:sms.webchinese.cn/default.sht…

新建短信接口处理类

  • 属性:服务器地址(SERVER_IP)+服务器端口(SERVER_PORT)+主账号名称(ACCOUNT_SID)+主账号令牌(ACCOUNT_TOKEN)+应用ID(App_ID)
  • 短信发送方法:

1、首先我们发送短信前需要对新建类的属性进行验证,这些都是发送短信必须的参数。这里我封装验证方法为accountValidate()

2、封装这个短信的方法也需要三个必须的参数(to,templateId,datas),其中datas是一个数组就是我们发送到手机上的内容。to代表的是手机号!templateId是我采用的模板,这里我提供了三种模板,这个后面有时间在详细赘述!

3、把需要的数据封装成两种格式的文本json/xml.最后我们只用将这两种方式的文本内容转换成字符串就可以发送到网络平台了。

1
2
3
4
5
6
java复制代码LoggerUtil.info("sendTemplateSMS Request body =  " + requsetbody);
BasicHttpEntity requestBody = new BasicHttpEntity();
requestBody.setContent(new ByteArrayInputStream(requsetbody.getBytes("UTF-8")));
requestBody.setContentLength(requsetbody.getBytes("UTF-8").length);
httppost.setEntity(requestBody);
HttpResponse response = httpclient.execute(httppost);

4、到这里就发送成功了。但是由于短信平台的限制我们短信每天发送
条数有限

总结

  • 现在实现发送短信大多都是通过已有的基站进行通信的,不可能自己再去搭建基站的。所以在软件层面上我们基本上都是对接已有的服务商,在他们的基础上我们进行定制内容发送。大多都是在他们的基础上通过appid进行分销发送。
  • 但是我们需要知道他们内部是如何实现短信状态告知的。最简单的实现方式是发送后直接告知发送成功。但是此时短信有没有被成功的接收并没有保障
  • 其次就是服务商会定期巡检短信发送状态,发现异常会告知客户端也就是我们,

本文转载自: 掘金

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

Mybatis-Plus,BaseMapper源码分析 抛出

发表于 2021-08-03

抛出疑问

Mybatis-plus的确能让我们写少很多重复代码,非常好用。那么其中最方便的就是Mapper接口继承BaseMapper就能获得增删改查的这个功能。那么这个功能的底层代码,究竟是怎么实现的呢?

原生Mybatis配置的原理

毕竟Mybatis-plus是Mybatis的加强,所以Mybatis-plus肯定是基于Mybatis原来的机制来扩展的,沿着这个思路,我们先搞清楚一个问题,就是原生的mapper.xml文件最后是怎么跟对应的Mapper接口产生联系的。

既然是配置,那么在Mybatis里肯定有对应的配置类,这个类就是MappedStatement。最终在Configuration类中把MappedStatement对象添加进mappedStatements集合中进行管理。源码如下:

1
2
3
4
5
6
7
java复制代码public class Configuration {
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
}

假如有个mapper.xml文件定义如下:

1
2
3
4
5
6
7
8
9
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.yehongzhi.mydemo.mapper.UserMapper">
<select id="getUserList" resultType="com.yehongzhi.mydemo.model.User">
select * from user
</select>
</mapper>

我们用debug模式来验证一下:

由此可以看出,Mybatis底层在解析Mapper.xml文件最后是转成一个MappedStatement对象进行管理。跟着这个思路,我们能不能根据特定的规律创建MappedStatement对象放进mappedStatements集合中,那不就能实现Mybatis-plus的BaseMapper的功能了吗!

Mybatis-plus源码分析

首先找到MybatisPlusAutoConfiguration配置类,会创建MybatisSqlSessionFactoryBean。

并设置MybatisConfiguration作为配置类。

这个MybatisConfiguration是很重要的类,里面会初始化一个mybatisMapperRegistry,后面有用。

1
2
3
4
5
6
xml复制代码public class MybatisConfiguration extends Configuration {
/**
* Mapper 注册
*/
protected final MybatisMapperRegistry mybatisMapperRegistry = new MybatisMapperRegistry(this);
}

当创建MybatisSqlSessionFactoryBean时,会调用afterPropertiesSet()方法创建sqlSessionFactory。

1
2
3
4
5
6
7
java复制代码@Override
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = buildSqlSessionFactory();
}

然后buildSqlSessionFactory()方法的主要内容是解析mapper的xml文件。

然后继续深入,看bindMapperForNamespace()方法。

接着用MapperAnnotationBuilder类进行解析。

接着在parse()方法里进行基本的SQL注入:

关键就在这个SQL注入器里。

所以关键在于AbstractMethod,这里用了模板模式。

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
java复制代码public abstract class AbstractMethod implements Constants {

protected Configuration configuration;
protected LanguageDriver languageDriver;
protected MapperBuilderAssistant builderAssistant;

/**
* 注入自定义方法
*/
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
this.configuration = builderAssistant.getConfiguration();
this.builderAssistant = builderAssistant;
this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
/* 注入自定义方法 */
injectMappedStatement(mapperClass, modelClass, tableInfo);
}

/**
* 注入自定义 MappedStatement
*
* @param mapperClass mapper 接口
* @param modelClass mapper 泛型
* @param tableInfo 数据库表反射信息
* @return MappedStatement
*/
public abstract MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo);
}

利用模板模式,子类只需要重写injectMappedStatement()方法,上面初始化的部分都可以共用。AbstractMethod的子类有很多,我们选个有代表性的看一下,就可以推断其他的用途,比如Insert类。

由此可看出,BaseMapper里的语句信息模板,来自于枚举SqlMethod。

最终就是转成MappedStatement对象,然后添加注册,于是乎就有了这些CRUD操作的方法。

总结

总结一下加载BaseMapper的过程:

  1. 初始化MybatisConfiguration和mybatisMapperRegistry。
  2. 解析Mapper类,获取AbstractMethod集合。
  3. 遍历AbstractMethod集合,然后调用各自实现的injectMappedStatement()方法,注入SQL。
  4. 添加注册MappedStatement对象。

非常感谢你的阅读,希望这篇文章能给到你帮助和启发。

文章持续更新,微信搜索『java技术爱好者』,关注后第一时间收到推送的技术文章,文章分类收录于github:github.com/yehongzhi,总能找到你感兴趣的

觉得有用就点个赞吧,你的点赞是我创作的最大动力~

我是一个努力让大家记住的程序员。我们下期再见!!!

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

本文转载自: 掘金

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

1…580581582…956

开发者博客

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