0%

深入分析,查找入侵原因

一、检查隐藏帐户及弱口令

  1. 检查服务器系统及应用帐户是否存在 弱口令
    • 检查说明:检查管理员帐户、数据库帐户、MySQL 帐户、tomcat 帐户、网站后台管理员帐户等密码设置是否较为简单,简单的密码很容易被黑客破解。
    • 解决方法:以管理员权限登录系统或应用程序后台,修改为复杂的密码。
    • 风险性:高。
  2. 使用last命令查看下服务器近期登录的帐户记录,确认是否有可疑 IP 登录过机器:
    • 检查说明:攻击者或者恶意软件往往会往系统中注入隐藏的系统帐户实施提权或其他破坏性的攻击。
    • 解决方法:检查发现有可疑用户时,可使用命令usermod -L 用户名禁用用户或者使用命令userdel -r 用户名删除用户。
    • 风险性:高。
  3. 通过less /var/log/secure|grep 'Accepted'命令,查看是否有可疑 IP 成功登录机器:
    • 检查说明:攻击者或者恶意软件往往会往系统中注入隐藏的系统帐户实施提权或其他破坏性的攻击。
    • 解决方法: 使用命令usermod -L 用户名禁用用户或者使用命令userdel -r 用户名删除用户。
    • 风险性:高。
  4. 检查系统是否采用 默认管理端口
    • 检查系统所用的管理端口(SSH、FTP、MySQL、Redis 等)是否为默认端口,这些默认端口往往被容易自动化的工具进行爆破成功。
    • 解决方法:
      1. 在服务器内编辑/etc/ssh/sshd_config文件中的 Port 22,将22修改为非默认端口,修改之后需要重启 ssh 服务。

        !当对端口进行修改时,需同时在 云服务器控制台 上修改对应主机的安全组配置,在其入站规则中,放行对应端口,详情请参见 添加安全组规则

      2. 运行/etc/init.d/sshd restart(CentOS)或 /etc/init.d/ssh restart(Debian / Ubuntu)命令重启是配置生效。
         3. 修改 FTP、MySQL、Redis 等的程序配置文件的默认监听端口21、3306、6379为其他端口。
      3. 限制远程登录的 IP,编辑/etc/hosts.deny/etc/hosts.allow两个文件来限制 IP。
    • 风险性:高。
  5. 检查/etc/passwd文件,看是否有非授权帐户登录:
    • 检查说明:攻击者或者恶意软件往往会往系统中注入隐藏的系统帐户实施提权或其他破坏性的攻击。
    • 解决方法: 使用命令usermod -L 用户名禁用用户或者使用命令userdel -r 用户名删除用户。
    • 风险性:中。

二、检查恶意进程及非法端口

  1. 运行netstat –antp查看下服务器是否有未被授权的端口被监听,查看下对应的 pid。
    • 检查服务器是否存在恶意进程,恶意进程往往会开启监听端口,与外部控制机器进行连接。
    • 解决方法:
      1. 若发现有非授权进程,运行ls -l /proc/$PID/exefile /proc/$PID/exe ($PID 为对应的 pid 号),查看下 pid 所对应的进程文件路径。
      2. 如果为恶意进程,删除下对应的文件即可。
    • 风险性:高。
  2. 使用ps -eftop命令查看是否有异常进程
    • 检查说明:运行以上命令,当发现有名称不断变化的非授权进程占用大量系统 CPU 或内存资源时,则可能为恶意程序。
    • 解决方法:确认该进程为恶意进程后,可以使用kill -9 进程名命令结束进程,或使用防火墙限制进程外联。
    • 风险性:高。

三、检查恶意程序和可疑启动项

  1. 使用chkconfig --listcat /etc/rc.local命令查看下开机启动项中是否有异常的启动服务。
    • 检查说明:恶意程序往往会添加在系统的启动项,在用户关机重启后再次运行。
    • 解决方法:如发现有恶意进程,可使用chkconfig 服务名 off命令关闭,同时检查/etc/rc.local中是否有异常项目,如有请注释掉。
    • 风险性:高。
  2. 进入 cron 文件目录,查看是否存在非法定时任务脚本。
    • 检查说明:查看/etc/crontab/etc/cron.d/etc/cron.dailycron.hourly/cron.monthlycron.weekly/是否存在可疑脚本或程序。
    • 解决方法:如发现有不认识的计划任务,可定位脚本确认是否正常业务脚本,如果非正常业务脚本,可直接注释掉任务内容或删除脚本。
    • 风险性:高。

四、检查第三方软件漏洞

  1. 如果您服务器内有运行 Web、数据库等应用服务,请您限制应用程序帐户对文件系统的写权限,同时尽量使用非 root 帐户运行。
    • 检查说明:使用非 root 帐户运行可以保障在应用程序被攻陷后攻击者无法立即远程控制服务器,减少攻击损失
    • 解决方法:
      1. 进入 web 服务根目录或数据库配置目录;
      2. 运行chown -R apache:apache /var/www/xxxxchmod -R 750 file1.txt命令配置网站访问权限。
    • 风险性:中。
    • 参考示例
  2. 升级修复应用程序漏洞
    • 检查说明:机器被入侵,部分原因是系统使用的应用程序软件版本较老,存在较多的漏洞而没有修复,导致可以被入侵利用。
    • 解决方法:比较典型的漏洞例如 ImageMagick、openssl、glibc 等,用户可以根据腾讯云已发布安全通告指导通过 apt-get/yum 等方式进行直接升级修复。
    • 风险性:高。


网站目录文件权限的参考示例如下:
场景:
我们假设 HTTP 服务器运行的用户和用户组是 www,网站用户为 centos,网站根目录是/home/centos/web
方法/步骤:

  1. 我们首先设定网站目录和文件的所有者和所有组为 centos,www,如下命令:
    1
    chown -R centos:www /home/centos/web
  2. 设置网站目录权限为750,750是 centos 用户对目录拥有读写执行的权限,设置后,centos 用户可以在任何目录下创建文件,用户组有有读执行权限,这样才能进入目录,其它用户没有任何权限。
    1
    find -type d -exec chmod 750 {} \;
  3. 设置网站文件权限为640,640指只有 centos 用户对网站文件有更改的权限,HTTP 服务器只有读取文件的权限,无法更改文件,其它用户无任何权限。
    1
    find -not -type d -exec chmod 640 {} \;
  4. 针对个别目录设置可写权限。例如,网站的一些缓存目录就需要给 HTTP 服务有写入权限、discuz x2 的/data/目录就必须要写入权限。
    1
    find data -type d -exec chmod 770 {} \;

