JWT【分布式鉴权方案】

JWT【分布式鉴权方案】,第1张

一、token在header中的传输规范

二、uuid在header中的传输规范

三、防重放攻击传输规范:

四、权限校验失败返回值

>

在调用接口时将线程号(多实例的情况下得用uuid,线程号可能会重复)存入redis队列,查询队首线程号(uuid)如果是当前线程,则执行逻辑、出队,否则等待。

后调用的接口后执行了,如果不加该注解的情况如下,执行快的先执行完:

假如不考虑多租户和多实例的情况,就有很多种实现方式

输出如下

实现如下:

输出如下:

实现方式和使用ConcurrentLinkedQueue方式一样,BlockingQueue关键在于 take 方法,当队列为空时,take会阻塞,适合实现生产者消费者模式。

看一下take的源码,当队列为空,调用的是await,当队列不为空时,出队,再判断队列还不为空,唤醒其他take的线程

再看一下offer的源码,当队列不为空,唤醒take的线程

分布式锁对应的是单机锁,我们在写多线程程序时,避免同时 *** 作一个共享变量而产生数据问题,通常会使用一把锁来实现互斥,其使用范围是在同一个进程中。(同一个进程内存是共享的,以争抢同一段内存,来判断是否抢到锁)。

如果是多个进程,如何互斥呢。就要引入分布式锁来解决这个问题。想要实现分布式锁,必须借助一个外部系统,所有进程都去这个系统上去申请加锁。

而这个外部系统,必须要实现互斥的能力,即两个请求同时进来,只会给一个进程返回成功,另一个返回失败(或等待)。

这个外部系统,可以是 MySQL,也可以是 Redis 或 Zookeeper。但为了追求更好的性能,我们通常会选择使用 Redis 或 Zookeeper 来做。

依赖mysql的行锁 select for update。

一个特例,唯一索引。唯一索引是找到了就直接停止遍历,非唯一索引还会向后遍历一行。移步第八个case。

现在的索引:

分析:

单条命中只会加行锁,不加间隙锁。所以RC/RR是一样的。

事务1对id=10这条记录加行锁。所以 场景1会锁等待,场景2不会锁等待

分析:

RC隔离级别:

事务1未命中,不会加任何锁。所以 场景1,场景2,场景3都不会锁等待

RR隔离级别:

事务1未命中,会加间隙锁。因为主键查询,只会对主键加锁。

在10和18加间隙锁。

间隙锁和查询不冲突。 场景1不会锁等待

间隙锁和插入冲突。 场景2和场景3会锁等待

分析

单条命中只会加行锁,不加间隙锁。所以RC/RR是一样的。

事务1对二级索引和主键索引加行锁。 事务1和事务2都会发生锁等待

分析

RC隔离级别:

事务1未命中,不会加任何锁。所以 场景1,场景2,场景3都不会锁等待

RR隔离级别:

事务1对二级索引N0007到正无穷上间隙锁,主键索引不上锁。 场景1会锁等待,场景2不会锁等待

分析:

RC隔离级别:

只会加行锁。 场景1场景2会锁等待 场景3不会发生锁等待

RR隔离级别:

会加行锁和间隙锁。 场景1场景2场景3都会锁等待

ps: 如果是唯一索引,只会加行锁。非唯一才会加间隙锁。

RC隔离级别:

事务1未命中,不会加任何锁。所以 场景1,场景2都不会锁等待

RR隔离级别:

事务1未命中,会加间隙锁。间隙锁与查询不冲突, 场景1不会发生锁等待 场景2会发生锁等待

分析

RC隔离级别:

事务1加了三个行锁。 场景1会锁等待。场景2,场景3不会发生锁等待

RR隔离级别:

事务1加个三个行锁和间隙锁。 场景1,场景3会发生锁等待 。间隙锁与查询不冲突, 场景2不会锁等待。

分析:

RC隔离级别:

事务1加的都是行锁。 场景1会发生锁等待 场景2,场景3不会发生锁等待

RR隔离级别:

事务1会对二级索引加行锁和间隙锁,对主键索引加行锁。

场景1,场景3会发生锁等待 。间隙锁与查询不冲突, 场景2不会锁等待。

