1、Redis
1.1、Redis是什么?
Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能非关系型(NoSQL) 的(key-value)键值对数据库。可以用作数据库、缓存、消息中间件等。
1.2、Redis 的存储结构有哪些?
String,字符串,是 redis 的最基本的类型,一个 key 对应一个 value。是二进制安全的,最大能存储 512MB。
Hash,散列,是一个键值(key=>value)对集合。string 类型的 field 和value 的映射表,特别适合用于存储对象。每个 hash 可以存储 232 -1 键值对(40 多亿)。
List,列表,是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列边或者尾部(右边)。最多可存储 232 - 1 元素(4294967295, 每个列表可存储 40 亿)
Set,集合,是 string 类型的无序集合,最大的成员数为232 -1(4294967295, 每个集合可存储 40 多亿个成员)。
Sortedset,有序集合,和 set 一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。
1.3、Redis 的优点?
1 因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value 数据库。Redis 支持事务 、持久化;
2、单线程操作,避免了频繁的上下文切换;
3、采用了非阻塞 I/O 多路复用机制。I/O 多路复用就是只有单个线程,通过跟踪每个 I/O 流的状态,来管理多个 I/O 流。
1.4、为什么要用Redis?
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
1.5、redis的持久化
Redis 提供了两种持久化的方式,分别是 RDB(Redis DataBase)和 AOF(Append Only File)。
RDB,简而言之,就是在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上。
AOF,则是换了一个角度来实现持久化,那就是将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
RDB 和 AOF 两种方式也可以同时使用,在这种情况下,如果 redis 重启的话,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。
1.6、Redis 的缺点
1.6.1、缓存和数据库双写一致性问题
一致性的问题很常见,因为加入了缓存之后,请求是先从 redis 中查询,如果 redis 中存在数据就不会走数据库了,如果不能保证缓存跟数据库的一致性就会导致请求获取到的数据不是最新的数据。
解决方案:
1、编写删除缓存的接口,在更新数据库的同时,调用删除缓存 的接口删除缓存中的数据。这么做会有耦合高以及调用接口失败的情况。
2、消息队列:ActiveMQ,消息通知。
1.6.2、缓存的并发竞争问题
并发竞争,指的是同时有多个子系统去 set 同一个 key 值。
解决方案:
1、最简单的方式就是准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可。
1.6.3缓存雪崩问题
缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。
解决方案:
1、给缓存的失效时间,加上一个随机值,避免集体失效;
2、使用互斥锁,但是该方案吞吐量明显下降了;
3、搭建 redis 集群。
1.6.4缓存击穿问题
缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上, 从而数据库连接异常。
解决方案:
1、利用互斥锁,缓存失效的时候,先去获得锁,得到锁了, 再去请求数据库。没得到锁,则休眠一段时间重试;
2、采用异步更新策略,无论 key 是否取到值,都直接返回, value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程 去读数据库,更新缓存。
1.7、Redis 集群
1.7.1、主从复制
主从复制原理:
从服务器连接主服务器,发送SYNC命令。主服务器接收到SYNC命名后,开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令。主服务器 BGSAVE 执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令。
从服务器收到快照文件后丢弃所有旧数据,载入收到的快照。主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令。从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令(从 服务器初始化完成)。主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服 务器接收并执行收到的写命令(从服务器初始化完成后的操作)。
优点:
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。为了分载 Master 的 读操作压力,Slave 服务器可以为客户端提供只读操作的服务,写服务仍然必须由 Master 来完成Slave 同样可以接受其它 Slaves 的连接和同步请求,这样可以有效的分载 Master 的同步压力Master Server 是以非阻塞的方式为 Slaves 提供服务。所以在Master-Slave 同步期间,客户端仍然可以提交查询或修改请求。Slave Server 同样是以非阻塞的方式完 成数据同步。在同步期间,如果有客户端提交查询请求,Redis 则返回同步之前的数据。
缺点:
Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败, 需要等待机器重启或者手动切换前端的 IP才能恢复。主机宕机,宕机前有部分数据未能及 时同步到从机,切换 IP 后还会引入数据不一致的问题,降低了系统的可用性。Redis 较难 支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
1.7.2、哨兵模式
当主服务器中断服务后,可以将一个从服务器升级为主服务器,以便继续提供服务,但是 这个过程需要人工手动来操作。为此,Redis2.8 中提供了哨兵工具来实现自动化的系统监 控和故障恢复功能。哨兵的作用就是监控 Redis 系统的运行状况,它的功能包括以下两个。
1、监控主服务器和从服务器是否正常运行。
2、主服务器出现故障时自动将从服务器转换为主服务器。
哨兵的工作方式:
每个 Sentinel (哨兵)进程以每秒钟一次的频率向整个集群中的 Master 主服务器, Slave 从务器以及其他 Sentinel(哨兵)进程发送一个 PING 命令。如果一个实例 (instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被Sentinel(哨兵)进程标记为主观下线(SDOWN)。如果一个 Master 主服务器被标记为主观下线(SDOWN),则正在监视这个 Master 主 服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入 了主观下线状态。当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值) 在指定的时间范围内确认 Master 主服务器进入了主观下线状态(SDOWN),则 Master 主服务器会被标记为客观下线(ODOWN)。
在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一 169 196 次的频率向集群中的所有 Master 主服务器,Slave 从服务器发送 INFO命令。当 Master 主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线 的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。若没有足够数量的 Sentinel(哨兵)进程同意 Master 主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel (哨兵)进程发送 PING 命令返回有效回复,Master 主服务器的主观下线状态就会被移除。
优点:
哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。主从可以自动切换,系 统更健壮,可用性更高。
缺点:
Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
1.7.3 Redis-Cluster 集群
redis 的哨兵模式基本已经可以实现高可用,读写分离,但是在这种模式下每台 redis 服务器都存储相同的数据,很浪费内存,所以在redis3.0 上加入了 cluster 模式,实现的redis 的分布式存储,也就是说每台 redis 节点上存储不同的内容。Redis-Cluster 采用无中心结构,它的特点如下:
所有的 redis 节点彼此互联(PING-PONG 机制),内部使用二进制协议优化传输速度和带 宽。节点的fail 是通过集群中超过半数的节点检测失效时才生效。客户端与 redis 节点直 连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
工作方式:
在 redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的取值范围是:0-16383。还有一个就是 cluster,可以理解为是一个集群管理的插件。当我们的存取的 key 到达的时候,redis 会根据 crc16 的算法得出一个结果,然后把结果对 16384 求余数, 这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的 插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。为了保证高可用, redis-cluster 集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机 的时候,就会启用从节点。当其它主节点 ping 一个主节点 A 时,如果半数以上的主节点 与A 通信超时,那么认为主节点 A 宕机了。如果主节点 A 和它的从节点 A1 都宕机了, 那么该集群就无法再提供服务了。
1.8 Redis的分布式锁
Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种 方式比原先的单节点的方法更安全。它可以保证以下特性:
1>安全特性:互斥访问,即永远只有一个 client 能拿到锁;
2>避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区;
3>容错性:只要大部分 Redis 节点存活就可以正常提供服务。
Redis实现分布式锁:
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对 Redis的连接并不存在竞争关系Redis 中可以使用SETNX 命令实现分布式锁。
当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作 SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。返回值:设置成功,返回 1 。设置失败,返回 0 。

