轻量级 SSO 方略:基于 OIDC 规范(二)

news2024/11/19 17:48:52

上一篇文章介绍了 SSO 相关的基础数据,这样有了 ClientId 和密钥后,我们就要准备客户端这边的代码。客户端当前指的便是一个网站(也就是 RP),这个网站要求有会员功能,典型地网站导航上通常会有“注册”或“登录”的链接。

在这里插入图片描述
假设我们这是最简单的网站,采用 Servlet Session 本地记录用户凭证。本身这个网站没有设计用户的模块,得通过 SSO 完成用户登录。

RP 的 Yaml 配置文件如下:

user:
  clientId: G5IFeG7Eesbny3f
  clientSecret: J1Bb4zhchfziuDipKI7sgo6iyk
  loginPage: http://127.0.0.1:8888/iam/oidc/authorization # 登录页面地址

另外我们还要准备一个登录页面,简单的例子如下。

在这里插入图片描述
这个页面存在 SSO 中心(也就是 OP)的,而不是客户端的。

标准 的 OIDC 流程

好,下面正式进入 OIDC 登录的流程,我们先大概了解一下整个流程。

OIDC 授权码模式的认证流程中涉及三方:用户、OIDC 服务器(OpenID Provider/OIDC Provider,简称 OP)、客户端业务应用(Relying Party,简称 Client)。用户、OP、RP 的交互目的分为以下几点:

  1. Client 希望用户登录,从而拿到用户身份信息。
  2. Client 发起登录,会跳转到 OP 的认证页面,OP 让用户登录,并授权自己的信息,然后 OP 将一个授权码(code)发给 Client。实际上这是在通过引用来传递用户信息。
  3. Client 收到授权码 code 后,结合 Client ID 和 Client Secret 到 OP 换取该用户的 access_token。
  4. Client 利用 access_token 到 OP 去获取用户的相关信息,从而得到一个可信的身份断言,让用户登录。

下面我们再逐一详解。

客户端发起登录

一开始,用户并未在客户端网站上登录。当用户点击“登录”按钮要求登录的时候,RP(客户端)的后台接口/user/login会组装所需的参数,形成 URI 返回303 Location=该 URI,告诉浏览器重定向到 OP /oidc/authorization

这是 RP 的接口,执行上述过程。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import com.ajaxjs.util.StrUtil;

@RestController
@RequestMapping("/user")
public class UserController {
	@Value("${user.loginPage}")
	private String userLoginCode;

	@Value("${user.clientId}")
	private String clientId;

	@GetMapping("/login")
	public ModelAndView get() {
		String url = userLoginCode + "?response_type=code&client_id=" + clientId;
		url += "&redirect_uri=http://localhost:8080/mywebsite/user/auth_code";
		url += "&state=" + StrUtil.getRandomString(6);

		return new ModelAndView(new RedirectView(url));
	}
}

浏览器的重定向,即返回:

HTTP/1.1 303 See Other
Location: http://127.0.0.1:8888/iam/oidc/authorization
?redirect_uri=http://localhost:8080/mywebsite/user/auth_code
&response_type=code
&state=J1Bb4zhchfziuDipKI7sgo6iyk
&client_id=G5IFeG7Eesbny3f

参数说明

参数说明
redirect_uri用户登录成功后,OP 回传授权码等信息给RP的接口,相当于回调地址
response_type固定值 code,表示授权码流程
client_idRP 在 OP 注册的 client_id
state不透明字符串,当 OP 重定向到 redirect_uri 时,会原样返回给 RP,用于防止 CSRF、 XSRF。由于 OP 会原样返回此参数,可将 state 值与用户在 RP 登录前最后浏览的 URI 绑定,便于登录完成后将用户重定向回最后浏览的页面

发送授权码

OP 端的/oidc/authorization接口里有这么两个分支的判断:

  • 发现用户未登录,返回 HTTP 303,通过浏览器重定向到登录页面
  • 用户已登录,于是执行授权逻辑,签发授权码

判断是否已登录