这么看,二级索引和唯一索引没什么区别。

那如果是 select from book where name < 'Jim' for update; 呢

如果name是唯一索引。因为找到jim就不会向后遍历了,所以jim和rose之间不会有间隙锁。

分析:

RC隔离级别:

由于没有走索引,所以只能全表扫描。在命中的主键索引上加行锁。 场景1会锁等待,场景2不会锁等待

RR隔离级别:

不开启innodb_locks_unsafe_for_binlog。 会发生锁表

开启innodb_locks_unsafe_for_binlog。和RC隔离级别一样。

RC隔离级别:

未命中不加锁。 场景1,场景2都不会锁等待

RR隔离级别:

未命中, 锁表

在RR隔离级别下,where条件没有索引,都会锁表。

加锁命令:

释放锁命令:

这里存在问题,当释放锁之前异常退出了。这个锁就永远不会被释放了。

怎么解决呢?加一个超时时间。

还有问题,不是原子 *** 作。

redis 2612之后,redis天然支持了

来看一下还有什么问题:

试想这样一个场景

看到了么,这里存在两个严重的问题:

释放别人的锁 :客户端 1 *** 作共享资源完成后,却又释放了客户端 2 的锁

锁过期 :客户端 1 *** 作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有

解决办法是:客户端在加锁时,设置一个只有自己知道的唯一标识进去。

例如,可以是自己的线程id,也可以是一个uuid

在释放锁时,可以这么写:

问题来了,还不是原子的。redis没有原生命令了。这里需要使用lua脚本

锁的过期时间如果评估不好,这个锁就会有「提前」过期的风险,一般的妥协方案是,尽量「冗余」过期时间,降低锁提前过期的概率。

其实可以有比较好的方案:

加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了, *** 作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。

这个守护线程我们一般把他叫做看门狗线程。

我们在使用 Redis 时,一般会采用 主从集群 + 哨兵 的模式部署,这样做的好处在于,当主库异常宕机时,哨兵可以实现「故障自动切换」,把从库提升为主库,继续提供服务,以此保证可用性。

试想这样的场景:

为此,Redis 的作者提出一种解决方案,就是我们经常听到的 Redlock(红锁)

现在我们来看,Redis 作者提出的 Redlock 方案,是如何解决主从切换后,锁失效问题的。

Redlock 的方案基于 2 个前提:

也就是说,想用使用 Redlock,你至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。

Redlock 具体如何使用呢?

整体的流程是这样的,一共分为 5 步:

有 4 个重点:

1) 为什么要在多个实例上加锁?

本质上是为了「容错」,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。

2) 为什么大多数加锁成功,才算成功?

多个 Redis 实例一起来用,其实就组成了一个「分布式系统」。

在分布式系统中,总会出现「异常节点」,所以,在谈论分布式系统问题时,需要考虑异常节点达到多少个,也依旧不会影响整个系统的「正确性」。

这是一个分布式系统「容错」问题,这个问题的结论是: 如果只存在「故障」节点,只要大多数节点正常,那么整个系统依旧是可以提供正确服务的。

3) 为什么步骤 3 加锁成功后,还要计算加锁的累计耗时?

因为 *** 作的是多个节点,所以耗时肯定会比 *** 作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在 延迟、丢包、超时 等情况发生,网络请求越多,异常发生的概率就越大。

所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经「超过」了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。

4) 为什么释放锁,要 *** 作所有节点?

在某一个 Redis 节点加锁时,可能因为「网络原因」导致加锁失败。

例如,客户端在一个 Redis 实例上加锁成功,但在读取响应结果时,网络问题导致 读取失败 ,那这把锁其实已经在 Redis 上加锁成功了。

所以,释放锁时,不管之前有没有加锁成功,需要释放「所有节点」的锁,以保证清理节点上「残留」的锁。

好了,明白了 Redlock 的流程和相关问题,看似 Redlock 确实解决了 Redis 节点异常宕机锁失效的问题,保证了锁的「安全性」。

在martin的文章中,主要阐述了4个论点:

第一:效率

使用分布式锁的互斥能力,是避免不必要地做同样的工作两次。如果锁失效,并不会带来「恶性」的后果,例如发了 2 次邮件等,无伤大雅。