使用 SETNX 完成同步锁的流程及事项如下:
使用 SETNX 命令获取锁,若返回 0(key 已存在,锁已存在)则获取失败,反之获取成功;
为了防止获取锁后程序出现异常,导致其他线程/进程调用 SETNX 命令总是返回 0而进 入死锁状态,需要为该 key 设置一个“合理”的过期时间 释放锁,使用 DEL 命令将锁数据删除。
2、RocketMQ
2.1、消息中间件的区别?

2.2、为什么要使用MQ?
因为项目比较大,做了分布式系统,所有远程服务调用请求都是同步执行经常出问题,所以 引入了mq。

2.3、RocketMQ由哪些角色组成,每个角色作用和特点是什么?
生产者(Producer):负责产生消息,生产者向消息服务器发送由业务应用程序系统 生成的消息。
消费者(Consumer):负责消费消息,消费者从消息服务器拉取信息并将其输入用户 应用程序。
消息服务器(Broker):是消息存储中心,主要作用是接收来自 Producer 的消息并 存储, Consumer 从这里取得消息。
名称服务器(NameServer):用来保存 Broker 相关 Topic 等元信息并给 Producer ,提供 Consumer 查找 Broker 信息。
2.4、RocketMQ消费模式有几种?
消费模型由Consumer 决定,消费维度为Topic。
集群消费:
1.一条消息只会被同 Group中的一个Consumer 消费;
2.多个 Group 同时消费一个 Topic 时,每个 Group 都会有一个 Consumer 消费到数
据。
广播消费:
消息将对一 个 Consumer Group 下的各个 Consumer 实例都消费一遍。即即使这些 Consumer 属于同一个 Consumer Group ,消息也会被 Consumer Group 中的每个 Consumer 都消费一次。
2.5、RocketMQ如何做负载均衡?
通过 Topic在多 Broker 中分布式存储实现。
(1)producer端
发送端指定message queue 发送消息到相应的broker,来达到写入时的负载均衡。
1、提升写入吞吐量,当多个producer 同时向一个 broker 写入数据的时候,性能会 下降;
2、 消息分布在多 broker中,为负载消费做准备。
默认策略是随机选择:
1、producer维护一个 index;
2、每次取节点会自增;
3、 index 向所有broker个数取余;
4、自带容错策略。
其他实现:
1、SelectMessageQueueByHash
2、hash的是传入的args
3、SelectMessageQueueByRandom
4、SelectMessageQueueByMachineRoom 没有实现
也可以自定义实现 MessageQueueSelector 接口中的select 方法
MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
(2) consumer端
采用的是平均分配算法来进行负载均衡。
其他负载均衡算法
平均分配策略(默认)(AllocateMessageQueueAveragely)
环形分配策略(AllocateMessageQueueAveragelyByCircle)
手动配置分配策略(AllocateMessageQueueByConfig)
机房分配策略(AllocateMessageQueueByMachineRoom)
一致性哈希分配策略(AllocateMessageQueueConsistentHash)
靠近机房策略(AllocateMachineRoomNearby)
追问:当消费负载均衡consumer 和queue不对等的时候会发生什么?
Consumer和 queue 会优先平均分配,如果Consumer 少于 queue 的个数,则会存在部分Consumer 消费多个queue 的情况,如果 Consumer 等于queue 的个数,那就是一个 Consumer消费一个queue,如果Consumer个数大于queue的个数,那么会有部分Consumer空余出来, 白白的浪费了。
2.6、消息重复消费如何解决?
影响消息正常发送和消费的重要原因是网络的不确定性。
出现原因:
正常情况下在consumer 真正消费完消息后应该发送 ack,通知broker 该消息已正常 消费,从 queue 中剔除;
当ack 因为网络原因无法发送到 broker,broker会认为词条消息没有被消费,此后会 开启消息重投机制把消息再次投递到 consumer。
消费模式:在CLUSTERING 模式下,消息在broker 中会保证相同 group的 consumer 消 费一次,但是针对不同 group的 consumer会推送多次。
解决方案:
数据库表:处理消息前,使用消息主键在表中带有约束的字段中 insert;
Map:单机时可以使用map 做限制,消费时查询当前消息id是不是已经存在;
Redis:使用分布式锁。
2.7、如何让RocketMQ保证消息的顺序消费?
你们线上业务用消息中间件的时候,是否需要保证消息的顺序性?
如果不需要保证消息顺序,为什么不需要?假如我有一个场景要保证消息的顺序,你们应该如何保证?
首先多个queue 只能保证单个 queue 里的顺序,queue 是典型的FIFO,天然顺序。多个 queue 同时消费是无法绝对保证消息的有序性的。
所以总结如下:同一topic,同一个 QUEUE,发消息的时候一个线程去发送消息,消费的时候 一个线程去消费一个 queue 里的消息。
追问:怎么保证消息发到同一个queue?
Rocket MQ给我们提供了 MessageQueueSelector 接口,可以自己重写里面的接口, 实现自己的算法,举个最简单的例子:判断 i % 2 == 0,那就都放到 queue1 里,否则放到queue2里。
2.8、RocketMQ如何保证消息不丢失?
首先在如下三个部分都可能会出现丢失消息的情况:
Producer 端
Broker 端
Consumer端
2.8.1、Producer端如何保证消息不丢失
采取 send()同步发消息,发送结果是同步感知的;
发送失败后可以重试,设置重试次数。默认3 次;
producer.setRetryTimesWhenSendFailed(10);
集群部署,比如发送失败了的原因可能是当前 Broker 宕机了,重试的时候会发送 到其他Broker上。
2.8.2、Broker端如何保证消息不丢失
修改刷盘策略为同步刷盘。默认情况下是异步刷盘的;
flushDiskType = SYNC_FLUSH;
集群部署,主从模式,高可用。
2.8.3、Consumer 端如何保证消息不丢失
完全消费正常后在进行手动 ack 确认。
2.9、RocketMQ的消息堆积如何处理?
1. 如果可以添加消费者解决,就添加消费者的数据量;
2、如果出现了 queue,但是消费者多的情况。可以使用准备一个临时的 topic,同时创建 一些 queue,在临时创建一个消费者来把这些消息转移到 topic中,让消费者消费。
2.10、RocketMQ如何实现分布式事务?
1、生产者向MQ 服务器发送half 消息。
2、half消息发送成功后,MQ 服务器返回确认消息给生产者。
3、生产者开始执行本地事务。
4、根据本地事务执行的结果(UNKNOW、commit、rollback)向 MQ Server 发送提交 或回滚消息。
5、如果错过了(可能因为网络异常、生产者突然宕机等导致的异常情况)提交/回滚消息, 则MQ 服务器将向同一组中的每个生产者发送回查消息以获取事务状态。
6、回查生产者本地事物状态。
7、生产者根据本地事务状态发送提交/回滚消息。
8、MQ 服务器将丢弃回滚的消息,但已提交(进行过二次确认的 half消息)的消息将投递给消费者进行消费。
Half Message:预处理消息,当broker收到此类消息后,会存储到 RMQ_SYS_TRANS_HALF_TOPIC的消息消费队列中;
检查事务状态:Broker 会开启一个定时任务,消费RMQ_SYS_TRANS_HALF_TOPIC 队列中的消息,每次执行任务会向消息发送者确认事务执行状态(提交、回滚、未知),如果是未知,Broker 会定时去回调在重新检查。
超时:如果超过回查次数,默认回滚消息。也就是他并未真正进入 Topic的queue,而是用了临时 queue 来放所谓的half message,等提交事务后才会真正的将 half message 转移到topic 下的queue。
2.11、任何一台Broker突然宕机了怎么办?
Broker主从架构以及多副本策略。Master 收到消息后会同步给 Slave,这样一条消息 就不止一份了,Master 宕机了还有 slave中的消息可用,保证了 MQ 的可靠性和高可用性。而且 RocketMQ4.5.0 开始就支持了 Dlegder模式,基于 raft 的,做到了真正意义的HA。
3、MongoDb
3.1、MongoDB是什么?
mongodb是属于文档型的非关系型数据库,是开源、高性能、高可用、可扩展的
数据逻辑层次关系:文档=>集合=>数据库
在关系型数据库中每一行的数据对应 mongodb里是一个文档。mongodb的文档 是以 BSON(binary json)格式存储的,其格式就是 json格式。