被入侵后的安全优化建议

  1. 尽量使用 SSH 密钥进行登录,减少暴力破解的风险。
  2. 在服务器内编辑/etc/ssh/sshd_config文件中的 Port 22,将 22 修改为其他非默认端口,修改之后重启 SSH 服务。可使用命令重启
    1
    /etc/init.d/sshd restart(CentOS)或 /etc/init.d/ssh restart(Debian/Ubuntu)

    !当修改端口时,需同时在 云服务器控制台 上修改对应主机安全组配置,在其入站规则中放行对应端口,详情请参见 添加安全组规则

  3. 如果必须使用 SSH 密码进行管理,选择一个好密码。
    • 无论应用程序管理后台(网站、中间件、tomcat 等)、远程 SSH、远程桌面、数据库,都建议设置复杂且不一样的密码。
    • 下面是一些好密码的实例(可以使用空格):
      1qtwo-threeMiles3c45jia
      caser, lanqiu streets
    • 下面是一些弱口令的示例,可能是您在公开的工作中常用的词或者是您生活中常用的词:
      公司名+日期(coca-cola2016xxxx)
      常用口语(Iamagoodboy)
  4. 使用以下命令检查主机有哪些端口开放,关闭非业务端口。
    1
    netstat -antp
  5. 通过腾讯云-安全组防火墙限制仅允许制定 IP 访问管理或通过编辑/etc/hosts.deny/etc/hosts.allow两个文件来限制 IP。
  6. 应用程序尽量不使用 root 权限。
    例如 Apache、Redis、MySQL、Nginx 等程序,尽量不要以 root 权限的方式运行。
  7. 修复系统提权漏洞与运行在 root 权限下的程序漏洞,以免恶意软件通过漏洞提权获得 root 权限传播后门。
    • 及时更新系统或所用应用程序的版本,如 Struts2、Nginx,ImageMagick、Java 等。
    • 关闭应用程序的远程管理功能,如 Redis、NTP 等,如果无远程管理需要,可关闭对外监听端口或配置。
  8. 定期备份云服务器业务数据。
    • 对重要的业务数据进行异地备份或云备份,避免主机被入侵后无法恢复。
    • 除了您的 home,root 目录外,您还应当备份 /etc 和可用于取证的 /var/log 目录。
  9. 安装腾讯云主机安全 Agent,在发生攻击后,可以了解自身风险情况。


之前每次添加注释模板的时候,都会去网上查一遍,但是每个人写的教程都不一样,出现注释模板百花齐放,因此在这里记录下自己认为比较好的一套模板,适应@JavaDoc编写的,支持多参数分别列出的一套模板,下面首先介绍下自定义模板组和模板的创建过程

创建模板组

  1. 首先第一步打开Idea的Setting界面,步骤:打开Idea–>菜单选择File–>选择Setting
  2. 不再描述,自己看图吧 需要说明的几点:
  • 上图中的Abbreviation是调起注释的快捷输入内容,再加上Expand with中选择的快捷键(个人使用的是Tab,看个人喜好进行选择,可选择Enter)
  • 上图中最下面有一排小字,显示最末尾有个Change,是选择注释快捷输入的作用域,也就是在什么地方你可以通过快捷字母进行输入,这个地方是可以修改的,默认的时候这个地方显示的是No Applicable context yet.Define,点击打开可以选择作用域,我这里选择的是Java,也可以选择第一个,全部选择,如果不选的话,在变量编辑界面会出现系统提供下拉选择变量没有的情况
  • 模板框中一定得现有内容,例如下面的代码注释模板,否则Edit variables按钮是不可点击状态的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    *
    *
    $params$
    * @return $returns$
    * @exception $exception$
    * @author $author$
    * @date $date$ $time$
    */
  • 上面的变量配置截图和内容如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    params:groovyScript("def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {result+=' * @param ' + params[i] + ((i < params.size() - 1) ? '\\r\\n' : '')}; return result", methodParameters())

    returns:methodReturnType()

    exception:expressionType(Expression)

    author:user()

    date:date()

    time:time()
    变量录入完的效果见下图,这个地方有个比较奇怪的地方,在输入groovy代码的时候会出现切换后变成空的,这里可以输入完之后,直接点击OK,就能够保存下来了

临时设置环境变量

这种设置办法一般用在临时使用,比如说导出数据库的时候需要设置字符集之类的

1
# export PATH=$PATH:/usr/local/htop/bin

永久生效设置

这种是永久生效的设置环境变量,比如说JAVA的环境变量等等

1
# vim /etc/profile

在文件最后添加上对应的变量

1
export PATH="$PATH:/usr/local/htop/bin"

添加完成后,还需要执行下面命令使之生效

1
source /etc/profile

验证是否成功

下面代码中第10行显示的declare -x OLDPWD="/usr/local/htop"表示设置成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@xxxxxx bin]# export
declare -x HISTCONTROL="ignoredups"
declare -x HISTSIZE="3000"
declare -x HISTTIMEFORMAT="%F %T "
declare -x HOME="/root"
declare -x HOSTNAME="VM_0_12_centos"
declare -x LANG="en_US.utf8"
declare -x LESSOPEN="||/usr/bin/lesspipe.sh %s"
declare -x LOGNAME="root"
declare -x MAIL="/var/spool/mail/root"
declare -x OLDPWD="/usr/local/htop"
declare -x PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/htop/bin"
declare -x PROMPT_COMMAND="history -a; printf \"\\033]0;%s@%s:%s\\007\" \"\${USER}\" \"\${HOSTNAME%%.*}\" \"\${PWD/#\$HOME/~}\""
declare -x PWD="/usr/local/htop/bin"
declare -x SHELL="/bin/bash"
declare -x SHLVL="1"
declare -x SSH_TTY="/dev/pts/0"
declare -x TERM="xterm"
declare -x USER="root"
declare -x XDG_RUNTIME_DIR="/run/user/0"
declare -x XDG_SESSION_ID="102676"


今天服务器又出问题了,内存32个G竟然被吃光了,就像准备排查下,使用top命令,想应该有更好用的,百度了下,果然找到了一个htop

htop下载

sourceforge搜索htop,看了下从2012年之后就没再更新,直接可以使用下面的命令进行下载即可

1
wget http://sourceforge.net/projects/htop/files/htop/1.0.2/htop-1.0.2.tar.gz

htop解压、编译、安装

1
2
3
4
5
# tar -zxvf htop-1.0.2.tar.gz
# cd htop-1.0.2/
# ./configure --prefix=/usr/local/htop //这里如果报错了,看下面的报错异常处理
# make
# make install

添加环境变量

1
2
3
4
编辑环境变量文件
# vim /etc/profile
# export PATH="$PATH:/usr/local/htop/bin"
# source /etc/profile

界面解析

首先执行下htop命令看下是否安装成功

上图主要分为四个区域:

  • 左上角:显示CPU、物理内存、交换分区信息
  • 右上角:任务数量、平均负载、运行时间等信息
  • 进程区域:显示当前系统中的所有进程
  • 底部区域:操作提示,分别是F1-F10功能键

左上角区域解析

CPU、内存、交换区的使用情况及占比,没有太多可以说的

右上角区域解析

Tasks:使用逗号隔开,分别是运行着的进程和进程总数
Load average:平均负载 1分钟 5分钟 10分钟
Uptime:开机时间

进程区域

  • PID:进程的标识号
  • USER:运行此进程的用户
  • PRI:进程的优先级
  • NI:进程的优先级别值,默认为0,可进行调整
  • VIRT:进程占用的虚拟内存值
  • RES:进程占用的物理内存值
  • SHR:进程占用的共享内存值
  • S:进程运行情况R表示运行、S表示休眠等待唤醒、Z表示僵尸
  • %CPU:该进程占用CPU百分比
  • %MEM:该进程占用的物理内存和总内存的百分比
  • TIME+:该进程启动后占用的总CPU时间
  • COMMAND:启动该进程使用的命令名称

使用

底部区域那是有F1-F10功能键的,通过这些功能键是能实现一个功能的,具体的功能描述如下

F1:显示帮助信息

F2:配置界面中的显示信息

F3:进程搜索

F4:过滤进程

从下图可以清楚的看到,搜索和过滤的区别,搜索是光标定位到对应的进程上,但是过滤是只显示符合的进程

F5:以进程树的形式进行展示

效果和F3和F4的一样,就是进程按照树的形式进行展示

F6:排序

F7/F8:修改进程Nice值(进程的优先级)

F7是降低进程的优先级,F8是提升进程的优先级

F9:杀掉指定的进程

F10:退出htop。

