首页
留言
导航
统计
Search
1
追番推荐!免费看动漫的网站 - 支持在线观看和磁力下载
2,512 阅读
2
推荐31个docker应用,每一个都很实用
1,312 阅读
3
PVE自动启动 虚拟机 | 容器 顺序设置及参数说明
931 阅读
4
一条命令,永久激活!Office 2024!
618 阅读
5
优选 Cloudflare 官方 / 中转 IP
490 阅读
默认分类
服务器
宝塔
VPS
Docker
OpenWRT
Nginx
群晖
前端编程
Vue
React
Angular
NodeJS
uni-app
后端编程
Java
Python
SpringBoot
SpringCloud
流程引擎
检索引擎
Linux
CentOS
Ubuntu
Debian
数据库
Redis
MySQL
Oracle
虚拟机
VMware
VirtualBox
PVE
Hyper-V
计算机
网络技术
网站源码
主题模板
登录
Search
标签搜索
Java
小程序
Redis
SpringBoot
docker
Typecho
Cloudflare
docker部署
虚拟机
WordPress
群晖
uni-app
CentOS
Vue
Java类库
Linux命令
防火墙配置
Mysql
脚本
Nginx
微醺
累计撰写
264
篇文章
累计收到
11
条评论
首页
栏目
默认分类
服务器
宝塔
VPS
Docker
OpenWRT
Nginx
群晖
前端编程
Vue
React
Angular
NodeJS
uni-app
后端编程
Java
Python
SpringBoot
SpringCloud
流程引擎
检索引擎
Linux
CentOS
Ubuntu
Debian
数据库
Redis
MySQL
Oracle
虚拟机
VMware
VirtualBox
PVE
Hyper-V
计算机
网络技术
网站源码
主题模板
页面
留言
导航
统计
搜索到
264
篇与
的结果
2023-02-13
最新v2ray搭建完全手册-科学上网必备
视频教程参考:Debian 系统基于 nginx 搭建 v2ray 服务端配置 vmess+tls+websocket 详细教程。篇首语:科学上网(fanqiang)的大体原理都是通过一台在防火墙之外的 VPS 作为媒介,实现国内网络和防火墙之外的网络之间的互通,因此一台不受防火墙骚扰的 VPS 是必备的。而 v2ray 负责的是你的网络和 VPS 之间的网络通信功能。借助 V2ray 你可以实现透过防火墙,自由穿梭于网络。你需要做的是分别在你的设备(手机、PC 等)和 VPS 上分别部署一下 V2ray。搭建 v2ray 的步骤大体如下:购买一个 VPS,购买后你会获得 VPS 的 IP、root 用户及密码、SSH 端口等信息;登录 VPS,可以借助Xshell这个工具;安装 v2ray,使用 xshell 成功登录 VPS 后,开始搭建;在你的设备上配置与 VPS 对应的 v2ray 信息,就可以成功使用了;在开始下面的搭建工作之前,请自行购买 VPS 并获得 VPS 的相关连接信息,保证能通过 Xshell 等远程连接工作连接后才能进行搭建,不论使用哪家的 VPS,第一步是购买完 VPS 之后,获取到 VPS 的 IP、root 用户及密码、SSH 端口等信息,本文主要说明搭建过程,因此这里将购买 VPS 及使用 XSHELL 进行连接的过程放在:史上最详细搬瓦工 VPS 注册/购买图文教程(内附优惠券) 。正文开始之前,先插播个广告,推荐一些性价比较高的 VPS,有购买 VPS 打算的,可以使用我的推介链接,这样你不仅可以优惠购买,我也会得到一定的返券,感谢你对我写文创作的支持!没有购买 VPS 打算的,可以跳过这一部分。一、搬瓦工 VPS 推荐速度、性价比都较好: 老牌商家,一分价钱一分货,套餐选择上肯定是越好的越贵,选择适合自己的,但从速度上来说,VPS 速度:香港线路 > CN2 GIA 线路 > CN2 线路 > 普通线路。搬瓦工优惠券: BWH3HYATVBJW,此券为全网当前最高优惠力度:6.58%。二、Rackerd VPS 推荐性价比较高: 提供大流量、大硬盘,支持 PayPal、支付宝、微信等多种付款方式,电信走 CN2 GT,联通直达机房(极佳),移动强制走联通链路,算是非 CN2 线路之外优化的相当不错的。三、Vultr 主机推荐国外 VPS Vultr 云主机:最低每月$2.5。下面再来通过图文,来详细说明上面的步骤。四、环境信息服务器系统:Debian GNU/Linux 10服务端:v2ray-core v5.1.0;客户端:v2rayN v5.38 ;五、搭建脚本说明使用到的官网安装脚本:https://github.com/v2fly/fhs-install-v2ray,该脚本在执行时会提供 info 和 error 等信息,请仔细阅读。六、开始搭建提个醒:搭建的过程中难免会碰到一些错误和难题,每个人的服务器版本之类的都有区别。6.1 连接 VPS6.2 更新apt update6.3 安装 curlapt install curl6.4 安装 v2ray使用 curl 进行安装bash <(curl -L https://raw.githubusercontent.com/v2fly/fhs-install-v2ray/master/install-release.sh)输入vi /usr/local/etc/v2ray/config.json命令,编辑配置文件的内容为以下内容:{ "log": { "access": "/var/log/v2ray/access.log", "error": "/var/log/v2ray/error.log", "loglevel": "warning" }, "inbounds": [{ "port": 11055, "protocol": "vmess", "settings": { "clients": [{ "id": "27848739-7e62-4138-9fd3-098a63964b6b", "level": 1, "alterId": 0 } ] }, "streamSettings": { "network": "ws", "wsSettings": { "path": "/tech" } } } ], "outbounds": [ { "protocol": "freedom" } ] }6.5 启动 v2ray 服务启动并加入开机自启systemctl start v2raysystemctl enable v2ray6.6 安装 nginx执行安装apt install -y nginx新建网页目录这里在假设是/root/www。mkdir -p /root/www新建首页在/root/www目录下新建一个index.html文件,vi /root/www/index.html内容如下:<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello Word</title> </head> <body> <div> <h1>Hello Word</h1> </div> </body> </html>新建配置文件注: 不同版本的 nginx 配置文件可能有区别,我的是nginx/1.18.0,配置文件/etc/nginx/sites-enabled/default。或者你的配置文件可能在/etc/nginx/conf.d/default.conf。输入vi /etc/nginx/sites-enabled/default命令,编辑 nginx 配置文件为如下内容: server{ listen 80; server_name v1.xxxx.com; index index.html; root /root/www/; }这里你可能需要将/etc/nginx/nginx.conf中第一行user www-data改为user root,即当前用户。启动 nginx 服务启动Nginx并设置为开机自启systemctl start nginxsystemctl enable nginx查看nginx启动状态systemctl status nginx在浏览器里访问v1.xxxx.com,如果正常则说明 nginx 配置没问题,这是是没有加密的,不带 HTTPS 的网址:http://v1.xxxx.com。6.7 安装 certbot 并申请 ssl 证书详细的申请证书流程参考: certbot instructions – Nginx on Debian 10 (buster)。具体如下:安装 snapdapt install -y snapd确保 snapd 为最新版本snap install core; snap refresh core安装 certbotsnap install --classic certbot创建软链ln -s /snap/bin/certbot /usr/bin/certbot申请证书运行certbot --nginx开始申请证书,如下:稍等一会,会提示证书安装成功! 你会发现 nginx 的配置已经更改,我的/etc/nginx/sites-enabled/default文件被自动修改为以下内容: server{ server_name v1.xxxx.com; index index.html; root /root/www/; listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/v1.xxxx.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/v1.xxxx.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server{ if ($host = v1.xxxx.com) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; server_name v1.xxxx.com; return 404; # managed by Certbot }此时,在浏览器里访问v1.xxxx.com已经是https开头的了,地址:https://v1.xxxx.com。6.8 添加 v2ray 转发将/etc/nginx/sites-enabled/default更改为以下内容: server{ server_name v1.xxxx.com; index index.html; root /root/www/; listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/v1.xxxx.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/v1.xxxx.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot ## 添加这部分内容,22055对应/usr/local/etc/v2ray/config.json 里面inbounds端口 ## /tech客户端配置的时候需要,对应/usr/local/etc/v2ray/config.json streamSettings里的path location /tech { proxy_redirect off; proxy_pass http://127.0.0.1:11055; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $http_host; } } server{ if ($host = v1.xxxx.com) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; server_name v1.xxxx.com; return 404; # managed by Certbot }然后运行systemctl restart nginx重启 nginx。6.9 客户端 v2rayN 配置大体截图:6.10 使用 Google BBR 开启加速细节参考: Debian 9/10 快速开启 Google BBR 实现 v2ray 高效单边加速。观看 Youtube 4k 高清视频,播放速度还可以:6.11 优化网络,隐藏真实 IP细节参考: 安装 warp 解决 Google 搜索出现人机验证、解锁 Netfix 非自制剧最新教程 。七、卸载执行脚本的卸载命令即可:bash <(curl -L https://raw.githubusercontent.com/v2fly/fhs-install-v2ray/master/install-release.sh) --remove
2023年02月13日
240 阅读
0 评论
0 点赞
2023-02-13
jodconvert 配置
版本: 4.2.0端口 和 管道文档转换使用TCP端口或者管道 . 管道比TCP更快, 但是需要为jvm指定本地库, 因此默认使用TCP端口模式.要为jvm配置本地库, 即配置 java.library.path 系统属性.On Linux it's e.g.: java -Djava.library.path=/opt/openoffice.org/ure/lib On Windows it's e.g.: java "-Djava.library.path=C:\Program Files (x86)\OpenOffice 4\program" 默认的TCP端口是2002 :OfficeManager officeManager = LocalOfficeManager.builder() .portNumbers(2002, 2003, 2004, 2005) .build(); 上面的代码指定了4个端口, 因此在OfficeManager启动时, 将启动4个office进程来进行处理转换.officeHome该属性是office的安装目录 .// This example will force JODConverter to use the OpenOffice 4 // installation that can be found using the specified path. // 示例设置officeHome OfficeManager officeManager = LocalOfficeManager.builder() .officeHome("D:\\Program Files (x86)\\OpenOffice 4") .build(); processManager当jodconvert需要工作时, 需要一个processManager , 用于在需要时关闭进程.默认情况下: 会根据os寻找最佳的processManager . 也可以自己实现ProcessManager接口来改变之.// This example will create an instance of the com.example.foo.CustomProcessManager // class that will be used by the created OfficeManager. OfficeManager officeManager = LocalOfficeManager.builder() .processManager("com.example.foo.CustomProcessManager") .build(); workingDir当每个office进程启动时都会创建一个临时目录. 临时目录创建在workingDir中. 该目录也用来存放转换的中间结果.默认使用: java.io.tmpdir 目录templateProfileDir每个LocalOfficeManager都会创建一个临时文件夹来存放当前进程属性, 从而可以避免多个进程间的相互干扰. 通过该属性可以提供一个属性模板来定制属性. OfficeManager 将从模板拷贝属性到临时文件夹中, 这样进程间使用相同的设置的同时避免相互干扰.这些设置可以是 Tools>Options菜单中的 . 如:Load/Save > General: 可以禁用保存互联网urlLoad/Save > Microsoft Office : 这个可以避免文档中的excel被丢失.默认: 创建时使用新的, 受 -nofirststartwizard命令影响.killExistingProcess在新进程启动时是否杀死已存在进程. 默认 : trueprocessTimeout处理超时时间, 单位毫秒 . 默认 : 120000 (2分钟)processRetryInterval重试执行的时间间隔, 毫秒.默认: 250taskExecutionTimeout允许进程执行一个task的最大时间 . 超时将终止, 然后处理下一个task.默认 : 120000(2分钟)maxTasksPerProcess每个office进程可执行的最大task数, 超过将重启. 默认: 200disableOpengl设置当前office进程启动时是否需要禁用opengl ( 仅 libreOffice) . 若opengl已禁止则不会进行任何处理 . 当该属性改变时, office必须重启. 若你遇到LO挂掉, 可以测试该属性.默认: falsetaskQueueTimeout设置task在队列的最大存活时间 , 超时将被从队列移除 并抛出 OiffceException .默认: 30000(30分钟)
2023年02月13日
41 阅读
0 评论
0 点赞
2023-02-13
记Ubuntu16.04 下 LibreOffice的安装与使用
LibreOffice主包和语言包wget http://mirrors.ustc.edu.cn/tdf/libreoffice/stable/6.4.6/deb/x86_64/LibreOffice_6.4.6_Linux_x86-64_deb.tar.gzwget http://mirrors.ustc.edu.cn/tdf/libreoffice/stable/6.4.6/deb/x86_64/LibreOffice_6.4.6_Linux_x86-64_deb_langpack_zh-CN.tar.gztar xf LibreOffice_6.4.6_Linux_x86-64_deb.tar.gzsudo dpkg -i LibreOffice_6.4.6.2_Linux_x86-64_deb/DEBS/*.debtar xf LibreOffice_6.4.6_Linux_x86-64_deb_langpack_zh-CN.tar.gzsudo dpkg -i LibreOffice_6.4.6.2_Linux_x86-64_deb_langpack_zh-CN/DEBS/*.deb字体wget https://mirrors.tuna.tsinghua.edu.cn/adobe-fonts/source-han-serif/SubsetOTF/SourceHanSerifCN.zipwget https://mirrors.tuna.tsinghua.edu.cn/adobe-fonts/source-han-sans/SubsetOTF/SourceHanSansCN.zipunzip SourceHanSansCN.zipunzip SourceHanSerifCN.zipsudo cp -r SourceHanSansCN /usr/share/fonts/sudo cp -r SourceHanSerifCN /usr/share/fonts/sudo apt install fontconfigfc-cache -fv其它依赖sudo apt install -y openjdk-8-jdk libxinerama1 libcairo2 libcups2 libsm6使用(word转pdf)libreoffice6.4 --invisible --convert-to pdf input.docx --outdir .
2023年02月13日
206 阅读
0 评论
0 点赞
2023-02-13
ubuntu怎么切换到root用户,切换到root账号方法
ubuntu怎么切换到root用户,我们都知道使用su root命令,去切换到root权限,此时会提示输入密码,可是怎么也输不对,提示“Authentication failure”,此时有两种情况一个是真的是密码错了,另一种就是刚安装好的Linux系统,没有给root设置密码。打开Ubuntu,输入命令:su root,回车提示输入密码,怎么输入都不对步骤1:给root用户设置密码:命令:sudo passwd root输入密码,并确认密码。步骤2:重新输入命令:su root然后输入密码:发现可以切换到root权限了。步骤3:使用su xyx命令,切换到普通用户。
2023年02月13日
122 阅读
0 评论
0 点赞
2023-02-13
Linux中unzip解压时中文乱码的解决办法
Linux 中 unzip 解压时中文乱码的解决办法当我们在 linux 中解压一个含有中文名字的压缩包如“资料.zip”时,如果直接使用如下的命令,将会出现中文乱码。unzip 资料.zip主要的原因是因为 unzip 在解压的时候会将编码转化为其内部默认的编码,而默认的编码根本不支持中文 CP936 编码。因此我们需要在解压的时候明确的指定需要使用的编码。目前可以采用如下两种方式解决方法一 在解压的时候直接指定编码格式指定 GBK GB18030 编码也是可以的unzip -O CP936 资料.zip方法二 配置环境变量,指定 unzip 的参数在环境变量中,指定 unzip 参数,总是以指定的字符集显示和解压文件比如,需要在/etc/environment中加入 2 行:UNZIP="-O CP936"ZIPINFO="-O CP936"
2023年02月13日
45 阅读
0 评论
0 点赞
2023-02-13
详解 Java8 Stream 用法
Java8(jdk1.8)的新特性主要是 Lambda 表达式和流,当流和 Lambda 表达式结合起来一起使用时,因为流申明式处理数据集合的特点,可以让代码变得简洁易读如果有一个需求,需要对数据库查询到的菜肴进行一个处理:筛选出卡路里小于 400 的菜肴对筛选出的菜肴进行一个排序获取排序后菜肴的名字菜肴:Dish.javapublic class Dish { private String name; private boolean vegetarian; private int calories; private Type type; // getter and setter } Java8 以前的实现方式private List<String> beforeJava7(List<Dish> dishList) { List<Dish> lowCaloricDishes = new ArrayList<>(); //1.筛选出卡路里小于400的菜肴 for (Dish dish : dishList) { if (dish.getCalories() < 400) { lowCaloricDishes.add(dish); } } //2.对筛选出的菜肴进行排序 Collections.sort(lowCaloricDishes, new Comparator<Dish>() { @Override public int compare(Dish o1, Dish o2) { return Integer.compare(o1.getCalories(), o2.getCalories()); } }); //3.获取排序后菜肴的名字 List<String> lowCaloricDishesName = new ArrayList<>(); for (Dish d : lowCaloricDishes) { lowCaloricDishesName.add(d.getName()); } return lowCaloricDishesName; } Java8 之后的实现方式 private List<String> afterJava8(List<Dish> dishList) { return dishList.stream() .filter(d -> d.getCalories() < 400) //筛选出卡路里小于400的菜肴 .sorted(comparing(Dish::getCalories)) //根据卡路里进行排序 .map(Dish::getName) //提取菜肴名称 .collect(Collectors.toList()); //转换为List } 不拖泥带水,一气呵成,原来需要写24代码实现的功能现在只需5行就可以完成了高高兴兴写完需求这时候又有新需求了,新需求如下:对数据库查询到的菜肴根据菜肴种类进行分类,返回一个Map<Type, List<Dish>>的结果这要是放在jdk8之前肯定会头皮发麻Java8 以前的实现方式private static Map<Type, List<Dish>> beforeJdk8(List<Dish> dishList) { Map<Type, List<Dish>> result = new HashMap<>(); for (Dish dish : dishList) { //不存在则初始化 if (result.get(dish.getType())==null) { List<Dish> dishes = new ArrayList<>(); dishes.add(dish); result.put(dish.getType(), dishes); } else { //存在则追加 result.get(dish.getType()).add(dish); } } return result; } 还好 jdk8 有 Stream,再也不用担心复杂集合处理需求Java8 以后的实现方式private static Map<Type, List<Dish>> afterJdk8(List<Dish> dishList) { return dishList.stream().collect(groupingBy(Dish::getType)); } 又是一行代码解决了需求,忍不住大喊Stream API牛批,接下来将详细介绍流什么是流流是从支持数据处理操作的源生成的元素序列,源可以是数组、文件、集合、函数。流不是集合元素,它不是数据结构并不保存数据,它的主要目的在于计算如何生成流生成流的方式主要有五种通过集合生成,应用中最常用的一种List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Stream<Integer> stream = integerList.stream(); 通过集合的stream方法生成流通过数组生成int[] intArr = new int[]{1, 2, 3, 4, 5}; IntStream stream = Arrays.stream(intArr); 通过Arrays.stream方法生成流,并且该方法生成的流是数值流【即IntStream】而不是Stream<Integer>。补充一点使用数值流可以避免计算过程中拆箱装箱,提高性能。Stream API提供了mapToInt、mapToDouble、mapToLong三种方式将对象流【即Stream<T>】转换成对应的数值流,同时提供了boxed方法将数值流转换为对象流通过值生成Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5); 通过Stream的of方法生成流,通过Stream的empty方法可以生成一个空流通过文件生成Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset()) 通过Files.line方法得到一个流,并且得到的每个流是给定文件中的一行通过函数生成 提供了iterate和generate两个静态方法从函数中生成流iteratorStream<Integer> stream = Stream.iterate(0, n -> n + 2).limit(5); iterate方法接受两个参数,第一个为初始化值,第二个为进行的函数操作,因为iterator生成的流为无限流,通过limit方法对流进行了截断,只生成 5 个偶数generatorStream<Double> stream = Stream.generate(Math::random).limit(5); generate方法接受一个参数,方法参数类型为Supplier<T>,由它为流提供值。generate生成的流也是无限流,因此通过limit对流进行了截断流的操作类型流的操作类型主要分为两种中间操作 一个流可以后面跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的,仅仅调用到这类方法,并没有真正开始流的遍历,真正的遍历需等到终端操作时,常见的中间操作有下面即将介绍的filter、map等终端操作 一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次,若想在遍历需要通过源数据在生成流。终端操作的执行,才会真正开始流的遍历。如下面即将介绍的count、collect等流使用流的使用将分为终端操作和中间操作进行介绍中间操作filter 筛选List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5); Stream<Integer> stream = integerList.stream().filter(i -> i > 3); 通过使用filter方法进行条件筛选,filter的方法参数为一个条件distinct 去除重复元素List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5); Stream<Integer> stream = integerList.stream().distinct(); 通过distinct方法快速去除重复的元素limit 返回指定流个数List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5); Stream<Integer> stream = integerList.stream().limit(3); 通过limit方法指定返回流的个数,limit的参数值必须>=0,否则将会抛出异常skip 跳过流中的元素 List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5); Stream<Integer> stream = integerList.stream().skip(2); 通过skip方法跳过流中的元素,上述例子跳过前两个元素,所以打印结果为2,3,4,5,skip的参数值必须>=0,否则将会抛出异常map 流映射所谓流映射就是将接受的元素映射成另外一个元素List<String> stringList = Arrays.asList("Java 8", "Lambdas", "In", "Action"); Stream<Integer> stream = stringList.stream().map(String::length); 通过map方法可以完成映射,该例子完成中String -> Integer的映射,之前上面的例子通过map方法完成了Dish->String的映射flatMap 流转换将一个流中的每个值都转换为另一个流List<String> wordList = Arrays.asList("Hello", "World"); List<String> strList = wordList.stream() .map(w -> w.split(" ")) .flatMap(Arrays::stream) .distinct() .collect(Collectors.toList()); map(w -> w.split(" "))的返回值为Stream<String[]>,我们想获取Stream<String>,可以通过flatMap方法完成Stream<String[]> ->Stream<String>的转换元素匹配提供了三种匹配方式allMatch 匹配所有List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); if (integerList.stream().allMatch(i -> i > 3)) { System.out.println("值都大于3"); } 通过allMatch方法实现anyMatch 匹配其中一个List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); if (integerList.stream().anyMatch(i -> i > 3)) { System.out.println("存在大于3的值"); } 等同于for (Integer i : integerList) { if (i > 3) { System.out.println("存在大于3的值"); break; } } 存在大于 3 的值则打印,java8中通过anyMatch方法实现这个功能noneMatch 全部不匹配List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); if (integerList.stream().noneMatch(i -> i > 3)) { System.out.println("值都小于3"); } 通过noneMatch方法实现终端操作统计流中元素个数通过 countList<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Long result = integerList.stream().count(); 通过使用count方法统计出流中元素个数通过 countingList<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Long result = integerList.stream().collect(counting()); 最后一种统计元素个数的方法在与collect联合使用的时候特别有用查找提供了两种查找方式findFirst 查找第一个List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> result = integerList.stream().filter(i -> i > 3).findFirst(); 通过findFirst方法查找到第一个大于三的元素并打印findAny 随机查找一个List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> result = integerList.stream().filter(i -> i > 3).findAny(); 通过findAny方法查找到其中一个大于三的元素并打印,因为内部进行优化的原因,当找到第一个满足大于三的元素时就结束,该方法结果和findFirst方法结果一样。提供findAny方法是为了更好的利用并行流,findFirst方法在并行上限制更多【本篇文章将不介绍并行流】reduce 将流中的元素组合起来假设我们对一个集合中的值进行求和jdk8 之前int sum = 0; for (int i : integerList) { sum += i; } jdk8 之后通过 reduce 进行处理int sum = integerList.stream().reduce(0, (a, b) -> (a + b)); 一行就可以完成,还可以使用方法引用简写成:int sum = integerList.stream().reduce(0, Integer::sum); reduce接受两个参数,一个初始值这里是0,一个BinaryOperator<T> accumulator 来将两个元素结合起来产生一个新值, 另外reduce方法还有一个没有初始化值的重载方法获取流中最小最大值通过 min/max 获取最小最大值Optional<Integer> min = menu.stream().map(Dish::getCalories).min(Integer::compareTo); Optional<Integer> max = menu.stream().map(Dish::getCalories).max(Integer::compareTo); 也可以写成:OptionalInt min = menu.stream().mapToInt(Dish::getCalories).min(); OptionalInt max = menu.stream().mapToInt(Dish::getCalories).max(); min获取流中最小值,max获取流中最大值,方法参数为Comparator<? super T> comparator通过 minBy/maxBy 获取最小最大值Optional<Integer> min = menu.stream().map(Dish::getCalories).collect(minBy(Integer::compareTo)); Optional<Integer> max = menu.stream().map(Dish::getCalories).collect(maxBy(Integer::compareTo)); minBy获取流中最小值,maxBy获取流中最大值,方法参数为Comparator<? super T> comparator通过 reduce 获取最小最大值Optional<Integer> min = menu.stream().map(Dish::getCalories).reduce(Integer::min); Optional<Integer> max = menu.stream().map(Dish::getCalories).reduce(Integer::max); 求和通过 summingIntint sum = menu.stream().collect(summingInt(Dish::getCalories)); 如果数据类型为double、long,则通过summingDouble、summingLong方法进行求和通过 reduceint sum = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum); 通过 sumint sum = menu.stream().mapToInt(Dish::getCalories).sum(); 在上面求和、求最大值、最小值的时候,对于相同操作有不同的方法可以选择执行。可以选择collect、reduce、min/max/sum方法,推荐使用min、max、sum方法。因为它最简洁易读,同时通过mapToInt将对象流转换为数值流,避免了装箱和拆箱操作通过 averagingInt 求平均值double average = menu.stream().collect(averagingInt(Dish::getCalories)); 如果数据类型为double、long,则通过averagingDouble、averagingLong方法进行求平均通过 summarizingInt 同时求总和、平均值、最大值、最小值IntSummaryStatistics intSummaryStatistics = menu.stream().collect(summarizingInt(Dish::getCalories)); double average = intSummaryStatistics.getAverage(); //获取平均值 int min = intSummaryStatistics.getMin(); //获取最小值 int max = intSummaryStatistics.getMax(); //获取最大值 long sum = intSummaryStatistics.getSum(); //获取总和 如果数据类型为double、long,则通过summarizingDouble、summarizingLong方法通过 foreach 进行元素遍历List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); integerList.stream().forEach(System.out::println); 而在 jdk8 之前实现遍历:for (int i : integerList) { System.out.println(i); } jdk8 之后遍历元素来的更为方便,原来的 for-each 直接通过 foreach 方法就能实现了返回集合List<String> strings = menu.stream().map(Dish::getName).collect(toList()); Set<String> sets = menu.stream().map(Dish::getName).collect(toSet()); 只举例了一部分,还有很多其他方法 jdk8 之前 List<String> stringList = new ArrayList<>(); Set<String> stringSet = new HashSet<>(); for (Dish dish : menu) { stringList.add(dish.getName()); stringSet.add(dish.getName()); } 通过遍历和返回集合的使用发现流只是把原来的外部迭代放到了内部进行,这也是流的主要特点之一。内部迭代可以减少好多代码量通过 joining 拼接流中的元素String result = menu.stream().map(Dish::getName).collect(Collectors.joining(", ")); 默认如果不通过map方法进行映射处理拼接的toString方法返回的字符串,joining 的方法参数为元素的分界符,如果不指定生成的字符串将是一串的,可读性不强进阶通过 groupingBy 进行分组Map<Type, List<Dish>> result = dishList.stream().collect(groupingBy(Dish::getType)); 在collect方法中传入groupingBy进行分组,其中groupingBy的方法参数为分类函数。还可以通过嵌套使用groupingBy进行多级分类Map<Type, List<Dish>> result = menu.stream().collect(groupingBy(Dish::getType, groupingBy(dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET; else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; else return CaloricLevel.FAT; }))); 进阶通过 partitioningBy 进行分区分区是特殊的分组,它分类依据是 true 和 false,所以返回的结果最多可以分为两组Map<Boolean, List<Dish>> result = menu.stream().collect(partitioningBy(Dish :: isVegetarian)) 等同于Map<Boolean, List<Dish>> result = menu.stream().collect(groupingBy(Dish :: isVegetarian)) 这个例子可能并不能看出分区和分类的区别,甚至觉得分区根本没有必要,换个明显一点的例子:List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); Map<Boolean, List<Integer>> result = integerList.stream().collect(partitioningBy(i -> i < 3)); 返回值的键仍然是布尔类型,但是它的分类是根据范围进行分类的,分区比较适合处理根据范围进行分类总结通过使用Stream API可以简化代码,同时提高了代码可读性,赶紧在项目里用起来。
2023年02月13日
47 阅读
0 评论
0 点赞
2023-02-11
移动APP开发框架盘点
移动APP开发框架盘点 总体概述 现在比较流行的移动APP开发框架有以下六种:网页、混合、渐进、原生、桥接、自绘。前三种体验与Web的体验相似,后三种与原生APP的体验相似。这六种框架形式,都有自己适用的范围。无所谓好坏,适用就是好。 l 网页应用适用于传统网站APP化,比如淘宝、京东,有大量WEB页面嵌入到APP中。 l 混合应用适用于小成本应用开发,全部代码都基于Web,好处是开发快速、成本低。 l 渐进应用适用于高机会成本的场合,边下载边使用,能快速获取,快速体验。 l 原生应用适用于大型和高体验要求的应用,能做出让人满意的体验效果。 l 桥接应用适用于高速迭代的创意类应用,让体验与成本都处于可接受的范围。 l 自绘应用适用于游戏和有特殊效果的应用,最大的好处是没有平台约束和表达瓶颈。 一、网页WebApp WebApp与传统Web的主要区别,在于前端框架,特别是V-DOM框架的应用。此类前端框架使得WebApp与NativeApp在机理上已经没有任何区别了。在众多的前端框架中, React、Vue和Angular是最有竞争力的选择。 React V-Dom技术的开创者,主流框架中的NO.1。React的贡献都是开创性的,在它基础上,也有很多兼容框架,比如Anu,Nerv。用以解决React在性能或IE兼容性上的问题。 主流技术方案: React + Redux + ReactRouter + Material-UI/AntD/Semantic-UI Vue 由国人创建,在中国拥有大量使用者,也有很多配套的开源项目。它的是要特点是学习成本低,容易上手。 主流技术方案: Vue + Vuex + Vue-Router + Vuetify/Quasar/vux/ Mint-UI Angular 大而全型的框架,为大型项目所推崇,深度整合Typescript和Rxjs。 主流技术方案: Angular + Typescript 二、原生NativeApp 由于操作系统的限制,原生应用只有那么几种。对于原生应用,架构是基础,框架是核心,加上海量的UI组件。 IOS iOS开发已经从OC全面转向Swift,最新的架构VIPER基本上可以视为MMVPP。 主流技术方案: VIPER + RxSwift + Moya + Alamofire + SwiftyJSON/ObjectMapper Android Android开发语言从Java更换为Kotlin。编译时依赖注入框架Dagger也成为不二法宝。 主流技术方案: MVP + Dagger + RxKotlin + Retrofit + OkHttp + Kotson(Gson) 三、混合HybridApp HybridApp的关键不在本身,而在WebApp。好的WebApp改为HybridApp很容易。所以混合应用框架实际是指WebApp的基础设施库,有Cordova(PhoneGap)就足够了。 Cordova PhoneGap开源而来。 官方网站: https://cordova.apache.org/ 四、桥接BridgeApp 桥接应用的特点在于使用原生界面,但应用逻辑使用脚本语言编写,通用桥接来控制原生界面。这样达到使用脚本编写原生应用的目的,甚至可以网页与原生应用使用同一套代码,节省大量开发成本。但是操作手感比网页应用强不少。 ReactNative与Weex代表了两种不同的思路。ReactNative提供工具,将平台差异化开放出来(Learn Once, Write Anywhere);而Weex提供框架,将平台差异化屏蔽(Write Once, Run Everywhere)。所以ReactNative最大的痛点是使用难度大,必须熟悉所有平台;Weex则注定功能相对弱小,并且坑比较多。 React Native React的大热,实际始于ReactNative的发布。 官方网站: https://facebook.github.io/react-native/ Weex 小众的框架,能不能壮大,关键在于学习者是否能有效率地编写应用。所以很多人认为文档是决定一个开源框架生死的关键。其实有几个使用框架的开源应用,比文档还要关键。因为这些应用活着,间接地证明了框架还有生存的价值和能力,也能成为更好的文档教材。Weex号称有一堆知名的应用,但开源项目就乏善可陈了。 官方网站: http://weex.apache.org/ Xamarin(C#) Xamarin在IOS与Android中的实现方式不一致,在IOS中是AOT直接编译,在Android中是使用桥接技术。 官方网站: http://xamarin.com/ RubyMotion(Ruby) 动态语言编写移动应用,对语言社区而言是能力问题,必须要证明语言的优越性和无所不能。但除开狂热爱好者,正确的做法是使用最有效率的平台和语言。Ruby的长处在于Web后端,所以编写APP并无多少继承性,编写效率也由于太过小众而存在掉坑的风险。有免费版本,但只支持最新的操作系统版本。 官方网站: http://www.rubymotion.com/ Titanium 这个框架的核心就是使用JavaScript开发应用,与Web开发的在形式上区别很大。所以从根本上,这就是一种脚本语言框架,和RubyMotion如出一辙。 官方网站: http://www.appcelerator.com/ 五、自绘OwndrawApp 自绘一直以来都是游戏界面的势力范围,事实上除开Flutter,其它的框架都是偏游戏开发的。所以Flutter的横空出世,吸引了很大的关注,毕竟这是真正跨平台的唯一可行方案。 Flutter(Dart) 除了使用Dart语言有些争议外,Flutter是真正值得关注的跨平台方案,没有之一。最近它的目标平台除开iOS 和 Android,Flutter Desktop Embedding项目将Flutter引入到桌面操作系统,Hummingbird项目将 Flutter 应用引入浏览器。它利用 Dart 平台的特性不仅可以编译原生 ARM 代码,还可以编译JavaScript 。这使得 Flutter 代码可以在基于标准的 Web 上运行而无需任何更改。 官方网站: https://flutter.dev/ CrossApp(C++) CrossApp是基于Cocos2d-x引擎的,而Cocos2d-x是基于OpenGL的 。9秒社团是由手游社区发展而来的,由此可见CrossApp的背景,使用C++开发也有一些忠实拥趸。 官方网站: https://crossapp.9miao.com/ Corona(Lua) 更适合做游戏,不适合做应用程序,主要是因为界面部分,官方提供的UI部分代码非常不好用,自己实现又很耗时耗力。 官方网站: https://coronalabs.com/ Kivy(Python) 又一个动态语言开发框架,和游戏引擎结合起来使得它在特定领域还是很有市场的。而且它还跨windows平台,可以在windows下直接运行,可以真正实现跨平台运行。 官方网站: https://kivy.org/ 六、渐进ProgressiveApp 渐进有边下载边使用这一层意思,也有下载完成后不依赖网络这一层意思。从类型上来讲,有Google主导的PWA(ProgressiveWebApp),还有微信主导的小程序。相比PWA一统天下的野心,小程序明显是实用主义导向,能用就好,没有长远的布局。急于与微信竞争的百度、支付宝、中国九大手机厂商联盟的QuickApp也复制了这种风格。随着各种跨平台转译工具(如Taro)的兴起,各个小程序平台也随之变成了一个专有浏览器实现,变成了前端千框万架大战中的小小注脚了。 PWA 全称Progressive Web App,即渐进式网页应用。相对于国内厂商的私有平台,谷歌主导的PWA从一开始就瞄准下一代浏览器标准。与传统网页最大的不同,是引入了Service Worker了,相当于本地服务器,能在离线时替代网站服务器继续工作。除此之外,PWA大致就是一个SPA(single page web app),开放标准的继承性还是比较高的。不过由于各平台厂商(如微软,苹果)对PWA的态度不明,PWA的推广进展缓慢。 MiniProgram 微信小程序,由于微信的体量与使用频度,使得小程序可以承载足够的野心。不过从技术上讲,也就是个使用人数较多的浏览器实现。小程序和大量的跟随者,促使多端统一框架也发展起来了。 官方网站: https://mp.weixin.qq.com/ Taro React兼容的跨平台多端统一开发框架。一键生成可以在微信/百度/支付宝/字节跳动小程序、H5、React Native等端运行的代码。虽然多端统一框架看似可以在不同类型的平台运行,但它还是要依赖那些平台工具,还是寄居之上的小程序而已。 官方网站: https://taro.aotu.io/ Nanachi 司徒正美开发的React兼容Anu框架的多端转译脚手架。但是对比Taro是一堆工具打包而成,nanachi仅是一个脚手架,未免寒酸。不包装一下? 官方网站: https://rubylouvre.github.io/nanachi/ Chameleon 类Vue的跨平台多端统一开发框架。专门拜读了昨天出炉的《Chameleon原理详解:其它跨多端统一框架都是假的?》。它有神奇的多态组件,就是重新定义了一个组件声明框架;严格全面的检查,不会不让你的代码不能不跨平台;更多的适配代码,听说有后端统一接口,而且还有一个后台管理系统。好吧,祝您玩得愉快。 官方网站: https://cmljs.org/ 趋势分析 国内由于微信开放了小程序流量主的广告收入,引爆了小程序的热潮,进一步催生了多端转译框架。但是这些框架所做的事并不多,引擎还是React/Vue,做了一套各平台统一的UI组件而已。由于个性化的原因,UI组件是大家一直都想统一,而不能如愿的目标。Taro框架也只是推销了自己的一套UI组件而已。在我看来,还不如提供一套组件“标准”,允许并鼓励用户自己实现。 如果有一套能在React/Vue,ReactNative/Weex,甚至Android,iOS,Flutter上使用的“标准”组件,那么全平台大一统,Write Once, Run Everywhere这个目标才有可能实现吧。 问题:试求React-Native、Taro、Weex、Chameleon框架组件的“最大公约数”和“最小公倍数”。 抽象的“公约数”好求,结合实现的“最小公倍数”只能在实践中证明了。 框架 组件 组件名称 抽象类型 ReactNative View 视图容器 <View> Text 文本 <Text> Image 图片 <Image> ImageBackground 图片背景 <View> TextInput 文本输入框 <Input> ScrollView 滚动视图 <View> Button 按钮 <Button> Picker 选择器 <Picker> Slider 范围值选择 <Slider> Switch 开关组件 <Checkbox> FlatList 简单列表 <List> SectionList 分组列表 <List> DatePickerIOS 日期/时间选择器 <DatePicker> MaskedViewIOS 带蒙版的视图 <Modal> ProgressViewIOS 进度条 <Progress> SegmentedControlIOS 分段显示多个选项 <Tab> SafeAreaView 非遮挡可视区域 <View> SnapshotViewIOS 截屏视图 <View> DrawerLayoutAndroid 抽屉导航 <View> ProgressBarAndroid 进度条 <Progress> ToolbarAndroid 工具栏 <Card> ActivityIndicator 加载提示符 <Loading> KeyboardAvoidingView 随键盘调整视图 <View> Modal 模式视图 <Modal> RefreshControl 下拉刷新 <Trigger> StatusBar 状态栏 <Card> TouchableHighlight 高亮触摸响应 <Trigger> TouchableOpacity 透明度触摸响应 <Trigger> Taro View 视图容器 <View> ScrollView 可滚动视图 <View> Swiper 滑块视图容器 < Swiper> MovableView 可移动的视图容器 <View> CoverView 覆盖在原生组件之上的文本视图 <View> Icon 图标 <Text> Text 文本 <Text> Progress 进度条 <Progress> RichText 富文本 <View> Button 按钮 <Button> CheckboxGroup 多项选择器 <View> Form 表单 <View> Input 文本输入框 <Input> Label 表单标签 <Text> Picker 普通选择器 <Picker> PickerView 嵌入的滚动选择器 <View> Radio 单项选择器 <Radio> Slider 滑动选择器 <Slider> Switch 开关选择器 <Checkbox> Textarea 多行输入框 <Input> Navigator 页面导航链接 <Card> Audio 音频 <Audio> Image 图片 <Image> Video 视频 <Video> Camera 系统相机 <Camera> LivePlayer 实时音视频播放 <Service> LivePusher 实时音视频录制 <Service> Map 地图 <Map> Canvas 画布 <Canvas> OpenData 展示微信开放的数据 <Card> WebView 网页承载容器 <Web> Weex a 页面间的跳转 <A> div 通用容器 <View> text 文本 <Text> image 图片 <Image> list 垂直列表 <List> cell 列表子组件 <View> loading 容器上拉加载 <Trigger> Refresh 容器下拉刷新 <Trigger> recycle-list 复用列表容器 <List> scroller 滚动的容器 <View> slider 轮播图 < Swiper> indicator 轮播图子组件 < Swiper> textarea 多行文本输入 <Input> input 输入 <Input> waterfall 瀑布流布局容器 <View> video 视频 <Video> web 网页 <Web richtext 富文本容器 <View> Chameleon view 视图容器 <View> text 文本容器 <Text> page 基础页面容器 <View> block 包装容器 <View> cell 子列表项容器 <View> scroller 可滚动视图区域 <View> list 可滚动长列表 <List> container 布局容器 <View> row flex布局行容器 <View> col flex布局列容器 <View> carousel 轮播图 < Swiper> carousel-item 轮播图子容器 < Swiper> button 按钮 <Button> input 输入框 <Input> textarea 多行输入框 <Input> switch 开关 <Checkbox> radio 单选框 <Radio> checkbox 复选框 <Checkbox> image 图片 <Image> video 视频播放器 <Video> c-animation 动画组件 <Animation> c-toast 提示框 <Toast> c-loading 加载中 <Loading> c-dialog 对话框 <Modal> c-popup 蒙层 <Modal> c-tip 提示 <Card> c-actionsheet 操作列表 <List> c-tab 标签页 <Tab> c-picker 底部弹起的选择器 <Picker> c-picker-panel 底部弹起的控制板 <Picker> c-picker-item 滚动选择器 <Picker> c-checkbox-group 复选框列表 <View> c-radio-group 单选框列表 <View> c-refresh 上拉&下拉刷新 <Trigger> <完>
2023年02月11日
99 阅读
0 评论
0 点赞
2023-02-11
Ubuntu SSH root 登录 Permission denied 错误
问题:$ ssh root@40.125.21.75 root@40.125.21.75's password: Permission denied, please try again.解决方式,编辑40.125.21.75服务器中的vim /etc/ssh/sshd_config配置文件:PermitRootLogin without-password --改为 PermitRootLogin yes然后重启 SSH:$ sudo service ssh restart
2023年02月11日
165 阅读
0 评论
0 点赞
2023-02-11
Centos7防火墙配置详细
一、条件防火墙是开启的 systemctl start firewalld 1、查看防火墙的配置 firewall-cmd --state firewall-cmd --list-all 2、开放80端口 firewall-cmd --permanent --add-port=80/tcp firewall-cmd --reload #重新加载防火墙配置才会起作用 3、移除以上规则 firewall-cmd --permanent --remove-port=80/tcp firewall-cmd --reload 4、放通某个端口段 firewall-cmd --permanent --zone=public --add-port=1000-2000/tcp firewall-cmd --reload 5、放通某个IP访问,默认允许 firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=192.168.1.169 accept' firewall-cmd --reload 6、禁止某个IP访问 firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.42 drop' firewall-cmd --reload 7、放通某个IP访问某个端口 firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=192.168.1.169 port protocol=tcp port=6379 accept' firewall-cmd --reload 8、移除以上规则 firewall-cmd --permanent --remove-rich-rule='rule family="ipv4" source address="192.168.1.169" port port="6379" protocol="tcp" accept' firewall-cmd --reload 9、放通某个IP段访问 firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.0/24 accept' 以下有图片效果 以下有图片效果 以下有图片效果 一、条件防火墙是开启的 systemctl start firewalld 1、查看防火墙的配置 firewall-cmd --state 2、开放80端口 firewall-cmd --permanent --add-port=80/tcp firewall-cmd --reload #重新加载防火墙配置才会起作用 3、移除以上规则 firewall-cmd --permanent --remove-port=80/tcp firewall-cmd --reload 4、放通某个端口段 firewall-cmd --permanent --zone=public --add-port=1000-2000/tcp firewall-cmd --reload 5、放通某个IP访问,默认允许 firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=192.168.1.169 accept' firewall-cmd --reload 6、禁止某个IP访问 firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.42 drop' firewall-cmd --reload 7、放通某个IP访问某个端口 firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=192.168.1.169 port protocol=tcp port=6379 accept' firewall-cmd --reload #禁止指定IP访问本机8080端口 firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.1" port protocol="tcp" port="8080" reject' 8、移除以上规则 firewall-cmd --permanent --remove-rich-rule='rule family="ipv4" source address="192.168.1.169" port port="6379" protocol="tcp" accept' firewall-cmd --reload
2023年02月11日
54 阅读
0 评论
0 点赞
2023-02-11
SpringBoot集成JWT实现token验证
JWT官网:https://jwt.io/ 什么是JWT Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。 JWT请求流程 1. 用户使用账号和面发出post请求; 2. 服务器使用私钥创建一个jwt; 3. 服务器返回这个jwt给浏览器; 4. 浏览器将该jwt串在请求头中像服务器发送请求; 5. 服务器验证该jwt; 6. 返回响应的资源给浏览器。 JWT的主要应用场景 身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。 优点 1.简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快 2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库 3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。 4.不需要在服务端保存会话信息,特别适用于分布式微服务。 JWT的结构 JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。 就像这样: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ JWT包含了三部分: Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型) Payload 负载 (类似于飞机上承载的物品) Signature 签名/签证 Header JWT的头部承载两部分信息:token类型和采用的加密算法。 { "alg": "HS256", "typ": "JWT" } 声明类型:这里是jwt 声明加密的算法:通常直接使用 HMAC SHA256 加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。 MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值 SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5 HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证 Payload 载荷就是存放有效信息的地方。 有效信息包含三个部分 1.标准中注册的声明 2.公共的声明 3.私有的声明 标准中注册的声明 (建议但不强制使用) : iss: jwt签发者 sub: 面向的用户(jwt所面向的用户) aud: 接收jwt的一方 exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间) nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密. 私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。 Signature jwt的第三部分是一个签证信息 这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。 密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。 下面来进行SpringBoot和JWT的集成 引入JWT依赖,由于是基于Java,所以需要的是java-jwt <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> 需要自定义两个注解 用来跳过验证的PassToken @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PassToken { boolean required() default true; } 需要登录才能进行操作的注解UserLoginToken @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface UserLoginToken { boolean required() default true; } @Target:注解的作用目标 @Target(ElementType.TYPE)——接口、类、枚举、注解 @Target(ElementType.FIELD)——字段、枚举的常量 @Target(ElementType.METHOD)——方法 @Target(ElementType.PARAMETER)——方法参数 @Target(ElementType.CONSTRUCTOR) ——构造函数 @Target(ElementType.LOCAL_VARIABLE)——局部变量 @Target(ElementType.ANNOTATION_TYPE)——注解 @Target(ElementType.PACKAGE)——包 @Retention:注解的保留位置 RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。 RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。 RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。 @Document:说明该注解将被包含在javadoc中 @Inherited:说明子类可以继承父类中的该注解 简单自定义一个实体类User,使用lombok简化实体类的编写 @Data @AllArgsConstructor @NoArgsConstructor public class User { String Id; String username; String password; } 需要写token的生成方法 public String getToken(User user) { String token=""; token= JWT.create().withAudience(user.getId()) .sign(Algorithm.HMAC256(user.getPassword())); return token; } Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。 withAudience()存入需要保存在token的信息,这里我把用户ID存入token中 接下来需要写一个拦截器去获取token并验证token public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired UserService userService; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token // 如果不是映射到方法直接通过 if(!(object instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod=(HandlerMethod)object; Method method=handlerMethod.getMethod(); //检查是否有passtoken注释,有则跳过认证 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //检查有没有需要用户权限的注解 if (method.isAnnotationPresent(UserLoginToken.class)) { UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class); if (userLoginToken.required()) { // 执行认证 if (token == null) { throw new RuntimeException("无token,请重新登录"); } // 获取 token 中的 user id String userId; try { userId = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { throw new RuntimeException("401"); } User user = userService.findUserById(userId); if (user == null) { throw new RuntimeException("用户不存在,请重新登录"); } // 验证 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); try { jwtVerifier.verify(token); } catch (JWTVerificationException e) { throw new RuntimeException("401"); } return true; } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } 实现一个拦截器就需要实现HandlerInterceptor接口 HandlerInterceptor接口主要定义了三个方法 1.boolean preHandle (): 预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行 postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。 2.void postHandle(): 后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。 3.void afterCompletion(): 整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中 主要流程: 1.从 http 请求头中取出 token, 2.判断是否映射到方法 3.检查是否有passtoken注释,有则跳过认证 4.检查有没有需要用户登录的注解,有则需要取出并验证 5.认证通过则可以访问,不通过会报相关错误信息 配置拦截器 在配置类上添加了注解@Configuration,标明了该类是一个配置类并且会将该类作为一个SpringBean添加到IOC容器内 @Configuration public class InterceptorConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录 } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } } WebMvcConfigurerAdapter该抽象类其实里面没有任何的方法实现,只是空实现了接口 WebMvcConfigurer内的全部方法,并没有给出任何的业务逻辑处理,这一点设计恰到好处的让我们不必去实现那些我们不用的方法,都交由WebMvcConfigurerAdapter抽象类空实现,如果我们需要针对具体的某一个方法做出逻辑处理,仅仅需要在 WebMvcConfigurerAdapter子类中@Override对应方法就可以了。 注: 在SpringBoot2.0及Spring 5.0中WebMvcConfigurerAdapter已被废弃 网上有说改为继承WebMvcConfigurationSupport,不过试了下,还是过期的 解决方法: 直接实现WebMvcConfigurer (官方推荐) @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } } InterceptorRegistry内的addInterceptor需要一个实现HandlerInterceptor接口的拦截器实例,addPathPatterns方法用于设置拦截器的过滤路径规则。 这里我拦截所有请求,通过判断是否有@LoginRequired注解 决定是否需要登录 在数据访问接口中加入登录操作注解 @RestController @RequestMapping("api") public class UserApi { @Autowired UserService userService; @Autowired TokenService tokenService; //登录 @PostMapping("/login") public Object login(@RequestBody User user){ JSONObject jsonObject=new JSONObject(); User userForBase=userService.findByUsername(user); if(userForBase==null){ jsonObject.put("message","登录失败,用户不存在"); return jsonObject; }else { if (!userForBase.getPassword().equals(user.getPassword())){ jsonObject.put("message","登录失败,密码错误"); return jsonObject; }else { String token = tokenService.getToken(userForBase); jsonObject.put("token", token); jsonObject.put("user", userForBase); return jsonObject; } } } @UserLoginToken @GetMapping("/getMessage") public String getMessage(){ return "你已通过验证"; } } 不加注解的话默认不验证,登录接口一般是不验证的。在getMessage()中我加上了登录注解,说明该接口必须登录获取token后,在请求头中加上token并通过验证才可以访问 下面进行测试,启动项目,使用postman测试接口 在没token的情况下访问api/getMessage接口 我这里使用了统一异常处理,所以只看到错误message 下面进行登录,从而获取token 登录操作我没加验证注解,所以可以直接访问 把token加在请求头中,再次访问api/getMessage接口 注意:这里的key一定不能错,因为在拦截器中是取关键字token的值 String token = httpServletRequest.getHeader("token"); 加上token之后就可以顺利通过验证和进行接口访问了
2023年02月11日
158 阅读
1 评论
0 点赞
1
...
25
26
27