怎么判断没有登录?没有携带特定的 cookie 便是表明用户未登录。一般 Java Web 的是 JSESSIONID,这样的话可以在后台直接使用 Session 对象(比较简单,但只是“单机”)。如果是 Redis 方案,也要使用 Cookie,并且要把 Cookie 与 Redis 缓存对应起来。


参考代码如下。

接口定义。

/**
 * 1、发现用户未登录,返回 303,通过浏览器重定向到登录页面
 * 2、用户已登录,于是执行授权逻辑,签发授权码
 *
 * @param req  请求对象
 * @param resp 响应对象
 */
@GetMapping("/authorization")
void authorization(
        @RequestParam("response_type") String responseType,
        @RequestParam("client_id") String clientId, @RequestParam("redirect_uri") String redirectUri, @RequestParam(required = false) String scope, @RequestParam String state,
        HttpServletRequest req, HttpServletResponse resp);

在这里插入图片描述

用户登录

若是未登录则跳到登录页面,则是/iam/login/?response_type=code&client_id=G5IFeG7Eesbny3f&redirect_uri=http://localhost:8080/mywebsite/user/auth_code&state=tMpnBJ

我们在跳转的时候加入一个提示的页面,让用户感知更好。

在这里插入图片描述

用户输入账密,表单 AJAX 提交时触发 OP 验证账密接口 POST op.com/user/login。下面是一个比较简单的例子,注意要附上所有的 QueryString 参数(location.search)。

在这里插入图片描述

如果账密正确则重定向回 OP 授权接口(返回 303 Location=步骤1 的 Location,即/oidc/authorization),并设置 OP 的会话状态(设置 cookie)。

发送授权码

此时再度进入/oidc/authorization,发现用户成功登录了于是就可以发送授权码,返回303 Location=redirect_uri,浏览器重定向到 redirect_uri。这样 RP 就可以得到授权码。

接口定义(跟前面的一样)。

/**
 * 1、发现用户未登录,返回 303,通过浏览器重定向到登录页面
 * 2、用户已登录,于是执行授权逻辑,签发授权码
 *
 * @param req  请求对象
 * @param resp 响应对象
 */
@GetMapping("/authorization")
void authorization(
        @RequestParam("response_type") String responseType,
        @RequestParam("client_id") String clientId, @RequestParam("redirect_uri") String redirectUri, @RequestParam(required = false) String scope, @RequestParam String state,
        HttpServletRequest req, HttpServletResponse resp);

在这里插入图片描述
生成授权码的规则是 clientId 加时间戳再 SHA1 得到 code,注意要把 code 和当前用户对应起来,然后存到 5 分钟超时的缓存中。

返回给 RP 类似于这样的响应:

HTTP/1.1 303 See Other
Location: http://rp.com/user/callback
?state=DJOfvYDSDxaPzOKR
&code=Z0FBQUFBQmVjc

为什么 OIDC 授权码流程要 code 换 token 再换用户信息?

答案出处。

OIDC 协议中,用户登录成功后,OIDC 认证服务器会将用户的浏览器回调到一个回调地址,并携带一个授权码(code)。此授权码一般有效期十分钟且一次有效,用后作废。这避免了在前端暴露 access_token 或者用户信息的风险,access_token 的有效期都比较长,一般为 1~2 个小时。如果泄露会对用户造成一定影响。

后端收到这个 code 之后,需要使用 Client Id + Client Secret + Code 去 OIDC 认证服务器换取用户的 access_token。在这一步,实际上 OIDC Server 对 OAuth Client 进行了认证,能够确保来 OIDC 认证服务器获取 access_token 的机器是可信任的,而不是任何一个人拿到 code 之后都能来 OIDC 认证服务器进行 code 换 token。

如果 code 被黑客获取到,如果他没有 Client Id + Client Secret 也无法使用,就算有,也要和真正的应用服务器竞争,因为 code 一次有效,用后作废,加大了攻击难度。相反,如果不经过 code 直接返回 access_token 或用户信息,那么一旦泄露就会对用户造成影响。

