SpringCloud gateway 防止XSS漏洞_springcloud项目添加xss防护
先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。






既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上网络安全知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip204888 (备注网络安全)

正文
从以上我们可以知道,存储型的XSS危害最大。因为他存储在服务器端,所以不需要我们和被攻击者有任何接触,只要被攻击者访问了该页面就会遭受攻击。而反射型和DOM型的XSS则需要我们去诱使用户点击我们构造的恶意的URL,需要我们和用户有直接或者间接的接触,比如利用社会工程学或者利用在其他网页挂马的方式。

3.XSS的防御
XSS防御的总体思路是:对用户的输入(和URL参数)进行过滤,对输出进行html编码。也就是对用户提交的所有内容进行过滤,对url中的参数进行过滤,过滤掉会导致脚本执行的相关内容;然后对动态输出到页面的内容进行html编码,使脚本无法在浏览器中执行。
对输入的内容进行过滤,可以分为黑名单过滤和白名单过滤。黑名单过滤虽然可以拦截大部分的XSS攻击,但是还是存在被绕过的风险。白名单过滤虽然可以基本杜绝XSS攻击,但是真实环境中一般是不能进行如此严格的白名单过滤的。
对输出进行html编码,就是通过函数,将用户的输入的数据进行html编码,使其不能作为脚本运行。
如下,是使用php中的htmlspecialchars函数对用户输入的name参数进行html编码,将其转换为html实体
#使用htmlspecialchars函数对用户输入的name参数进行html编码,将其转换为html实体
$name = htmlspecialchars( $\_GET[ 'name' ] );
二.Java开发中防范XSS跨站脚本攻击的思路
-
- 防堵跨站漏洞
阻止攻击者利用在被攻击网站上发布跨站攻击语句不可以信任用户提交的任何内容,首先代码里对用户输入的地方和变量都需要仔细检查长度和对”<”,”>”,”;”,”’”等字符做过滤;其次任何内容写到页面之前都必须加以encode,避免不小心把html tag 弄出来。这一个层面做好,至少可以堵住超过一半的XSS 攻击。
- 防堵跨站漏洞
-
- Cookie 防盗
首先避免直接在cookie 中泄露用户隐私,例如email、密码等等。其次通过使cookie 和系统ip 绑定来降低cookie 泄露后的危险。这样攻击者得到的cookie 没有实际价值,不可能拿来重放。
- Cookie 防盗
-
- 尽量采用POST 而非GET 提交表单
POST 操作不可能绕开javascript 的使用,这会给攻击者增加难度,减少可利用的跨站漏洞。
- 尽量采用POST 而非GET 提交表单
-
- 严格检查refer
检查http refer 是否来自预料中的url。这可以阻止第2 类攻击手法发起的http 请求,也能防止大部分第1 类攻击手法,除非正好在特权操作的引用页上种了跨站访问。
- 严格检查refer
-
- 将单步流程改为多步,在多步流程中引入效验码
多步流程中每一步都产生一个验证码作为hidden 表单元素嵌在中间页面,下一步操作时这个验证码被提交到服务器,服务器检查这个验证码是否匹配。
首先这为第1 类攻击者大大增加了麻烦。其次攻击者必须在多步流程中拿到上一步产生的效验码才有可能发起下一步请求,这在第2 类攻击中是几乎无法做到的。
- 将单步流程改为多步,在多步流程中引入效验码
-
- 引入用户交互
简单的一个看图识数可以堵住几乎所有的非预期特权操作。
- 引入用户交互
-
- 只在允许anonymous 访问的地方使用动态的javascript。
-
- 对于用户提交信息的中的img 等link,检查是否有重定向回本站、不是真的图片等可疑操作。
-
- 内部管理网站的问题
很多时候,内部管理网站往往疏于关注安全问题,只是简单的限制访问来源。这种网站往往对XSS 攻击毫无抵抗力,需要多加注意。安全问题需要长期的关注,从来不是一锤子买卖。XSS 攻击相对其他攻击手段更加隐蔽和多变,和业务流程、代码实现都有关系,不存在什么一劳永逸的解决方案。此外,面对XSS,往往要牺牲产品的便利性才能保证完全的安全,如何在安全和便利之间平衡也是一件需要考虑的事情。
web应用开发者注意事项:
- 1.对于开发者,首先应该把精力放到对所有用户提交内容进行可靠的输入验证上。这些提交内容包括URL、查询关键字、http头、post数据等。只接受在你所规定长度范围内、采用适当格式、你所希望的字符。阻塞、过滤或者忽略其它的任何东西。
- 2.保护所有敏感的功能,以防被bots自动化或者被第三方网站所执行。实现session标记(session tokens)、CAPTCHA系统或者HTTP引用头检查。
- 3.如果你的web应用必须支持用户提供的HTML,那么应用的安全性将受到灾难性的下滑。但是你还是可以做一些事来保护web站点:确认你接收的HTML内容被妥善地格式化,仅包含最小化的、安全的tag(绝对没有JavaScript),去掉任何对远程内容的引用(尤其是样式表和JavaScript)。为了更多的安全,请使用httpOnly的cookie。
三.相关代码(适用于spring cloud gateway)

