-
三路排序算法 一、概念及其介绍 三路快速排序是双路快速排序的进一步改进版本,三路排序算法把排序的数据分为三部分,分别为小于 v,等于 v,大于 v,v 为标定值,这样三部分的数据中,等于 v 的数据在下次递归中不再需要排序,小于 v 和大于 v 的数据也不会出现某一个特别多的情况),通过此方式三路快速排序算法的性能更优。 二、适用说明 时间和空间复杂度同随机化快速排序。 三路快速排序算法是使用三路划分策略对数组进行划分,对处理大量重复元素的数组非常有效提高快速排序的过程。它添加处理等于划分元素值的逻辑,将所有等于划分元素的值集中在一起。 三、过程图示 图片看转载链接哈~我们分三种情况进行讨论 partiton 过程,i 表示遍历的当前索引位置: (1)当前处理的元素 e=V,元素 e 直接纳入蓝色区间,同时i向后移一位。 (2)当前处理元素 e<v,e 和等于 V 区间的第一个位置数值进行交换,同时索引 lt 和 i 都向后移动一位(3)当前处理元素 e>v,e 和 gt-1 索引位置的数值进行交换,同时 gt 索引向前移动一位。 最后当 i=gt 时,结束遍历,同时需要把 v 和索引 lt 指向的数值进行交换,这样这个排序过程就完成了,然后对 <V 和 >V 的数组部分用同样的方法再进行递归排序。 四、Java 实例代码 源码包下载: Download https://www.runoob.com/wp-content/uploads/2020/09/runoob-algorithm-QuickSort3Ways.zip QuickSort3Ways.java 文件代码: package runoob; /** * 三路快速排序 */ public class QuickSort3Ways { //核心代码---开始 // 递归使用快速排序,对arr[l...r]的范围进行排序 private static void sort(Comparable[] arr, int l, int r){ if (l >= r) { return; } // 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot swap( arr, l, (int)(Math.random()*(r-l+1)) + l ); Comparable v = arr[l]; int lt = l; // arr[l+1...lt] < v int gt = r + 1; // arr[gt...r] > v int i = l+1; // arr[lt+1...i) == v while( i < gt ){ if( arr[i].compareTo(v) < 0 ){ swap( arr, i, lt+1); i ++; lt ++; } else if( arr[i].compareTo(v) > 0 ){ swap( arr, i, gt-1); gt --; } else{ // arr[i] == v i ++; } } swap( arr, l, lt ); sort(arr, l, lt-1); sort(arr, gt, r); } //核心代码---结束 public static void sort(Comparable[] arr){ int n = arr.length; sort(arr, 0, n-1); } private static void swap(Object[] arr, int i, int j) { Object t = arr[i]; arr[i] = arr[j]; arr[j] = t; } // 测试 QuickSort3Ways public static void main(String[] args) { // 三路快速排序算法也是一个O(nlogn)复杂度的算法 // 可以在1秒之内轻松处理100万数量级的数据 int N = 1000000; Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000); sort(arr); SortTestHelper.printArray(arr); } } ———————————————— 版权声明:本文为CSDN博主「彼岸的菜鸟」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/2301_78835635/article/details/132144215
-
导言 在 Java 中,迭代器是一种常用的设计模式,用于遍历集合中的元素。它提供了一种统一的方式来访问集合中的元素,而不必暴露集合的内部实现细节。本文将介绍 Java 迭代器的概念、使用方法和常见技巧,并提供一些示例代码。 一、迭代器的概念 迭代器是一种对象,它允许按顺序访问集合中的元素,而不需要知道集合的底层结构。通过使用迭代器,我们可以遍历集合并访问其中的元素,而无需关心集合的具体实现方式。 Java 提供了 Iterator 接口作为迭代器的基础接口。该接口定义了一组用于访问集合元素的方法,包括 hasNext、next 和 remove 等。 二、使用迭代器 要使用迭代器遍历集合,我们需要进行以下步骤: 获取集合的迭代器对象:通过调用集合的 iterator 方法获取迭代器对象。例如,对于 ArrayList 集合,可以使用 iterator() 方法获取迭代器对象。 List<String> list = new ArrayList<>(); // 添加元素到集合中 Iterator<String> iterator = list.iterator(); 遍历集合元素:通过使用迭代器的 hasNext 和 next 方法来遍历集合中的元素。hasNext 方法用于检查是否还有下一个元素,next 方法用于获取下一个元素的值。 while (iterator.hasNext()) { String element = iterator.next(); // 处理元素 } 可选操作:迭代器还提供了 remove 方法,用于从集合中删除当前迭代的元素。需要注意的是,该方法只能在调用 next 方法后才能调用,且每次只能调用一次。 iterator.remove(); 三、迭代器的优势 使用迭代器遍历集合具有以下优势: 抽象集合的实现:通过使用迭代器,我们可以在不了解集合内部实现的情况下遍历和访问集合元素。这样,集合的实现细节对外部代码是透明的。 安全性:迭代器提供了一种安全的方式来遍历集合。它通过维护迭代器的状态来保证在遍历过程中不会出现并发修改的问题。 通用性:迭代器是一种通用的设计模式,在 Java 中被广泛应用于各种集合类型。无论是数组、列表、集合还是映射,我们都可以使用迭代器来遍历和访问元素。 四、迭代器的常见技巧 除了基本的使用方法外,还有一些常见的技巧可以帮助我们更好地使用迭代器。 1. 使用增强的 for 循环 Java 提供了增强的 for 循环(foreach 循环),可以简化迭代器的使用。它可以直接遍历集合中的元素,而不需要显式地使用迭代器。 for (String element : list) { // 处理元素 } 2. 遍历过程中的修改 在使用迭代器遍历集合时,如果需要在遍历过程中修改集合,应使用迭代器的 remove 方法,而不是直接操作集合。直接操作集合可能会导致并发修改异常。 Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); if (shouldRemove(element)) { iterator.remove(); // 使用迭代器删除元素 } } 3. 避免重复创建迭代器 在迭代器的使用过程中,应避免在每次迭代时都创建新的迭代器对象。如果需要多次遍历集合,可以在第一次遍历时创建迭代器,并在后续的遍历中重复使用该迭代器。 Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); // 处理元素 } // 再次使用同一个迭代器进行遍历 while (iterator.hasNext()) { String element = iterator.next(); // 处理元素 } 4. 使用迭代器的限制功能 迭代器提供了一些限制功能,如只读迭代器和单向迭代器。如果在遍历过程中不需要修改集合或只需要向前遍历,可以使用只读或单向迭代器,以提高性能和安全性。 List<String> list = new ArrayList<>(); // 添加元素到集合中 Iterator<String> readOnlyIterator = list.iterator(); Iterator<String> forwardIterator = list.listIterator(); 五、示例代码 下面是一个使用迭代器遍历集合并打印元素的示例代码: List<String> list = new ArrayList<>(); list.add("Apple"); list.add("Banana"); list.add("Orange"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(element); } 在上面的示例中,我们创建了一个 ArrayList 集合,并使用迭代器遍历集合中的元素,然后打印每个元素的值。 总结 迭代器是一种在 Java 中常用的设计模式,用于遍历集合中的元素。通过使用迭代器,我们可以统一访问集合元素,而不需要了解集合的具体实现。本文介绍了迭代器的概念、使用方法和常见技巧,并提供了示例代码。 希望本文对你理解和使用 Java 迭代器提供了帮助。如果你有任何问题或建议,请随时留言。 ———————————————— 版权声明:本文为CSDN博主「繁依Fanyi」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_21484461/article/details/131424468
-
前言 在面向对象(OOP)的程序设计语言中,多态与封装、继承合称为OOP的三大特性。在今天,我们就来学习一下JAVA中的多态是什么样子的。、 多态 指一个对象在不同情况下可以表现出不同的行为。Java多态性分为两种:编译时多态性(静态多态性)和运行时多态性(动态多态性)。 编译时多态性:也称为静态多态性,是指在编译期间就能确定方法的调用方式、参数类型及返回值类型等,主要通过方法重载实现。 运行时多态性:也称为动态多态性,是指在运行期间才能确定方法的调用方式,主要通过方法重写实现。Java中实现运行时多态性的关键是继承和方法重写。 具体来说,当一个类的子类重新定义了一个或多个已在父类中定义的方法时,那么子类的对象将可以调用重新定义的方法,而不是调用父类中的方法。这种现象称为方法重写。通过父类的引用变量来引用一个子类的对象时,父类引用变量不能直接调用子类中重新定义的方法,而是要调用子类中的方法,这种多态性称为动态多态性。 多态实现的重要条件 Java多态实现的重要条件包括: 继承:必须有继承关系,子类必须继承父类。 覆盖:子类必须重写父类的方法。 向上转型:可以声明一个父类引用类型的变量,将它指向一个子类对象。通过这种方式调用方法,就可以实现多态。 动态绑定:在运行时而不是编译时进行方法调用。这样就能够根据实际调用的对象类型来决定调用哪个方法,实现多态。 虚函数 在Java中,所有方法都是虚函数,因为它们都是在运行时动态绑定的。当子类继承父类时,它可以覆盖父类的方法,但具体调用哪个方法取决于对象的实际类型而不是变量的声明类型。因此,可以在运行时动态处理对象的多态性。 使用关键字“override”来覆盖父类的方法。子类中的方法必须与父类中被覆盖的方法具有相同的名称、参数列表和返回类型。这样,当调用子类中的方法时,根据对象的类型来确定应该调用哪个方法。 实例 假设我们有一个Animal类和一个Cat类,Cat类是Animal类的子类。 Animal类中定义了一个虚方法makeSound(),它会输出动物发出的声音: public class Animal { public void makeSound() { System.out.println("The animal makes a sound"); } } Cat类继承了Animal类并覆盖了makeSound()方法: public class Cat extends Animal { public void makeSound() { System.out.println("The cat meows"); } } 现在,我们可以实例化一个Animal对象和一个Cat对象,并调用它们makeSound()方法: Animal animal = new Animal(); Cat cat = new Cat(); animal.makeSound(); // 输出 "The animal makes a sound" cat.makeSound(); // 输出 "The cat meows" 注意,由于Cat类继承了Animal类,因此可以使用Cat对象来代替Animal对象,因为它们都是Animal类的实例。这就是面向对象编程中的多态性。 多态的实现方式 方式一:重写: 这个内容已经在上一篇文章中我有详细的讲过Java 重写(Override)与重载(Overload) 方式二:接口 生活中的接口最具代表性的就是插座,例如一个三接头的插头都能接在三孔插座中,因为这个是每个国家都有各自规定的接口规则,有可能到国外就不行,那是因为国外自己定义的接口类型。 java中的接口类似于生活中的接口,就是一些方法特征的集合,但没有方法的实现。具体可以看 java接口 这一章节的内容。 方式三:抽象类和抽象方法 ———————————————— 版权声明:本文为CSDN博主「许思王」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_73602725/article/details/133038845
-
错误 Java:不支持发行版本5 详细错误 同学在github上找到一个微服务项目(基于maven进行构建),进行二开,导入项目运行控制台报错 Java:不支持发行版本5,笔者修改项目结构(F i l e FileFile→ \rightarrow→P r o j e c t S t r u c t u r e Project StructureProjectStructure)以及设置(F i l e FileFile→ \rightarrow→S e t t i n g s SettingsSettings)后,依旧报错 解决方案 对于pom.xml依赖文件加入如下配置 <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> 报错原因 报错提示 “Java:不支持发行版本5” 表明项目使用了 Java 5 的发行版本,但当前环境不支持该版本。 解决原因 通过将项目的编译器源码和目标版本设置为 1.8,确保项目在 Java 8 环境下进行编译和运行。这样可以与当前环境的 Java 版本保持一致,避免报错。 笔者在此对pom.xml依赖文件加入如下配置进行详细解释 <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <properties> </properties> 标签用于定义项目的属性。在上述解决方案中,我们向 标签添加了两个属性: <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>:指定项目源代码的 Java 版本。在此处,我们将其设置为 1.8,表示使用 Java 8 的语法和特性编写源代码。 <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>:指定编译生成的字节码的 Java 版本。同样地,我们将其设置为 1.8,以确保编译后的代码与 Java 8 兼容。 通过将这两个属性设置为 1.8,我们告诉 Maven 使用 Java 8 的编译器进行编译,并生成与 Java 8 兼容的字节码文件。这样可以解决报错问题,确保项目在 Java 8 环境下正确编译和运行。 参考文献 解决方案参考错误“ Java:不支持发行版本5”的正确解决方案 解决原因参考chatgpt 原创不易 转载请标明出处 如果对你有所帮助 别忘啦点赞支持哈 ———————————————— 版权声明:本文为CSDN博主「飞滕人生TYF」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/T_Y_F_/article/details/131317847
-
1.设计来源 主旨: 简洁,干净,明亮,舒适的风格; 语言: 可以html+js+css,也可以html+css; 版块: 人信息,基本资料,专业技能,教育经历(支持更多扩展); 适用: 个人简介,个人简历,个人主页,个人博客,个人空间等方向; 1.1 主界面 主界面动态的朦胧天空背景图,可以配置自己的图标,个人名言,名字,职称,邮箱,手机号,工作地址等信息,也可以扩展新的。 1.2 基本资料 基本资料动态的朦胧天空背景图,可以配置个人职业技能,大图标,姓名,年龄,性别,手机号,邮箱,qq号,居住地,户籍,学历,学校,专业等信息,也可以扩展新的。 1.3 专业技能 专业技能动态的科技线路背景图,可以配置个人专业技能,按列表排序出来,个人的掌握的工具,及熟练度等信息,也可以扩展新的。 1.4 教育经历 教育经历动态的科技线路背景图,可以配置个人教育的学校,时间,几学到的东西等信息,也可以扩展新的。 1.5 工作经验 工作经验动态的科技线路背景图,可以配置个人工作过的公司,工作的时间,负责的项目,开发工具等信息,也可以扩展新的。 2.效果和源码 2.1 动态效果 下面咋们一起来看看这个个人简历的动态效果,改变图片,可以配置多种风格,灵活运用,效果酷炫。 html简洁漂亮的个人简历,个人主页,个人简介网页版 2.2 源代码 这里是主界面的代码,其他图片、js、css等代码,见下面的 源码下载 ,里面有所有代码资源和相关说明。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>徐纯宇 - 个人简历,个人主页,个人介绍,个人简介</title> <link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon"> <link rel="stylesheet" type="text/css" href="css/main.css"> </head> <body oncontextmenu="return false;" onselectstart="return false;" unselectable="on" ondragstart="return false;"> <aside> <a class="cur_a"><span>徐纯宇</span></a> <a><span>基本资料</span></a> <a><span>专业技能</span></a> <a><span>教育经历</span></a> <a><span>工作经验</span></a> </aside> <section class="page_one" id="page1"> <div class="cen_con"> <div class="portrait"> <img onmousemove="this.src='images/user_c.jpg'" onmouseout="this.src='images/user.jpg'"/ src="images/user.jpg"> </div> <div class="cen_text"> <h2>业精于勤;荒于嬉;行成于思;毁于随。</h2> <hr> <h3>徐纯宇</h3> <h3>全栈工程师</h3> <h3> 1376174032@qq.com | 13300000000</h3> <h3>在职北京 ▣ 海淀</h3> </div> </div> <div class="down_arrow"> <a class="scroll"><span></span></a> </div> </section> <section class="page_two" id="page2"> <div class="con_wrap"> <div class="tit_wrap"> <h1 style="font-weight: bold;">基本资料</h1> <div class="scissors" style="border-top:1px dashed orange;"> </div> <h2> 毕业于北京理工大学的计算机应用专业。从业十年有余,熟悉C#和Java前后端开发,熟悉oracle和mysql数据库,熟悉Html和js及css前端开发,熟悉uniapp跨平台开发小程序和app。</h2> </div> <div class="myinfo"> <table> <tbody> <tr> <td rowspan="6"> <img src="images/user.jpg"> </td> <td>姓名 | 徐纯宇</td> <td>手机 | 13300000000</td> </tr> <tr> <td>性别 | 男</td> <td>邮箱 | 1376174032@qq.com</td> </tr> <tr> <td>出生 | 1992.02.20</td> <td>QQ号 | 1376174032</td> </tr> <tr> <td>居住 | 北京市海淀区</td> <td>户籍 | 北京市海淀区</td> </tr> <tr> <td>学历 | 本科</td> <td>学校 | 北京理工大学</td> </tr> <tr> <td>专业 | 计算机应用</td> <td></td> </tr> </tbody> </table> </div> </div> <div class="down_arrow"> <a class="scroll"><span></span></a> </div> </section> <section class="page_three" id="page3"> <div class="con_wrap"> <div class="tit_wrap"> <h1 style="font-weight: bold;">专业技能</h1> <div class="scissors" style="border-top:1px dashed orange;"> </div> <h2>长年累月后,精进又向前。唯有勤学练,擅长最优先。</h2> </div> <div class="skill_con"> <div class="canvas_wrap"> <div class="canvas_con"> <div class="text_con"> <p class="percent">98%</p> <p class="chart_title">C#</p> </div> <canvas id="html5" width=160 height=160></canvas> </div> <div class="canvas_con"> <div class="text_con"> <p class="percent">93%</p> <p class="chart_title">JAVA</p> </div> <canvas id="css3" width=160 height=160></canvas> </div> <div class="canvas_con"> <div class="text_con"> <p class="percent">96%</p> <p class="chart_title">HTML</p> </div> <canvas id="js" width=160 height=160></canvas> </div> <div class="canvas_con"> <div class="text_con"> <p class="percent">99%</p> <p class="chart_title">ORACLE</p> </div> <canvas id="jq" width=160 height=160></canvas> </div> </div> <div class="text_wrap"> <p>1. 熟练使用java,基于ssh能快速搭建系统框架。</p> <p>2. 熟练使用java,基于spring boot能快速搭建系统框架。</p> <p>3. 熟练使用HTML/CSS技术,精通js/jquery编程,能够熟练使用angularjs,vue,reactjs等前端框架。</p> <p>4. 熟悉oracle,mysql,sqlserver等各平台安装使用,熟练使用SQL语句增删改查,触发器,存储过程,索引,序列。</p> <p>5. 熟练的使用grunt,gulp等前端工具。</p> <p>6. 熟悉c#语言研发,基于asp.new mvc能快速搭建系统框架,新技术asp.net core做后台数据服务能快速搭建,跨平台使用。</p> <p>7. 熟悉c#语言研发,基于winform的c/s应用程序能快速搭建框架。</p> </div> </div> </div> <div class="down_arrow"> <a class="scroll"><span></span></a> </div> </section> <section class="page_four" id="page4"> <div class="con_wrap"> <div class="tit_wrap"> <h1 style="font-weight: bold;">教育经历</h1> <div class="scissors" style="border-top:1px dashed orange;"> </div> <h2>学则智,不学则愚;学则治,不学则乱。自古圣贤,成大业,未有不由学而成者。</h2> </div> <div class="work_con"> <div class="programe"> <div class="work_time">4年<br>北京理工大学</div> <div class="work_text"> <div class="triangle-left"></div> <div class="exCon"> <h4>学习时间:2008/06 -- 2012/06</h4> <p>学习技能:</p> <p>大学四年时间完成了所有的学业,并参与了很多项目研发工作,配合老师和同学攻关了很多难题。</p> <p> 1.掌握了办公五剑客,Word,PPT,excel,ps,viso。</p> <p> 2.掌握了C#和java的前后端开发,数据库基本应用。</p> </div> </div> </div> <div class="programe"> <div class="work_time">1年<br>Java培训班</div> <div class="work_text"> <div class="triangle-left"></div> <div class="exCon"> <h4>学习时间:2011/05 -- 2012/04</h4> <p>学习技能:</p> <p>参加了一年的JAVA培训班,跟着老师一起做了多个项目。积累了很多经验,有了很多经历。</p> <p>1.掌握了springboot框架下的api开发,并开发了自己的一套数据接口。</p> <p>2.掌握了ssm框架下的前后端开发,并开发了自己的博客。</p> </div> </div> </div> </div> </div> <div class="down_arrow"> <a class="scroll"><span></span></a> </div> </section> <section class="page_five" id="page5"> <div class="con_wrap"> <div class="tit_wrap"> <h1 style="font-weight: bold;">工作经验</h1> <div class="scissors" style="border-top:1px dashed orange;"> </div> <h2>生命,需要我们去努力。年轻时,我们要努力锻炼自己的能力,掌握知识、掌握技能、掌握必要的社会经验。</h2> </div> <div class="work_con"> <div class="programe"> <div class="work_time">22个月<br>汽车之家官网</div> <div class="work_text"> <div class="triangle-left"></div> <div class="exCon"> <h4>开发时间:2019 /12--2021 /10</h4> <h5>开发工具:VS2015,VSCode,ORACLE</h5> <p>项目描述:</p> <p>参与与客户交流需求,参与项目整体开发,主要负责首页面各方面数据综合显示,系统采用预加载,通过wcf对硬件控制,实时更新,一些页面设计,数据库的设计,数据测试。</p> <p> 1.数据信息展示块,2.软硬件交互块,3.数据解析块。</p> </div> </div> </div> <div class="programe"> <div class="work_time">8个月<br>联想后台研发</div> <div class="work_text"> <div class="triangle-left"></div> <div class="exCon"> <h4>开发时间:2021 /10--2022 /06</h4> <h5>开发工具:IDEA,VSCode,MYSQL</h5> <p>项目描述:</p> <p>参与与客户交流需求,参与项目整体开发,主要负责首页面各方面数据综合显示,系统采用预加载,实时更新,一些页面设计,数据库的设计,数据测试。 </p> <p>1.设备基础信息 2.部门信息 3.用户信息 4.数据报表 5.调试工具,6.数据监测,7.系统配置等7个大功能块 系统采用角色分配,操作分配等权限,实现各个角色的权限功能。精确划分各个部门的职责。</p> </div> </div> </div> </div> </div> </section> </body> <script type="text/javascript" src="js/main.js"></script> </html>
-
一.Spring Security安全框架实现密码加密方法简述 1.首先:Spring Security提供了强大的加密工具PasswordEncoder,PasswordEncoder接口的代码如下: package org.springframework.security.crypto.password; public interface PasswordEncoder { String encode(CharSequence var1);//是是对密码加密的方法 boolean matches(CharSequence var1, String var2);//是用来验证密码和加密后密码是否一致的如果一致则返回true } 2.其次:Spring Security提供了BCryptPasswordEncoder类,该类实现了Spring的PasswordEncoder接口,使用BCrypt强哈希方法来对密码进行加密,通过BCrypt强哈希方法每一次加密的结果都不一样:可以看看示例加密后的密码,需要说明的是这两种加密后的密码,其明文密码都是:123 3.加密的代码,可以看到每次加密产生的都是随机字符串: public String encode(CharSequence rawPassword) { String salt; if (this.strength > 0) { if (this.random != null) { salt = BCrypt.gensalt(this.strength, this.random); } else { salt = BCrypt.gensalt(this.strength); } } else { salt = BCrypt.gensalt(); } return BCrypt.hashpw(rawPassword.toString(), salt); } 二.加密的具体实现步骤 1.在配置文件中配置加密所需要的工具类 <!--配置加密工具类--> <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/> 2.在service的实现层中注入BCryptPasswordEncoder @Autowired private BCryptPasswordEncoder passwordEncoder;//通过注解拿到加密对象 3.然后在在service的实现层中的需要加密的操作方法中添加比如保存操作 //保存一个用户,使用加密算法把加密后的密码保存入数据库 @Override public void save(SysUser sysUser) { //二.使用安全框架加密方式 //1.Security安全框架加密操作:加密密码并存入sysUser对象中 String encode = passwordEncoder.encode(sysUser.getPassword());//拿到加密后的密码 sysUser.setPassword(encode);//将加密后的密码设置到sysUser对象中 userDao.save(sysUser); } 4.以上配置结束你需要运行一下,添加一个用户(这个用户的密码是加密的),否则用之前没有加密的密码登录是会失败的 ;然后去除"{noop}" //2.Security安全框架加密操作:去除+"{noop}"+————————————非常重要 User user = new User(sysUser.getUsername(), sysUser.getPassword(), authorities); return user; 5.在配置文件中spring_security.xml 里面引用第一步配置的id <!-- Security安全框架加密操作:引入加密操作--> <security:password-encoder ref="passwordEncoder"/> ———————————————— 原文链接:https://blog.csdn.net/weixin_43330884/article/details/104632267
-
博主在之前已经介绍了Spring Security的用户UserDetails与用户服务UserDetailsService,本篇博客介绍Spring Security的密码编码器PasswordEncoder,它们是相互联系的,博主会带大家一步步深入理解Spring Security的实现原理,也会带来Spring Security的实战分享。 Spring Security:用户UserDetails源码与Debug分析 Spring Security:用户服务UserDetailsService源码分析 为什么是介绍而不是源码分析?博主虽然在研一上过密码学的课,但毕竟没有细致研究过密码学领域,因此不敢管中窥豹。再者,这些加密算法的实现原理、是否能抵御攻击、明文与密文(明文经过加密得到)的匹配方法以及时间成本等因素都不是学习Spring Security框架的核心内容,因此本篇博客只会简单介绍Spring Security的密码编码器PasswordEncoder及其实现类,以及密码编码器在Spring Security中的使用时机。 PasswordEncoder PasswordEncoder接口有很多实现类,也有被标记了@Deprecated注解的实现类,一般是该类表示的密码编码器(加密算法)不安全,比如可以在能接受的时间内被破解,比如彩虹表攻击。 这里不去分析每个密码编码器的实现原理,因为密码编码器的种类太多了,而且没有必要,密码编码器的主要作用无非就是对密码进行编码(加密),以及原始密码(客户端登录验证时输入的密码)与编码密码(正确原始密码通过密码编码器编码的结果)的正确匹配,因此密码编码器必定需要实现PasswordEncoder接口的两个方法,而其他方法的实现是服务于这两个方法。 package org.springframework.security.crypto.password; // 首选实现是BCryptPasswordEncoder public interface PasswordEncoder { /** * 对原始密码进行编码 */ String encode(CharSequence rawPassword); /** * 验证从存储(比如数据库或者内存等)中获取的编码密码是否与需要验证的密码匹配 * 如果密码匹配,则返回 true,否则返回 false * 存储的编码密码永远不会被解码 * 因此会将需要验证的密码进行编码,然后与编码密码进行匹配 */ boolean matches(CharSequence rawPassword, String encodedPassword); /** * 如果为了更好的安全性需要再次对编码的密码进行编码,则返回 true,否则返回 false * 默认实现始终返回 false */ default boolean upgradeEncoding(String encodedPassword) { return false; } } 很显然密码编码器的主要作用是为了编码与匹配,而有些加密算法需要经过多次迭代加密,因此也需要实现upgradeEncoding方法,比如BCryptPasswordEncoder类的实现(strength属性越大,需要做更多的工作来加密密码,默认值为10): @Override public boolean upgradeEncoding(String encodedPassword) { if (encodedPassword == null || encodedPassword.length() == 0) { logger.warn("Empty encoded password"); return false; } Matcher matcher = BCRYPT_PATTERN.matcher(encodedPassword); if (!matcher.matches()) { throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword); } else { int strength = Integer.parseInt(matcher.group(2)); return strength < this.strength; } } PasswordEncoderFactories PasswordEncoderFactories类源码: package org.springframework.security.crypto.factory; import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; import java.util.HashMap; import java.util.Map; /** * 用于创建PasswordEncoder实例 */ public class PasswordEncoderFactories { /** * 使用默认映射创建一个DelegatingPasswordEncoder * 可能会添加其他映射,并且将更新编码以符合最佳实践 * 但是,由于DelegatingPasswordEncoder的性质,更新不应影响用户 */ @SuppressWarnings("deprecation") public static PasswordEncoder createDelegatingPasswordEncoder() { // 默认bcrypt String encodingId = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put(encodingId, new BCryptPasswordEncoder()); encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder()); encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder()); encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1")); encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256")); encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder()); encoders.put("argon2", new Argon2PasswordEncoder()); return new DelegatingPasswordEncoder(encodingId, encoders); } private PasswordEncoderFactories() {} } PasswordEncoderFactories类可以看作密码编码器工厂,它将已有的密码编码器存储在HashMap中,通过静态方法createDelegatingPasswordEncoder即可获取,该方法的返回值是一个DelegatingPasswordEncoder实例。 DelegatingPasswordEncoder DelegatingPasswordEncoder类源码: public class DelegatingPasswordEncoder implements PasswordEncoder { private static final String PREFIX = "{"; private static final String SUFFIX = "}"; private final String idForEncode; private final PasswordEncoder passwordEncoderForEncode; private final Map<String, PasswordEncoder> idToPasswordEncoder; private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder(); /** * 创建一个新实例 * idForEncode:用于查找应使用哪个PasswordEncoder进行encode * idToPasswordEncoder:id到PasswordEncoder的映射,用于确定应使用哪个PasswordEncoder进行matches */ public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) { if (idForEncode == null) { throw new IllegalArgumentException("idForEncode cannot be null"); } if (!idToPasswordEncoder.containsKey(idForEncode)) { throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder); } for (String id : idToPasswordEncoder.keySet()) { if (id == null) { continue; } // 如果id有'{'或者'}'字符则会出现问题,比如: // 基于prefixEncodedPassword获取id,是根据'{'和'}'字符对第一次出现的位置来截取 // 以及去除{id}得到encodedPassword,是根据'}'字符第一次出现的位置来截取 if (id.contains(PREFIX)) { throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX); } if (id.contains(SUFFIX)) { throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX); } } // 初始化 this.idForEncode = idForEncode; this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode); this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder); } /** * 设置defaultPasswordEncoderForMatches,默认为UnmappedIdPasswordEncoder实例 */ public void setDefaultPasswordEncoderForMatches( PasswordEncoder defaultPasswordEncoderForMatches) { if (defaultPasswordEncoderForMatches == null) { throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null"); } this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches; } // 编码,{id}前缀拼接委托的PasswordEncoder的编码结果 @Override public String encode(CharSequence rawPassword) { return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword); } // 匹配 @Override public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { if (rawPassword == null && prefixEncodedPassword == null) { return true; } // 根据prefixEncodedPassword提取id String id = extractId(prefixEncodedPassword); // 根据id获取PasswordEncoder PasswordEncoder delegate = this.idToPasswordEncoder.get(id); // 是否有对应的PasswordEncoder if (delegate == null) { // 没有对应的PasswordEncoder // 则使用defaultPasswordEncoderForMatches进行匹配 return this.defaultPasswordEncoderForMatches .matches(rawPassword, prefixEncodedPassword); } // 有对应的PasswordEncoder // 提取encodedPassword,即去掉{id}前缀 String encodedPassword = extractEncodedPassword(prefixEncodedPassword); // 返回匹配结果 return delegate.matches(rawPassword, encodedPassword); } // 提取id private String extractId(String prefixEncodedPassword) { if (prefixEncodedPassword == null) { return null; } // 第一个'{'字符的位置 int start = prefixEncodedPassword.indexOf(PREFIX); if (start != 0) { return null; } // 从start开始的第一个'}'字符的位置 int end = prefixEncodedPassword.indexOf(SUFFIX, start); if (end < 0) { return null; } // 截取得到id return prefixEncodedPassword.substring(start + 1, end); } @Override public boolean upgradeEncoding(String prefixEncodedPassword) { // 提取id String id = extractId(prefixEncodedPassword); // id与idForEncode属性不匹配,则返回true if (!this.idForEncode.equalsIgnoreCase(id)) { return true; } else { // 提取encodedPassword String encodedPassword = extractEncodedPassword(prefixEncodedPassword); // 根据id获取PasswordEncoder // 返回该PasswordEncoder的upgradeEncoding方法基于encodedPassword的返回值 return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword); } } // 提取encodedPassword private String extractEncodedPassword(String prefixEncodedPassword) { // 第一个'}'字符的位置 int start = prefixEncodedPassword.indexOf(SUFFIX); // 截取得到encodedPassword return prefixEncodedPassword.substring(start + 1); } /** * 引发异常的默认PasswordEncoder */ private class UnmappedIdPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { throw new UnsupportedOperationException("encode is not supported"); } @Override public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { String id = extractId(prefixEncodedPassword); throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\""); } } } DelegatingPasswordEncoder是基于前缀标识符并委托给另一个PasswordEncoder的密码编码器,可以使用PasswordEncoderFactories类创建一个DelegatingPasswordEncoder实例,也可以创建自定义的DelegatingPasswordEncoder实例。 密码存储格式为 {id}encodedPassword(prefixEncodedPassword),id是用于查找应该使用哪个PasswordEncoder的标识符,encodedPassword是使用PasswordEncoder对原始密码进行编码的结果,id必须在密码的开头,以{开头,}结尾。 如果找不到id,则id将为空。 例如,以下可能是使用不同id(PasswordEncoder)编码的密码列表: {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG {noop}password {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 第一个密码的id为bcrypt,encodePassword为$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG,匹配时,将委托给BCryptPasswordEncoder。第二个密码的id为noop,encodePassword为password。匹配时,将委托给NoOpPasswordEncoder。以此类推。 传递给构造函数的idForEncode确定将使用哪个PasswordEncoder来编码原始密码。匹配是基于id和构造函数中提供的idToPasswordEncoder来完成的。matches方法可能使用带有未映射id(包括空id)的密码,调用matches方法将抛出IllegalArgumentException异常, 可以使用setDefaultPasswordEncoderForMatches方法自定义此行为,即设置defaultPasswordEncoderForMatches属性,当根据id获取不到PasswordEncoder时使用。 Debug分析 依赖: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.kaven</groupId> <artifactId>security</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> </parent> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project> 接口: @RestController public class MessageController { @GetMapping("/message") public String getMessage() { return "hello kaven, this is security"; } } 启动类: @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } } Debug启动应用,访问接口,会被重定向到默认登录页。 使用Spring Security自动创建的用户(用户名为user,密码在启动日志中)进行登录验证。 用户验证时Spring Security会使用DelegatingPasswordEncoder类的matches方法进行密码匹配,提取的id为noop(prefixEncodedPassword有{noop}前缀,应用启动时,如果没有用户与用户源的相关配置,Spring Security会创建一个默认用户,即一个UserDetails实例,该实例的密码就是prefixEncodedPassword),因此委托给NoOpPasswordEncoder进行密码匹配。 NoOpPasswordEncoder类的matches方法只是简单的字符串匹配,上图的rawPassword和encodedPassword很显然是匹配的。 public boolean matches(CharSequence rawPassword, String encodedPassword) { return rawPassword.toString().equals(encodedPassword); } 验证成功。 配置PasswordEncoder 增加配置: package com.kaven.security.config; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // 重写验证处理的配置 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(new UserDetailsServiceImpl()).passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); } // 自定义的用户服务 public static class UserDetailsServiceImpl implements UserDetailsService { // 使用PasswordEncoderFactories工厂创建DelegatingPasswordEncoder实例作为该用户服务的密码编码器 private static final PasswordEncoder PASSWORD_ENCODER = PasswordEncoderFactories.createDelegatingPasswordEncoder(); @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // TODO 查找数据库 // 使用密码编码器对原始密码进行编码 String encodedPassword = PASSWORD_ENCODER.encode("itkaven"); // 默认存在该用户名的用户,并且原始密码都为itkaven,角色都为USER和ADMIN return User.withUsername(username).password(encodedPassword).roles("USER", "ADMIN").build(); } } } Debug启动应用,访问接口,然后进行验证登录,由于自定义的用户服务默认任意用户名的用户都存在,并且原始密码都为itkaven,角色都为USER和ADMIN,因此登录时用户名可任意,但密码必须为itkaven才能通过验证。 客户端进行验证登录时,Spring Security通过用户服务加载匹配用户名的UserDetails实例,而博主自定义的用户服务直接默认该UserDetails实例存在,并且设置默认的密码(编码后的密码,使用UserDetailsServiceImpl类中的PASSWORD_ENCODER进行编码)与角色(权限)。密码匹配使用在重写验证处理的配置时指定的密码编码器来完成(passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()))。 id为bcrypt,使用BCryptPasswordEncoder进行密码匹配,因为通过自定义的用户服务加载的UserDetails实例的密码就是DelegatingPasswordEncoder的{bcrypt}前缀与BCryptPasswordEncoder对原始密码(itkaven)的编码的拼接,最后会返回true。 所以,密码编码器在用户验证时用于密码的匹配,以及创建UserDetails实例时对密码进行编码(可选,如果是基于用户服务加载的UserDetails实例创建的新实例,新实例一般不更改该UserDetails实例的密码,因此,通过用户服务加载的UserDetails实例的密码应该是编码后的密码),因此密码的编码与匹配过程需要使用相同的密码编码器,不然一样的原始密码也有可能匹配不成功。 Spring Security的密码编码器PasswordEncoder的介绍与Debug分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。 ———————————————— 原文链接:https://blog.csdn.net/qq_37960603/article/details/122318269
-
在 Spring Security 中有一个加密的类 BCryptPasswordEncoder ,它的使用非常的简单而且也比较有趣。让我们来看看它的使用。 BCryptPasswordEncoder 的使用 首先创建一个 SpringBoot 的项目,在创建项目的时候添加 Spring Security 的依赖。然后我们添加一个测试类,写如下的代码: final private String password = "123456"; @Test public void TestCrypt() { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String encode1 = bCryptPasswordEncoder.encode(password); System.out.println("encode1:" + encode1); String encode2 = bCryptPasswordEncoder.encode(password); System.out.println("encode2:" + encode2); } 上面的代码中,首先实例化了一个 BCryptPasswordEncoder 类,然后使用该类的 encode 方法对同一个明文字符串进行了加密,并输出。运行上面的代码,查看输出。 encode1:$2a$10$SqbQb0pD3KYrH7ZVTWdRZOhPAelQqa..lUnysXoWag6RvMkyC5SE6 encode2:$2a$10$0sjBLlwrrch2EjgYls197e9dGRCMbQ7KUIt/ODPTSU0W.mEPaGkfG 从上面的输出可以看出,同一个明文加密两次,却输出了不同的结果。是不是很神奇?但是这样有一个问题,如果使用 BCryptPasswordEncoder 去加密登录密码的话,还能进行验证么?当然是可以验证的。验证的话,使用的是 BCryptPasswordEncoder 的 matches 方法,代码如下。 final private String password = "123456"; @Test public void TestCrypt() { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String encode1 = bCryptPasswordEncoder.encode(password); System.out.println("encode1:" + encode1); boolean matches1 = bCryptPasswordEncoder.matches(password, encode1); System.out.println("matches1:" + matches1); String encode2 = bCryptPasswordEncoder.encode(password); System.out.println("encode2:" + encode2); boolean matches2 = bCryptPasswordEncoder.matches(password, encode2); System.out.println("matches2:" + matches2); } 使用 matches 方法可以对加密前和加密后是否匹配进行验证。输出如下: encode1:$2a$10$qxU.rFLeTmZg47FyqJlZwu.QNX9RpEvqBUJiwUvUE0p4ENR.EndfS matches1:true encode2:$2a$10$NyGEOsQ1Hxv2gvYRmaEENueORlVDtSqoB/fHN76KkvQDeg7fbTy22 matches2:true 可以看到两次加密后的字符串虽然不同,但是通过 matches 方法都可以匹配出它们是 “123456” 这个明文加密的结果。同样很神奇,这是为什么呢? encode 和 matches 方法的原理 我们通过源码来看看它们的原理吧。 首先我们将依赖的源码下载到本地,方便我们进行调试。开始我使用 IDEA 进行调试时是没有源码的,后来下载了源码,发现没有源码时调试的是 Class 文件,IDEA 提供了 Class 文件与源码等价的反编译代码。而源码逻辑性更好一些。 首先在 encode 代码处下断点,然后我们单步步入,去查看 encode 的实现,代码如下: @Override public String encode(CharSequence rawPassword) { if (rawPassword == null) { throw new IllegalArgumentException("rawPassword cannot be null"); } String salt = getSalt(); return BCrypt.hashpw(rawPassword.toString(), salt); } 直接运行到 return 语句处,查看一下 salt 的值,该值如下: $2a$10$H73jYFFLWWf.VV/mwonqru 然后继续单步步入到 BCrypt.hashpw 方法内,该方法代码如下: public static String hashpw(String password, String salt) { byte passwordb[]; passwordb = password.getBytes(StandardCharsets.UTF_8); return hashpw(passwordb, salt); } 该方法的重点同样是 hashpw 方法,没有做什么处理,继续进入 hashpw 方法中。代码如下: public static String hashpw(byte passwordb[], String salt) { BCrypt B; String real_salt; byte saltb[], hashed[]; char minor = (char) 0; int rounds, off; StringBuilder rs = new StringBuilder(); if (salt == null) { throw new IllegalArgumentException("salt cannot be null"); } int saltLength = salt.length(); if (saltLength < 28) { throw new IllegalArgumentException("Invalid salt"); } if (salt.charAt(0) != '$' || salt.charAt(1) != '2') { throw new IllegalArgumentException("Invalid salt version"); } if (salt.charAt(2) == '$') { off = 3; } else { minor = salt.charAt(2); if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') { throw new IllegalArgumentException("Invalid salt revision"); } off = 4; } // Extract number of rounds if (salt.charAt(off + 2) > '$') { throw new IllegalArgumentException("Missing salt rounds"); } if (off == 4 && saltLength < 29) { throw new IllegalArgumentException("Invalid salt"); } rounds = Integer.parseInt(salt.substring(off, off + 2)); real_salt = salt.substring(off + 3, off + 25); saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); if (minor >= 'a') { passwordb = Arrays.copyOf(passwordb, passwordb.length + 1); } B = new BCrypt(); hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0); rs.append("$2"); if (minor >= 'a') { rs.append(minor); } rs.append("$"); if (rounds < 10) { rs.append("0"); } rs.append(rounds); rs.append("$"); encode_base64(saltb, saltb.length, rs); encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs); return rs.toString(); } 代码很长,但是真正关键的代码在上面第 43 、44 和 51 行的位置处,43 行处获取真正的 salt ,44 行是使用 base64 进行解码,然后 51 行用 密码、salt 进行处理。在来看看返回值是 rs,在第 63 行和 64 行,对 salt 进行 base64 编码后放入了 rs 中,然后对 hashed 进行 base64 编码后也放入了 rs 中,最后 rs.toString() 返回。 虽然上面代码很长,其实真正关键的就只有上面我提到的几句,其余的部分不用看。我们接着看 matches 的源码,同样单步进入,代码如下: @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { if (rawPassword == null) { throw new IllegalArgumentException("rawPassword cannot be null"); } if (encodedPassword == null || encodedPassword.length() == 0) { this.logger.warn("Empty encoded password"); return false; } if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) { this.logger.warn("Encoded password does not look like BCrypt"); return false; } return BCrypt.checkpw(rawPassword.toString(), encodedPassword); } 单步到第 14 行的 return 处,这里调用 BCrypt.checkpw 的方法,rawPassword.toString() 是我们的密码,即 ”123456“, 后面的 encodePassword 是我们加密后的密码,单步步入进去,代码如下: public static boolean checkpw(String plaintext, String hashed) { return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed)); } 这里只有一行代码,但是代码中同样调用了前面的 hashpw 这个方法,传入的参数是 plaintext 和 hashed,plaintext 是我们的密码,即 “123456”, hashed 是加密后的密码。到这里基本就明白了。hashed 在进入 hashpw 函数后,会通过前面说到第 43 行代码取出真正的 salt,然后对通过 salt 和 我们的密码进行加密,这样流程就串联起来了。 总结 当时看到使用 BCryptPasswordEncoder 时,同样的密码可以生成不同的 密文 而且还可以通过 matches 方法进行匹配验证,觉得很神奇。后来经过调试发现,密文中本身包含了很多信息,包括 salt 和 使用 salt 加密后的 hash。因为每次的 salt 不同,因此每次的 hash 也不同。这样就可以使得相同的 明文 生成不同的 密文,而密文中包含 salt 和 hash,因此验证过程和生成过程也是相同的。 ———————————————— 原文链接:https://blog.csdn.net/easysec/article/details/121719949
-
BCryptPasswordEncoder 是一种使用 BCrypt 加密算法来加密密码的方法。它是在 Spring Security 中用来加密用户密码的一个类,其目的是为了防止密码被明文存储在数据库中。BCrypt 是一种强哈希算法,它能很好地防止被暴力破解。采用SHA-256 +随机盐+密钥对密码进行加密 首先导入依赖 <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>5.7.6</version> </dependency> 编写配置类 package com.qingyun.kunba.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.security.SecureRandom; @Data @Configuration @ConfigurationProperties(prefix = "encoder.crypt") public class PasswordConfig { /** * 加密强度 */ private int strength; /** * 干扰因子 */ private String secret; @Bean public BCryptPasswordEncoder passwordEncoder() { //System.out.println("secret = " + secret); //对干扰因子加密 SecureRandom secureRandom = new SecureRandom(secret.getBytes()); //对密码加密 return new BCryptPasswordEncoder(strength, secureRandom); } } 配置yml encoder: crypt: secret: ${random.uuid} # 随机的密钥,使用uuid strength: 6 # 加密强度4~31,决定盐加密时的运算强度,超过10以后加密耗时会显著增加 接着就可以测试了 @Autowired private BCryptPasswordEncoder encoder; @Test void savePassword() { // encode():对明文字符串进行加密 //注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。 String pw = encoder.encode("abcd"); System.out.println(pw); // matches():对加密前和加密后是否匹配进行验证 //用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的), // 而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。 // 如果两者相同,说明用户输入的密码正确。 System.out.println(encoder.matches("asdsad", pw)); System.out.println(encoder.matches("abcd", pw)); } 测试结果如下 ———————————————— 原文链接:https://blog.csdn.net/qq_52458633/article/details/130990910
-
在前段代码中引入 weui.css ,weuix.css 和js jquery-weui.min.js就可以使用weui的一些样式了按照官方的文档中 $.toast("我是文本","text");弹出的样式应该如下: 但是我在实际中使用弹出的结果却是这个样子: 为什么 原因肯定是weui的样式和其他的样式冲突了 我们看看 $.toast 这一串代码: t.toast=function(t,a,r) { "function"==typeof a&&(r=a); var o,s="weui-icon-success-no-circle", c=i.duration; "cancel"==a?(o="weui-toast_cancel",s="weui-icon-cancel"):"forbidden"==a?(o="weui-toast--forbidden",s="weui-icon-warn"):"text"==a?o="weui-toast--text":"number"==typeof a&&(c=a), e('<i class="'+s+' weui-icon_toast"></i><p class="weui-toast_content">'+(t||"已经完成")+"</p>",o),setTimeout(function(){n(r)},c)} 大概能看出来 cancel ,forbidden,text 这些都是一些选项,当选择而不同的时候,出现不同的样式 text 应该是没有图标,为什么有图标了 我在页面上设置100,来查看这个样式 可以看到的是图标来源于这一部分 我们选中 i 这个标签,看右侧使用的样式: 果然是其他的样式干扰的 如果我们引入的其他的css作用不大,我们可以找到直接删除了 或者将 important 删除了 最终结果: 因为这次的样式调试用了很长的时间,特此记录 ———————————————— 原文链接:https://blog.csdn.net/datouniao1/article/details/111311058
-
用到了wrapper,ge、le、ne、eq等的用法,及多表查询自写sql整理资料记录一下,以备后续复习。 目录------------(可点击相应目录直接跳转) 一、条件构造器关系介绍 条件构造器关系介绍 : wapper介绍 : 二、项目实例 1、根据主键或者简单的查询条件进行查询 2、MyBatis-Plus还提供了Wrapper条件构造器,具体使用看如下代码: 三、具体使用操作 1、ge、gt、le、lt、isNull、isNotNull 2、eq、ne 3、between、notBetween 4、allEq 5、like、notLike、likeLeft、likeRight 6、in、notIn、inSql、notinSql、exists、notExists 7、or、and 8、嵌套or、嵌套and 9、orderBy、orderByDesc、orderByAsc 10、last 11、指定要查询的列 12、set、setSql 四、项目中实际应用代码实例 实例1--包含 eq相等的比较方法 实例2--包含 ge le ge等比较方法,及分页查询方法 实例3--多表查询,手写sql示例,五表联查 先了解一下内外连接: SQL内连接(INNER JOIN) SQL外连接(OUTER JOIN 一、条件构造器关系介绍 条件构造器关系介绍 : 上图绿色框为抽象类abstract 蓝色框为正常class类,可new对象 黄色箭头指向为父子类关系,箭头指向为父类 wapper介绍 : Wrapper : 条件构造抽象类,最顶端父类 AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件 QueryWrapper : Entity 对象封装操作类,不是用lambda语法 UpdateWrapper : Update 条件封装,用于Entity对象更新操作 AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。 LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper LambdaUpdateWrapper : Lambda 更新封装Wrapper 二、项目实例 1、根据主键或者简单的查询条件进行查询 /** * 通过单个ID主键进行查询 */ @Test public void selectById() { User user = userMapper.selectById(1094592041087729666L); System.out.println(user); } /** * 通过多个ID主键查询 */ @Test public void selectByList() { List<Long> longs = Arrays.asList(1094592041087729666L, 1094590409767661570L); List<User> users = userMapper.selectBatchIds(longs); users.forEach(System.out::println); } /** * 通过Map参数进行查询 */ @Test public void selectByMap() { Map<String, Object> params = new HashMap<>(); params.put("name", "张雨琪"); List<User> users = userMapper.selectByMap(params); users.forEach(System.out::println); } 2、MyBatis-Plus还提供了Wrapper条件构造器,具体使用看如下代码: /** * 名字包含雨并且年龄小于40 * <p> * WHERE name LIKE '%雨%' AND age < 40 */ @Test public void selectByWrapperOne() { QueryWrapper<User> wrapper = new QueryWrapper(); wrapper.like("name", "雨").lt("age", 40); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 名字包含雨 * 年龄大于20小于40 * 邮箱不能为空 * <p> * WHERE name LIKE '%雨%' AND age BETWEEN 20 AND 40 AND email IS NOT NULL */ @Test public void selectByWrapperTwo() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.like("name", "雨").between("age", 20, 40).isNotNull("email"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 名字为王性 * 或者年龄大于等于25 * 按照年龄降序排序,年龄相同按照id升序排序 * <p> * WHERE name LIKE '王%' OR age >= 25 ORDER BY age DESC , id ASC */ @Test public void selectByWrapperThree() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.likeRight("name", "王").or() .ge("age", 25).orderByDesc("age").orderByAsc("id"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 查询创建时间为2019年2月14 * 并且上级领导姓王 * <p> * WHERE date_format(create_time,'%Y-%m-%d') = '2019-02-14' AND manager_id IN (select id from user where name like '王%') */ @Test public void selectByWrapperFour() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.apply("date_format(create_time,'%Y-%m-%d') = {0}", "2019-02-14") .inSql("manager_id", "select id from user where name like '王%'"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 查询王姓 * 并且年龄小于40或者邮箱不为空 * <p> * WHERE name LIKE '王%' AND ( age < 40 OR email IS NOT NULL ) */ @Test public void selectByWrapperFive() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.likeRight("name", "王").and(qw -> qw.lt("age", 40).or().isNotNull("email")); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 查询王姓 * 并且年龄大于20 、年龄小于40、邮箱不能为空 * <p> * WHERE name LIKE ? OR ( age BETWEEN ? AND ? AND email IS NOT NULL ) */ @Test public void selectByWrapperSix() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.likeRight("name", "王").or( qw -> qw.between("age", 20, 40).isNotNull("email") ); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * (年龄小于40或者邮箱不为空) 并且名字姓王 * WHERE ( age < 40 OR email IS NOT NULL ) AND name LIKE '王%' */ @Test public void selectByWrapperSeven() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.nested(qw -> qw.lt("age", 40).or().isNotNull("email")) .likeRight("name", "王"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 查询年龄为30、31、32 * WHERE age IN (?,?,?) */ @Test public void selectByWrapperEight() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.in("age", Arrays.asList(30, 31, 32)); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 查询一条数据 * limit 1 */ @Test public void selectByWrapperNine() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.in("age", Arrays.asList(30, 31, 32)).last("limit 1"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } 三、具体使用操作 注意:以下条件构造器的方法入参中的 column 均表示数据库字段 1、ge、gt、le、lt、isNull、isNotNull @Test public void testDelete() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper .isNull("name") .ge("age", 12) .isNotNull("email"); int result = userMapper.delete(queryWrapper); System.out.println("delete return count = " + result); } SQL:UPDATE user SET deleted=1 WHERE deleted=0 AND name IS NULL AND age >= ? AND email IS NOT NULL 2、eq、ne 注意:seletOne返回的是一条实体记录,当出现多条时会报错 @Test public void testSelectOne() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", "Tom"); User user = userMapper.selectOne(queryWrapper); System.out.println(user); } 3、between、notBetween 包含大小边界 @Test public void testSelectCount() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.between("age", 20, 30); Integer count = userMapper.selectCount(queryWrapper); System.out.println(count); } SELECT COUNT(1) FROM user WHERE deleted=0 AND age BETWEEN ? AND ? 4、allEq @Test public void testSelectList() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); Map<String, Object> map = new HashMap<>(); map.put("id", 2); map.put("name", "Jack"); map.put("age", 20);9 queryWrapper.allEq(map); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); } SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 AND name = ? AND id = ? AND age = ? 5、like、notLike、likeLeft、likeRight selectMaps返回Map集合列表 @Test public void testSelectMaps() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper .notLike("name", "e") .likeRight("email", "t"); List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);//返回值是Map列表 maps.forEach(System.out::println); } SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 AND name NOT LIKE ? AND email LIKE ? 6、in、notIn、inSql、notinSql、exists、notExists in、notIn: notIn("age",{1,2,3})--->age not in (1,2,3) notIn("age", 1, 2, 3)--->age not in (1,2,3) inSql、notinSql:可以实现子查询 例: inSql("age", "1,2,3,4,5,6")--->age in (1,2,3,4,5,6) 例: inSql("id", "select id from table where id < 3")--->id in (select id from table where id < 3) @Test public void testSelectObjs() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); //queryWrapper.in("id", 1, 2, 3); queryWrapper.inSql("id", "select id from user where id < 3"); List<Object> objects = userMapper.selectObjs(queryWrapper);//返回值是Object列表 objects.forEach(System.out::println); } SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 AND id IN (select id from user where id < 3) 7、or、and 注意:这里使用的是 UpdateWrapper 不调用or则默认为使用 and 连 @Test public void testUpdate1() { //修改值 User user = new User(); user.setAge(99); user.setName("Andy"); //修改条件 UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>(); userUpdateWrapper .like("name", "h") .or() .between("age", 20, 30); int result = userMapper.update(user, userUpdateWrapper); System.out.println(result); } UPDATE user SET name=?, age=?, update_time=? WHERE deleted=0 AND name LIKE ? OR age BETWEEN ? AND ? 8、嵌套or、嵌套and 这里使用了lambda表达式,or中的表达式最后翻译成sql时会被加上圆括号 @Test public void testUpdate2() { //修改值 User user = new User(); user.setAge(99); user.setName("Andy"); //修改条件 UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>(); userUpdateWrapper .like("name", "h") .or(i -> i.eq("name", "李白").ne("age", 20)); int result = userMapper.update(user, userUpdateWrapper); System.out.println(result); } UPDATE user SET name=?, age=?, update_time=? WHERE deleted=0 AND name LIKE ? OR ( name = ? AND age <> ? ) 9、orderBy、orderByDesc、orderByAsc @Test public void testSelectListOrderBy() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.orderByDesc("id"); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); } SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 ORDER BY id DESC 10、last 直接拼接到 sql 的最后 注意:只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用 @Test public void testSelectListLast() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.last("limit 1"); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); } SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 limit 1 11、指定要查询的列 @Test public void testSelectListColumn() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.select("id", "name", "age"); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); } SELECT id,name,age FROM user WHERE deleted=0 12、set、setSql 最终的sql会合并 user.setAge(),以及 userUpdateWrapper.set() 和 setSql() 中 的字段 @Test public void testUpdateSet() { //修改值 User user = new User(); user.setAge(99); //修改条件 UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>(); userUpdateWrapper .like("name", "h") .set("name", "老李头")//除了可以查询还可以使用set设置修改的字段 .setSql(" email = '123@qq.com'");//可以有子查询 int result = userMapper.update(user, userUpdateWrapper); } UPDATE user SET age=?, update_time=?, name=?, email = '123@qq.com' WHERE deleted=0 AND name LIKE ? 四、项目中实际应用代码实例 (此部分更新于2022年7月20日) 实例1--包含 eq相等的比较方法 实例2--包含 ge le ge等比较方法,及分页查询方法 实例3--多表查询,手写sql示例,五表联查 先了解一下内外连接: 什么是连接表? 多表查询原理:将多个表通过笛卡尔积形成一个虚表,再根据查询条件筛选符合条件的数据。 在关系数据库中,数据分布在多个逻辑表中。 要获得完整有意义的数据集,需要使用连接来查询这些表 中的数据。 SQL Server支持多种 连接包括 INNER JOIN:内连接,关键字在表中存在至少一个匹配时返回行。 left join : 左连接,返回左表中所有的记录以及右表中连接字段相等的记录。 right join : 右连接,返回右表中所有的记录以及左表中连接字段相等的记录。 inner join : 内连接,又叫等值连接,只返回两个表中连接字段相等的行。 full join : 外连接,返回两个表中的行:left join + right join。 cross join : 结果是笛卡尔积,就是第一个表的行数乘以第二个表的行数。 GROUP BY:全外连接, 子句必须放在 WHERE 子句中的条件之后,必须放在 ORDER BY 子句之前 每种连接类型指定SQL Server如何使用一个表中的数据来选择另一个表中的行 SQL内连接(INNER JOIN) 返回两张表中符合连接条件的数据行 内连接是从结果表中删除与被连接表中未匹配行的所有行,所以内连接可能会丢失信息 SQL外连接(OUTER JOIN) 外连接(OUTER JOIN)分 为左连接、右连接和全连接 左连接:返回左表中的所以行,如果左表中行在右表中没有匹配行,则结果中右表中的列返回空值NULL 语法:SELECT * FROM 表1 LEFT OUTER JOIN 表2 ON 条件 eg:我们左连接Student表、Score表查询学生的成绩,SQL 语句如下: SELECT * FROM Student LEFT OUTER JOIN Score ON Student.id = Score.studentID 右 连 接:返回右表中的所以行,如果右表中行在左表中没有匹配行,则结果中左表中的列返回空值NULL 语法:SELECT * FROM 表1 RIGHT OUTER JOIN 表2 ON 条件 eg:我们右连接Student表、Score表查询学生的成绩,SQL 语句如下: SELECT * FROM Student RIGHT OUTER JOIN Score ON Student.id = Score.studentID 全连接:返回左表和右表中的所有行,当某行在另一表中没有匹配行,则另一表中的补NULL 这个是mybatis-plus插件中mapper的一个表查询写法,由多个表内连接或外连接组成的数据。 用到的表结构如下: 分别为问题库表、数据字典、数据字典类型、部门表、用户表 数据示例如下:obdis_problem表里去查type字段内容并返回对应的汉字语义 查询每个字段都进行一次表连接,拿到对应的值,得出一个表结果值,即A\B\C\D等等结果,最后拼起来返回前端展示即可。搞懂了这个,所有的多表查询就基本迎刃而解。 <!-- 问题库列表返回与查询--> <select id="searchMoreProblem" resultType="com.hollysys.obdis.vo.problem.SearchObdisProblem"> select p.id , p.level, A.dict_value as level_text, p.dept_id_mgr, B.org_name as deptIdMgr_text, p.dept_id_work, C.org_name as deptIdWork_text, p.station, D.dict_value as station_text, p.source, E.dict_value as source_text, p.categroy, F.dict_value as categroy_text, p.type, G.dict_value as type_text, p.dev_code, H.dict_value as devCode_text, p.check_time, p.check_user, I.username as checkUser_text, p.deadline, p.deadline_real, p.duty_user, J.username as dutyUser_text, p.correct_user, K.username as correctUser_text, p.state, p.problem, p.requirement, p.solution from obdis_problem p left join t_dict A on p.level = A.id left join t_dept B on p.dept_id_mgr = B.org_id left join t_dept C on p.dept_id_work = C.org_id left join t_dict D on p.station = D.id left join t_dict E on p.source = E.id left join t_dict F on p.categroy = F.id left join t_dict G on p.type = G.id left join t_dict H on p.dev_code = H.id left join t_user I on p.check_user = I.user_id left join t_user J on p.duty_user = J.user_id left join t_user K on p.correct_user = K.user_id where 1=1 <if test="searchObdisProblem.levelText != null and searchObdisProblem.levelText != ''"> and A.dict_value like concat('%', #{searchObdisProblem.levelText}, '%') </if> <if test="searchObdisProblem.deptIdMgrText != null and searchObdisProblem.deptIdMgrText != ''"> and B.org_name like concat('%', #{searchObdisProblem.deptIdMgrText}, '%') </if> <if test="searchObdisProblem.deptIdWorkText != null and searchObdisProblem.deptIdWorkText != '' "> and C.org_name like concat('%', #{searchObdisProblem.deptIdWorkText}, '%') </if> <if test="searchObdisProblem.stationText != null and searchObdisProblem.stationText != ''"> and D.dict_value = #{searchObdisProblem.stationText} </if> <if test="searchObdisProblem.sourceText != null and searchObdisProblem.sourceText != ''"> and E.dict_value = #{searchObdisProblem.sourceText} </if> <if test="searchObdisProblem.categroyText != null and searchObdisProblem.categroyText != ''"> and F.dict_value = #{searchObdisProblem.categroyText} </if> <if test="searchObdisProblem.typeText != null and searchObdisProblem.typeText != ''"> and G.dict_value = #{searchObdisProblem.typeText} </if> <if test="searchObdisProblem.devCodeText != null and searchObdisProblem.devCodeText != ''"> and H.dict_value = #{searchObdisProblem.devCodeText} </if> <if test="searchObdisProblem.checkTimeStart != null "> and p.check_time >= #{searchObdisProblem.checkTimeStart} </if> <if test="searchObdisProblem.checkTimeEnd != null "> and p.check_time <= #{searchObdisProblem.checkTimeEnd} order by p.check_time </if> <if test="searchObdisProblem.checkUserText != null and searchObdisProblem.checkUserText != ''"> and I.username = #{searchObdisProblem.checkUserText} </if> <if test="searchObdisProblem.deadline != null "> and p.deadline = #{searchObdisProblem.deadline} </if> <if test="searchObdisProblem.deadlineReal != null "> and p.deadline_real = #{searchObdisProblem.deadlineReal} </if> <if test="searchObdisProblem.dutyUserText != null and searchObdisProblem.dutyUserText != ''"> and J.username = #{searchObdisProblem.dutyUserText} </if> <if test="searchObdisProblem.correctUserText != null and searchObdisProblem.correctUserText != ''"> and K.username = #{searchObdisProblem.correctUserText} </if> <if test="searchObdisProblem.state != null and searchObdisProblem.state != ''"> and state = #{searchObdisProblem.state} </if> <if test="searchObdisProblem.problem != null and searchObdisProblem.problem != ''"> and p.problem like concat('%', #{searchObdisProblem.problem}, '%') </if> <if test="searchObdisProblem.requirement != null and searchObdisProblem.requirement != ''"> and p.requirement like concat('%', #{searchObdisProblem.requirement}, '%') </if> <if test="searchObdisProblem.solution != null and searchObdisProblem.solution != ''"> and p.solution like concat('%', #{searchObdisProblem.solution}, '%') </if> </select> ———————————————— 原文链接:https://blog.csdn.net/qq_39715000/article/details/120090033
-
el-cascader(联机选择器)动态加载+编辑默认值回显 最近又在工作中遇到了一个问题,就是在我们使用el-cascader加载默认值的时候,如果我们无法拿到全部的options数据,cascader的输入框和联级选择框都会遇到回显问题(只能显示第一层的数据),这个时候我们要怎么做呢,首先我们来看一下我们想要的效果 效果展示 先来看一下效果(由于我不太会用截屏动图工具 所以分成2张): 输入框中的回显数据 联级选择框中的已选数据 解决思路 其实cascader归根结底也就是那么几个属性的事,我们首先来看一下文档,这里列出了一些我们要用到的: 参数 说明 value / v-model 选中项绑定值 options 可选项数据源,键名可通过 Props 属性配置 lazy 是否动态加载子节点,需与 lazyLoad 方法结合使用 lazyload 加载动态数据的方法,仅在 lazy 为 true 时有效 那么首先我们来分析一下问题,问题的原因是因为我们的接口无法提供给我们完全的树形结构options数据,导致即使我们将获取到的已选数据传给v-model也加载不出来,所以我们要做的就是以下几步: 1.获取预选值 需要注意的是,我们这里获取的预选值最好是我们通过el-cascader提交时的数组数据,即每一个数据都是带有选择路径的数组,例如图中的PMO,获取的数据最好是[‘58集团’,‘技术功能平台群’,‘研发管理部’,‘PMO’],如果做不到这样的形式,那我们无论如何也要从其他接口获取到之前的路径项(不然我们就只能从最基础部分遍历获取全部的树形结构了),而后我们需要将取得的集合合并为一个路径群数组,如果和我是一样的多选联机选择框,最好在获取预选值的时候就是用promise const queue = res.result.map(item => { return new Promise(resolve => { that.$axios.get('获取预选值接口').then(data => { resolve(data.单一路径数组) }) }) }) Promise.all(queue).then(result => { result.forEach(i => { that.路径群数组.push(i) }) }) 2.根据预选值制作直线结构数据的数组对象 在我们拿到了想要的数据之后,我们需要将预选值规整为一个数组,并进行去重操作 路径数组 = Array.from(new Set(that.路径群数组.flat())) 这样我们就得到了一个包含路径中所有项的数组,注意:如果是多选型的cascader,那么在这个数组里就会有同级若干项数据,我们不用在意,遍历数组并调用获取下层数据的接口获得下层数据res,并拼接成数组对象,这里要活用Promise,最后我们要的数据形式是(注意这里的pid是指每个数组上一层的父级id) result = [{id: value1, Name: label1, children:res1 , pid: pid1}, {id: value2, Name: label2, children:res2 , pid: pid2}, {id: value3, Name: label3, children:res3 , pid: pid3}] 3.将直线结构的数组对象转换为树形结构 之后再将我们得到的数组对象转化成树形结构,网上有很多方法,这里我随便贴一个: var data = [] this.toTree(result, data, 0) toTree (list, data, fatherId) { list.forEach(item => { if (item.pid === fatherId) { var child = { orgName: item.orgName, id: item.id, children: [] } this.toTree(list, child.children, item.id) data.push(child) } }) }, 这样一来我们就制作了一个包含预选项及其各个父级的树形结构 4.将树形结构赋值给options 将树形结构赋值给options,这样他就可以在最开始的文本框中加载出预选项,并且不影响其他选项动态加载的处理 Options = data 5.总结 最后要声明一下,完成这样效果的方法不止这一种,这是在我走了很多弯路之后做出来的,属于笨办法之一吧; 网上还有大佬说虚拟一个el-cascader的输入框,将预选值放进去,点击的时候再进行动态加载,这也是一种不错的方法; 总之我个人感觉表达的不是很清晰,不过真的尽力了,大家如果有建议或者问题请给我留言,谢谢观看 ———————————————— 原文链接:https://blog.csdn.net/yzy13521758223/article/details/108055393
-
微信分享网页不显示缩略图片的原因为规范自定义分享链接功能在网页上的使用,自2017年4月25日起,JSSDK“分享到朋友圈”及“发送给朋友”接口,自定义的分享链接,其域名或路径必须与当前页面对应的公众号JS安全域名一致,否则将调用失败。也就是说如果你想你的网站在被分享时显示缩略图,那么你要有一个公众号,并且在公众号内设置JS安全域名,也就是添加你网站的网址,这是必须的,如果这一步不能实现,那么你不用往下看了。一、最快的解决方法如果你不是专业的程序员,虽然微信官方给出了代码,以及我下面也会再次给出代码,你可能仍然无法实现。因为在微信官方代码与你的网站之间,还需要二次开发对接代码,把微信代码与你的网站连接起来,这个功能才能最终实现。同时因为每个网站的源程序不一样,所以这个对接代码也不能通用,需要根据你的网站程序单独开发。请在微信内打开链接分享测试。二、微信官方办法步骤一:绑定域名先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。步骤二:设置开发者密码和IP白名单登录微信公众平台,进入开发――基本配置――开发者密码(AppSecret)和IP白名单步骤三:引入JS文件在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.2.0.js备注:支持使用AMD/CMD 标准模块加载方法加载步骤四:通过config接口注入权限验证配置所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用)。wx.config({debug: true,// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。appId: ‘‘,// 必填,公众号的唯一标识timestamp:,// 必填,生成签名的时间戳nonceStr: ‘‘,// 必填,生成签名的随机串signature: ‘‘,// 必填,签名,见附录1jsApiList: [] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2});微信开发文档说明:https://mp.weixin.qq.com/advanced/wiki?t=t=resource/res_main&id=mp1421141115微信JS 接口签名校验工具https://mp.weixin.qq.com/debug/cgi-bin/sandBox?t=jsapisign
-
前言在平时的开发过程中,我们总是先写好一个组件,然后在需要的页面中用 import 引入即可,但如果是下面这种类型的组件呢上面这种类型的浮层提示有一个很大的特点,就是使用频率特别高,几乎每个页面都会用到它,于是乎我们就要在每个页面中去引入该组件,并且在每个页面都得通过一个变量来控制它的显隐,这显然不是我们想要的。。。那我们想要的是什么样呢?用过一些 UI 框架的同学们应该知道有这样一种用法:js复制代码this.$toast({ duration: 3000, content: '这是一条消息提示' }); 没错,就是这么简单的一句话就万事大吉了(就是用 js 调用组件而已啦)。那这种效果究竟是怎么实现的呢?今天就让我们来(手把手 )一探究竟吧!前置知识不知道小伙伴们有没有用过 Vue.extend() 这个东东,反正我是很少碰过,印象不深,所以这里我们先来短暂了解一下 Vue.extend() 主要是用来干嘛的。先来个官方说明(不多的,坚持下): 没怎么看懂?没关系,不重要,你只要记住(加少许理解)以下用法即可:js复制代码// 导入以往的普通组件 import Main from './main.vue'; // 用 Vue.extend 创建组件的模板(构造函数) let mainConstructor = Vue.extend(Main); // 实例化组件 let instance = new mainConstructor(); // 挂载到相应的元素上 instance.$mount('#app'); 不知道你看懂没有,上面的 Vue.extend(Main) 就是一个基于 main.vue 的组件模板(构造函数),instance 是实例化的组件,$mount() 是手动挂载的意思。其中 Vue.extend() 和 $mount() 就是我们通过 js 调用、渲染并挂载组件的精髓所在,相当于早前的 createElement 和 appendChild,有异曲同工之效。这个点需要我们好好熟悉一下,所以你可以先停下来屡屡思路。补充一下:$mount() 里面如果没有参数,说明组件只是渲染了但还没有挂载到页面上,如果有正确的(元素)参数则直接挂载到元素下面。写一个 toast 组件js 调用归调用,最原始的组件还是要有的,只是我们不通过 import 来引入到页面中而已。ok,我们就以最开始的那个 toast 图片来简单写一下这个 vue 组件(message 和 alert 也是一样的)。这里就直接上代码啦,毕竟它的结构简单到爆了,也不是本章节的重点:html复制代码<!-- main.vue --> <template> <div class="toast"> <p>服务器错误,请稍后重试</p> </div> </template> <script> export default { name: "Toast", mounted() { setTimeout(() => { // 3s 后通过父级移除子元素的方式来移除该组件实例和 DOM 节点 this.$destroy(true); this.$el.parentNode.removeChild(this.$el); }, 3000); } }; </script> <style lang="scss" scoped> .toast { display: flex; align-items: center; justify-content: center; position: fixed; top: 0; bottom: 0; left: 0; right: 0; color: #fff; z-index: 9999; background: transparent; > p { padding: 12px 22px; font-size: 18px; border-radius: 4px; background: rgba(17, 17, 17, 0.7); } } </style> 上面的内容想必大家应该都能看懂,所以这里就直接讲下面的重点了。写一个 main.js我们在 main.vue 的同级目录下新建一个 main.js 文件。我们先瞟一眼文件内容(也不多,已经是个最简版了):js复制代码// main.js import Vue from "vue"; // 引入 Vue 是因为要用到 Vue.extend() 这个方法 import Main from "./main.vue"; // 引入刚才的 toast 组件 let ToastConstructor = Vue.extend(Main); // 这个在前面的前置知识内容里面有讲到 let instance; const Toast = function() { instance = new ToastConstructor().$mount(); // 渲染组件 document.body.appendChild(instance.$el); // 挂载到 body 下 }; export default Toast; 上面的代码暴露了一个 Toast 函数。为什么要暴露一个函数呢?原因很简单:你想想,我们最终是不是要根据 this.$toast() 来调用一个组件,说白了,通过 js 调用,本质就是调用一个 函数。也就是说 this.$toast() 就是执行了上面代码中导出的 export default Toast,也就是执行了 Toast 函数(const Toast = function() {}),所以当我们调用 this.$toast() 的时候其实就是执行了 Toast() 函数。而 Toast() 函数只做了一件事情:就是通过手动挂载的方式把组件挂载到 body 下面。补充一下:一般来说我们常见的是 $mount("#app"),也就是把组件挂载到 #app 下面,<router-view /> 也包含在 #app 中,但是我们这种 toast 提示是放在 body 下面的,也就是说它不受 #app 和 <router-view /> 的管控,所以当我们切换页面(路由)的时候,这个 toast 组件是不会跟着立马消失的,这点要注意哦。这里顺便给个组件的目录结构,如下图所示: 开始调用调用方式很简单,首先我们在入口文件 main.js(和上面不是同一个) 里加上两行代码,这样我们就能在需要的地方直接用 js 调用它了,如下图所示: 然后在页面中测试一下,就像下面这样子: 运行一下代码: 嗯,挺好,小有成就的 feel 。支持可传参数别急,我们好像还漏了点什么。。。对了,现在还不支持传参呢,直接调用 this.$toast() 就只能显示————服务器错误,请稍后重试(这下全都是后端的锅了)。但我们可是个有追求的前端,不能局限于此,所以现在让我们来尝试增加下两个可配置参数,这里拿 duration 和 content 举个栗子。首先我们要修改 main.vue 组件里面的内容(其实没啥大变化),就像下面这样:html复制代码<!-- main.vue 可配置版 --> <template> <div class="toast"> <p>{{ content }}</p> </div> </template> <script> // 主要就改了 data export default { name: "Toast", data() { return { content: "", duration: 3000 }; }, mounted() { setTimeout(() => { this.$destroy(true); this.$el.parentNode.removeChild(this.$el); }, this.duration); } }; </script> 上面的代码应该算是浅显易懂了,接下来我们看下 main.js 里面改了啥:js复制代码// main.js 可配置版 import Vue from "vue"; import Main from "./main.vue"; let ToastConstructor = Vue.extend(Main); let instance; const Toast = function(options = {}) { // 就改了这里,加了个 options 参数 instance = new ToastConstructor({ data: options // 这里的 data 会传到 main.vue 组件中的 data 中,当然也可以写在 props 里 }); document.body.appendChild(instance.$mount().$el); }; export default Toast; 其实 main.js 也没多大变化,就是在函数里面加了个参数。要注意的是 new ToastConstructor({ data: options }) 中的 data 就是 main.vue 组件中的 data,不是随随便便取的字段名,传入的 options 会和组件中的 data 合并(Vue 的功劳)。em。。。是的,就这么简单,现在让我们继续来调用一下它:xml复制代码<script> export default { methods: { showToast() { this.$toast({ content: "哈哈哈哈,消失的贼快", duration: 500 }); } } }; </script> 运行一下就可以看到: 当然,这还没完,我们继续添加个小功能点。。。支持 this.$toast.error()这里我们打算支持 this.$toast.error() 和 this.$toast.success() 这两种方式,所以我们第一步还是要先去修改一下 main.vue 文件的内容(主要就是根据 type 值来修改组件的样式),就像下面这样:html复制代码<!--main.vue--> <template> <div class="toast" :class="type ? `toast--${type}` : ''"> <p>{{ content }}</p> </div> </template> <script> export default { ... data() { return { type: "", content: "", duration: 3000 }; }, ... }; </script> <style lang="scss" scoped> .toast { ... &--error p { background: rgba(255, 0, 0, 0.5); } &--success p { background: rgba(0, 255, 0, 0.5); } } </style> 其次,this.$toast.error() 其实就等价于 Toast.error(),所以我们现在的目的就是要给 Toast 函数扩充方法,也比较简单,就先看代码再解释吧:js复制代码// main.js const Toast = function(options = {}) { ... }; // 以下就是在 Toast 函数中拓展 ["success", "error"] 这两个方法 ["success", "error"].forEach(type => { Toast[type] = options => { options.type = type; return Toast(options); }; }); export default Toast; 我们可以看到 Toast.error() 和 Toast.success() 最终还是调用 Toast(options) 这个函数,只不过在调用之前需要多做一步处理,就是将 ["success", "error"] 作为一个 type 参数给合并进 options 里面再传递,仅此而已。那就试试效果吧:html复制代码<script> export default { methods: { showToast() { this.$toast({ content: "这是正常的" }); }, showErrorToast() { this.$toast.error({ content: "竟然失败了" }); }, showSuccessToast() { this.$toast.success({ content: "居然成功了" }); } } }; </script> 结语至此,一个通过 js 调用的简单 toast 组件就搞定啦,短短的几行代码还是挺考验 js 功底的。当然这只是个超简易版的 demo,显然不够完善和健壮,所以我们可以在此基础上扩充一下,比如当 duration <= 0 的时候,我们让这个 toast 一直显示,然后扩展一个 close 方法来关闭等等之类的。不过还是那句老话,实践才是检验真理的唯一标准。纸上得来终觉浅,绝知此事要躬行。step by step, day day up !作者:尤水就下链接:https://juejin.cn/post/6844903825711562766
-
在 Spring Boot 中,将 long 类型传输到前端时,会发现该类型的值可能会出现精度丢失的问题。 这是因为在 JavaScript 中,数字类型默认会被转换为双精度浮点数,而双精度浮点数的精度有限,只能精确表示 2 的 53 次方以内(即 Number.MAX_SAFE_INTEGER,约为 9 x 10^15)的整数。对于超过该范围的长整数,JavaScript 会发生精度丢失,导致值变得不准确。 解决方案一:将 long 转换为字符串 1:在后端将 long 类型的值转换为字符串类型,可以使用 String.valueOf() 方法或者 Long.toString() 方法,如下所示: long num = 123456789012345L; String str = String.valueOf(num); // 或者 String str = Long.toString(num); 2:在前端通过 AJAX 请求获取该字符串类型的值,并将其解析为数字类型。由于 JavaScript 中的数值类型默认使用 IEEE 754 标准的双精度浮点数表示,因此需要使用 JavaScript 的 BigInt() 方法将其转换为大整数类型。 let str = "123456789012345"; let num = BigInt(str); 解决方案二:使用第三方库进行高精度运算 1:在后端将 long 类型的值转换为 BigDecimal 类型(Java 中的高精度类型),并通过 JSON 序列化后传递到前端。这里以 Spring Boot 中使用 FastJSON 序列化为例,如下所示: BigDecimal num = new BigDecimal("123456789012345"); String jsonStr = JSON.toJSONString(num); 2:在前端使用第三方库 big.js 或 bignumber.js 进行高精度运算。这里以 big.js 为例,首先需要引入 big.min.js 文件,在代码中使用 Big() 类构造高精度对象,并进行相应的运算。 <script src="big.min.js"></script> let num = new Big("123456789012345"); let result = num.plus(1); 此外还可以使用注解来解决 long 类型的精度丢失问题 Spring Boot 中提供了 @JsonFormat 注解,可以对实体类中的属性进行序列化和反序列化格式化。对于 long 类型的属性,可以设置其格式为字符串类型,并在前端进行相应的处理,以保持其精度不丢失。 具体实现方式: 1:在实体类中添加 @JsonFormat 注解,设置其 shape 属性为 JsonFormat.Shape.STRING,如下所示: public class Example { @JsonFormat(shape = JsonFormat.Shape.STRING) private Long num; } 2:在前端获取该值时,直接使用字符串类型进行处理,如下所示: let numStr = data.num; Spring Boot 中可以通过配置文件来解决 long 类型的精度丢失问题。 在 Spring Boot 的配置文件 application.properties 中添加如下配置: spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false # 将 long 类型序列化为字符串类型 spring.jackson.serialization.WRITE_NUMBERS_AS_STRINGS=true 其中,WRITE_DATES_AS_TIMESTAMPS 表示是否将日期类型序列化为时间戳类型,默认为 true,这里设置为 false 如果需要将日期类型序列化为时间戳类型,则不需要设置此属性。而 WRITE_NUMBERS_AS_STRINGS 则表示是否将数字类型序列化为字符串类型,默认为 false,这里设置为 true 即可将 long 类型序列化为字符串类型。 ———————————————— 版权声明:本文为CSDN博主「源末coco」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_58724261/article/details/130974162
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签