获取 Token

RP 后台请求 Token

RP 接收到 OP 回传的 code 参数后,接着就是向 OP /oidc/token接口获取 AccessToken 令牌,也就是用 code 换 token。注意这是在服务端请求完成的(此处使用 Spring RestTemplate 请求)。

其中请求头Authorization字段通过Basic关键字传递 RP 在 OP 注册的client_idclient_secret。请见下面源码:

@Value("${user.tokenApi}")
private String tokenApi;

@RequestMapping("/callback")
public void token(@RequestParam String code, @RequestParam(required = false) String state) {
	RestTemplate restTemplate = new RestTemplate();
//        restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));// basic 认证

	HttpHeaders headers = new HttpHeaders();
	headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
	headers.setBasicAuth(clientId, clientSecret);

	MultiValueMap<String, String> bodyParams = new LinkedMultiValueMap<>();
	bodyParams.add("grant_type", "authorization_code");
	bodyParams.add("code", code);
	bodyParams.add("state", state);

	ResponseEntity<String> responseEntity = restTemplate.exchange(tokenApi, HttpMethod.POST,
			new HttpEntity<>(bodyParams, headers), String.class);


	System.out.println(responseEntity);
	// TODO 获取 Token 的后续工作……
//        if (responseEntity.getStatusCode().is2xxSuccessful()) {
//            // 处理授权成功的逻辑,例如解析并保存访问令牌和刷新令牌等
//            return "success";
//        } else {
//            // 处理授权失败的逻辑
//            return "error";
//        }
}

具体传参方式是将client_idclient_secret通过 ‘:’ 号拼接,并使用 Base64 进行编码得到字符串。将此编码字符串放到请求头 Authorization 去发送请求。不过借助框架的封装headers.setBasicAuth(clientId, clientSecret),很简单就完成了。这个封装相当于下面的逻辑:

public void requestWithBasic(String clientId, String clientSecret) {
    String clientAndSecret = clientId + ":" + clientSecret;
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    headers.set("Authorization", "Basic " + StrUtil.base64Encode(clientAndSecret)); // 请求头

    MultiValueMap<String, Object> bodyParams = new LinkedMultiValueMap<>();
    bodyParams.add("grant_type", "client_credentials");

    ResponseEntity<String> responseEntity = restTemplate.exchange(tokenEndPoint, HttpMethod.POST, new HttpEntity<>(bodyParams, headers), String.class);

    if (responseEntity.getStatusCode().is2xxSuccessful()) {

    }
}

好,我们看看正式请求是怎么样子的:

POST op.com//oidc/token
Authorization: Basic cUZFeFZtQlE4blBZOjVjNWVkYjA2OTA2MTZjZGJkNGNmOWMwYjBlMjg3MWVkNjM2MzE2Z
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&state=DJOfvYDSDxaPzOKRoyaT
&code=Z0FBQUFBQm

有文章说要传redirect_uri参数,——我这是在服务器请求,跳转对我没用呀,——不知为何要传呢?而且这个接口已经返回 token 给 RP 了,要跳转也是我 RP 的事情呀。

最后,获取 Token 的后续工作……这里是个服务端的请求,暂且不表,看看 OP 如何颁发 Token 先。

OP 颁发 Token

颁发 Token 是/oidc/token,定义如下:

/**
 * 获取 Token
 *
 * @param authorization client 信息
 * @param code          授权码
 * @param state         不透明字符串
 * @param grantType     授权码流程
 * @return 令牌 Token
 */
@PostMapping("/token")
Result<JwtAccessToken> token(@RequestHeader String authorization, @RequestParam String code, @RequestParam String state, @RequestParam("grant_type") String grantType);

要实现的逻辑如下:

  1. 校验 RP 在请求头authorization字段通过 HTTP Basic 认证传入的client_idclient_secret
  2. 从缓存中,根据 code 获取对应的用户
  3. 如果都校验通过,则生成 access token、id token 并返回

OidcService源码:

在这里插入图片描述