第二:正确性

使用锁用来防止并发进程相互干扰。如果锁失败,会造成多个进程同时 *** 作同一条数据,产生的后果是数据严重错误、永久性不一致、数据丢失等恶性问题,后果严重。

他认为,如果你是为了前者——效率,那么使用单机版redis就可以了,即使偶尔发生锁失效(宕机、主从切换),都不会产生严重的后果。而使用redlock太重了,没必要。

而如果是为了正确性,他认为redlock根本达不到安全性的要求,也依旧存在锁失效的问题!

一个分布式系统,存在着你想不到的各种异常。这些异常场景主要包括三大块,这也是分布式系统会遇到的三座大山: NPC

martin用一个进程暂停(GC)的例子,指出了redlock安全性的问题:

又或者,当多个Redis节点时钟发生了问题时,也会导致redlock锁失效。

在混乱的分布式系统中,你不能假设系统时钟就是对的。

个人理解,相当于在业务层再做一层乐观锁。

一个好的分布式锁,无论 NPC 怎么发生,可以不在规定时间内给出结果,但并不会给出一个错误的结果。也就是只会影响到锁的「性能」(或称之为活性),而不会影响它的「正确性」。

1、Redlock 不伦不类 :它对于效率来讲,Redlock 比较重,没必要这么做,而对于正确性来说,Redlock 是不够安全的。

2、时钟假设不合理 :该算法对系统时钟做出了危险的假设(假设多个节点机器时钟都是一致的),如果不满足这些假设,锁就会失效。

3、无法保证正确性 :Redlock 不能提供类似 fencing token 的方案,所以解决不了正确性的问题。为了正确性,请使用有「共识系统」的软件,例如 Zookeeper。

好了,以上就是 Martin 反对使用 Redlock 的观点,看起来有理有据。

下面我们来看 Redis 作者 Antirez 是如何反驳的。

首先,Redis 作者一眼就看穿了对方提出的最为核心的问题: 时钟问题

Redis 作者表示,Redlock 并不需要完全一致的时钟,只需要大体一致就可以了,允许有「误差」。

例如要计时 5s,但实际可能记了 45s,之后又记了 55s,有一定误差,但只要不超过「误差范围」锁失效时间即可,这种对于时钟的精度的要求并不是很高,而且这也符合现实环境。

对于对方提到的「时钟修改」问题,Redis 作者反驳到:

Redis 作者继续论述,如果对方认为,发生网络延迟、进程 GC 是在客户端确认拿到了锁,去 *** 作共享资源的途中发生了问题,导致锁失效,那这 不止是 Redlock 的问题,任何其它锁服务例如 Zookeeper,都有类似的问题,这不在讨论范畴内

这里我举个例子解释一下这个问题:

Redis 作者这里的结论就是:

所以,Redis 作者认为 Redlock 在保证时钟正确的基础上,是可以保证正确性的。

这个方案必须要求要 *** 作的「共享资源服务器」有拒绝「旧 token」的能力。

例如,要 *** 作 MySQL,从锁服务拿到一个递增数字的 token,然后客户端要带着这个 token 去改 MySQL 的某一行,这就需要利用 MySQL 的「事物隔离性」来做。

但如果 *** 作的不是 MySQL 呢?例如向磁盘上写一个文件,或发起一个 >

package redis;

import javautilList;

import javautilUUID;

import redisclientsjedisJedis;

import redisclientsjedisShardedJedis;

import redisclientsutilShardInfo;

/

@author Andy

/

