前言
当今互联网Web各种应用H5、Android、ios、web、小程序等开发时大都采用前后端分离架构,公司为了商业变现会开放自己系统接口给其它公司使用。例如: 调用微信支付。
既然涉及到前后端分离,前端页面调用后端API接口,那么接口的安全设计是非常重要的一项工作。项目的架构师在项目布局过程中,会着重考虑安全,最常见的安全问题就是,用户在移动端提交数据向后端传输,黑客在传输过程中拦截提交的数据,进行篡改,进而达到伪造请求数据的目的。
例如前端提交金额,商品编号信息,黑客中途拦截,修改成低价商品,然后请求下单,早年间国内某电商技术不成熟时,抓包分析下单是很常见的。这时如果我们对一些常规的项目可以通过请求数据报文进行签名、加密、加盐、加时间戳、后端根据数据再次加密,与报文中的签名进行对比是否一致来控制接口安全,这种做法在大厂项目中也是常用手法。
什么是加密解密
- 加密:数据加密的基本过程,就是对原来为明文,用户输入的数据通过某种处理,变成一串不可直接提取信息的代码,类似于英文字母加阿拉伯数字组合,通常称之为 密文。在战争年代的电报发报加密成密文,对方电台人员收到电文,根据约定的密码本进行破译便可得到明文,这就是为什么密码本对一个军队如此重要。* 解密:加密的逆过程,也就是破译电报。常见的加密算法
加密技术通常分为三大类:对称式、非对称式、散列算法。
-
对称式:通俗的说就是锁上一把锁与打开这把锁,用的都是同一样一把钥匙。常见的对称加密算法有:DES、3DES、AES等* 非对称式:俗名公开秘钥加密算法,它需要一对代码,一个为公钥 (public key)、另一个为私钥(private key) 加密解密用的不是一个秘钥,所以被称之非对称加密。* 使用公钥对明文加密,有且只有对应的私钥才能解开密文。* 使用私钥对明文加密,有且只有对应的公钥才能解开密文。* 大多数做法:公钥加密,私钥解密,公钥会在加密前发放给解密方。> 例子:Git 中ssh连接Github,本地电脑生成public key,与private key,将public key提前配置到GitHub账户中,private key留在本地,上传文件时Git便会自动识别认证身份。常见的非对称性加密算法:RSA、DSA 等* 散列算法:主要用于验证,防止信息被修。具体用途如:文件校验、数字签名、鉴权协议。常见的Hash散列算法:MD5、SHA1、SHA256、HMAC等等
-
MD5: MD5是一种不可逆的加密算法,目前是最牢靠的加密算法之一,尚没有能够逆运算的程序被开发出来,它对应任何字符串都可以加密成一段唯一的固定长度的代码。
其他算法介绍查看文末连接详情
使用MD5算法开放接口加密验签实现
需求分析:
1.外部应用调用接口,做到极简丝滑调用。
2.接口提供方系统不能影响原有业务。
3.对接口需求方提交的数据进行校验,若不合法在接口被请求前就应终止这一次请求。
4.符合主流大厂接口开放方式。
实现思路:
- 接口提供方给接口需求方也就是第三方公司发放appid、secret,并要求严格保管。在系统内新建一个合作公司表,使用UUID生成appid与secret,对合作公司进行增删改查。简单 在此文章中略
- 接口需求方使用约定的MD5算法将appid应用唯一识别、secret秘钥、timestamp时间戳、nonce随机数、业务参数、生成sign签名并一起传递给接口提供方。
- 接口提供方接收获取appid、secret、timestamp、nonce、并逐个判断是否为空,为空就停止请求,并给第三方友好提示。
- 接口提供方获取第三方公司提交的timestamp与当前系统时间做对比,如果差值大于120秒,则timestamp无效,如果差值小于120秒,则timestamp有效。目的是防止过期的提交。
- 根据第三方提交的appid查询数据库内secret,与提交的secret进行对比,这一步可以根据appid判断权限 高级做法
- 接口提供方获取第三方公司提交的nonce,比较redis中存储的nonce,不一致则通过。防止暴力请求接口。
- 接口提供方将获取的appid、secret、timestamp、nonce、业务参数通过MD5算法运算得到sign2,与第三方公司提交的sign对比,如果不一致则为不合法请求。
- 将nonce存入redis,过期时间设置为120秒。
上代码
pom.xml
引入依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><<img src="https://mvnrepository.com/artifact/com.alibaba/fastjson --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.75</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><!-- XSS --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-text</artifactId><version>1.8</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>" style="margin: auto" />
MD5签名算法
public class MD5 {
/**
* 生成 MD5
* @param data 待处理数据
* @return MD5结果
*/
public static String md5(String data) {
StringBuilder sb = null;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
sb = new StringBuilder();
for (byte item : array) {sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return sb.toString().toUpperCase();
}
}
签名是否一致验证工具
/**
* MD5 sign签名校验宇生成工具
*/
public class GenerateSignatureUtil {public static final String FIELD_SIGN = "sign";/** * 判断签名是否正确,必须包含sign字段,否则返回false。 * @param data Map类型数据 * @param keyAPI密钥 * @return 签名是否正确 * @throws Exception */public static boolean isSignatureValid(Map<String, String> data, String key){if (!data.containsKey(FIELD_SIGN)) {return false;}String sign = data.get(FIELD_SIGN);return generateSignature(data, key).equals(sign);}public static String generateSignature(final Map<String, String> data, String key) {try {Set<String> keySet = data.keySet();String[] keyArray = keySet.toArray(new String[keySet.size()]);Arrays.sort(keyArray);StringBuilder sb = new StringBuilder();for (String k : keyArray) {if (k.equals(FIELD_SIGN)) {continue;}// 参数值为空,则不参与签名if (data.get(k).trim().length() > 0){ sb.append(k).append("=").append(data.get(k).trim()).append("&");}}sb.append("key=").append(key);return MD5.md5(sb.toString());} catch (Exception e) {e.printStackTrace();}return "";}
}
错误信息提示工具类
/**
* 客户端工具类
* @author
*/
public class ServletUtils { // 获取requestpublic static HttpServletRequest getRequest() {return getRequestAttributes().getRequest();}// 获取ServletRequestAttributespublic static ServletRequestAttributes getRequestAttributes() {RequestAttributes attributes = RequestContextHolder.getRequestAttributes();return (ServletRequestAttributes) attributes;}/** * 将字符串渲染到客户端 * @param response 渲染对象 * @param string 待渲染的字符串 * @return null */public static String renderString(HttpServletResponse response, String string) {try {response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(string);} catch (IOException e) {e.printStackTrace();}return null;}}
重写WebMvcConfigurer
/**
* @Author 真香
* @Date 2021/4/20 16:30
* @Version 1.0
*/
@Slf4j
@Configuration
public class OpenSignWebMvcConfigimplements WebMvcConfigurer {@Autowiredprivate SignAuthInterceptor signAuthInterceptor;@Autowiredprivate OpenSignProperties openSignProperties;// 拦截器配置private OpenSignInterceptorProperties interceptorConfig;// 注入spring 容器@Beanpublic SignAuthInterceptor signAuthInterceptor () {return new SignAuthInterceptor();}@PostConstructpublic void init () {interceptorConfig = openSignProperties.getInterceptor();log.debug("openSignProperties:{}", JSON.toJSONString(interceptorConfig));}@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册签名拦截器if (interceptorConfig.getSign().isEnable()) {registry.addInterceptor(signAuthInterceptor()).addPathPatterns(interceptorConfig.getSign().getIncludePaths()).excludePathPatterns(interceptorConfig.getSign().getExcludePaths());}}
}
application.yml
配置文件
server: port: 9999 ######################## Spring Shiro start ######################## shiro: # 是否启用enable: true # 权限配置anon:# 排除登录登出 - /login,/logout,# 排除静态资源 - /static/**,/templates/**# 排除actuator - /actuator/** # 排除首页, 不再开放此页面 #- /,/welcome.html# 排除测试路径 - /hello/world, # 多行字符串权限配置filter-chain-definitions: | /resource/**=anon /upload/**=anon /verificationCode/**=anon /enum=anon# 权限配置permission:# 排除登陆登出相关 - urls: permission: anon
######################## Spring Shiro end ##########################
##############################open sign start ######################
open-sign:# Filter配置 filter:request: enable: true url-patterns: /* order: 1 async: truexss: enable: true url-patterns: /* order: 2 async: truerepeatedlyread: enable: true url-patterns: /* order: 2 async: true# 拦截器配置 interceptor:# 配置需要进行签名拦截的接口地址sign: enable: true include-paths:
SignAuthInterceptor
最重要的拦截器拦截请求
@Slf4j
public class SignAuthInterceptor implements HandlerInterceptor {private static final String NONCE_KEY_STR = "nonce-";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {Map<String, String[]> map = request.getParameterMap();// 从数组中取出参数放入Map中Map<String,String> param = new ConcurrentHashMap<>(10);for (Map.Entry<String, String[]> entry: map.entrySet()) {String key = entry.getKey();String[] values = entry.getValue();for (int i = 0; i < values.length; i++) {String value = values[i];param.put(key,value);}}//1、获取请求参数appIdString appid = param.get("appid");if (StringUtils.isBlank(appid)) {log.info("appid不能为空");ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("appid不能为空")));return false;}// 2、获取请求参数secretString secret =request.getParameter("secret");if (StringUtils.isBlank(secret)){log.info("secret不能为空...........");ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("secret不能为空")));return false;}/**3、验证secret权限、来源是否合法 *此处可以用appId,条件为 已开启,未封禁等进行数据库合作机构表查询,有可能已经终止合作禁止了此有用访问, *业务上达到一定条件的可以根据appId分配权限,选择不同的接口能力进行开放 */TDrivingCooperation drivingCooperationByPartnerkey = drivingCooperationService.getDrivingCooperationByPartnerkey(partnerkey);if (drivingCooperationByPartnerkey == null || drivingCooperationByPartnerkey.getStatus().equals(StatusEnum.DISABLE.getCode())) {ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("partnerkey无法查询到合作公司信息或已被封禁")));return false;}// 获取secret 与数据库值对比,判断请求来源是否合法 if (!secret.equals(drivingCooperationByPartnerkey.getSecret())) {log.debug("secret与接口提供方不一致...........");System.out.println("secret与接口提供方不一致...........");ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("secret与接口提供方不一致")));return false;}// 4、 获取请求参数timestamp 时间戳,String timestamp = request.getParameter("timestamp");if (StringUtils.isBlank(timestamp)){log.info("timestamp不能为空...........");ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("timestamp不能为空")));return false;}/** 5、 防止过期时间的提交 * 从前端传递的timestamp 与服务器端当前系统时间之差大于120s,则此次请求的timestamp无效 *留出短时间考虑网络问题提交速度慢,若时间过长中间时间足以挟持篡改参数,所以折中考虑了120秒 */Long time = System.currentTimeMillis()/1000;if (Math.abs(Long.valueOf(timestamp)-time)>120) {log.info("timestamp失效...........");ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("timestamp失效")));return false;}// 6、获取请求参数nonce随机数,防止重复的暴力请求String nonce = param.get("nonce");if (StringUtils.isBlank(nonce)) {log.debug("nonce不能为空...........");ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("nonce不能为空")));return false;}/** *如果设计得规范一些可以防止重复提交,我这因为是小项目,Demo演示就不做redis缓存随机数了 * 流程:1、获取当前提交的随机数,作为key前往redis 查询,若有值则为重复提交 *2、redis中查询不到结果,将当前随机数作为key,value为随机数,过期时间设置为120s */// 7、获取请求sign签名参数,String sign = param.get("sign");if (StringUtils.isBlank(sign)){log.info("sign不能为空...........");ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("sign不能为空")));return false;}//8.通过后台MD5重新签名校验与前端签名sign值比对,确认当前请求数据是否被篡改boolean reuslt = GenerateSignatureUtil.isSignatureValid(param, secret);if (!reuslt){log.debug("sign签名校验失败...........");ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("sign签名校验失败")));return false;}log.info("签名校验通过,放行...........";// 获取sign签名,与服务端生成的sign 签名对比return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("SignAuthInterceptor postHandle======");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("SignAuthInterceptor afterCompletion======");}
}
测试接口
/**
* @Author 真香
* @Date 2021/4/20 17:01
* @Version 1.0
*/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {@RequestMapping(value = "/add",method = RequestMethod.POST)public ApiResult<Boolean> addUser (User user) {log.info("user=="+user);return ApiResult.ok(true);}
}
以上展示了一些关键代码,还有一些辅助代码因为排版问题未一一展示,后续可以通过仓库地址克隆。
来吧 展示
模拟请求正常
这里采用HttpClient 跟接近真实开发方式 先来一次所有参数都正常的请求
@Test public void testOpenSign() { Map<String, String> params = new ConcurrentHashMap<>(10); String secret = "1ae41230bd1b4383a44f1b114ceba13c"; params.put("appid","6ee781ae6ef4496a"); // 获取时间戳单位S Long timesTamp = System.currentTimeMillis()/1000; System.out.println("time ==" + timesTamp); params.put("timestamp",String.valueOf(timesTamp)); params.put("nonce",UUIDUtil.getUuid()); params.put("secret",secret); params.put("name", "张三"); params.put("address","中国"); params.put("sex","0"); // 调用MD5算法加密生成签名 String signature = GenerateSignatureUtil.generateSignature(params, secret); System.out.println("sign = " + signature); // 签名加入请求参数 params.put("sign",signature); log.info("开始请求open-sign接口==========:{}",params); String result = HttpClientUtil.doPost("http://localhost:9999/user/add", params); System.out.println(result); }
拿到参数
第三方得到请求返回值
{"code":200,"success":true,"message":"操作成功","data":true,"time":"2021-04-23 08:47:56"}
模拟错误请求
这一次我故意去除appid不传
@Testpublic void testOpenSign() {Map<String, String> params = new ConcurrentHashMap<>(10);String secret = "1ae41230bd1b4383a44f1b114ceba13c";Long timesTamp = System.currentTimeMillis()/1000;System.out.println("time ==" + timesTamp);params.put("timestamp",String.valueOf(timesTamp));params.put("nonce",UUIDUtil.getUuid());params.put("secret",secret);params.put("name", "张三");params.put("address","中国");params.put("sex","0");// 调用MD5算法加密生成签名String signature = GenerateSignatureUtil.generateSignature(params, secret);System.out.println("sign = " + signature);// 签名加入请求参数params.put("sign",signature);log.info("开始请求open-sign接口==========:{}",params);String result = HttpClientUtil.doPost("http://localhost:9999/user/add", params);System.out.println(result);}
断点捕获
第三方得到友好返回值
{"code":500000,"message":"appid不能为空","success":false,"time":"2021-04-23 16:55:25"}
以上只使用了appid作为例子,其余的都是大差不差,算法代码已经写好,错了,漏传、篡改都会帮我们校验。
项目中使用了安全框架例如Shiro、SpringSecurity需要提前放开权限校验,否则请求还没有到签名拦截器就被安全框架拦截了,我这项目因为核心是校验签名所以没搭建shiro认证,我们使用签名校验简化了权限校验,不再需要注册账号,通过颁发token方式给第三方公司。
仓库地址
:gitee.com/JameZhan/ch… 欢迎Issues与PR
写在最后
我是顾北,一个集才华与技术的男生,有问题欢迎后台加微信交流,文章最新的微信公众发布,要吃刚出锅的哦。
ps://link.juejin.cn/?target=https%3A%2F%2Fgitee.com%2FJameZhan%2Fcheck-sign.git “https://gitee.com/JameZhan/check-sign.git”) 欢迎Issues与PR
写在最后
我是顾北,一个集才华与技术的男生,有问题欢迎后台加微信交流,文章最新的微信公众发布,要吃刚出锅的哦。
## 题外话
初入计算机行业的人或者大学计算机相关专业毕业生,很多因缺少实战经验,就业处处碰壁。下面我们来看两组数据:
2023届全国高校毕业生预计达到1158万人,就业形势严峻;
国家网络安全宣传周公布的数据显示,到2027年我国网络安全人员缺口将达327万。
一方面是每年应届毕业生就业形势严峻,一方面是网络安全人才百万缺口。
6月9日,麦可思研究2023年版就业蓝皮书(包括《2023年中国本科生就业报告》《2023年中国高职生就业报告》)正式发布。
2022届大学毕业生月收入较高的前10个专业
本科计算机类、高职自动化类专业月收入较高。2022届本科计算机类、高职自动化类专业月收入分别为6863元、5339元。其中,本科计算机类专业起薪与2021届基本持平,高职自动化类月收入增长明显,2022届反超铁道运输类专业(5295元)排在第一位。
具体看专业,2022届本科月收入较高的专业是信息安全(7579元)。对比2018届,电子科学与技术、自动化等与人工智能相关的本科专业表现不俗,较五年前起薪涨幅均达到了19%。数据科学与大数据技术虽是近年新增专业但表现亮眼,已跻身2022届本科毕业生毕业半年后月收入较高专业前三。五年前唯一进入本科高薪榜前10的人文社科类专业——法语已退出前10之列。
“没有网络安全就没有国家安全”。当前,网络安全已被提升到国家战略的高度,成为影响国家安全、社会稳定至关重要的因素之一。
网络安全行业特点
1、就业薪资非常高,涨薪快 2022年猎聘网发布网络安全行业就业薪资行业最高人均33.77万!
2、人才缺口大,就业机会多
2019年9月18日《中华人民共和国中央人民政府》官方网站发表:我国网络空间安全人才 需求140万人,而全国各大学校每年培养的人员不到1.5W人。猎聘网《2021年上半年网络安全报告》预测2027年网安人才需求300W,现在从事网络安全行业的从业人员只有10W人。
行业发展空间大,岗位非常多
网络安全行业产业以来,随即新增加了几十个网络安全行业岗位︰网络安全专家、网络安全分析师、安全咨询师、网络安全工程师、安全架构师、安全运维工程师、渗透工程师、信息安全管理员、数据安全工程师、网络安全运营工程师、网络安全应急响应工程师、数据鉴定师、网络安全产品经理、网络安全服务工程师、网络安全培训师、网络安全审计员、威胁情报分析工程师、灾难恢复专业人员、实战攻防专业人员…
职业增值潜力大
网络安全专业具有很强的技术特性,尤其是掌握工作中的核心网络架构、安全技术,在职业发展上具有不可替代的竞争优势。
随着个人能力的不断提升,所从事工作的职业价值也会随着自身经验的丰富以及项目运作的成熟,升值空间一路看涨,这也是为什么受大家欢迎的主要原因。
从某种程度来讲,在网络安全领域,跟医生职业一样,越老越吃香,因为技术愈加成熟,自然工作会受到重视,升职加薪则是水到渠成之事。
黑客&网络安全如何学习
今天只要你给我的文章点赞,我私藏的网安学习资料一样免费共享给你们,来看看有哪些东西。
1.学习路线图
行业发展空间大,岗位非常多
网络安全行业产业以来,随即新增加了几十个网络安全行业岗位︰网络安全专家、网络安全分析师、安全咨询师、网络安全工程师、安全架构师、安全运维工程师、渗透工程师、信息安全管理员、数据安全工程师、网络安全运营工程师、网络安全应急响应工程师、数据鉴定师、网络安全产品经理、网络安全服务工程师、网络安全培训师、网络安全审计员、威胁情报分析工程师、灾难恢复专业人员、实战攻防专业人员…
职业增值潜力大
网络安全专业具有很强的技术特性,尤其是掌握工作中的核心网络架构、安全技术,在职业发展上具有不可替代的竞争优势。
随着个人能力的不断提升,所从事工作的职业价值也会随着自身经验的丰富以及项目运作的成熟,升值空间一路看涨,这也是为什么受大家欢迎的主要原因。
从某种程度来讲,在网络安全领域,跟医生职业一样,越老越吃香,因为技术愈加成熟,自然工作会受到重视,升职加薪则是水到渠成之事。
黑客&网络安全如何学习
今天只要你给我的文章点赞,我私藏的网安学习资料一样免费共享给你们,来看看有哪些东西。
1.学习路线图
攻击和防守要学的东西也不少,具体要学的东西我都写在了上面的路线图,如果你能学完它们,你去就业和接私活完全没有问题。
2.视频教程
网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我自己录的网安视频教程,上面路线图的每一个知识点,我都有配套的视频讲解。
内容涵盖了网络安全法学习、网络安全运营等保测评、渗透测试基础、漏洞详解、计算机基础知识等,都是网络安全入门必知必会的学习内容。
(都打包成一块的了,不能一一展开,总共300多集)
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!
3.技术文档和电子书
技术文档也是我自己整理的,包括我参加大型网安行动、CTF和挖SRC漏洞的经验和技术要点,电子书也有200多本,由于内容的敏感性,我就不一一展示了。
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!
4.工具包、面试题和源码
“工欲善其事必先利其器”我为大家总结出了最受欢迎的几十款款黑客工具。涉及范围主要集中在 信息收集、Android黑客工具、自动化工具、网络钓鱼等,感兴趣的同学不容错过。
还有我视频里讲的案例源码和对应的工具包,需要的话也可以拿走。
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!
最后就是我这几年整理的网安方面的面试题,如果你是要找网安方面的工作,它们绝对能帮你大忙。
这些题目都是大家在面试深信服、奇安信、腾讯或者其它大厂面试时经常遇到的,如果大家有好的题目或者好的见解欢迎分享。
参考解析:深信服官网、奇安信官网、Freebuf、csdn等
内容特点:条理清晰,含图像化表示更加易懂。
内容概要:包括 内网、操作系统、协议、渗透测试、安服、漏洞、注入、XSS、CSRF、SSRF、文件上传、文件下载、文件包含、XXE、逻辑漏洞、工具、SQLmap、NMAP、BP、MSF…
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!