返回 JSON 如下例子:

HTTP/1.1 200 OK
Content-Type: application/json

{
	"state": "DJOfvYDSDxaPzOKRoyaTaQWCoWywdeKU", 
	"scope": "openid profile email address phone", 
	"access_token":"Z0FBQUFBQmVjdWxLcFBWVn", 
	"token_type": "Bearer", 
	"id_token":"eyJhbGciOiJSUzI1NiIsIm……"
}

RP 构建自身的会话状态

RP 构建自身的会话状态(设置一个 cookie,表明用户已在 RP 登录)。找出 state 绑定的 URI ,将用户重定向回登录前最后浏览的页面。

@RequestMapping("/callback")
public ModelAndView token(@RequestParam String code, @RequestParam(required = false) String state,
		HttpSession session) {
//        restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));// basic 认证

	HttpHeaders headers = new HttpHeaders();
	headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
	headers.setBasicAuth(clientId, clientSecret);

	MultiValueMap<String, String> bodyParams = new LinkedMultiValueMap<>();
	bodyParams.add("grant_type", "authorization_code");
	bodyParams.add("code", code);
	bodyParams.add("state", state);

	ResponseEntity<JwtAccessToken> responseEntity = getRestTemplate().exchange(getTokenApi(), HttpMethod.POST,
			new HttpEntity<>(bodyParams, headers), new ParameterizedTypeReference<JwtAccessToken>() {
			});

//		ResponseEntity<String> responseEntity = getRestTemplate().exchange(getTokenApi(), HttpMethod.POST,
//				new HttpEntity<>(bodyParams, headers), String.class);

	if (responseEntity.getStatusCode().is2xxSuccessful()) {// 处理授权成功的逻辑,例如解析并保存访问令牌和刷新令牌等
		onAccessTokenGot(responseEntity.getBody(), session);

		return new ModelAndView("redirect:/");
	} else {
		System.out.println(responseEntity);
//			 处理授权失败的逻辑
		throw new SecurityException("获取 JWT Token 是吧");
	}
}

@Value("${user.jwtSecretKey}")
private String jwtSecretKey;

@Bean
JWebTokenMgr jWebTokenMgr() {
	JWebTokenMgr mgr = new JWebTokenMgr();
	mgr.setSecretKey(jwtSecretKey);

	return mgr;
}

@Override
public void onAccessTokenGot(JwtAccessToken token, HttpSession session) {
	String idToken = token.getId_token();
	JWebTokenMgr mgr = jWebTokenMgr();
	JWebToken jwt = mgr.parse(idToken);

	if (mgr.isValid(jwt)) {
		User user = new User();
		user.setId(Long.parseLong(jwt.getPayload().getSub()));
		user.setName(jwt.getPayload().getName());
		
		AccessToken accessToken = new AccessToken();
		BeanUtils.copyProperties((AccessToken) token, accessToken);

		user.setAccessToken(accessToken);

		session.setAttribute(UserLogined.USER_IN_SESSION, user);
	} else
		throw new SecurityException("返回非法 JWT Token");
	}

RP 整合 SSO,实际上有 SDK 提供的,是为 aj-iam-client。

获取用户信息

JWT 所包含的用户信息当前只有 userId 和 userName,若要获取用户更多的信息如手机号码的,可以查询用户信息接口。使用 Access Token 在后台向 OP 的用户详情接口GET op.com/userinfo发请求,获取用户详细信息。其中请求头authorization字段使用Bearer关键字传递 Access Token,注意这是 AccessToken 而非 JWT Token。

GET op.com/userinfo
authorization: Bearer Z0FBQUFBQmVjdHFwcW1Xc08ybG9BaG5PRGNJSTR3alFjTjJEcEF4aVl3VDZCMW5OTmFhOXdZe

小结

基本上 SSO 就可以这样跑起来了。但是一个完整 SSO 系统还有很多地方需要去完善,比如 JWT 密钥的交换,用户注销等待——我们下一节再讲!

