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

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


  • 首页

  • 归档

  • 搜索

若依系统分页工具学习-PageHelper篇十四

发表于 2021-11-26

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

在前几篇文章中,我们查看了PageHeler中的包com.github.pagehelper以及子包com.github.pagehelper.cache中的类与结构,并且借着cache包中类的写法了解到工厂模式以及java中另外一种类加载与创建对象的方法:Class.forName,以及通过GuavaCache以及SimpleCache熟悉了xxxBuilder创建对象的方式。

读源码就跟读文章一样

今天我们来学习另外一个子包:com.github.pagehelper.page。(按照字母大小应该讲com.github.pagehelper.dialect子包,但是dialect比较复杂,我们暂且放一下,了解了其他部分再反过来看它。)

page包

page包中有4个文件,以及其中的基础注释如下:

PageAutoDialect.java:基础方言信息。

PageBoundSqlInterceptors.java。

PageMethod.java: 基础分页方法。

PageParams.java: 参数信息

我们一一来看。

PageParams

PageParams,参数信息,包括属性包括:offsetAsPageNum(相当于pageNum),rowBoundsWithCount(布尔值,是否进行count查询),pageSizeZero(布尔值,true时不执行分页,返回全部结果),reasonable(布尔值,分页合理化),supportMethodsArguments(布尔值,是否支持接口参数来传递分页参数),countColumn(字符串,默认值0)。
方法除基础的setter/getter外,还有两个方法:

Page getPage(Object parameterObject, RowBounds rowBounds)

以及通过配置来初始对象的方法:

void setProperties(Properties properties)

在setProperties中大量使用了java.util.Properties类的方法,我们来了解一下这个java通用工具。

java.util.Properties实现了接口Map,Serializable, Clonable,并且该类是线程安全的,即多个线程可以共享单个 Properties 对象而无需进行外部同步。最主要的两个方法是:获取属性值getProperty(String key)与设置属性值setProperty(String key, String value)。

其他常用方法还有:从文件加载:load(InputStream), 将配置写入文件store(OutStream, String comments) 等。

PageBoundSqlInterceptors

该类包括一个接口参数值BoundSqlInterceptor.Chain及其getter方法,以及另外一个方法void setProperties(Properties properties)。

通过阅读代码,我们发现setProperties其实就是对Chain的setter方法:通过在参数boundSqlInterceptors上配置多个类名,并通过Class.forName(xx).newInstance()创建其对象实体,之后调用BoundSqlInterceptorChain(Chain接口的一个实现)的构造函数创建一个Chain实体对象。

换句话说,这样就将sql的拦截器按照一定的次序保存在了PageBoundSqlInterceptors.chain中。

本文转载自: 掘金

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

MySQL索引失效之函数操作

发表于 2021-11-26

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

条件字段函数操作

在SQL语句中,对索引字段做函数操作时,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。但这并不是说MySQL放弃使用索引,而只是放弃了索引的树搜索功能,只使用了全索引扫描。

例如sql查每年7月的交易数据

1
sql复制代码select count(*) from tradeLog where month(createTime)=7;

使用explain语句查看执行情况,发现虽然会使用索引,但扫描行数接近全表数据,意味着走了整个索引的所有值。

解决办法是,在SQL改为范围查询。例如

1
2
3
4
csharp复制代码mysql> select count(*) from tradeLog where
-> (createTime >= '2016-7-1' and createTime<'2016-8-1') or
-> (createTime >= '2017-7-1' and createTime<'2017-8-1') or
-> (createTime >= '2018-7-1' and createTime<'2018-8-1');

有时候SQL语句不会改变有序性,优化器也不会考虑使用索引。例如

1
csharp复制代码select * from tradeLog where id+1=10000;

解决办法是,手动改为 id=10000-1即可。

隐式类型转换

1
csharp复制代码mysql> select * from tradeLog where tradeid=1007;

这里tradeid是索引,类型是varchar(32),由于输入的参数是整型,所以需要做类型转换。对于优化器来说,这条SQL语句相当于

1
csharp复制代码mysql> select * from tradeLog where CAST(tradid AS signed int) = 110717;

也就是说,这条语句触发了我们上面说到的规则:对索引字段做函数操作,优化器会放弃走树搜索功能。

这里类型转换的规则是什么,有一个简单的方法,看 select “10” > 9 的结果:

  • 如果规则是“将字符串转成数字”,那么就是做数字比较,结果应该是 1;
  • 如果规则是“将数字转成字符串”,那么就是做字符串比较,结果应该是 0。

这里select “10” > 9 返回的是1,表示为数字比较,需要将字符串转成数字。即要将tradid字段转化为整型去比较。

隐式字符编码转换

当进行联表查询时,如果关联字段对应的两张表各自的字符集不同,可能会将字段进行字符集转换,这里字符集转换用的是内部函数操作,此时优化器会放弃走树搜索的功能。

需要注意的是,字符集不同只是条件之一,连接过程中要求在查询字段(即sql等号左边的字段)的索引字段上加函数操作,是直接导致对被驱动表做全表扫描的原因。

本文转载自: 掘金

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

设计模式之建造者模式

发表于 2021-11-26

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

本篇文章是设计模式专题的第五篇文章,我会将遇到的设计模式都一一总结在该专题下,我会把自己对每一种设计模式的感悟写下来,以及在实际工作中我们该如何去灵活应用这些设计模式,欢迎大家关注。本篇文章我们就来讲一讲,构建复杂对象的建造者模式。

建造者模式的简单介绍