其他

  • 空格键:用于标记选中的进程,用于实现对多个进程的同时操作
  • U:取消所有选中的额进程
  • s: 显示光标所在进程执行的系统调用命令
  • |:显示光标所在进程的文件列表
  • I:对排序顺序进行反序排列
  • a:绑定进程到指定的CPU
  • u:显示指定用户的进程
  • M:按照内存使用百分比进行排序,对应的列是MEM%
  • P:按照CPU使用百分比排序,对应CPU%
  • T:按照进程运行时间排序,对应TIME+
  • K:隐藏内核线程
  • H:隐藏用户线程
  • #:快速定位光标到PID所指定的进程上

htop参数

d:设置刷新时间

单位不是很确定,设置为5的时候刷新的很快,50又不是5s

1
# htop -d 50

u:显示指定用户进程

1
# htop -u oracle

s:按照指定列进行排序

报错异常处理

安装过程中在执行configure的时候,有可能会报下面这个错误,这说明缺少lib包,只需要执行这个命令安装即可yum install ncurses-devel

1
configure: error: You may want to use --disable-unicode or install libncursesw.

发现问题

Arrays.asList()在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合,但其实底层是没有转换成List的还是一个数组,假的转换完成后是没法使用List的相关方法的,比如说add/remove/clear,会抛出异常UnsupportedOperationException异常

1
2
3
4
String[] strArr = {"zhangsan","lisi","wangwu"};
List<String> strList = Arrays.asList(strArr);
//上面两句等价于下面这一句
List<String> strList = Arrays.asList("zhangsan","lisi","wangwu");

Arrays.asList()的源码如下

1
2
3
4
5
6
/**
*返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。
*/
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

解决方案

如何正确的将数组转化为集合List呢,下面有几种方式

  1. (只是教育目的,不推荐使用)自己动手通过泛型实现
    1
    2
    3
    4
    5
    6
    7
    public static <T> List<T> arrayToList(final T[] array){
    final List<T> l = new ArrayList<>(array.length);
    for(final T t:array){
    l.add(t);
    }
    return l;
    }
  2. (推荐)最简便的方法
    1
    List l = new ArrayList<>(Arrays.asList("a","b","c"))
  3. (推荐)使用Java8的Stream
    1
    2
    3
    4
    5
    Integer[] myArray1 = {1,2,3};
    List myList1 = Arrays.stream(myArray1).collect(Collectors.toList());
    //基本类型也可以实现转换(依赖boxed的装箱操作)
    int [] myArray2 = { 1, 2, 3 };
    List myList2 = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
  4. 使用Guava
    对于不可变集合,你可以使用ImmutableList类及其of()与copyOf()工厂方法:(参数不能为空)
    1
    2
    List<String> il = ImmutableList.of("string", "elements");  // from varargs
    List<String> il = ImmutableList.copyOf(aStringArray); // from array
    对于可变集合,你可以使用Lists类及其newArrayList()工厂方法:
    1
    2
    3
    List<String> l1 = Lists.newArrayList(anotherListOrCollection);    // from collection
    List<String> l2 = Lists.newArrayList(aStringArray); // from array
    List<String> l3 = Lists.newArrayList("or", "string", "elements"); // from varargs

为什么要用分布式

什么是分布式ID

拿MySQL数据库举个例子:
在业务数据量不大的时候,单库单表完全可以支撑现有业务,数据量再大点可以弄MySQL主从同步读写分离来对付。
但是随着数据日渐增长,主从也扛不住了,就需要对数据库进行分库分表,但分库分表需要有一个唯一ID来标识一条数据,数据库的自增ID显然是不能满足需求;特别一点的如订单、优惠券也都需要唯一ID作为标识。此时一个能够生成全局唯一ID的系统是非常必要的。那这个全局唯一ID就叫做分布式ID

分布式ID需要满足哪些条件

  • 全局唯一:必须保证ID是全局唯一的
  • 高性能:高可用低延迟,ID生成相应快,否则会成为业务瓶颈
  • 高可用:需要无线接近于100%的可用性
  • 好接入:要秉承拿来即用的原则
  • 趋势递增:最好趋势递增,这个要求就看具体业务场景,不严格要求

分布式ID都有哪些生成方式

下面有9种:

  • UUID
  • 数据库自增ID
  • 数据库多主模式
  • 号段模式
  • Redis
  • 雪花算法(SnowFlake)
  • 滴滴出品(TinyID)
  • 百度(Uidgenerator)
  • 美团(Leaf)

几种分布式生成ID的优缺点

基于UUID

在Java的世界里,想要得到一个具有唯一性的ID,首先想到的就是UUID,UUID是全球唯一的特性。UUID也是可以做分布式ID的,但是不推荐

1
2
3
4
public static void main(String args[]){
String uuid = UUID.randomUUID().toString.replaceAll("-","");
System.out.print(uuid);
}

UUID的生成简单到只有一行代码,但是UUID缺并不适用于实际的业务需求,像作为订单号UUID这样的字符串没有丝毫意义,看不出订单的相关信息;而对于数据库来说作为业务主键ID,不仅太长还是字符串,存储性能差,查询也很好使,所以不推荐作为分布式ID
优点

  • 生成足够简单,本地生成无网络小号,具有唯一性

缺点

  • 无序的字符串,不具备趋势自增特性
  • 没有具体的业务含义
  • 长度过长,对数据性能消耗过大,MySQL官方明确建议主键应该尽量越短越好,作为数据库主键UUID的无序性会导致数据位置频繁变动,影响性能

基于数据库自增ID

基于数据库的auto_increment自增ID完全可以充当分布式ID,具体实现需要一个单独的MySQL实例来完成,建表结构如下