AJ-IAM 源码:https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-backend/aj-iam

参考

推荐下面文章,我也是跟他们学习的。

  • 基于 OIDC 实现单点登录 SSO、第三方登录 https://blog.csdn.net/u012324798/article/details/105612729 (强烈推荐)
  • 理解 OIDC 与 OAuth2.0 协议 https://blog.csdn.net/ruanchengshen/article/details/129166819,出处。授权码只是其中一个模式,还有其他不同的模式以适应不同的场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1204380.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解释tqdm模块显示进度条:

1. 在Python中&#xff0c;当你使用tqdm模块&#xff08;一个快速、可扩展的Python进度条库&#xff09;时&#xff0c;你可能会看到类似的输出&#xff1a;[6:20:38<6:34:14, 31.25s/it]。 这个输出提供了关于循环进度的详细信息&#xff1a; 6:20:38: 这是已经过去的时…

基于猫群算法优化概率神经网络PNN的分类预测 - 附代码

基于猫群算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于猫群算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于猫群优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络的光滑…

深度学习之基于Pytorch服装图像分类识别系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介系统组成1. 数据集准备2. 数据预处理3. 模型构建4. 模型训练5. 模型评估 PyTorch的优势 二、功能三、系统四. 总结 一项目简介 深度学习在计算机视觉领域的…

在 uniapp 中 一键转换单位 (px 转 rpx)

在 uniapp 中 一键转换单位 px 转 rpx Uni-app 官方转换位置利用【px2rpx】插件Ctrl S一键全部转换下载插件修改插件 Uni-app 官方转换位置 首先在App.vue中输入这个&#xff1a; uni.getSystemInfo({success(res) {console.log("屏幕宽度", res.screenWidth) //屏…

Obsidian同步技巧

Obsidian介绍 Obsidian支持Markdown语法&#xff0c;所见即所得。 软件支持多仓库功能&#xff0c;支持笔记文件夹和分层文件夹&#xff0c;等功能。 值得一提的是&#xff0c;软件的笔记同步功能需要付费。 同步技巧 官方同步方法 若资金充足&#xff0c;则可在Obsidian官网…

服务器常见问题排查(一)—cpu占用高、上下文频繁切换、频繁GC

一般而言cpu异常往往还是比较好定位的。原因包括业务逻辑问题(死循环)、频繁gc以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑)导致的&#xff0c;可以使用jstack来分析对应的堆栈情况。 使用jstack排查占用率问题 当使用jstack排查占用率问题时&#xff0c;可以…

5+干湿结合的佳作,可另外添加分析升级

今天给同学们分享一篇生信文章“PCTAIRE Protein Kinase 1 (PCTK1) Suppresses Proliferation, Stemness,and Chemoresistance in Colorectal Cancer through the BMPR1B-Smad1/5/8 Signaling Pathway”&#xff0c;这篇文章发表在Int J Mol Sci期刊上&#xff0c;影响因子为5.…

Hafnium之工程目录结构介绍

安全之安全(security)博客目录导读 Hafnium存储库包含Hafnium源代码以及与集成测试和单元测试相关的测试代码。为了帮助集成测试,存储库还包含一个用于分区的小型客户端库,以及构建和运行测试所需的预构建工具二进制文件。构建系统由gn支持。 每个平台都有一个单独的…

【Git】第一篇:Git安装(centos)

git查看安装版本 以我自己的centos7.6为例&#xff0c;我们可以输入以下指令查看自己是否安装了git. git --version安装了的话就会显示自己安装的版本。 git 安装 安装很简单&#xff0c;一条命令即可 sudo yum install git -ygit 卸载 sudo yum remove git -y

Kotlin之控制语句和表达式

原文链接 Kotlin Controls and Expressions 有结果返回的是表达式&#xff0c;没有返回的称之为语句&#xff0c;语句最大的问题是它没有返回值&#xff0c;那么想要保存结果就必然会产生副作用&#xff0c;比如改变变量。很多时候这是不够方便的&#xff0c;并且在多线程条件…