建造者模式和工厂模式的职责比较相像,他们都是为了封装创建对象的创建过程。但是工厂模式的侧重点是为我们提供了一系列具有相同特点对象的创建,而建造者模式关注的是复杂对象的创建,建造者模式可以让我们灵活的编排对象内属性的构建顺序。

建造者模式的类图

image.png

建造者模式的各个角色:

  • 指挥者角色(Director):指挥者的职责,是指挥建造者按照一定顺序完成产品对象的构建。
  • 抽象建造者角色(Builder):为创建一个Product对象的各个部件指定的抽象接口。
  • 具体建造者角色(ConcreteBuilder):实现Builder接口,构造和装配各个部件。
  • 产品角色(Product)

建造者模式的具体实现思路

  • 创建复杂对象。
  • 创建抽象的建造者角色。
  • 创建具体的建造者角色。
  • 创建指挥者。

建造者模式的具体实现方案

  • 创建复杂对象
1
2
3
4
5
6
7
8
9
10
11
typescript复制代码public class Product {
private String partA;

private String partB;

private String partC;
​
public void show(){
       system.out.println(this)
  }
}
  • 创建抽象的建造者角色
1
2
3
4
5
6
7
8
9
10
11
csharp复制代码public interface Builder {

public void setPartA();

public void setPartB();

public void setPartC();

public Product build();
}
​
  • 创建具体的建造者角色
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
typescript复制代码public class ConcreteBuilder implements Builder {

private Product product;

public ConcreteBuilder() {
product = new Product();
}
​
@Override
public void setPartA() {
product.setPartA("Part A");
}
​
@Override
public void setPartB() {
product.setPartB("Part B");
}
​
@Override
public void setPartC() {
product.setPartC("Part C");
}
​
@Override
public Product build() {
return product;
}
​
}
  • 创建指挥者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码public class Director {
private Builder builder;

// 指定建造者
public Director(Builder builder) {
this.builder = builder;
}

public Product construct() {
builder.setPartA();
builder.setPartB();
builder.setPartC();
return builder.build();
}
}

建造者模式的优缺点

优点

  • 封装性好,构建和表示分离,客户端不需要知道对象的构建过程。
  • 低耦合,各个建造者相互独立。
  • 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。

缺点

  • 如果产品内部发生变化,建造者也需要同步修改,维护成本较高。

建造者模式的适用场景

  1. 当一个类构造函数的参数个数超过4个,而且这些参数有部分是可选的参数,就应该考虑使用构造者模式。
  2. 创建复杂对象,拥有多个需要构建的属性。
  3. 建造者模式比较关系对象内部创建的顺序。
  4. 不同顺序构建对象,产生的结果不同。
  5. 建造者创建的对象一般都是由多个部件组成的。

建造者模式总结

总而言之,建造者模式重在打造,复杂对象,该对象由多个部件组成,而且需要对构建这些部件的顺序进行控制,就把房子比作是我们要构建的对象,那么我们在构建房子的时候必须要先打地基,再砌墙,如果不按照一定规则来是盖不成房子的,而这个指挥者就是包工头,或者是房子的设计师,他会让工人按照一定规则去搭建房子。

本文转载自: 掘金

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

函数重载---CPP 什么是函数重载 为什么C语言不支持函数

发表于 2021-11-26

什么是函数重载

C++允许在同一个作用域中声明几个功能类似的同名函数,这些同名函数的形式参数的个数,顺序,类型不同,常常用于实现功能类似,但是数据类型不同的函数。

1
2
3
4
5
6
7
8
9
10
11
12
cpp复制代码int funtion(int a, int b, int c)
{
return a + b + c;
}
int funtion(double a, double b, int c)
{
return a + b + c;
}
int funtion(int a)
{
return a;
}

但是下面的却不构成函数重载

1
2
3
4
5
6
7
8
cpp复制代码nt funtion(int a, int b, int c)
{
return a + b + c;
}
double function(double a, double b, double c)
{
return a + b + c;
}

它们的返回值类型不同

为什么C语言不支持函数重载

这是因为,在C语言中,编译器在编译.c文件时,只会给函数进行简单的重命名;具体的方法是给函数名之前加上”_”;所以加入两个函数名相同的函数在编译之后的函数名也照样相同;调用者会因为不知道到底调用那个而出错;
而在CPP中虽然函数名字是一样的,但是符号表却是不一样的,符号表的命名规则是返回类型+函数名+参数列表
我们来验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c复制代码#include<iostream>
using namespace std;

void print(int i)
{
cout<<"print a integer :"<<i<<endl;
}

void print(string str)
{
cout<<"print a string :"<<str<<endl;
}

int main()
{
print(12);
print("hello world!");
return 0;
}

在这段代码中先用g++编译,再执行我们执行命令objdump -d a.out >log.txt反汇编并将结果重定向到log.txt文件中,然后分析log.txt文件。

发现函数void print(int i) 编译之后为:(注意它的函数签名变为—— _Z5printi)

image.png

发现函数void print(string str) 编译之后为:(注意它的函数签名变为—— _Z5printSs)

image.png
函数的名字变了,所以不存在命名冲突了,而且名字起名是有依据的——_+返回值+函数名+函数参数

如何在c++文件中实验C语言

非常简单 我们在C语言函数前面加上extern”C”就可以了
extern "C" void add(int a, int b) { printf("%d\n", a + b); }

本文转载自: 掘金

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

Python matplotlib 绘制提琴图 复习回顾 1

发表于 2021-11-26

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

复习回顾