public class RedisMasterSlaveTest {

private static final String HOST = "";

private static final int PORT = 0;

/

添加测试数据

/

private static void setData(Jedis jedis) {

for (int i = 0; i < 100; i++) {

final String a = UUIDrandomUUID()toString();

jedisset(a, a);

}

}

/

dbsize 数据库key总数

/

private static long getDBSize(Jedis jedis) {

return jedisdbSize();

}

/

查询持久化策略

/

private static List<String> getSaveConfig(Jedis jedis) {

return jedisconfigGet("save");

}

/

设置持久化策略

/

private static String setSaveConfig(Jedis jedis) {

String celue_1 = "800 1";

String celue_2 = "400 2";

return jedisconfigSet("save", celue_1 + " " + celue_2);

}

/

阻塞IO后持久化数据然后关闭redis (shutdown)

/

private static String shutdown(Jedis jedis) {

return jedisshutdown();

}

/

将此redis设置为master主库

/

private static String slaveofNoOne(Jedis jedis) {

return jedisslaveofNoOne();

}

/

将此redis根据host/port设置为slaveof从库

/

private static String slaveof(Jedis jedis) {

return jedisslaveof(HOST, PORT);

}

/

查询redis的info信息

/

private static String info(Jedis jedis) {

return jedisinfo();

}

/

select

/

private static String select(Jedis jedis) {

return jedisselect(1);

}

}

////不要谢

系统要限定用户的某个行为在指定的时间里只能允许发生 N 次(例如:帖子的评论数,1分钟之内只允许2次评论),可以使用 Redis 的zset数据结构来实现这个限流的功能

这个限流需求中存在一个滑动时间窗口, zset 数据结构的 score 值,可以通过 score 来圈出这个时间窗口来。而且我们只需要保留这个时间窗口,窗口之外的数据都可以砍掉。那这个 zset 的 value 填什么比较合适呢?它只需要保证唯一性即可,用 uuid 会比较浪费空间,那就改用毫秒时间戳吧。

如图所示,用一个 zset 结构记录用户的行为历史,每一个行为都会作为 zset 中的一个 key 保存下来。同一个用户同一种行为用一个 zset 记录。

为节省内存,我们只需要保留时间窗口内的行为记录,同时如果用户是冷用户,滑动时间窗口内的行为是空记录,那么这个 zset 就可以从内存中移除,不再占用空间。

通过统计滑动窗口内的行为数量与阈值 max_count 进行比较就可以得出当前的行为是否允许

zset 集合中只有 score 值非常重要,value 值没有特别的意义,只需要保证它是唯一的就可

以了。

因为这几个连续的 Redis *** 作都是针对同一个 key 的,使用 pipeline 可以显著提升

Redis 存取效率。但这种方案也有缺点,因为它要记录时间窗口内所有的行为记录,如果这

个量很大,比如限定 60s 内 *** 作不得超过 100w 次这样的参数,它是不适合做这样的限流

的,因为会消耗大量的存储空间

使用

1、导入依赖

在maven仓库中查找,发现只有这个依赖,所以直接复制这个即可

<pre data-language="xml" id="IsjaY" class="ne-codeblock" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">

<dependency>

<groupId>comgithubwhvcse</groupId>

<artifactId>easy-captcha</artifactId>

<version>162</version>

</dependency></pre>

2、使用

总共有这么多种验证类型

3、测试

<pre data-language="java" id="TG4I5" class="ne-codeblock" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959"> public static void main(String[] args) {

返回结果:

<pre data-language="java" id="TwqMn" class="ne-codeblock" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">//算数验证码返回结果

arithmeticString:3x6+5=

text:23

:data:image/png;base64

//中文返回结果

chineseRes:个动来紧

chineseUrl:data:image/png;base64

</pre>

4、最佳实践

可以结合redis,使用uuid作为key,结果作为value存储起来

<pre data-language="java" id="KxZcD" class="ne-codeblock" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959"> @GetMapping(value = "/code")

public ResponseEntity<Object> getCode() {

// 算术类型

ArithmeticCaptcha captcha = new ArithmeticCaptcha(111, 36);

// 几位数运算,默认是两位

captchasetLen(2);

// 获取运算的结果

String result = "";

try {

result = new Double(DoubleparseDouble(captchatext()))intValue() + "";

} catch (Exception e) {

result = captchatext();

}

//生成uuid,用于判断

String uuid = propertiesgetCodeKey() + IdUtilsimpleUUID();

以上就是关于JWT【分布式鉴权方案】全部的内容,包括:JWT【分布式鉴权方案】、异步处理http请求同步返回结果、springBoot接口排队(串行执行)等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9453301.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-28
下一篇 2023-04-28

发表评论

登录后才能评论

评论列表(0条)

保存