代码执行顺序
CacheBodyGlobalFilter—>XssRequestGlobalFilter—>XssResponseGlobalFilter
1.CacheBodyGlobalFilter.java
这个过滤器解决body不能重复读的问题(在低版本的spring-cloud不需要这个过滤器),为后续的XssRequestGlobalFilter重写请求body做准备
package com.xxx.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/\*\*
\* @Author:
\* @Description: 这个过滤器解决body不能重复读的问题,为后续的XssRequestGlobalFilter重写post|put请求的body做准备
\* @Date:
\* <p>
\* 没把body的内容放到attribute中去,因为从attribute取出body内容还是需要强转成 Flux<DataBuffer>,然后转换成String,和直接读取body没有什么区别
\*/
@Component
public class CacheBodyGlobalFilter implements Ordered, GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
HttpMethod method = exchange.getRequest().getMethod();
String contentType = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT\_TYPE);
if (method == HttpMethod.POST || method == HttpMethod.PUT) {
if (MediaType.APPLICATION\_FORM\_URLENCODED\_VALUE.equalsIgnoreCase(contentType)
|| MediaType.APPLICATION\_JSON\_VALUE.equalsIgnoreCase(contentType)
|| MediaType.APPLICATION\_JSON\_UTF8\_VALUE.equals(contentType)) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux
.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST\_PRECEDENCE;
}
}
2.XssRequestGlobalFilter.java
自定义防XSS攻击网关全局过滤器。
package com.xxx.gateway.filter;
import io.netty.buffer.ByteBufAllocator;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/\*\*
\* @Author:
\* @Description: 自定义防XSS攻击网关全局过滤器
\* @Date:
\*/
@Component
public class XssRequestGlobalFilter implements GlobalFilter, Ordered {
private Logger logger = LoggerFactory.getLogger(XssRequestGlobalFilter.class);
/\*\*
\*
\* @param exchange
\* @param chain
\* @return
\*
\* get请求参考spring cloud gateway自带过滤器:
\* @see org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory
\*
\* post请求参考spring cloud gateway自带过滤器:
\* @see org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
\*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){
// grab configuration from Config object
logger.info("----自定义防XSS攻击网关全局过滤器生效----");
String path = exchange.getRequest().getPath().toString();
ServerHttpRequest serverHttpRequest = exchange.getRequest();
HttpMethod method = serverHttpRequest.getMethod();
String contentType = serverHttpRequest.getHeaders().getFirst(HttpHeaders.CONTENT\_TYPE);
Boolean postFlag = (method == HttpMethod.POST || method == HttpMethod.PUT) &&
(MediaType.APPLICATION\_FORM\_URLENCODED\_VALUE.equalsIgnoreCase(contentType) || MediaType.APPLICATION\_JSON\_VALUE.equals(contentType) || MediaType.APPLICATION\_JSON\_UTF8\_VALUE.equals(contentType));
// get 请求, 参考的是 org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory
if (method == HttpMethod.GET) {
URI uri = exchange.getRequest().getURI();
String rawQuery = uri.getRawQuery();
if (StringUtils.isBlank(rawQuery)){
return chain.filter(exchange);
}
rawQuery = XssCleanRuleUtils.xssClean(rawQuery);
try {
URI newUri = UriComponentsBuilder.fromUri(uri)
.replaceQuery(rawQuery)
.build(true)
.toUri();
ServerHttpRequest request = exchange.getRequest().mutate()
.uri(newUri).build();
return chain.filter(exchange.mutate().request(request).build());
} catch (Exception e) {
logger.error("get请求清理xss攻击异常", e);
throw new IllegalStateException("Invalid URI query: \"" + rawQuery + "\"");
}
}
//post请求时,如果是文件上传之类的请求,不修改请求消息体
else if (postFlag){
// 参考的是 org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory
//从请求里获取Post请求体
String bodyStr = resolveBodyFromRequest(serverHttpRequest);
// 这种处理方式,必须保证post请求时,原始post表单必须有数据过来,不然会报错
if (StringUtils.isBlank(bodyStr)) {
logger.error("请求异常:{} POST请求必须传递参数", serverHttpRequest.getURI().getRawPath());
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.BAD\_REQUEST);
byte[] bytes = "{\"code\":400,\"msg\":\"post data error\"}".getBytes(StandardCharsets.UTF\_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
//白名单处理(看业务需求)
boolean containsTarget = WhiteListUtils.richTextUrls.stream().anyMatch(s -> path.contains(s));
if (containsTarget) {
//bodyStr = XssCleanRuleUtils.xssRichTextClean(bodyStr);
bodyStr = XssCleanRuleUtils.xssClean2(bodyStr);
} else {
bodyStr = XssCleanRuleUtils.xssClean(bodyStr);
}
URI uri = serverHttpRequest.getURI();
URI newUri = UriComponentsBuilder.fromUri(uri).build(true).toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
// 定义新的消息头
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// 由于修改了传递参数,需要重新设置CONTENT\_LENGTH,长度是字节长度,不是字符串长度
int length = bodyStr.getBytes().length;
headers.remove(HttpHeaders.CONTENT\_LENGTH);
headers.setContentLength(length);
// 设置CONTENT\_TYPE
if (StringUtils.isNotBlank(contentType)) {
headers.set(HttpHeaders.CONTENT\_TYPE, contentType);
}
// 由于post的body只能订阅一次,由于上面代码中已经订阅过一次body。所以要再次封装请求到request才行,不然会报错请求已经订阅过
request = new ServerHttpRequestDecorator(request) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
// this causes a 'HTTP/1.1 411 Length Required' on httpbin.org
httpHeaders.set(HttpHeaders.TRANSFER\_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
//封装request,传给下一级
request.mutate().header(HttpHeaders.CONTENT\_LENGTH, Integer.toString(bodyStr.length()));
return chain.filter(exchange.mutate().request(request).build());
} else {
return chain.filter(exchange);
}
}
@Override
public int getOrder() {
return -90;
}
/\*\*
\* 从Flux<DataBuffer>中获取字符串的方法
\* @return 请求体
\*/
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
//获取请求体
Flux<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF\_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
//获取request body
return bodyRef.get();
}
/\*\*
\* 字符串转DataBuffer
\* @param value
\* @return
\*/
private DataBuffer stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF\_8);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
}
3.XssResponseGlobalFilter.java
重写Response,防止xss攻击。
package com.xxx.gateway.filter;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
/\*\*
\* @Author:
\* @Description: 重写Response,防止xss攻击
\* @Date:
\*/
@Component
public class XssResponseGlobalFilter implements Ordered, GlobalFilter {
private Logger logger = LoggerFactory.getLogger(XssResponseGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求url
String path = exchange.getRequest().getPath().toString();
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
String contentType = getDelegate().getHeaders().getFirst(HttpHeaders.CONTENT\_TYPE);
Boolean flag = MediaType.APPLICATION\_JSON\_VALUE.equals(contentType) || MediaType.APPLICATION\_JSON\_UTF8\_VALUE.equals(contentType);
if (body instanceof Flux && flag) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
//如果响应过大,会进行截断,出现乱码,
//然后看api DefaultDataBufferFactory有个join方法可以合并所有的流,乱码的问题解决
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = null;
try {
join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
//释放掉内存
DataBufferUtils.release(join);
String result = new String(content, Charset.forName("UTF-8"));
//logger.info("result:"+result);
//若为带有富文本的接口,走富文本xss过滤
boolean containsTarget = WhiteListUtils.richTextUrls.stream().anyMatch(s -> path.contains(s));
if (containsTarget) {
//result = XssCleanRuleUtils.xssRichTextClean(result);
result = XssCleanRuleUtils.xssClean2(result);
} else {
//result就是response的值,对result进行去XSS
result = XssCleanRuleUtils.xssClean(result);
}
byte[] uppedContent = new String(result.getBytes(), Charset.forName("UTF-8")).getBytes();
return bufferFactory.wrap(uppedContent);
} catch (Exception e) {
// 处理异常,记录日志等
throw e;
} finally {
if (join != null) {
//释放掉内存
DataBufferUtils.release(join);
}
}
}));
}
// if body is not a flux. never got there.
return super.writeWith(body);
}
};
// replace response with decorator
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return -50;
}
}
4.XssCleanRuleUtils.java (工具类)
上面几个过滤器使用的工具类
后续我自己这边没有用Jsoup了(感觉有坑,就放弃了)
package com.xxx.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Whitelist;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.regex.Pattern;
/\*\*
\* @Author:
\* @Description: xss过滤工具
\* @Date:
\*/
public class XssCleanRuleUtils {
//xss过滤规则(对于script、src及加载事件和弹窗事件的代码块)
private final static Pattern[] scriptPatterns = {
Pattern.compile("<script>(.\*?)</script>", Pattern.CASE\_INSENSITIVE),
Pattern.compile("src[\r\n]\*=[\r\n]\*\\\'(.\*?)\\\'", Pattern.CASE\_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("</script>", Pattern.CASE\_INSENSITIVE),
Pattern.compile("<script(.\*?)>", Pattern.CASE\_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("eval\\((.\*?)\\)", Pattern.CASE\_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("expression\\((.\*?)\\)", Pattern.CASE\_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("javascript:", Pattern.CASE\_INSENSITIVE),
Pattern.compile("vbscript:", Pattern.CASE\_INSENSITIVE),
Pattern.compile("onload(.\*?)=", Pattern.CASE\_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
};
//非富文本的
public static String xssClean(String value) {
if (value != null) {
value = value.replaceAll("\0|\n|\r", "");
for (Pattern pattern : scriptPatterns) {
value = pattern.matcher(value).replaceAll("");
}
value = value.replaceAll("<", "<").replaceAll(">", ">");
}
return value;
}
//富文本的
public static String xssClean2(String value) {
if (value != null) {
value = value.replaceAll("\0|\n|\r", "");
for (Pattern pattern : scriptPatterns) {
value = pattern.matcher(value).replaceAll("");
}
}
return value;
}
### 给大家的福利
**零基础入门**
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

同时每个成长路线对应的板块都有配套的视频提供:

因篇幅有限,仅展示部分资料
网络安全面试题

绿盟护网行动

还有大家最喜欢的黑客技术

**网络安全源码合集+工具包**


**所有资料共282G**,朋友们如果有需要全套《网络安全入门+黑客进阶学习资源包》,可以扫描下方二维码领取(如遇扫码问题,可以在评论区留言领取哦)~
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注网络安全)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
/img-blog.csdnimg.cn/img_convert/5912337446dee53639406fead3d3f03c.jpeg)
**网络安全源码合集+工具包**


**所有资料共282G**,朋友们如果有需要全套《网络安全入门+黑客进阶学习资源包》,可以扫描下方二维码领取(如遇扫码问题,可以在评论区留言领取哦)~
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注网络安全)**
[外链图片转存中...(img-B1wN8pNA-1713175969218)]
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
更多推荐
所有评论(0)