1>集合
集合是一组文档(即上面的 users 集合)。集合相当于关系数据库中的表,但集合 中的文档长度可不同(集合中的文档中的键值对个数可不同)、集合中文档的key可不同。向集合中插入第一个文档时,集合会被自动创建。
2>文档
文档是一组键值对,用{ }表示,字段之间用逗号分隔。相当于关系数据库中的一行 (一条记录)。示例:一个文档

说明:
文档中的键值对是有序的;
一个文档中不能有重复的key(对应关系数据库中的一条记录);
以"_"开头的key 是保留的,有特殊含义。
3>字段
即一个键值对,key必须是 String类型,value 可以是任意类型。
3.2、MongoDB和关系型数据库mysql区别?

3.3、MongoDB有3个数据库。
一个 MongoDB中可以建立多个数据库,这些数据库是相互独立的,有自己的集合和权 限。不同的数据库使用不同的文件存储(不存储在一个文件中)。
MongoDB默认有3个数据库:
admin:从权限的角度来看,这是"root"数据库。将一个用户添加到这个数据库,这个 用户会自动继承所有数据库的权限。一些特定的服务器端命令也只能在这个数据库中运行, 比如列出所有的数据库或者关闭服务器;
local: 这个数据库永远不会被复制,里面的数据都是本地的(不会复制到其他MongoDB 服务器上),可以用来存储限于本地单台服务器的任意集合;
config: 当 Mongo 用于分片设置时,config 数据库在内部使用,用于保存分片的相关信息。
3.4、Mongo中的数据类型
1. null
2. false 和true
3. 数值
4. UTF-8 字符串
5. 日期 new Date()
6. 正则表达式
7. 数组
8. 嵌套文档
9. 对象 ID ObjectId()
10. 二进制数据
11. 代码
3.5、MongoDB适用业务场景
网站数据:MongoDB 非常适合实时的插入,更新与查询,并具备网站实时数据存储所 需的复制及高度伸缩性;
缓存:由于性能很高,MongoDB 也适合作为信息基础设施的缓存层。在系统重启之后, 由 MongoDB 搭建的持久化缓存层可以避免下层的数据源过载;
大尺寸,低价值的数据:使用传统的关系型数据库存储一些数据时可能会比较昂贵,在此之前,很多时候程序员往往会选择传统的文件进行存储;
高伸缩性的场景:MongoDB 非常适合由数十或数百台服务器组成的数据库。MongoDB 的路线图中已经包含对 MapReduce 引擎的内置支持 用于对象及 JSON 数据的存储:MongoDB的 BSON 数据格式非常适合文档化格式的 存储及查询。
4、Nginx
4.1、Nginx是什么?
Nginx 是一个高性能的 HTTP 和反向代理服务器,及电子邮件代理服务器,同时也是一个非常高效的反向代理、负载平衡。
4.2、Nginx的作用?
1.反向代理,将多台服务器代理成一台服务器;
2.负载均衡,将多个请求均匀的分配到多台服务器上,减轻每台服务器的压力,提高 服务的吞吐量;
3.动静分离,nginx 可以用作静态文件的缓存服务器,提高访问速度。
4.3、Nginx的优势?
(1) 可以高并发连接(5 万并发,实际也能支持 2~4 万并发)。
(2) 内存消耗少。
(3) 成本低廉。
(4) 配置文件非常简单。
(5) 支持 Rewrite 重写。
(6) 内置的健康检查功能。
(7) 节省带宽。
(8) 稳定性高。
(9) 支持热部署。
4.4、什么是反向代理?
反向代理是指以代理服务器来接受 internet 上的连接请求,然后将请求,发给内部网络上的服务器,并将从服务器上得到的结果返回给 internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
反向代理总结就一句话:代理端代理的是服务端。
4.5、什么是正向代理?
一个位于客户端和原始服务器之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。
正向代理总结就一句话:代理端代理的是客户端。
4.6、什么是负载均衡?
负载均衡即是代理服务器将接收的请求均衡的分发到各服务器中,负载均衡主要解决网络拥塞问题,提高服务器响应速度,服务就近提供,达到更好的访问 质量,减少后台服务器大并发压力。
4.7、Nginx 是如何处理一个请求的?
首先,nginx 在启动时,会解析配置文件,得到需要监听的端口与 ip地址,然后在 nginx 的 master 进程里面先初始化好这个监控的 socket,再进行 listen,然后再 fork 出 多个子进程出来, 子进程会竞争 accept 新的连接。
此时,客户端就可以向 nginx 发起连接了。当客户端与 nginx 进行三次握手,与 nginx 建立好一个连接后,此时,某一个子进程会 accept 成功,然后创建 nginx 对连接的封 装,即 ngx_connection_t 结构体,接着,根据事件调用相应的事件处理模块,如 http 模 块与客户端进行数据的交换。
最后,nginx 或客户端来主动关掉连接,到此,一个连接就寿终正寝了。
4.8、为什么Nginx性能这么高?
得益于它的事件处理机制:异步非阻塞事件处理机制:运用了 epoll 模型,提供了一 个队列,排队解决。
5、JWT
JSON Web token 简称 JWT, 是用于对应用程序上的用户进行身份验证的标记。也 就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他 session 数 据。此特性便于可伸缩性, 同时保证应用程序的安全。
在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且 必须在本地保存 (通常在本地存储中)。
每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求 一起发送 JWT, 通常在授权标头中使用 Bearer schema。后端服务器接收到带有 JWT 的 请求时, 首先要做的是验证 token。
5.1、组成
一个 JWT 实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
头部(Header)
头部用于描述关于该JWT 的最基本的信息,例如其类型以及签名所用的算法等。这也 可以被表示成一个 JSON对象。{"typ":"JWT","alg":"HS256"}
在 头 部 指 明 了 签 名 算 法 是 HS256 算 法 。我 们 进 行 BASE64 编 码 ( http://base64.xpcha.com/ ) , 编 码 后 的 字 符 串 如 下 :eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷(playload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包 含三个部分:
1. 标准中注册的声明(建议但不强制使用)
iss: jwt 签发者
sub: jwt 所面向的用户
aud: 接收 jwt 的一方 exp: jwt 的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt 都是不可用的
iat: jwt 的签发时间
jti: jwt 的唯一身份标识,主要用来作为一次性token
2 .公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息. 但不建议添加敏感信息,因为该部分在客户端可解密。
3. 私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64是对称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的 claim。比如前面那个结构举例中的 admin 和 name 都属 于自定的 claim。这些 claim 跟JWT 标准规定的 claim 区别在于:JWT 规定的 claim, JWT 的接收方在拿到 JWT 之后,都知道怎么对这些标准的 claim 进行验证(还不 知道是否能够验证);而private claims 不会验证,除非明确告诉接收方要对这些 claim 进行验证以及规则才行。定义一个payload:
{"sub":"1234567890","name":"John Doe","admin":true}
然后将其进行 base64加密,得到 Jwt 的第二部分。eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRta W4iOnRydWV9
签证(signature)
jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64 后的)
payload (base64 后的)
secret
这个部分需要 base64加密后的 header 和base64 加密后的 payload使用.连接组成 的字符串,然后通过header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。
注意:
secret是保存在服务器端的, jwt 的签发生成也是在服务器端的,secret 就是用来进行 jwt 的签发和 jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发 jwt 了。
5.2、使用场景
1. 一次性验证
比如用户注册后需要发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接 需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活), 不能被篡改以激活其它可能的账户……这种场景就和 jwt 的特性非常贴近,jwt 的 payload中固定的参数:iss 签发者和 exp 过期时间正是为其做准备的。
2. restful api 的无状态认证
使用 jwt 来做 restful api 的身份认证也是值得推崇的一种使用方案。客户端和服务端共享 secret;过期时间由服务端校验,客户端定时刷新;签名信息不可被修改......spring security oauth jwt 提供了一套完整的 jwt 认证体系,以笔者的经验来看:使用 oauth2 或 jwt 来做 restfulapi 的认证都没有大问题,oauth2 功能更多,支持的场景更丰富,后者实现简单。
3.使用 jwt 做单点登录+会话管理(不推荐)。
5.3、面试问题:
1. JWT token 泄露了怎么办?(常问)
使用 https 加密你的应用,返回 jwt 给客户端时设置 httpOnly=true 并且使用 cookie 而不是 LocalStorage 存储 jwt,这样可以防止 XSS 攻击和 CSRF 攻击。
2. Secret如何设计?
jwt 唯一存储在服务端的只有一个 secret,个人认为这个 secret 应该设计成和用户 相关的属性,而不是一个所有用户公用的统一值。这样可以有效的避免一些注销和修改密码 时遇到的窘境。
3. 注销和修改密码?
传统的 session+cookie 方案用户点击注销,服务端清空 session 即可,因为状态保 存在服务端。但 jwt 的方案就比较难办了,因为 jwt 是无状态的,服务端通过计算来校验 有效性。没有存储起来,所以即使客户端删除了 jwt,但是该 jwt 还是在有效期内,只不 过处于一个游离状态。分析下痛点:注销变得复杂的原因在于 jwt 的无状态。提供几个方 案,视具体的业务来决定能不能接受:
仅仅清空客户端的 cookie,这样用户访问时就不会携带 jwt,服务端就认为用户需要重新登录。这是一个典型的假注销,对于用户表现出退出的行为,实际上这个时候携带对应 的 jwt 依旧可以访问系统。
清空或修改服务端的用户对应的 secret,这样在用户注销后,jwt 本身不变,但是由 于 secret 不存在或改变,则无法完成校验。这也是为什么将 secret 设计成和用户相关的 原因。
借助第三方存储自己管理 jwt 的状态,可以以 jwt 为 key,实现去 Redis 一类的缓 存中间件中去校验存在性。方案设计并不难,但是引入 Redis 之后,就把无状态的 jwt 硬 生生变成了有状态了,违背了 jwt 的初衷。实际上这个方案和 session 都差不多了。
修改密码则略微有些不同,假设号被到了,修改密码(是用户密码,不是 jwt 的 secret) 之后,盗号者在原 jwt 有效期之内依旧可以继续访问系统,所以仅仅清空 cookie 自然是 不够的,这时,需要强制性的修改 secret。
4. 如何解决续签问题
传统的 cookie 续签方案一般都是框架自带的,session 有效期 30 分钟,30 分钟内 如果有访问,session 有效期被刷新至 30 分钟。而 jwt 本身的 payload 之中也有一个 exp 过期时间参数,来代表一个 jwt 的时效性,而 jwt 想延期这个 exp 就有点身不由己 了,因为 payload 是参与签名的,一旦过期时间被修改,整个 jwt 串就变了,jwt 的特 性天然不支持续签。
解决方案:
1. 每次请求刷新 jwt。
jwt 修改 payload 中的 exp 后整个 jwt 串就会发生改变,那就让它变好了,每次请 求都返回一个新的 jwt 给客户端。只是这种方案太暴力了,会带来的性能问题。
2.只要快要过期的时候刷新
jwt 此方案是基于上个方案的改造版,只在前一个 jwt的最后几分钟返回给客户端一个新的 jwt。这样做,触发刷新 jwt 基本就要看运气了,如果用户恰巧在最后几分钟访问了服务器, 触发了刷新,万事大吉。如果用户连续操作了 27 分钟,只有最后的 3 分钟没有操作,导 致未刷新 jwt,无疑会令用户抓狂。
3. 完善 refreshToken
借鉴 oauth2 的设计,返回给客户端一个 refreshToken,允许客户端主动刷新 jwt。一般而言,jwt 的过期时间可以设置为数小时,而 refreshToken 的过期时间设置为数天。
4. 使用 Redis 记录独立的过期时间
在 Redis 中单独为每个 jwt 设置了过期时间,每次访问时刷新 jwt 的过期时间,若 jwt 不存在与 Redis 中则认为过期。
5. 如何防止令牌被盗用?
如果令牌被盗,只要该令牌不过期,任何服务都可以使用该令牌,有可能引起不安
全操作。我们可以在每次生成令牌的时候,将用户的客户端信息获取,同时获取用户的 IP
信息,然后将 IP 和客户端信息以 MD5 的方式进行加密,放到令牌中作为载荷的一部分,
用户每次访问微服务的时候,要先经过微服务网关,此时我们也获取用户客户端信息,同时
获取用户的 IP,然后将 IP 和客户端信息拼接到一起再进行 MD5 加密,如果 MD5 值和载
荷不一致,说明用户的IP 发生了变化或者终端发生了变化,有被盗的嫌疑,此时不让访问
即可。这种解决方案比较有效。
当然,还有一些别的方法也能减少令牌被盗用的概率,例如设置令牌超时时间不要太长。