1
2
3
4
5
6
CREATE DATABASE 'SEQ_ID';
CREATE TABLE `sequence_id` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`value` char(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

当我们需要一个ID的时候,向表中插入一条记录返回主键ID,但是这种方式由一个致命的缺点,访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐

优点

  • 实现简单,ID单调自增,数据类型查询速度快

缺点

  • DB单点存在宕机风险,无法抗住高并发场景

基于数据库集群模式

前边说了单点数据库方式不可取,那对上边的方式做一些高可用优化,换成主从模式集群。害怕一些主节点挂点没法用,那就做双主模式集群,也就是两个MySQL实例都能单独生成自增ID。那这样还会有个问题,两个MySQL实例的自增ID都是从1开始,会生成重复的ID怎么办
解决方案
设置起始值自增步长

  1. MySQL_1配置:
    1
    2
    set @@auto_increment_offset = 1;     -- 起始值
    set @@auto_increment_increment = 2; -- 步长
  2. MySQL_2配置:
    1
    2
    set @@auto_increment_offset = 2;     -- 起始值
    set @@auto_increment_increment = 2; -- 步长
    水平扩展的数据库集群,有利于解决数据库单点的压力问题,同时为了ID生成特性,将自增步长按照机器数量来设置。
    增加第三台MySQL实例需要人工修改一、二两台MySQL实例的起始值和步长,把第三台的ID其实生成位置设置为比现有自增ID的位置远一些,但必须在前两台MySQL实例ID还没有增长到第三台实例的其实ID值的时候,否则会出现ID重复,必要时还需要停机修改

优点

  • 解决DB单点问题

缺点
不利于后续扩容,而且实际上单个数据库自身压力还是大,已久无法满足高并发场景

基于数据库的号段模式

号段模式是当下分布式ID生成器的主流实现方式之一,号段可以理解为从数据库批量的获取自增ID,每次从数据库去除一个号段范围,例如(0,1000]代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。表结构如下

1
2
3
4
5
6
7
8
CREATE TABLE `id_generator` (
`id` int(10) NOT NULL,
`max_id` bigint(20) NOT NULL COMMENT '当前最大id',
`step` int(20) NOT NULL COMMENT '号段的步长',
`biz_type` int(20) NOT NULL COMMENT '业务类型',
`version` int(20) NOT NULL COMMENT '版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

biz_type:代表不同业务类型
max_id:当前最大的可用id
step:代表号段的长度
version:是一个乐观锁,每次都更新version,保证并发数据的正确性
|id|biz_type|max_id|step|version|
|-|-|-|-|-|
|1|101|1000|2000|0|
等这批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,update max_id = max_id+step,update 成功后则说明新号段获取成功,新的号段范围是(max_id,max_id+step]

1
update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX

由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖与数据库,不会频繁的访问数据库,对数据库的压力小很多

基于Redis模式

Redis也同样可以实现,原理就是利用Redis的incr命令实现ID的原子性自增

1
2
3
4
127.0.0.1:6379> set seq_id 1     // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增加1,并返回递增后的数值
(integer) 2

用Redis实现需要注意一点,要考虑redis的持久化的问题。redis有两种持久化方式分别是RDB和AOF

  • RDB会定时打一个快照进行持久化,加入持续自增但Redis没及时持久化,而这会Redis挂掉了,重启Redis会出现ID重复的情况
  • AOF会对每条写命令都进行持久化,即使Redis挂掉了也不会出现重复ID的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长

基于雪花算法(Snowflake)模式

雪花算法(Snowflake)介绍

雪花酸防是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器

Snowflake生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。
Snowflake ID组成结构:正数位(占1比特)+时间戳(占41比特)+机器ID(占5比特)+数据中心(占5比特)+自增值(占12比特),共64比特组成的一个Long类型

  • 第一个bit(1bit):Java中long类的最高位是代表正负,正数是0,附属是1,一般生成ID都是正数,所以默认为0
  • 时间戳部分(41bit):毫秒级时间,不建议存当前时间戳,而是用(当前时间戳-固定开始时间戳)的差值,可以是产生的ID从更小的值开始;41位的时间可以使用69年,(1L<<41)/(1000L606024365) = 69年,解析下:
    1
    2^41/(1000*365*24*60*60)=69
  • 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以
  • 序列号部分(12bit),自增值支持统一毫秒内同一个节点可以生成4096个ID
    根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器ID即可,而不需要单独去搭建分布式ID的应用

    Java实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    /**
    * Twitter的SnowFlake算法,使用SnowFlake算法生成一个整数,然后转化为62进制变成一个短地址URL
    *
    * https://github.com/beyondfengyu/SnowFlake
    */
    public class SnowFlakeShortUrl {

    /**
    * 起始的时间戳
    */
    private final static long START_TIMESTAMP = 1480166465631L;

    /**
    * 每一部分占用的位数
    */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5; //机器标识占用的位数
    private final static long DATA_CENTER_BIT = 5; //数据中心占用的位数

    /**
    * 每一部分的最大值
    */
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);

    /**
    * 每一部分向左的位移
    */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;

    private long dataCenterId; //数据中心
    private long machineId; //机器标识
    private long sequence = 0L; //序列号
    private long lastTimeStamp = -1L; //上一次时间戳

    private long getNextMill() {
    long mill = getNewTimeStamp();
    while (mill <= lastTimeStamp) {
    mill = getNewTimeStamp();
    }
    return mill;
    }

    private long getNewTimeStamp() {
    return System.currentTimeMillis();
    }

    /**
    * 根据指定的数据中心ID和机器标志ID生成指定的序列号
    *
    * @param dataCenterId 数据中心ID
    * @param machineId 机器标志ID
    */
    public SnowFlakeShortUrl(long dataCenterId, long machineId) {
    if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
    throw new IllegalArgumentException("DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0!");
    }
    if (machineId > MAX_MACHINE_NUM || machineId < 0) {
    throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");
    }
    this.dataCenterId = dataCenterId;
    this.machineId = machineId;
    }

    /**
    * 产生下一个ID
    *
    * @return
    */
    public synchronized long nextId() {
    long currTimeStamp = getNewTimeStamp();
    if (currTimeStamp < lastTimeStamp) {
    throw new RuntimeException("Clock moved backwards. Refusing to generate id");
    }

    if (currTimeStamp == lastTimeStamp) {
    //相同毫秒内,序列号自增
    sequence = (sequence + 1) & MAX_SEQUENCE;
    //同一毫秒的序列数已经达到最大
    if (sequence == 0L) {
    currTimeStamp = getNextMill();
    }
    } else {
    //不同毫秒内,序列号置为0
    sequence = 0L;
    }

    lastTimeStamp = currTimeStamp;

    return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //时间戳部分
    | dataCenterId << DATA_CENTER_LEFT //数据中心部分
    | machineId << MACHINE_LEFT //机器标识部分
    | sequence; //序列号部分
    }

    public static void main(String[] args) {
    SnowFlakeShortUrl snowFlake = new SnowFlakeShortUrl(2, 3);

    for (int i = 0; i < (1 << 4); i++) {
    //10进制
    System.out.println(snowFlake.nextId());
    }
    }
    }

百度(uid-generator)

uid-generator是一个由百度技术开发,Github地址为https://github.com/baidu/uid-generator
uid-generator是基于SnowFlake算法实现的,与原始snowflake算法不同在于,uid-id_generator支持自定义时间戳、工作ID和序列号等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略
uid-generator需要与数据库配合使用,需要新增一个WORKER_NODE表,当应用启动时会想数据库中插入一条数据,插入后返回的自增ID就是该机器的workId数据由host和port组成

对于uid-generator ID组成结构
workId,占用22个bit,时间占用了28个bit位,序列化占用了13个bit位,需要注意的是,和原始的snowflake不太一样,时间单位是秒,而不是毫秒,workId也不一样,而且同一应用每次重启就会消费一个workId

美团(Leaf)