matplotlib 是Python专门提供绘制功能的库,我们前面也学习pyplot提供绘制折线、散点、柱状、直方、饼图等常见的图像外,还提供animation类绘制动态图。同时对特殊的图表如应用在物理学上的量场图、关注数据分布情况的箱型图等。以上所述的往期文章例举如下。

  • pyplot.boxplot()方法绘制箱型图,用于展示数据的分布情况:matplotlib 绘制箱型图
  • pyplot.quiver()方法绘制量场图,常用于电磁场分析:matplotlib 绘制量场图
  • pyplot.ion()交互模式结合for循环可以绘制动态图:matplotlib 绘制动态图
  • 使用animation类绘制数据动态图:matplotlib Animation类

继上次我们学习能快速展示一组数据的中位数、上下四分位数、最大小值的箱型图pyplot.boxplot()方法。同样表示数据分布情况的还有一个图形提琴图。

提琴图.png

本期,我们学习matplotlib.pyplot.violinplot绘制提琴图相关属性学习,Let’s go~

  1. 提琴图概述

  • 什么是提琴图?

+ 提琴图是外形类似提琴,具有箱型图和密度图的特征
+ 提琴图是用来显示数据分布和出现概率的密度
+ 提琴图同展示一组数据的中位数、四分位数范围、密度、95%置信区间![image.png](https://gitee.com/songjianzaina/juejin_p6/raw/master/img/22335788499aa4eecfca1a586cc9e44f85d5a4c37f2c10307a7d8513824d7f9c)
  • 提琴图应用场景

+ 提琴图可以用于比较不同样本之间的密度分布情况
+ 提琴图可用于显示数据完整的分布情况
+ 提琴图常用于在制作精确图表前对离散数据整理
+ 提琴图通常用于基因样本分布、人口年龄分布等等
  • 获取提琴图方法

1
2
python复制代码import matplotlib.pyplot as plt 
plt.violinplot(dataset)
  1. 提琴图属性

  • 设置提琴图位置

+ 关键字:vert
+ 默认值为:True
+ 当vert为True时则创建垂直提琴图
+ 当vert为False时则创建水平提琴图
  • 设置提琴宽度

+ 关键字:width
+ 默认值为:0.5
  • 设置提琴数据

+ 设置显示均值关键字:showmeans,默认为False
+ 设置显示极值关键字:showextrema,默认为True
+ 设置显示中值关键字:showmedians,默认为False
  • 设置提琴样式

+ 需要使用pyplot.violinplot()返回字段“bodies”
+ 结合for循环遍历pyplot.violinplot()["bodies"]
+ 调用set\_facecolor设置提琴颜色
+ 调用set\_edgecolor设置提琴边框颜色
+ 调用set\_alpha设置提琴图透明度
+ 调用set\_linestyle设置提琴边框样式
  1. 绘制提琴图步骤

  • 导入matplotlib.pyplot类
1
python复制代码import matplotlib.pyplot as plt
  • 使用numpy库里的normal(),random(),randint()等方法准备data数组向量序列
1
python复制代码data = [np.random.normal(0,2,100)]
  • 调用pyplot.violinplot(data)方法绘制提琴
1
python复制代码part = plt.violinplot(data,showmedians=True)
  • 调用pyplot.show()方法渲染显示提琴图表
1
python复制代码plt.show()

image.png

  • 通过使用yplot.violinplot()方法返回值,设置提琴图的颜色、边框样式等
1
2
3
4
5
6
python复制代码for pc in part["bodies"]:
print(pc)
pc.set_facecolor("pink")
pc.set_edgecolor("black")
pc.set_alpha(1)
pc.set_linestyle("--")

image.png

  1. 小试牛刀

我们学习以matplotlib提供的绘制提琴图violinplot()方法后,我们来实操一下与scatter结合

  • 调用pyplot.grid()方法为画布添加网格
  • 调用numpy.percentile()计算下四分为数,均值数,上四分为数
  • 调用pyplot.scatter()在提琴图上标记均值的符号
  • 调用pyplot.vline()在提琴图上标记四分位数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码data = [np.random.normal(0,i,100) for i in range(2,6)]
plt.grid()
part = plt.violinplot(data,showmedians=True,widths=[0.3,0.4,0.5,0.6])
for pc in part["bodies"]:
print(pc)
pc.set_facecolor("orange")
pc.set_edgecolor("black")
pc.set_alpha(1)
pc.set_linestyle("--")

q1,me,q3 = np.percentile(data,[25,50,75],axis=1)
ind = np.arange(1,len(me)+1)

plt.scatter(ind,me,marker="o",color="r",s=50,zorder=3)
plt.vlines(ind,q1,q3,color="k",linestyles="-",lw=5)

image.png

总结

本期,我们对matplotlib.pyplot.violinplot()方法绘制提琴图相关属性。提琴图结合箱型图和密度图的特点,不仅展示数据的分布情况,还对数据的密度情况也展示出来。

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

本文转载自: 掘金

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

初步了解断言机制 断言机制 组合断言 异常断言 快速失败

发表于 2021-11-26

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

断言机制

编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。 可以将断言看作是异常处理的一种高级形式。断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真。 可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。使用断言可以创建更稳定,品质更好且易于除错的代码。

这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。

接下来,我们就演示一下,我们的断言机制

我们写一个简单的逻辑代码,让我们两个数相加,然后我们通过返回的数和我们理想的数进行判断

1
2
3
4
5
6
7
8
9
csharp复制代码 	@DisplayName("测试简单断言")
void testSimpleAssertions(){
int addition = addition(3, 2);
assertEquals(5,addition);
}

int addition(int i,int j){
return i+j;
}

这是我们断言机制正确运行

注意哦,我们修改了一个数,我们就可以看到我们的正确结果和我们的测试结果有哪里不同,我们也可以自定义我们的错误提示信息

刚刚的这个函数有用来判断我们的返回的值是否相等,assertSame方法用来判断我们的对象是否相同

如果前面断言失败,那么后面的程序都不会执行

组合断言

就是我们判定,只有全部的断言都成功,我们的才算成功,只要有一个断言失败,就全部失败,使用assertAll

1
2
3
4
5
6
7
less复制代码	@Test
@DisplayName("组合断言")
void all(){
assertAll("test",
()->assertTrue(true),
()->assertEquals(1,1));
}

我们的组合断言要是写错误信息的话,要在单个断言中写出

异常断言

这可能就比较诡异了,我们要有错误才能正常运行,没错误就运行不了,我们写了一个数学运算错误,使用异常断言,

1
2
3
4
5
6
7
less复制代码    @Test
@DisplayName("异常断言")
void testException(){
assertThrows(ArithmeticException.class,()->{
int i=0/0;
},"10/0正常?");
}

但是他没有报错,运算正确时确报错了

快速失败

如果突然不想测了怎么办,那就不测了啊,使用快速失败,直接导致测试失败,其实它的用法并不是这样的,一旦我们出现了这个结果,测下去也没有任何意义的时候,我们就可以使用快速失败

1
2
3
4
5
6
scss复制代码    @DisplayName("快速失败")
void testFail(){
if(2==2){
fail("测试失败");
}
}

本文转载自: 掘金

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

「Rust 中方便和习惯性的转换」example2

发表于 2021-11-26

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


Example: PacketType

让我们举一个不同的例子。假设我们现在正在为一个网络协议实现一个库,其中数据包头的第一个字节告诉我们数据包的类型。一个合理的解决方案是用一个枚举来表示数据包类型,其中每个变量都映射到一个数据包类型。比如说:

1
2
3
4
5
6
7
8
9
rust复制代码/// Represents a packet type.
/// Associated with each variant is its raw numeric representation.
enum PacketType {
Data = 0, // packet carries a data payload
Fin = 1, // signals the end of a connection
State = 2, // signals acknowledgment of a packet
Reset = 3, // forcibly terminates a connection
Syn = 4, // initiates a new connection with a peer
}

考虑到这种表示法,我们应该如何转换为字节和从字节转换出去?

传统的方法,在C和C++程序中非常常见,就是简单地将数值从一种类型转换为另一种类型。这在Rust中也可以做到;例如,将 PacketType::Data 转换为字节,就像 PacketType::Data 转换为u8一样简单。这似乎已经解决了将PacketType编码为字节表示的问题,但我们还没有完成。

你注意到每个PacketType变量都有一个相关的值吗?它们定义了变体在生成的代码中的表示。如果我们遵循通常的Rust风格,不给变量分配任何值,那么每个变体的数字表示将取决于它们被声明的顺序,如果我们简单地将枚举变体转换成数字类型,就会导致错误。将枚举变体转换为正确值的一个更好的方法是显式匹配。

1
2
3
4
5
6
7
8
9
10
11
rust复制代码impl From<PacketType> for u8 {
fn from(original: PacketType) -> u8 {
match original {
PacketType::Data => 0,
PacketType::Fin => 1,
PacketType::State => 2,
PacketType::Reset => 3,
PacketType::Syn => 4,
}
}
}

很直接,对吧!由于从PacketType到u8的映射包含在From的实现中,我们可以删除分配给PacketType变体的值,从而得到一个更简洁的枚举定义。

相反的转换?

根据常见问题,将枚举转换为整数可以通过cast实现,正如我们看到的那样。然而,相反的转换可以(而且我认为,在很多情况下,应该)用匹配语句来实现。为了便于使用和更好的工程学,为两个(对称)方向的转换实现 From<T> 通常是个好主意。

将一个 PacketType 转换成 u8 通常是安全和正确的,除了我们之前看到的注意事项,因为对于每个 PacketType 变量,都有一个与 u8 兼容的对应表示。然而,反过来说就不对了:在没有相应的 PacketType 变量的情况下转换u8值是未定义的行为。

尽管我们可以将任何 PacketType 变量映射到u8值中,但我们不能反过来将任何u8映射到PacketType中:u8太多,而 PacketType 不够用!所以对于u8到PacketType的转换,我们不能简单地匹配u8值,并返回相应的PacketType变量。

因此,对于u8到PacketType的转换,我们不能简单地匹配u8值并返回适当的PacketType变体,就像我们对反向转换所做的那样。我们需要一种方法来提示转换失败,但调用panic!()并不是一个可接受的选择。我们需要一个易犯错误的From。

本文转载自: 掘金

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

Spring OAuth2 授权服务器搭建SSO单点登录实践

发表于 2021-11-26

授权服务器版本:

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.0</version>
</dependency>

👉 Spring 授权服务器 GitHub
源码里有几个案例比较适合上手

使用 spring-security-oauth2-authorization-server 搭SSO单点登录需要对SpringSecurity和OAuth2有比较深入的了解。

单点的SpringSecurity配置方面坑不多,值得一提的就是

1
java复制代码http.authorizeRequests().antMatchers("/login/**").permitAll()

而不能是

1
java复制代码http.authorizeRequests().antMatchers("/login").permitAll()

或者

1
java复制代码http.formLogin().permitAll()

因为spring-security-oauth2-authorization-server对OAuth2标准的重定向都是使用各种savedRequest,使用HttpSessionRequestCache缓存,然后结束后跳转,由于security 对于所有不公开访问的 url 都会进行 saveRequest 操作 所以 这里必须放开/login/** 而不是/login,否则错误页面/login?error 也会被缓存至 savedRequest 中,导致用户输错密码时会反复登录,或者覆盖掉oauth2的重定向回调

其他就按照官方案例老老实实配就行,一共两个SpringSecurity的配置,另外一个就是常见Security配置,配置一些登录、登出、rememberMe、cors、csrf之类的就不展示了,在用的OIDC的Security配置如下

1
2
3
4
5
6
7
java复制代码    @Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.formLogin(withDefaults()).logout().and().cors();
return http.build();
}

OIDC登录会重定向至默认Security的登录页面,具体的用户登录判断是在默认Security设置的。

单点登出

重点是logout , OidcProviderConfigurationEndpointFilter 会注册一个标准协议中的/.well-known/openid-configuration端点,但是这个版本的spring授权服务器这个端点返回的元数据不包括end_session_endpoint的信息,故需要客户端自定义登出操作。

客户端如果需要单点登出,就参考org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler自己实现一个,把endSessionEndpoint方法修改一下(或者偷懒让前端发两个登出请求(笑),再或者内部用WebClinet发一个请求到单点服务器上,不让用户跳转了)

1
2
3
4
5
6
7
8
9
10
11
java复制代码    private URI endSessionEndpoint(ClientRegistration clientRegistration) {
if (clientRegistration != null) {
ClientRegistration.ProviderDetails providerDetails = clientRegistration.getProviderDetails();
Object endSessionEndpoint = providerDetails.getConfigurationMetadata().get("issuer");//oidc认证端点的网址
if (endSessionEndpoint != null) {
//组装自己的单点的登出请求地址,可以和SSO的普通登出地址不一致
return URI.create(endSessionEndpoint + "/logout");
}
}
return null;
}

然后在客户端的Security配置里把successHandler注册进去

1
2
3
java复制代码.logout(logout ->
logout.logoutSuccessHandler(mySsoLogoutSuccessHandler))
)

然后在单点服务器这边,如果没有启用csrf那么LogoutFilter会拦截所有请求,如果开启了只会拦截POST,而客户端发起的单点登出实际上是由浏览器的重定向请求(如果登出请求使用Ajax发出的那么浏览器只会发出请求但是不会跳转页面,需要刷新一下页面),且请求中包含了jwt可以通过SpringSecurity的授权过滤器

如果开启csrf,那么需要额外的配置:增加一个登出的controller,因为客户端发起的重定向连接如果没有被LogoutFilter捕获(捕获也就直接登出了)就会到达controller层 在controller层登出是非常简单的,因为org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter对Request进行了包装,直接HttpServletRequest.logout()即可

1
2
3
4
5
6
java复制代码@GetMapping("/logout")
@ResponseBody
public Result<String> logoutWithJson(HttpServletRequest request) throws ServletException {
request.logout();
return Result.ok("登出成功");//Result是我自己的返回类
}

传递用户数据

授权服务器搭起来是比较简单的,坑比较多的是客户端,如果想要客户端通过单点共享用户数据,比如共享一个自定义的UserDetails的实现类UserEntity,但是客户端通过Oauth2登录,SecurityContextHolder.getContext().getAuthentication().getPrincipal()就不是UserDetails的子类而是OidcUser的子类,需要实现一个自定义的OidcUser将UserEntity组装进去,传递UserDetails可以使用资源服务器形式,但是如果需要在客户端的登录认证阶段就把UserEntity特别是其中的authorities加载好,就需要自己实现一个OidcUserService去发出http请求加载,而且不能使用正常的资源服务器的Oauth2请求流程,而是拿着OIDC的AccessToken去找单点交换资源,否则会登录一半重新跳转到资源服务器的登录。

而且UserEntity不能直接被jackson JSON序列化传输,否则要么authorities不序列化,要么SpringSecurity无法加载UserEntity,需要自定义个一个专门用来传输的UserEntityDTO

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
java复制代码public class SsoOidcUserService extends OidcUserService {
private WebClient webClient;
public SsoOidcUserService(OAuth2AuthorizedClientManager authorizedClientManager){
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
webClient = WebClient.builder()
.apply(oauth2Client.oauth2Configuration()).build();
}
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(userRequest);
UserEntity userEntity = getSsoUserEntity(new OAuth2AuthorizedClient(userRequest.getClientRegistration(),oidcUser.getName(),userRequest.getAccessToken()));
//....
}
public UserEntity getSsoUserEntity(OAuth2AuthorizedClient oAuth2AuthorizedClient) {
UserEntity entity = null;
try {
entity = UserEntityDTO.toUserEntity(webClient
.get()
.uri(oAuth2AuthorizedClient.getClientRegistration().getProviderDetails().getIssuerUri()+"/你的User rest请求端点")
.attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(oAuth2AuthorizedClient))
.retrieve()
.bodyToMono(UserEntityDTO.class)
.block());
} catch (Exception e) {
e.printStackTrace();
}
return entity;
}
}

前后端分离

然后就是前后端分离的一些问题,前后端分离,访问请求是Ajax发起的,但是Ajax发起的请求不能被302重定向,需要和前端约定好客户端需要登录时返回JSON,让前端自行重定向。后端实现一个AuthenticationEntryPoint塞到SpringSecurity设置里。 这点跟postman很像,postman测试单点也是不行的,重定向请求会直接后台默默转发,直到401,点一下postman,以为只请求了一次,实际上postman控制台显示发出了n次请求,没有一点提示,也很坑

然后就是登录成功后跳转的问题,Spring OAuth2 登陆完会从HttpSessionRequestCache取出savedRequest重定向,这样的问题就是登陆完,页面重定向到了后端接口,显示一大堆JSON数据,这样肯定是不行的,可以自定义AuthenticationSuccessHandler,塞到loginSuccessHandler里,跳转到固定页面,或者还是在AuthenticationEntryPoint里,和前端约定好请求里都带上一个自定义的header,值是JS代码window.location.href,然后在org.springframework.security.web.AuthenticationEntryPoint#commence方法执行的时候从header里面取出来,替换掉Session中的savedRequest,替换方法就不展示了,HttpSessionRequestCache怎么存的就怎么替换。

然后就是Nginx反向代理,代理时需要设置

1
bash复制代码proxy_set_header Host   $http_host;

否则org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint#buildRedirectUrlToLoginPage方法里request.getServerName()就会取到的是Nginx主机地址而不是原始host地址

顺带一提,如果开发阶段用的IP地址测试,和域名之间是不同的,如果用了域名,但是不用Nginx反向代理,前端页面、后台、单点登录三者不同端口号会导致Cookies不能正确传递,一登录完就丢失会话信息,但是数字ip的不同端口号却又能正确传递Cookies…

开发用了很久,开发完了,部署阶段也被坑了好多次,花了一个星期时间才算部署上去

本文转载自: 掘金

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

(实用)防火墙软件firewall的常用命令 结束语

发表于 2021-11-26

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

一.firewall简要介绍

firewall是Centos7默认启用的防火墙工具

firewalld通过定义的在/usr/lib/firewalld下面的xml配置信息,在启动时自动载入默认iptables配置,并应用到系统中,当使用firewalld-cmd添加防火墙规则时,它实际是转换成iptables规则后,再应用到系统中

firewall-cmd是firewalld客户端命令行,可以用来控制firewalld

此文章的定义是实用

二.firewal启动,停用,关闭,查看状态

1.查看状态

systemctl status firewalld 或者 firewall-cmd --state

2.启动

systemctl start firewalld

3.关闭

systemctl stop firewalld

4.停用

systemctl disable firewalld

5.其他

开机启动systemctl enable firewalld

查看已启动的服务列表systemctl list-unit-files|grep enabled

查看启动失败的服务列表systemctl --failed

三.查看firewall所有的策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码$ firewall-cmd --list-all

target: default
icmp-block-inversion: no
interfaces: enp0s3 enp0s8
sources:
services: ssh dhcpv6-client
ports: 80/tcp 8099/tcp 8098/tcp
protocols:
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:

四.添加删除ip和端口

--permanent永久生效,没有此参数重启后失效

1.更新防火墙规则

1
css复制代码firewall-cmd --reload

修改之后必须重新载入

2.端口相关

1).添加一个永久端口

1
css复制代码firewall-cmd --add-port=80/tcp --permanent

(此处添加的是80端口)

2).删除一个端口

1
css复制代码firewall-cmd --remove-port=80/tcp --permanent

3).查看所有打开的端口

1
css复制代码firewall-cmd  --list-ports

此小结均没有添加 zone 参数, 如不添加 默认为[–zone=public]

3.ip相关

1).禁止允许特定ip访问8080端口

1
ini复制代码firewall-cmd --permanent  --add-rich-rule="rule family="ipv4" source address="192.168.0.4/24" port protocol="tcp" port="8080" reject"

2).允许特定ip访问8080端口

1
ini复制代码firewall-cmd --permanent --add-rich-rule="rule family="ipv4" source address="192.168.0.4/24" port protocol="tcp" port="8080" accept"

3).删除规则

1
ini复制代码firewall-cmd --permanent --remove-rich-rule="rule family="ipv4" source address="192.168.0.4/24" port protocol="tcp" port="8080" accept"

将add变成remove即为删除规则

五.转发

1).开启伪装IP

1
css复制代码firewall-cmd --permanent --add-masquerade

2).将80端口的流量转发至8080

1
ruby复制代码firewall-cmd --add-forward-port=port=80:proto=tcp:toport=8080  --permanent

3).将80端口的流量转发至192.168.0.1

1
ruby复制代码firewall-cmd --add-forward-port=proto=80:proto=tcp:toaddr=192.168.0.1  --permanent

4).将80端口的流量转发至192.168.0.1的8080端口

1
ruby复制代码firewall-cmd --add-forward-port=proto=80:proto=tcp:toaddr=192.168.0.1:toport=8080  --permanent

结束语

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

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

1
2
3
4
arduino复制代码    作者:ZOUZDC
链接:https://juejin.cn/post/7028963866063306760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文转载自: 掘金

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

Verilog 实现一个简单 CPU 计算器

发表于 2021-11-26

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

写在前面

  • 实验在不用 [+,-, *,>,<] 符号的基础之上实现有符号数的加法、减法、乘法,逻辑运算、比较 功能。【计算机组成与设计】实验—-PS: 今天是安装 vivido 的第六天,几乎从零开始的,太痛苦了。
  • 因为代码太长了,下面只放了部分代码,完整代码我放在了 另外一篇博客 了哈。

1、实验任务

  • 本次实验需要实现一个 CPU 的运算器。简易运算器由三个 8 位寄存器 R0、R1、R2 和一个算术逻辑单元(ALU)构成,其中 ALU 应该至少支持加法、减法、乘法,按位与、按位或、按位异或、逻辑非运算。
  • 输入由开关控制;每一步运算后,相应标志位(标志位设置同实验四)的情况通过 LED灯表示;运算结果以十进制通过数码管显示。读取数据的结果以十进制通过数码管显示。时钟信号、复位信号等控制信号允许用开关控制。
  • 注:1. 本次实验涉及的数据皆为补码。2. 实验说明给出的默认指令集由定长指令构成,其中指令的操作码为变长操作码。

2、实验说明

  • 实验使用八位二进制串( b7b6b5b4b3b2b1b0)表示指令,与开发板上的八个开关对应。实验使用四位二进制补码作为输入,与开发板上的四个开关对应。三个寄存器 R0,R1,R2 分别对应二进制地址码 00、01、10。
  • 注:运算结果始终默认存放至寄存器 R2 因此不在指令中显式指出。有符号乘法的结果若超过 8 比特应当解释为溢出。

1、运算指令格式

  • 高四位(b7b6b5b4)为操作码,次低两位(b3b2)为地址码指明存放第一个操作数的寄存器,最低两位(b1b0)为地址码指明存放第二个操作数的寄存器。
  • 运算指令的操作码(b7b6b5b4): 运算操作
  • 0000 ——-> A + B
  • 0001 ——-> A + 1
  • 0010 ——-> A - B
  • 0011 ——-> A - 1
  • 1100 ——-> A * B
  • 0100 ——-> 按位与
  • 0101 ——-> 按位或
  • 0110 ——-> 按位异或
  • 0111 ——-> A >= B ?

2、存储指令格式

  • 最高两位(b7b6)为操作码置 10,次高两位(b5b4)为写入寄存器的地址码,低四位(b3b2b1b0)为待写入数据的二进制补码。

3、读取指令格式

  • 最高六位(b7b6b5b4b3b2)为操作码置 111100,最低两位(b1b0)为地址码置 10。要求该指令可读取寄存器 R2 的值并以十进制通过数码管显示。

3、主要实验代码以及注解

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
224
225
226
227
228
229
230
231
232
ini复制代码module adder(
input ia,
input ib,
input cin,
output cout,
output cur
);
assign cur = ia ^ ib ^ cin; //当前位
assign cout = ia & cin | ib & cin | ia & ib; // 进位
endmodule

module add_8(
input [7:0] a,
input [7:0] b,
input cin,
output [7:0] s,
output o, // overflow,
output cout
);
wire c0,c1,c2,c3,c4,c5,c6,c7;
adder d0(a[0],b[0],cin,c0,s[0]);
adder d1(a[1],b[1],c0,c1,s[1]);
adder d2(a[2],b[2],c1,c2,s[2]);
adder d3(a[3],b[3], c2,c3,s[3]);
adder d4(a[4],b[4],c3,c4,s[4]);
adder d5(a[5],b[5],c4,c5,s[5]);
adder d6(a[6],b[6],c5,c6,s[6]);
adder d7(a[7],b[7],c6,c7,s[7]);
assign cout = c7;
assign o = (a[7] & b[7] & ~s[7]) | (~a[7] & ~b[7] & s[7]); // 溢出
endmodule

module add_sub_8(
input [7:0] a,
input [7:0] b,
input cin,
input op,
output [7:0] res,
output overflow,
output cout
);
add_8 d4(a,{op,op,op,op,op,op,op,op} ^ b,op ^ cin,res,overflow,cout);
endmodule

module slt(
input [7:0] a,
input [7:0] b,
output result
);
wire o, c;
wire [7:0] r;
add_sub_8 sub(a, b, 0, 1, r, o, c);
assign result = o ^ r[7];

endmodule



module shift(
input [7:0] a,
input b,
input [1:0]m,
output [7:0] out
); // 将相与的值向左移动 m 位
assign out = b == 1'b0 ? 8'b0000_0000 : ((8'b1111_1111 & a) << m);

endmodule

module mult(
input [3:0] a,
input [3:0] b,
output [7:0] out
);
wire [7:0] abs_a; // a 的绝对值
wire [7:0] abs_b; // b 的绝对值
wire flag;
assign flag = a[3] ^ b[3]; // 通过对符号位的异或取得乘积的符号
wire [3:0] t1;
wire [3:0] t2;
wire o1,c1,o2,c2,o3,c3,o4,c4,o5,c5,o6,c6;
add_sub_8 m0(a,1,0,1,t1,o1,c1); // a - 1
add_sub_8 m1(b,1,0,1,t2,o2,c2); // b - 1
assign abs_a[7:4] = 4'b0000; // 绝对值都是正数,所以拓展成 8 位后,前面四位都要补上 0
assign abs_b[7:4] = 4'b0000;
assign abs_a[3:0] = a[3] == 1'b1 ? ~t1 : a; // 取绝对值完成
assign abs_b[3:0] = b[3] == 1'b1 ? ~t2 : b;
wire [7:0] T1;
wire [7:0] T2;
wire [7:0] T3;
wire [7:0] T4;
shift h1(abs_a,abs_b[0],2'b00,T1); // 四位数乘法转换成 四次加法运算
shift h2(abs_a,abs_b[1],2'b01,T2);
shift h3(abs_a,abs_b[2],2'b10,T3);
shift h4(abs_a,abs_b[3],2'b11,T4);

wire [7:0] T5;
wire [7:0] T6;
wire [7:0] res;
wire [7:0] T7;
add_sub_8 m2(T1,T2,0,0,T5,o3,c3); // T5 = T1 + T2
add_sub_8 m3(T5,T3,0,0,T6,o4,c4); // T6 = T5 + T3
add_sub_8 m4(T6,T4,0,0,res,o5,c5); // res = T6 + T4

add_sub_8 m5(~res,1,0,0,T7,o6,c6); // T7 = ~res + 1 即是取 res 的补码
assign out = flag == 1'b1 ? T7 : res; // 由乘积符号标志,确定输出的是原值还是它的补码
endmodule







module CPU(
input read_to_R2, // 是否把值存入 R2
input alu, // 控制是否开启 alu 部分计算
input clk, // 时钟
input [7:0] b, // 数据源
output reg [7:0] led_id,
output reg [6:0] out_led, // 数码管显示
output ZF, // 运算结果全零,则为 1
CF, // 进借位标志位
OF, // 溢出标志位
SF, // 符号标志位,与 F 的最高位相同
PF // 奇偶标志位,F 有奇数个 1,则 PF=1,否则为 0
);
reg [7:0] R0; // 对应地址码 00
reg [7:0] R1; // 对应地址码 01
reg [7:0] R2; // 对应地址码 10

reg [7:0] data_to_R2; // 在 alu 计算 和 存入 R2 的值的零时变量

wire [7:0] res1,res2,res3,res4,res5;
wire res9;
wire OF1,OF2,OF3,OF4;
wire CF1,CF2,CF3,CF4;
reg of,cf;

reg [7:0] A1;
reg [7:0] B1;
wire [7:0] A; // 第一个源数据
wire [7:0] B; // 第二个源数据
assign A = A1;
assign B = B1;

add_sub_8 fun1(A,B,0,0,res1,OF1,CF1); // res1 = A + B
add_sub_8 fun2(A,1,0,0,res2,OF2,CF2); // res2 = A - 1
add_sub_8 fun3(A,B,0,1,res3,OF3,CF3); // res3 = A - B
add_sub_8 fun4(A,1,0,1,res4,OF4,CF4); // res4 = A - 1;
mult fun5(A[3:0],B[3:0],res5); // res5 = A * B
slt fun9(A,B,res9); // 比较 A 和 B 的大小,A >= B, res9 = 0; A < B, res9 = 1;


always@(b) begin

if (read_to_R2 == 1'b1) R2 = data_to_R2; // 是否将临时变量的值存如 R2

if (b[7:6] == 2'b10 && alu == 1'b0) begin // 写入数据进入寄存器
if (b[5:4] == 2'b00) begin // 根据地址码,存入值
if (b[3] == 1'b1) R0[7:4] = 4'b1111; // 根据写入数(补码)最高位,判断并 4 位长拓展为 8 位,下同
else R0[7:4] = 4'b0000;
R0[3:0] = b[3:0];
end
else if (b[5:4] == 2'b01) begin
if (b[3] == 1'b1) R1[7:4] = 4'b1111;
else R1[7:4] = 4'b0000;
R1[3:0] = b[3:0];
end
else if (b[5:4] == 2'b10) begin
if (b[3] == 1'b1) begin
R2[7:4] = 4'b1111;
data_to_R2[7:4] = 4'b1111;
end
else begin
R2[7:4] = 4'b0000;
data_to_R2[7:4] = 4'b0000;
end
R2[3:0] = b[3:0];
data_to_R2 = b[3:0];
end
end
else if (alu == 1'b1) begin
case(b[3:2])
2'b00: A1 = R0;
2'b01: A1 = R1;
2'b10: A1 = R2;
endcase
case(b[1:0])
2'b00: B1 = R0;
2'b01: B1 = R1;
2'b10: B1 = R2;
endcase
case(b[7:4]) // 以下模拟 alu 部分进行取出对应状态计算的结果和状态
4'b0000: begin
data_to_R2 = res1;
of = OF1;
cf = CF1;
end
4'b0001: begin
data_to_R2 = res2;
of = OF2;
cf = CF2;
end
4'b0010: begin
data_to_R2 = res3;
of = OF3;
cf = ~CF3;
end
4'b0011: begin
data_to_R2 = res4;
of = OF4;
cf = ~CF4;
end
4'b1100: data_to_R2 = res5;
4'b0100: data_to_R2 = A & B;
4'b0101: data_to_R2 = A | B;
4'b0110: data_to_R2 = A ^ B;
4'b0111: data_to_R2 = res9;
endcase
end
end


// 数码管灯显示数据
wire [7:0] n;
assign n = b[7:0] == 8'b1111_0010 ? R2 : // 读取 R2 的值
b[7:0] == 8'b1111_0000 ? R0 : // 读取 R0 的值
b[7:0] == 8'b1111_0001 ? R1 : // 读取 R1 的值
b[7:0] == 8'b1111_0011 ? data_to_R2 : // 读取储存 R2 值的中间变量
b[7:0] == 8'b1111_0100 ? A : // 读取第二个源数据
b[7:0] == 8'b1111_0101 ? B : // 读取第二个源数据
8'b0000_0000;

4、遇到的坑

  • assign 类型后面的连续赋值 等号左边 必须是 wire 类型的,等号右边可以是 reg 类型或者 wire 类型都可以。
  • always 块里面 等号左边 必须是 reg 类型的,等号右边 可以是 reg 类型或者 wire 类型。
  • 每个 alway 块里面的代码是并发的,当敏感列表发生变化,就会执行一次,所以在使用时要格外小心。

vivido ! bye!

本文转载自: 掘金

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

1…166167168…956

开发者博客

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