『MySQL快速上手』-⑦-内置函数

文章目录 1.日期函数1.1 获得年月日1.2 获得时分秒1.3 获得时间戳1.4 在日期的基础上加日期1.5 在日期的基础上减去时间1.6 计算两个日期之间相差多少天案例1案例22.字符串函数案例3.数学函数4.其他函数1.日期函数 1.1 获得年月日

CLK_CFG_AD9516时钟芯片(配置代码使用说明)

目录 1 概述2 例程功能3 例程端口4 数据时序5 注意事项6 调用例程7附录&#xff08;代码以及寄存器&#xff09; 1 概述 本文用于讲解CLK_CFG_AD9516例程配置代码的使用说明&#xff0c;方便使用者快速上手。 2 例程功能 本例程 是采用verilog hdl编写&#xff0c;实现AD951…

Netty Review - 快速上手篇

文章目录 基础概念官网Whats NettyWhy NettyAbout Netty Author & LeaderWhat can Netty doNetty开发流程Flow HL View客户端开发Handler客户端启动类 服务端开发Handler服务器端启动类 运行示例 基础概念 BIO、NIO和AIO这三个概念分别对应三种通讯模型&#xff1a;阻塞、…

本地化小程序运营 同城小程序开发

时空的限制让本地化的线上平台成为一种追求&#xff0c;58及某团正式深挖人们城镇化、本地化的信息和商业需求而崛起的平台&#xff0c;将二者结合成本地化小程序&#xff0c;显然有着巨大的市场机会。本地化小程序运营可以结合本地化生活需求的一些信息&#xff0c;以及激发商…

Nginx-基础-基础配置(Server,Location语法,匹配优先级,rewrite)

请求定位(Server模块) nginx有两层指令来匹配请求 URL &#xff1a; 第一个层次是 server 指令&#xff0c;它通过域名、ip和端口来做第一层级匹配&#xff0c;当找到匹配的 server 后就进入此 server 的 location 匹配。第二个层次是location指令&#xff0c;它通过请求uri来…

同城小程序怎么运作 本地化生活小程序开发

同城小程序可以采取公域加私域的运营方式&#xff0c;进行运作。 在社交媒体平台上分享有趣的本地生活内容、社区动态&#xff0c;可以通过举办本地活动、合作推广等方式进行线下宣传&#xff0c;可以通过抖音本地化生活服务进行线下门店推广。 本地化生活小程序开发需要结合自…

【数据结构】堆(Heap):堆的实现、堆排序

目录 堆的概念及结构 ​编辑 堆的实现 实现堆的接口&#xff1a; 堆的初始化&#xff1a; 堆的打印&#xff1a; 堆的销毁&#xff1a; 获取最顶的根数据&#xff1a; 交换&#xff1a; 堆的插入&#xff1a;&#xff08;插入最后&#xff09; 向上调整&#xff1a;&#xff0…

解决Chrome无法自动同步书签

前提&#xff1a;&#xff08;要求能正常访问google&#xff09; 准备一个谷歌账号 安装Chrome浏览器 开启集装箱插件&#xff08;或者其他能访问谷歌的工具&#xff09; 步骤&#xff1a;&#xff08;使用集装箱插件/能正常访问谷歌的其他工具&#xff09; 下载安装使用“集…

as启动Internal error. Please refer to https://code.google.com/p/android/issues

打开AndroidStudio时遇到nternal error. Please report to https://code.google.com/p/android/issues 解决方法&#xff1a; 1、在AndroidStudio项目安装目录的/Applications/Android\ Studio.app/Contents/bin/idea.properties 文件中最后一行添加disable.android.first.runt…

云流量回溯的重要性和应用

云流量回溯是指利用云计算和相关技术来分析网络流量、数据传输或应用程序操作的过程。这个过程包括了对数据包、通信模式和应用程序性能的审查和跟踪。本文将介绍云流量回溯重要性和应用! 1、网络安全: 云流量回溯是网络安全的重要组成部分。通过监测和回溯网络流量&#xff0c…