Leaf是由美团开发,Github地址https://github.com/Meituan-Dianping/Leaf
Leaf同时支持号段模式和snowflake算法模式,可以切换使用

  1. 号段模式
    首先导入源码https://link.zhihu.com/?target=https%3A//github.com/Meituan-Dianping/Leaf,再新建一张表leaf_alloc
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    DROP TABLE IF EXISTS `leaf_alloc`;

    CREATE TABLE `leaf_alloc` (
    `biz_tag` varchar(128) NOT NULL DEFAULT '' COMMENT '业务key',
    `max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '当前已经分配了的最大id',
    `step` int(11) NOT NULL COMMENT '初始步长,也是动态调整的最小步长',
    `description` varchar(256) DEFAULT NULL COMMENT '业务key的描述',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据库维护的更新时间',
    PRIMARY KEY (`biz_tag`)
    ) ENGINE=InnoDB;
    然后在项目中启动号段模式,配置对应的数据库信息,并关闭snowflake模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    leaf.name=com.sankuai.leaf.opensource.test
    leaf.segment.enable=true
    leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf_test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
    leaf.jdbc.username=root
    leaf.jdbc.password=root

    leaf.snowflake.enable=false
    #leaf.snowflake.zk.address=
    #leaf.snowflake.port=
    启动leaf-server 模块的 LeafServerApplication项目就跑起来了
    号段模式获取分布式自增ID的测试url :http://localhost:8080/api/segment/get/leaf-segment-test
    监控号段模式:http://localhost:8080/cache
  2. snowflake模式
    Leaf的snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。
    1
    2
    3
    leaf.snowflake.enable=true
    leaf.snowflake.zk.address=127.0.0.1
    leaf.snowflake.port=2181
    snowflake模式获取分布式自增ID的测试url:http://localhost:8080/api/snowflake/get/test

滴滴(Tinyid)

Tinyid由滴滴开发,Github地址https://github.com/didi/tinyid
Tinyid是基于号段模式原理实现的与Leaf如出一辙,每个服务获取一个较短(1000,2000],(2000,3000],(3000,4000]

Tinyid提供http和tinyid-client两种方式接入

  1. HTTP 方式接入
  • 导入源码
    1
    git clone https://github.com/didi/tinyid
  • 创建数据表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    CREATE TABLE `tiny_id_info` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '业务类型,唯一',
    `begin_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '开始id,仅记录初始值,无其他含义。初始化时begin_id和max_id应相同',
    `max_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '当前最大id',
    `step` int(11) DEFAULT '0' COMMENT '步长',
    `delta` int(11) NOT NULL DEFAULT '1' COMMENT '每次id增量',
    `remainder` int(11) NOT NULL DEFAULT '0' COMMENT '余数',
    `create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
    `update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
    `version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uniq_biz_type` (`biz_type`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'id信息表';

    CREATE TABLE `tiny_id_token` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
    `token` varchar(255) NOT NULL DEFAULT '' COMMENT 'token',
    `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '此token可访问的业务类型标识',
    `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
    `create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
    `update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'token信息表';

    INSERT INTO `tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
    VALUES
    (1, 'test', 1, 1, 100000, 1, 0, '2018-07-21 23:52:58', '2018-07-22 23:19:27', 1);

    INSERT INTO `tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
    VALUES
    (2, 'test_odd', 1, 1, 100000, 2, 1, '2018-07-21 23:52:58', '2018-07-23 00:39:24', 3);


    INSERT INTO `tiny_id_token` (`id`, `token`, `biz_type`, `remark`, `create_time`, `update_time`)
    VALUES
    (1, '0f673adf80504e2eaa552f5d791b644c', 'test', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');

    INSERT INTO `tiny_id_token` (`id`, `token`, `biz_type`, `remark`, `create_time`, `update_time`)
    VALUES
    (2, '0f673adf80504e2eaa552f5d791b644c', 'test_odd', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');
  • 配置数据库
    1
    2
    3
    4
    5
    datasource.tinyid.names=primary
    datasource.tinyid.primary.driver-class-name=com.mysql.jdbc.Driver
    datasource.tinyid.primary.url=jdbc:mysql://ip:port/databaseName?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
    datasource.tinyid.primary.username=root
    datasource.tinyid.primary.password=123456
  • 启动tinyid-server后测试
    1
    2
    3
    4
    5
    6
    获取分布式自增ID: http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c'
    返回结果: 3

    批量获取分布式自增ID:
    http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c&batchSize=10'
    返回结果: 4,5,6,7,8,9,10,11,12,13
  1. Java客户端方式接入
  • 重复HTTP中的2和3步骤
  • 引入依赖
    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.xiaoju.uemc.tinyid</groupId>
    <artifactId>tinyid-client</artifactId>
    <version>${tinyid.version}</version>
    </dependency>
    1
    2
    tinyid.server =localhost:9999
    tinyid.token =0f673adf80504e2eaa552f5d791b644c
    test 、tinyid.token是在数据库表中预先插入的数据,test 是具体业务类型,tinyid.token表示可访问的业务类型
    1
    2
    3
    4
    5
    // 获取单个分布式自增ID
    Long id = TinyId . nextId( " test " );

    // 按需批量分布式自增ID
    List< Long > ids = TinyId . nextId( " test " , 10 );

认证(Authentication)和授权(Authorization)

  • 认证(Authentication):登录,也就是你是谁,验证你身份的凭据,例如用户名和密码,通过这个凭据,系统能够知道你是谁,也就是说系统存在你这个用户,所以Authentication被称为身份/用户验证
  • 授权(Authorization):权限,也就是你能够干什么,其发生在Authentication之后,长官你访问的权限,比如有些特定资源只能具有特定权限的人才能够访问,有些系统资源操作比如删除、添加、更新只有特定的人才能具有

Cookie

Cookie是什么,有什么作用

Cookie和Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。
Cookie:Cookies是某些网站为了辨别客户身份二存储在用户本地终端上的数据(通常是加密过的)。简单来说,Cookie存放在客户端,一般用来保存用户信息。

应用场景

首先要知道HTTP是无状态的,意思是HTTP协议对交互场景没有记忆能力,简单来说就是很多人请求html资源文件时,每次请求,每个人的请求,返回的内容都是一样的,返回的都是相同的内容,也就是HTTP协议没法记录你是你这块的信息,没法区分你和别人的信息,就像你购物的时候,应该是你登陆账号显示的是你的订单的数据,别人登录显示别人的订单数据,但是HTTP协议无法是你还是别人

  1. 在Cookie中保存已经登陆过的用户信息,下次访问网站的时候页面可以自动帮你登陆的一些基本信息给填了。除此之外,Cookie还能保存用户的首选项,主题和其他设置信息
  2. 使用Cookie保存session或者token,向后端发送请求的时候带上Cookie,这样后端就能够取到session或者token了。这样就能够记录用户当前的状态了
  3. Cookie还可以用来记录和分析用户的行为。举个简单的例子,你在网站上购物的时候,因为HTTP协议无状态,如果服务器想要获取你在某个网页的停留状态或者看哪些商品,一种常用的方式就是将这些信息存放在Cookie中,当你再打开APP或者继续刷新产品列表的时候可以根据Cookie中存储的信息给你进行产品的推荐或者后台进行相应的数据分析,你停留的时间长了,说不定你对产品的关注度就高,有想买的想法,从而对你的推荐进行优化,推荐你经常停留的产品

服务端使用Cookie

服务端设置Cookie返回客户端

  1. 常规设置方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @GetMapping("/change-username")
    public String setCookie(HttpServletResponse response) {
    // 创建一个 cookie
    Cookie cookie = new Cookie("username", "Jovan");
    //设置 cookie过期时间
    cookie.setMaxAge(7 * 24 * 60 * 60); // expires in 7 days
    //添加到 response 中
    response.addCookie(cookie);

    return "Username is changed!";
    }

    读取客户端传上来的Cookie的值

  2. 使用Spring注解@CookieValue获取指定Cookie的值
    1
    2
    3
    4
    @GetMapping("/getSpecifiedCookie")
    public String getSpecifiedCookie(@CookieValue(value="username",defaultValue="tempUsername") String username){
    return "Hey! Your Cookie save your Name is "+username;
    }
  3. 读取所有的Cookie的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @GetMapping("/getAllCookies")
    public String getAllCookies(HttpServletRequest request){
    Cookie[] cookies = request.getCookies();
    if(cookies != null){
    return Arrays.stream(cookies).map(c->c.getName()+"="+c.getValue()).collect(Collectors.joining(","))
    }else{
    return "No Cookie"
    }
    }

Cookie和Session的区别

Session的主要作用就是利用服务端记录用户的状态。典型的场景就是购物车,当你添加商品到购物车的时候,系统不知道是哪个用户操作的。服务端给特定的用户创建特定的Session之后就可以标识这个用户并且将商品添加到你的购物车中。
Cookie数据保存在客户端(浏览器),Session数据保存在服务端。相对来说Session的安全性更高。如果使用Cookie,一些敏感信息就不要写入Cookie中,最好能将Cookie信息加密后使用,到时候再去服务器端解密

使用Session进行身份验证

很多时候我们通过SessionID来识别对应的客户,SessionID一般会存放在Redis中。举个例子:用户成功登陆系统后,然后返回给客户端具有SessionID的Cookie,当用户发起后端请求的时候,会把SessionID带上,这样后端就知道你的身份状态了,下图详解过程:

步骤解析:

  1. 用户想服务器发送用户名和密码用于登陆系统
  2. 服务器验证通过后,服务器为用户创建一个Session,并将Session信息存储起来
  3. 服务器向用户返回一个SessionID,写入用户的Cookie
  4. 当用户保持登录状态时,Cookie将于每个后续请求一起发送出去
  5. 服务器可以将存储在Cookie上的SessionID与存储在内存中或者数据库中的Session信息进行比较,来验证用户的身份,返回用户客户端相应的信息的时候会附带用户当前的登录状态。

Token

Token定义

上面我们讨论了使用Session来鉴别用户身份。我们知道Session信息需要保存一份在服务端。这种方式会带来一些麻烦,比如需要我们保存Session信息服务器的可用心、不适用于移动端APP(移动端没有Cookie)等,为了解决这个问题,Token就上场了。JWT(JSON Web Token)就是通过Token实现的用户信息的数据保存,而不保存Session数据了,只要在客户端保存服务端返回给客户的Token就可以了。
JWT本质上就是一段签名的JSON格式的数据。由于带有签名,因此接收者就可以验证它的真实性。

JWT的构成:

  1. Header:描述JWT的源数据。定义了生成签名的算法及Token的类型
  2. Payload:负载,就是用来存放实际需要传递的数据
  3. Signature:签名,服务器通过Patload、Hreader和一个密钥(secret)使用Header里面指定的签名算法生成,默认算法是HMAC SHA256

在基于Token进行身份验证的应用中,服务器通过Payliad、Header和一个密钥secret创建一个令牌,也就是Token并将Token发送给客户端,客户端将Token保存在Cookie或者localStorage里面,以后客户端发出的请求都会携带这个令牌。你就可以把放在Cookie里面自动发,但是这样是没法跨域的,所以更好的做法是放在HTTP Header的Authorzation字段中:Authorization: Bearer <token>

步骤解析:

  1. 用户向服务器发送用户名和密码用于登陆系统
  2. 身份验证服务相应返回了签名的JWT,上面包含了用户是谁的内容
  3. 用户以后每次想后端发送请求都在Header中带上JWT
  4. 服务端检查JWT并从中获取用户相关信息

JWT的几个特点

  1. JWT默认是不加密的,但是也可以加密。生成原始Token以后,可以用密钥再加密一次。
  2. JWT不加密的情况下,不能将秘密数据写入JWT
  3. JWT不仅可以用于认证,也可以用于交换信息。有效使用JWT,可以降低服务器查询数据库的次数
  4. JWT 最大的缺点是,由于服务器不保存session状态,因此无法在使用过程中废止某个token,或者更改token的权限。也就是说,一旦JWT签发后,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT本身会包含认证信息,一旦泄露,任何人都可以获得该令牌的权限。为了减少盗用,JWT的有效期应该设置的比较短。对于一些比较重要的权限,使用时,应该再次对用户进行认证。
  6. 为了减少盗用,JWT不应该使用HTTP协议明码传输,要使用HTTPS协议传输

OAuth 2.0

什么是OAuth2.0

OAuth是一个行业的标准授权协议,主要是来授权第三方应用获取有限的网页权限。实际上他是一种授权机制,他的最终目的是为第三方办法一个有时效性的令牌token,是的第三方能够通过该令牌获取相应的资源。

  • resource owner:资源所有者,能够允许访问受保护资源的实体
  • resource server:资源服务器,托管受保护资源的服务器
  • client:客户端,使用资源所有者的授权代表资源所有者发起对受保护资源的请求的应用程序
  • authorization server: 授权服务器,能够向客户端颁发令牌
  • user-agent:用户代理,帮助资源所有者与客户端沟通的工具,一般为web浏览器,移动APP等
    简单来说:加入想在某个网站上用QQ的账号登录,那这个网站就相当于QQ的客户端。而我们使用浏览器操作,浏览器就是一个用户代理。当从QQ授权服务器获得token后,这个网站是需要请求qq账号信息的,从哪里请求,从QQ的资源服务器上请求

    使用场景

    OAuth2.0比较常用的场景就是三方登录,当你的网站接入第三方登录一般都是使用的OAuth2.0协议,具体的使用方法,可以使用下面这个网站

10 分钟理解什么是 OAuth 2.0 协议

发现问题

今天在给前端写接口的时候,发现了个问题,当从数据库中查数据时,如果数据字段是NULL,就会出现通过JSON.toJSONString转化后的字符串中,字段值为NULL的字段都消失了,这个对于前端来说可算是噩梦,因为他们给页面赋值的时候,会出现字段值为undefined的情况,当然,这个他们也是可以解决的,直接使用this.data.objval = res.data.objval||'';就能够解决,但是本着方便他人就是方便自己的原则,还是查了下,到底问题出在了哪里

解决问题

解决问题的方案,查了下,载使用JSON.toJSONString的时候,它不仅接收需要转化的对象,还会接收一个对象,也就是标题SerializerFeature的一个变量,可以讲待转化对象中的值为null的对象,转化为空字符串,下面是对应的解释内容及使用方法

名称 含义
QuoteFieldNames 输出key时是否使用双引号,默认为true
UseSingleQuotes 使用单引号而不是双引号,默认为false
WriteMapNullValue 是否输出值为null的字段,默认为false
WriteEnumUsingToString Enum输出name()或者original,默认为false
UseISO8601DateFormat Date使用ISO8601格式输出,默认为false
WriteNullListAsEmpty List字段如果为null,输出为[],而非null
WriteNullStringAsEmpty 字符类型字段如果为null,输出为””,而非null
WriteNullNumberAsZero 数值字段如果为null,输出为0,而非null
WriteNullBooleanAsFalse Boolean字段如果为null,输出为false,而非null
SkipTransientField 如果是true,类中的Get方法对应的Field是transient,序列化时将会被忽略。默认为true
SortField 按字段名称排序后输出。默认为false
WriteTabAsSpecial 把\t做转义输出,默认为false
PrettyFormat 结果是否格式化,默认为false
WriteClassName 序列化时写入类型信息,默认为false。反序列化是需用到
DisableCircularReferenceDetect 消除对同一对象循环引用的问题,默认为false
WriteSlashAsSpecial 对斜杠’/’进行转义
BrowserCompatible 将中文都会序列化为\uXXXX格式,字节数会多一些,但是能兼容IE 6,默认为false
WriteDateUseDateFormat 全局修改日期格式,默认为false。JSON.DEFFAULT_DATE_FORMAT = “yyyy-MM-dd”;
JSON.toJSONString(obj, SerializerFeature.WriteDateUseDateFormat);
DisableCheckSpecialChar 一个对象的字符串属性中如果有特殊字符如双引号,将会在转成json时带有反斜杠转移符。
如果不需要转义,可以使用这个属性。默认为false
NotWriteRootClassName
BeanToArray 将对象转为array输出
WriteNonStringKeyAsString
NotWriteDefaultValue
BrowserSecure
IgnoreNonFieldGetter
WriteEnumUsingName 含义

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
public class SerializerFeatureTest {

private static Word word;

private static void init() {
word = new Word();
word.setA("a");
word.setB(2);
word.setC(true);
word.setD("d");
word.setE("");
word.setF(null);
word.setDate(new Date());

List<User> list = new ArrayList<User>();
User user1 = new User();
user1.setId(1);
user1.setOld("11");
user1.setName("用户1");
user1.setAdd("北京");
User user2 = new User();
user2.setId(2);
user2.setOld("22");
user2.setName("用户2");
user2.setAdd("上海");
User user3 = new User();
user3.setId(3);
user3.setOld("33");
user3.setName("用户3");
user3.setAdd("广州");

list.add(user3);
list.add(user2);
list.add(null);
list.add(user1);

word.setList(list);

Map<String , Object> map = new HashedMap();
map.put("mapa", "mapa");
map.put("mapo", "mapo");
map.put("mapz", "mapz");
map.put("user1", user1);
map.put("user3", user3);
map.put("user4", null);
map.put("list", list);
word.setMap(map);
}

public static void main(String[] args) {
init();
// useSingleQuotes();
// writeMapNullValue();
// useISO8601DateFormat();
// writeNullListAsEmpty();
// writeNullStringAsEmpty();
// sortField();
// prettyFormat();
// writeDateUseDateFormat();
// beanToArray();
showJsonBySelf();
}

/**
* 9:自定义
* 格式化输出
* 显示值为null的字段
* 将为null的字段值显示为""
* DisableCircularReferenceDetect:消除循环引用
*/
private static void showJsonBySelf() {
System.out.println(JSON.toJSONString(word));
System.out.println(JSON.toJSONString(word, SerializerFeature.PrettyFormat,
SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty,
SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.WriteNullListAsEmpty));
}

/**
* 8:
* 将对象转为array输出
*/
private static void beanToArray() {
word.setMap(null);
word.setList(null);
System.out.println(JSON.toJSONString(word));
System.out.println(JSON.toJSONString(word, SerializerFeature.BeanToArray));
}

/**
* 7:
* WriteDateUseDateFormat:全局修改日期格式,默认为false。
*/
private static void writeDateUseDateFormat() {
word.setMap(null);
word.setList(null);
System.out.println(JSON.toJSONString(word));
JSON.DEFFAULT_DATE_FORMAT = "yyyy-MM-dd";
System.out.println(JSON.toJSONString(word, SerializerFeature.WriteDateUseDateFormat));
}

/**
* 6:
* PrettyFormat
*/
private static void prettyFormat() {
word.setMap(null);
word.setList(null);
System.out.println(JSON.toJSONString(word));
System.out.println(JSON.toJSONString(word, SerializerFeature.PrettyFormat));
}

/**
* SortField:按字段名称排序后输出。默认为false
* 这里使用的是fastjson:为了更好使用sort field martch优化算法提升parser的性能,fastjson序列化的时候,
* 缺省把SerializerFeature.SortField特性打开了。
* 反序列化的时候也缺省把SortFeidFastMatch的选项打开了。
* 这样,如果你用fastjson序列化的文本,输出的结果是按照fieldName排序输出的,parser时也能利用这个顺序进行优化读取。
* 这种情况下,parser能够获得非常好的性能。
*/
private static void sortField() {
System.out.println(JSON.toJSONString(word));
System.out.println(JSON.toJSONString(word, SerializerFeature.SortField));
}

/**
* 5:
* WriteNullStringAsEmpty:字符类型字段如果为null,输出为"",而非null
* 需要配合WriteMapNullValue使用,现将null输出
*/
private static void writeNullStringAsEmpty() {
word.setE(null);
System.out.println(JSONObject.toJSONString(word));
System.out.println("设置WriteMapNullValue后:");
System.out.println(JSONObject.toJSONString(word, SerializerFeature.WriteMapNullValue));
System.out.println("设置WriteMapNullValue、WriteNullStringAsEmpty后:");
System.out.println(JSONObject.toJSONString(word, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty));
}


/**
* 4:
* WriteNullListAsEmpty:List字段如果为null,输出为[],而非null
* 需要配合WriteMapNullValue使用,现将null输出
*/
private static void writeNullListAsEmpty() {
word.setList(null);
System.out.println(JSONObject.toJSONString(word));
System.out.println("设置WriteNullListAsEmpty后:");
System.out.println(JSONObject.toJSONString(word, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullListAsEmpty));
}

/**
* 3:
* UseISO8601DateFormat:Date使用ISO8601格式输出,默认为false
*/
private static void useISO8601DateFormat() {
System.out.println(JSONObject.toJSONString(word));
System.out.println("设置UseISO8601DateFormat后:");
System.out.println(JSONObject.toJSONString(word, SerializerFeature.UseISO8601DateFormat));
}

/**
* 2:
* WriteMapNullValue:是否输出值为null的字段,默认为false
*/
private static void writeMapNullValue() {
System.out.println(JSONObject.toJSONString(word));
System.out.println("设置WriteMapNullValue后:");
System.out.println(JSONObject.toJSONString(word, SerializerFeature.WriteMapNullValue));
}

/**
* 1:
* UseSingleQuotes:使用单引号而不是双引号,默认为false
*/
private static void useSingleQuotes() {
System.out.println(JSONObject.toJSONString(word));
System.out.println("设置useSingleQuotes后:");
System.out.println(JSONObject.toJSONString(word, SerializerFeature.UseSingleQuotes));
}
}

既然上面说到JSON.toJSONString会出现这种问题,那么@Responsebody估计也会出现这种问题吧,没做深究,如果有的话,后期再来补这篇文章吧

本文引自:fastjson SerializerFeature详解

下拉框(picker)开发

原因

常规下下拉框开发的,后台返回的数据都是下面这种类型的,但是uni-app的数据是类似于这种的array: ['中国', '美国', '巴西', '日本'],所以使用起来不是很方便,谁TM要保存汉字到数据库,所以下面记录下项目中使用的公共方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"id": 1,
"name": "男"
},
{
"id": 2,
"name": "女"
},
{
"id": 3,
"name": "未知"
}
]

下拉框技巧

下面的这段代码是一个公共下拉框的封装方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<template>
<view class="content">
<view class="uni-list">
<view class="uni-list-cell">
<picker @change="pickSelect($event,'sex',data.sexData,'sex')" :value="indexs.sex" :range="data.sexData" range-key="name">
<view class="uni-input">{{data.sexData[indexs.sex]?data.sexData[indexs.sex].name:'请选择'}}</view>
</picker>
<button type="primary" @tap="consoleData()">获取数据</button>
</view>
</view>
</view>
</template>

<script>
export default {
data() {
return {
indexs: {
sex: ''
},
data:{
sexData: [{
"id": 1,
"name": "男"
},
{
"id": 2,
"name": "女"
},
{
"id": 3,
"name": "未知"
}
],
sex:''
}
}
},
onLoad() {

},
methods: {
pickSelect(e, indexKey, optionArr, baseInfoKey) {
debugger;
this.indexs[indexKey] = e.target.value
this.data[baseInfoKey] = optionArr[e.target.value].id
},
consoleData(){
console.log(this.data);
}
}
}
</script>

<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.uni-list-cell-left{
width:150rpx;
text-align: left;
}
</style>

代码解析:

  1. indexs是用做一个页面中如果有多个picker的时候,可以在indexs中添加一个变量,当做下拉框索引变量使用,也就是选择的是第几个
  2. pickSelect方法中的四个变量说明:第一个变量是事件,这个就不说了;第二个是1中说的索引的变量名称;第三个是下拉框的数组数据;第四个是data变量中的变量名称
  3. pickSelect方法说明:当下拉框选择发生变化的时候,触发该方法,首先会通过事件获取它选择的是第几个,然后通过设置indexs中的索引值来实现picker中汉字的切换,最后设置data变量中的值,这个地方有个使用json对象的技巧,直接对象名称[变量]就能获取到json中的变量对应变量名的对象
  4. :value属性说明:是1中所说indexs中变量
  5. :range属性说明:是下拉框的数组数据
  6. :range-key属性说明:是下拉框数组数据中对应需要显示在下拉框中的汉字对应的变量名
  7. 添加一个button按钮,用于打印表单数据

后端返回数据处理

问题及解决方案

后端目前给前端扔数据的时候会出现如果字段在数据库中存储的空的时候,直接用JSON转完后,JSON串中就没有这个字段了,因此有两种解决方案:

  1. 如果是要查出来的数据转化了一遍字符串之后又扔到前台的(像我们这种内外网分离,外网查数据库得去内网执行以下的这种),可以JSON.toJSONString(custList,SerializerFeature.WRITE_MAP_NULL_FEATURES);,把null的字段转成””,数字转0等等,具体没试过,可以自己尝试下
  2. 第二种就是前端进行处理,处理的方法也很简单var objVal = this.data.objVal||’’;也就是当接收到为undefined或者null的时候,直接转成’’了

本文引自A Complete Guide to Flexbox

背景

The Flexbox Layout (Flexible Box) module (a W3C Candidate Recommendation as of October 2017) aims at providing a more efficient way to lay out, align and distribute space among items in a container, even when their size is unknown and/or dynamic (thus the word “flex”).
Flex布局是W3C推荐的一种更有效的布局方式,即使他们的大小都是未知的或者动态的,都可以进行合理的布局

The main idea behind the flex layout is to give the container the ability to alter its items’ width/height (and order) to best fill the available space (mostly to accommodate to all kind of display devices and screen sizes). A flex container expands items to fill available free space or shrinks them to prevent overflow.
flex布局的主要思想是使容器能够更改其项目的宽度/高度(和顺序),以最好地填充可用空间(主要是适应各种显示设备和屏幕尺寸),Flex容器会扩展项目以填充可用的可用空间,或收缩它们以防止溢出。(这里的项目个人理解是容器中的对象,以DOM为例,那就是子标签了)

Note: Flexbox layout is most appropriate to the components of an application, and small-scale layouts, while the Grid layout is intended for larger scale layouts.
Flexbox布局最适合应用程序的组件和小规模布局,而Grid布局则用于较大规模的布局

基础和术语

这块根据英语翻译过来的,我真是搞不懂,看来英语水平有限啊

  • flex container:parent element,也就是父级标签
  • flex items:child element,也就是子标签
  • main axis(主轴):flex items会沿着flex container的主轴进行布局,但是不一定是水平的,它依赖于flex-direction属性
  • main-start|main-end:flex items在flex container 中的起始到截止的位置
  • main-size:flex item的高度和宽度,是flex item的主大小
  • cross axis(翻译为横轴,但是不理解为啥叫横轴):垂直于主轴的叫横轴,他的方向依据与主轴的方向
  • cross-start|cross-end:Flex线填充有物品,并从Flex容器的交叉起点侧开始向交叉端侧放置。
  • cross-size:flex items的宽度或高度(以横截面尺寸中的较大者为准)为item的横截面尺寸。交叉尺寸属性是交叉尺寸中的“宽度”或“高度”中的任意一个。

属性讲解

flex container的属性

  1. display属性
    这个是定义一个flex container;行模式还是块模式依据与设定的值。它能够为直接的子标签设置flex 布局
    1
    2
    3
    4
    5
    <style>
    .container{
    display: flex;/*或者display: inline-flex;*/
    }
    </style>

  1. flex-direction

    1
    2
    3
    4
    .container {
    display: flex;
    flex-direction: row | row-reverse | column | column-reverse;
    }
    属性值 作用
    row(默认) 从左到右横向布局
    row-reverse 从右向左横向布局
    column 从上到下纵向布局
    column-reverse 从下到上纵向布局
  2. flex-wrap
    默认情况下,flex items会自动尝试在一行中布局,当然你可以通过是否允许换行来控制他

    1
    2
    3
    4
    .container{
    display: flex;
    flex-wrap: wrap|nowrap|wrap-reverse;
    }
    属性值 作用
    nowrap 不换行
    wrap 换行
    wrap-reverse 如果总共占两行,先排满二行,然后再排第一行
  1. flex-flow
    flex-flowflex-directionflex-wrap的缩写

    1
    2
    3
    4
    .container{
    display: flex;
    flex-flow: row wrap-reverse;
    }
  2. justify-content
    定义的是子标签在父标签中主轴上的对齐方式

    1
    2
    3
    4
    .container{
    display: flex;
    justify-content: start;
    }
    属性值 作用
    flex-start 主轴上向左对齐
    flex-end 主轴上右对齐
    center 主轴上居中
    space-between 两端对齐,项目之间的间隔相等
    space-around 每个子对象都间隔相等,项目之间的间隔比项目与父项目的间隔大一倍
  3. align-items属性
    定义子标签在交叉轴上的对齐方式

    1
    2
    3
    4
    5
    .container{
    display: flex;
    height: 500px;
    align-items: flex-start | flex-end | center | baseline | stretch;
    }
属性值 作用
flex-start 交叉轴起点对齐
flex-end 交叉轴的终点对齐
center 交叉轴的中点对齐
baseline 项目的第一行文字的基线对齐
stretch 如果flex item未设置高度或设置为auto,将沾满整个容器的高度
  1. align-content属性
    align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用
    1
    2
    3
    4
    5
    .box {
    display: flex;
    height: 500px;
    align-content: flex-start | flex-end | center | space-between | space-around | stretch;
    }
    属性值 作用
    flex-start 和交叉轴的起点对齐
    flex-end 和交叉轴的终点对齐
    center 和交叉轴的中点对齐
    space-between 和交叉轴两端对齐,轴线之间的间隔平均分布
    space-around 每根轴线两侧的间隔都相等。所以轴线之间的间隔比轴线与边框的间隔大一倍
    stretch 轴线沾满整个交叉轴

    flex items的属性

    以下的6个属性是用在item上面的
  • order
  • flex-grow
  • flex-shrink
  • flex-basis
  • flex
  • align-self
  1. order
    order 是定义item的排序顺序的,数值越小,排列越靠前,默认是0

    1
    2
    3
    .item {
    order:<integer>;
    }
  2. flex-grow
    flex-grow定义item的放大比例,默认是0,即如果存在剩余空间也不放大

    1
    2
    3
    .item{
    flex-grow:<number>
    }

    如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。

  3. flex-shrink
    flex-shrink定义了item的缩小比例,默认为1,即如果空间不足时,该项目将被缩小

    1
    2
    3
    .item {
    flex-shrink: <number>; /* default 1 */
    }

    如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小,因为负值对该属性无效。

  4. flex-basis(这个属性没太弄懂)
    flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间。他默认是auto,即项目的本来大小。

    1
    2
    3
    .item{
    flex-basis:
    }

    它可以设为跟width或height属性一样的值(比如350px),则项目将占据固定空间。

  5. flex
    flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。

    1
    2
    3
    .item {
    flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
    }

    该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。
    建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。

  6. align-self
    align-self属性允许单个item与其他item不一样的对其方式,可覆盖align-item属性。默认是auto,表示集成父元素的align-item属性,如果没有父元素,则等同于stretch

    1
    2
    3
    .item{
    align-self:auto|flex-start|flex-end|center|baseline|stretch
    }

实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<template>
<view style="width: 100%;background-color: #000000;padding: 20px;">
<view class="face box">
<view class="column">
<view class="point item"></view>
<view class="point item"></view>
</view>
<view class="column">
<view class="point item"></view>
<view class="point item"></view>
</view>
</view>
</view>
</template>

<script>
export default {
data() {
return {};
},
methods: {}
};
</script>

<style>
.face {
height: 120px;
width: 120px;
background-color: #ffffff;
border: 2px solid #c8c7cc;
border-radius: 20px;
padding: 5px;
}

.point {
height: 30px;
width: 30px;
background-color: #000000;
border: #8f8f94 2px solid;
border-radius: 17px;
margin: 3px;
}

.box {
display: flex;
flex-wrap: wrap;
align-content: space-between;
}

.column{
display: flex;
flex-basis: 100%;
justify-content: space-between;
}

.item {

}
</style>