2024年4月

接口地址

  • keycloak开放接口地址:/auth/realms/fabao/.well-known/openid-configuration

rsa算法相关术语

RSA算法是一种非对称加密算法,其安全性基于大整数分解的困难性。在RSA算法中,有以下几个关键参数:

  1. n(模数)
    :n 是一个大整数,通常为两个大素数 p 和 q 的乘积,即 n = p * q。n 用于生成公钥和私钥,并且决定了加密和解密的计算过程。

  2. e(公钥指数)
    :e 是一个与 φ(n) 互质的小整数,其中 φ(n) 是欧拉函数,表示小于 n 且与 n 互质的正整数的个数。e 在加密时使用,作为公钥的一部分。

  3. 公钥
    :公钥由 (n, e) 组成,其中 n 是模数,e 是公钥指数。公钥用于加密消息,任何人都可以获得公钥进行加密操作。

  4. 私钥
    :私钥由 (n, d) 组成,其中 n 是模数,d 是私钥指数。私钥用于解密已经被公钥加密的消息,只有私钥的持有者才能获得解密能力。

总结来说,RSA算法通过公钥加密、私钥解密的方式实现信息的安全传输,公钥用于加密数据,私钥用于解密数据;反过来,私钥可以用来生成签名,而公钥可以用来验证签名的有效性。

RSA和RS256

  1. RSA
    :RSA是一种非对称加密算法,可以用于数据的加密和数字签名。在RSA中,公钥和私钥是成对存在的,公钥用于加密数据或验证数字签名,私钥用于解密数据或生成数字签名。

  2. RS256
    :RS256是一种基于RSA算法的数字签名算法,其中“RS”代表RSA算法,“256”表示使用SHA-256哈希算法生成摘要。RS256常用于JWT(JSON Web Token)的数字签名过程中,用于验证数据的完整性和真实性。

因此,可以说RS256是RSA算法的一种特定应用,用于数字签名,并且结合了SHA-256哈希算法。RSA算法还可以用于加密数据等其他用途,而RS256主要用于数字签名。

获取keycloak颁发的公钥

  • 从jwks公钥开放地址获取公钥 /auth/realms/fabao/protocol/openid-connect/certs

  • 从keycloak后台获取公钥

keycloak中jwt的验证

  • 在客户端验证keycloak的token是否在传输过程中被篡改,即签名验证是否通过
  • 下面代码中定义了公钥字符串,从开放地址返回的rsa公钥的n模数和e指数
  • 从kecloak认证中心颁发的公钥字符串
  • 从现在有keycloak认证中心获取的jwt字符串
// RSA公钥的模数
String modulus = "yOCNCy8x280...";

// RSA公钥的指数
String exponent = "AQAB";

// keycloak拿到的公钥
String publicKeyString = "MIIBIjANBg...B";

String KcJwtToken = "eyJh...";

  • 根据公钥开放地址返回的公钥信息n和e来验证签名
@Test
public void verifySign() throws Exception {
	String[] jwtParts = KcJwtToken.split("\\.");
	String header = jwtParts[0];
	String payload = jwtParts[1];
	// 解码Base64格式的模数和指数
	byte[] decodedModulus = Base64.getUrlDecoder().decode(modulus);// getMimeDecoder()会忽略非Base64字符(如换行符、空格等)
	byte[] decodedExponent = Base64.getUrlDecoder().decode(exponent);
	// 构建RSA公钥对象
	RSAPublicKeySpec publicSpec = new RSAPublicKeySpec(new BigInteger(1, decodedModulus),
			new BigInteger(1, decodedExponent));
	// 验征RSA签名
	PublicKey publicKey = KeyFactory.getInstance("rsa").generatePublic(publicSpec);
	boolean result = RSAUtils.verify(header + "." + payload, publicKey, jwtParts[2]);
	System.out.print("验签结果:" + result);

}
  • 根据keycloak后台颁发的公钥字符串来验证签名
// 根据认证平台颁发的公钥字符串来验证签名
@Test
public void verifyJwtToken() throws Exception {
	String[] jwtParts = KcJwtToken.split("\\.");
	String header = jwtParts[0];
	String payload = jwtParts[1];
	String sign = jwtParts[2];
	PublicKey publicKey = RSAUtils.getPublicKey(publicKeyString);
	boolean result = RSAUtils.verify(header + "." + payload, publicKey, sign);
	System.out.print("验签结果:" + result);
}

需要注意的是,以上jwt的token签名使用rs256(SHA256withRSA)算法生成的签名,所以本例子都是采用这种签名算法实现的,例外,也有h256,h512等哈希算法。

keycloak支持的签名算法

public static final String RS256 = "SHA256withRSA";
public static final String RS384 = "SHA384withRSA";
public static final String RS512 = "SHA512withRSA";
public static final String HS256 = "HMACSHA256";
public static final String HS384 = "HMACSHA384";
public static final String HS512 = "HMACSHA512";
public static final String ES256 = "SHA256withECDSA";
public static final String ES384 = "SHA384withECDSA";
public static final String ES512 = "SHA512withECDSA";
public static final String PS256 = "SHA256withRSAandMGF1";
public static final String PS384 = "SHA384withRSAandMGF1";
public static final String PS512 = "SHA512withRSAandMGF1";
public static final String AES = "AES";

public static final String SHA256 = "SHA-256";
public static final String SHA384 = "SHA-384";
public static final String SHA512 = "SHA-512";

大家好,我是小富~

本文是《ShardingSphere5.x分库分表原理与实战》系列的第

篇,目前系列的前几篇制作成了
PDF
,需要的可以在文末获取下载方式,持续更新中。今天咱们继续一起来探究下,分布式ID在分库分表中起到的作用以及如何使用,
ShardingSphere-jdbc
中已经为我们提供了多种分布式主键ID生成策略。接下来将分别介绍这些策略的优缺点,看看它们在实际应用中的场景和效果。

                                                    小富的技术站:https://xiaofucode.com

为什么用分布式主键ID

在传统的单库单表结构时,通常可以使用自增主键来保证数据的唯一性。但在分库分表的情况下,每个表的默认自增步长为1,这导致了各个库、表之间可能存在重叠的主键范围,从而使得主键字段失去了其唯一性的意义。

为了解决这一问题,我们需要引入专门的分布式 ID 生成器来生成全局唯一的ID,并将其作为每条记录的主键,以确保全局唯一性。通过这种方式,我们能够有效地避免数据冲突和重复插入的问题,从而保障系统的正常运行。

除了满足唯一性的基本要求外,作为主键 ID,我们还需要关注主键字段的数据类型、长度对性能的影响。因为主键字段的
数据类型

长度
直接影响着数据库的查询效率和整体系统性能表现,这一点也是我们在选方案时需要考虑的因素。

内置算法


ShardingSphere 5.X
版本后进一步丰富了其框架内部的主键生成策略方案。此前仅提供了
UUID

Snowflake
两种策略,现在又陆续提供了
NanoID

CosId

CosId-Snowflake
三种策略。下面我们将逐个的过一下。

注意

SQL中不要主动拼接主键字段(包括持久化工具自动拼接的)否则一律走默认的
Snowflake
策略!!!

ShardingSphere
中为分片表设置主键生成策略后,执行插入操作时,会自动在
SQL
中拼接配置的主键字段和生成的分布式ID值。所以,在创建分片表时
主键字段无需再设置 自增 AUTO_INCREMENT
。同时,在插入数据时应
避免为主键字段赋值
,否则会覆盖主键策略生成的ID。

CREATE TABLE `t_order` (
  `id` bigint NOT NULL,
  `order_id` bigint NOT NULL,
  `user_id` bigint NOT NULL,
  `order_number` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
  `customer_id` bigint NOT NULL,
  `order_date` datetime DEFAULT NULL,
  `interval_value` varchar(125) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `total_amount` decimal(10,2) NOT NULL,
  PRIMARY KEY (`order_id`) USING BTREE
) ;

UUID

想要获得一个具有唯一性的ID,大概率会先想到UUID,因为它不仅具有全球唯一的特性使用还简单。但并
不推荐
将其作为主键ID。

  • UUID的无序性。在插入新行数据后,
    InnoDB
    无法像插入有序数据那样直接将新行追加到表尾,而是需要为新行寻找合适的位置来分配空间。由于ID无序,页分裂操作变得不可避免,导致大量数据的移动。频繁的页分裂会导致数据碎片化(即数据在物理存储上分散分布)。这种随机的ID分配过程需要大量的额外操作,导致频繁的对数据进行无序的访问,导致磁盘寻道时间增加。数据的无序性进一步加剧了数据碎片化,降低了数据访问效率。

  • UUID字符串类型。字符串比数字类型占用更多的存储空间,对存储和查询性能造成较大的消耗;字符串类型的长度可变,可变长度的数据行会破坏索引的连续性,导致索引查找性能下降。

算法类型:UUID

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # UUID生成算法
          uu-id-gen:
            type: UUID
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            database-strategy:  # 分库策略
              standard:
                sharding-column: order_id
                sharding-algorithm-name: t_order_database_mod
            table-strategy: # 分表策略
              standard:
                sharding-column: order_id
                sharding-algorithm-name: t_order_table_mod
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: uu-id-gen

NanoID

或许很多人都不熟悉
NanoID
,它是一款用类似 UUID 生成唯一标识符的轻量级库。不过,与 UUID 不同的是 NanoID 生成的字符串ID长度较短,仅为21位。但仍然不推荐将它作为主键ID,理由和UUID一样。

算法类型:NANOID

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # nanoid生成算法
          nanoid-gen:
            type: NANOID
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: nanoid-gen

定制雪花算法

雪花算法是比较主流的分布式ID生成方案,在 ShardingSphere 中的
Snowflake
算法生成的是 Long 类型的 ID,通常作为
默认的主键生成策略
使用。

内置的雪花算法生成的ID主要由
时间戳
、工作机器ID
workId
、序列号
sequence
三部分组成。

@Override
  public synchronized Long generateKey() {
      ..........
      return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
  }

定制 Snowflake 算法有三个可配置的属性:

worker-id
:工作机器唯一标识,单机模式下会直接取此属性值计算ID,默认是0;集群模式下则由系统自动生成,此属性无效

max-vibration-offset
:最大抖动上限值,范围
[0, 4096)
,默认是1。那么如何理解这个属性呢?
这个属性是用来控制上边生成雪花ID中的
sequence
。通过限制抖动范围,同一毫秒内生成的ID中引入微小的变化,让数据更均匀地分散到不同的分片上。

private void vibrateSequenceOffset() {
    sequenceOffset = sequenceOffset >= maxVibrationOffset ? 0 : sequenceOffset + 1;
}

若使用此算法生成值作分片值,建议配置此属性。此算法在不同毫秒内所生成的 key 取模 2^n (2^n一般为分库或分表数) 之后结果总为 0 或 1。为防止上述分片问题,建议将此属性值配置为 (2^n)-1

max-tolerate-time-difference-milliseconds
:最大容忍时钟回退时间(毫秒)。服务器在校对时间时可能会发生时钟回拨的情况(当前时间回退),由于根据时间戳参与计算ID,这可能导致生成相同的ID,而这对系统来说是不可接受的。

ShardingSphere 雪花算法针对时钟回拨场景进行了处理,记录最后一次生成ID的时间
lastMilliseconds
,并与回拨后的当前时间
currentMilliseconds
进行比对。如果时间差超过了设置的最大容忍时钟回退时间,系统将直接抛出异常;如果未超过,则系统会休眠等待两者时间差的时长,
核心原则确保不会发放重复的ID

@SneakyThrows(InterruptedException.class)
private boolean waitTolerateTimeDifferenceIfNeed(final long currentMilliseconds) {
    if (lastMilliseconds <= currentMilliseconds) {
        return false;
    }
    long timeDifferenceMilliseconds = lastMilliseconds - currentMilliseconds;
    Preconditions.checkState(timeDifferenceMilliseconds < maxTolerateTimeDifferenceMilliseconds,
            "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastMilliseconds, currentMilliseconds);
    Thread.sleep(timeDifferenceMilliseconds);
    return true;
}

算法类型:SNOWFLAKE

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # 雪花ID生成算法
          snowflake-gen:
            type: SNOWFLAKE
            props:
              worker-id: # 工作机器唯一标识
              max-vibration-offset: 1024 # 最大抖动上限值,范围[0, 4096)。注:若使用此算法生成值作分片值,建议配置此属性。此算法在不同毫秒内所生成的 key 取模 2^n (2^n一般为分库或分表数) 之后结果总为 0 或 1。为防止上述分片问题,建议将此属性值配置为 (2^n)-1
              max-tolerate-time-difference-milliseconds: 10 # 最大容忍时钟回退时间,单位:毫秒
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: snowflake-gen

CosId

CosId 是一个高性能的分布式ID生成器框架,Shardingsphere 将其引入到自身的框架内,只简单的使用了 CosId 算法。
但目前亲测 5.2.0版本该算法处于不可用状态!!!我已经给官方提了issue,看看他们咋回复吧

CosId 框架内提供了 3 种算法:

  • SnowflakeId
    : 单机 TPS 性能:409W/s , 主要解决时钟回拨问题 、机器号分配问题并且提供更加友好、灵活的使用体验。
  • SegmentId
    : 每次获取一段 (Step) ID,来降低号段分发器的网络IO请求频次提升性能,提供多种存储后端:关系型数据库、Redis、Zookeeper 供用户选择。
  • SegmentChainId
    (
    推荐
    ): SegmentChainId (lock-free) 是对 SegmentId 的增强。性能可达到近似 AtomicLong 的 TPS 性能 12743W+/s。

该算法使用对外提供了两个属性:

  • id-name
    :ID 生成器名称。
  • as-string
    :是否生成字符串类型ID,将 long 类型 ID 转换成 62 进制 String 类型(Long.MAX_VALUE 最大字符串长度11位),并保证字符串 ID 有序性。

算法类型:COSID

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # COSID生成算法
          cosId-gen:
            type: COSID
            props:
              id-name: share
              as-string: false
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: cosId-gen

CosId-Snowflake

CosId-Snowflake 是 CosId 框架内提供的 Snowflake 算法,它的实现原理和上边的定制版雪花算法类似,ID主要也是由
时间戳
、工作机器ID、序列号
sequence
三部分组成。同样处理了时钟回拨等问题。

public synchronized long generate() {
    long currentTimestamp = this.getCurrentTime();
    if (currentTimestamp < this.lastTimestamp) {
        throw new ClockBackwardsException(this.lastTimestamp, currentTimestamp);
    } else {
        if (currentTimestamp > this.lastTimestamp && this.sequence >= this.sequenceResetThreshold) {
            this.sequence = 0L;
        }

        this.sequence = this.sequence + 1L & this.maxSequence;
        if (this.sequence == 0L) {
            currentTimestamp = this.nextTime();
        }

        this.lastTimestamp = currentTimestamp;
        long diffTimestamp = currentTimestamp - this.epoch;
        if (diffTimestamp > this.maxTimestamp) {
            throw new TimestampOverflowException(this.epoch, diffTimestamp, this.maxTimestamp);
        } else {
            return diffTimestamp << (int)this.timestampLeft | this.machineId << (int)this.machineLeft | this.sequence;
        }
    }
}

这个算法提供了两个属性:

  • epoch
    :固定的起始时间点,雪花ID算法的 epoch 变量值,默认值:1477929600000。用它的目的提高生成的ID的时间戳部分的可读性、稳定性和范围限制,使得生成的ID更加可靠和易于管理。
  • as-string
    :是否生成字符串类型ID,将 long 类型 ID 转换成 62 进制 String 类型(Long.MAX_VALUE 最大字符串长度11位),并保证字符串 ID 有序性。

算法类型:COSID_SNOWFLAKE

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # cosId-snowflake生成算法
          cosId-snowflake-gen:
            type: COSID_SNOWFLAKE
            props:
              epoch: 1477929600000
              as-string: false
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: cosId-snowflake-gen

自定义分布式主键

上边咱们介绍了 ShardingSphere 内提供的 5 种生成主键的ID算法,这些算法基本可以满足大部分的业务场景。不过,在某些情况下,我们可能会要求生成的ID具有特殊的含义或遵循特定的规则。ShardingSphere 也支持我们自定义生成主键ID,来满足定制的业务需求。

实现接口

要实现自定义的主键生成算法,首先需要实现
KeyGenerateAlgorithm
接口,并实现内部 4 个方法,
其中有两个方法比较关键:

  • getType()
    :我们自定义的算法类型,方便配置使用;
  • generateKey()
    :处理主键生成的核心逻辑,我们可以根据业务需求选择合适的主键生成算法,比如美团的 Leaf、滴滴的 TinyId 等。
@Data
@Slf4j
public class SequenceAlgorithms implements KeyGenerateAlgorithm {

    // 这个方法用于指定我们自定义的算法的类型。它会返回一个字符串,表示所使用算法的类型,方便在配置和识别时使用。
    @Override
    public String getType() {
        // 返回算法类型表示
        return "custom";
    }

    // 这是生成主键的核心逻辑所在。在这个方法内部,我们可以根据业务需求选择合适的主键生成算法,比如美团的Leaf、滴滴的TinyId等。这个方法的具体实现会根据所选算法的特点和要求来设计
    @Override
    public Comparable<?> generateKey() {
        return null;
    }

    @Override
    public Properties getProps() {
        return null;
    }
    // 这个方法用于初始化主键生成算法所需的资源或配置
    @Override
    public void init(Properties properties) {
    }
}

在引入外部的分布式ID生成器时,应尽量遵循以下原则:

  • 全局唯一
    :必须保证ID是全局性唯一的,基本要求
  • 高性能
    :高可用低延时,ID生成响应要块,否则反倒会成为业务瓶颈
  • 高可用
    :100%的可用性是骗人的,但是也要无限接近于100%的可用性
  • 好接入
    :要秉着拿来即用的设计原则,在系统设计和实现上要尽可能的简单

SPI 注册

通过 SPI 方式加载我们自定义的主键算法,需要在
resource/META-INF/services
目录下创建一个文件,文件名为
org.apache.shardingsphere.sharding.spi.KeyGenerateAlgorithm
,并将我们自定义的主键算法的完整类路径放入文件内,每行一个。在系统启动时会自动加载到这个文件,读取其中的类路径,然后通过反射机制实例化对应的类,完成主键算法的注册和加载。

resource
    |_META-INF
        |_services
           |_org.apache.shardingsphere.sharding.spi.KeyGenerateAlgorithm

配置使用

上边完成了自定义算法的逻辑,使用上与其他的算法一致。只需将我们刚刚定义的算法类型
custom
配置上即可。

spring:
  shardingsphere:
    rules:
      sharding:
        key-generators:  # 分布式序列算法配置
          # 自定义ID生成策略
          xiaofu-id-gen:
            type: custom
        tables:
          t_order:  # 逻辑表名称
            actual-data-nodes: db$->{0..1}.t_order_${0..2} # 数据节点:数据库.分片表
            key-generate-strategy: # 分布式主键生成策略
              column: id
              keyGeneratorName: xiaofu-id-gen

当执行插入操作时,debug 看已经进入到了定义的主键算法内了。

总结

我们介绍了 ShardingSphere 的几种内置主键生成策略以及如何自定义主键生成策略,市面上还有许多优秀的分布式ID框架都可以整合进来,但具体选择何种策略还是要取决于自身的业务需求。关于分布式 ID 生成器,我曾经撰写过一篇
一口气说出 9种 分布式ID生成方式
,详细介绍了多种生成器的优缺点,大家可以作为参考。

案例GitHub地址

https://github.com/chengxy-nds/Springboot-Notebook/tree/master/shardingsphere101/shardingsphere-sequence-algorithm

视频地址:【WebApi+Vue3从0到1搭建《权限管理系统》系列视频:搭建JWT系统鉴权-哔哩哔哩】 https://b23.tv/R6cOcDO

qq群:801913255

一、在appsettings.json中设置鉴权属性

 /*jwt鉴权*/
 "JwtSetting": {"Issuer": "zhangsan", //发行人
   "Audience": "zhangsan", //订阅人
   "ExpireSeconds": 120, //过期时间,默认分钟
   "ENAlgorithm": "HS256", //秘钥算法
   "SecurityKey": "Zmz=Start2024013OverallAuth.WebApi" //秘钥构成
 },

二、新建模型

添加模型JwtSettingModel其中字段和appsettings.json中的字段一样,如下

/// <summary>
///jwt 配置模型/// </summary>
public classJwtSettingModel
{
/// <summary> ///发行人/// </summary> public string Issuer { get; set; }/// <summary> ///订阅人/// </summary> public string Audience { get; set; }/// <summary> ///过期时间,默认分钟/// </summary> public int ExpireSeconds { get; set; }/// <summary> ///秘钥算法/// </summary> public string ENAlgorithm { get; set; }/// <summary> ///秘钥构成/// </summary> public string SecurityKey { get; set; }

}

三、新建解析appsettings.json节点的帮助类

 /// <summary>
 ///配置文件解析帮助类/// </summary>
 public classConfigurationHelper
{
/// <summary> ///配置项/// </summary> public static IConfiguration configuration { get; set; }/// <summary> ///构造实例化/// </summary> staticConfigurationHelper()
{
configuration
= new ConfigurationBuilder().Add(new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true}).Build();
}
/// <summary> ///获取appsetings 配置节点/// </summary> /// <typeparam name="T"></typeparam> /// <param name="node"></param> /// <returns></returns> public static T GetNode<T>(string node) where T : new()
{
T mode
= configuration.GetSection(node).Get<T>();returnmode;
}

}

四、在Startup.cs编写鉴权代码

找到ConfigureServices方法,在方法中添加如下代码

 //添加jwt鉴权
 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, option =>{var jwtsetting = ConfigurationHelper.GetNode<JwtSettingModel>("JwtSetting");
Configuration.Bind(
"JwtSetting", jwtsetting);
option.SaveToken
= true;
option.TokenValidationParameters
= newTokenValidationParameters()
{
ValidIssuer
= jwtsetting.Issuer,//发行人 ValidAudience = jwtsetting.Audience,//订阅人 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtsetting.SecurityKey)),//解密的密钥 ValidateIssuerSigningKey = true,//是否验证签名,不验证的画可以篡改数据,不安全 ValidateIssuer = true,//是否验证发行人,就是验证载荷中的Iss是否对应ValidIssuer参数 ValidateAudience = true,//是否验证订阅人,就是验证载荷中的Aud是否对应ValidAudience参数 ValidateLifetime = true,//是否验证过期时间,过期了就拒绝访问 ClockSkew = TimeSpan.Zero,//这个是token缓冲过期时间,如果设置了,token过期时间就是缓冲时间+过期时间//RequireExpirationTime = true, };

});

并在Configure方法中添加jwt授权代码 app.UseAuthorization();

五、编写Jwt帮助类

/// <summary>
///jwt帮助类/// </summary>
public static classJwtHelper
{
/// <summary> ///生成token/// </summary> /// <param name="loginResult"></param> /// <returns></returns> public static stringBuildToken(LoginModel loginResult)
{
LoginModel result
= new();var jwtsetting = ConfigurationHelper.GetNode<JwtSettingModel>("JwtSetting");//获取登录信息 var calime = loginResult.PropValueType().Select(x => newClaim(x.Name, x.Value.ToString(), x.Type)).ToList();//记录登录信息 var key = newSymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtsetting.SecurityKey));var creds = newSigningCredentials(key, SecurityAlgorithms.HmacSha256);var header = newJwtHeader(creds);var paylod = newJwtPayload(jwtsetting.Issuer, jwtsetting.Audience, calime, DateTime.Now, DateTime.Now.AddMinutes(jwtsetting.ExpireSeconds));//正式创建令牌 var token = newJwtSecurityToken(header, paylod);var tokenStr = newJwtSecurityTokenHandler().WriteToken(token);var ddd = token.ValidTo.AddHours(8).ToString();returntokenStr;
}
/// <summary> ///反射获取字段信息/// </summary> /// <param name="obj">模型</param> /// <returns></returns> public static IEnumerable<(string Name, object Value, string Type)> PropValueType(this objectobj)
{
List
<(string a, object b, string c)> result = new();var type =obj.GetType();var props =type.GetProperties();foreach (var item inprops)
{
result.Add((item.Name, item.GetValue(obj), item.PropertyType.Name));
}
returnresult;
}
}

然后再webapi接口控制器上方添加鉴权特性[Authorize],这样所有接口都会遵守jwt鉴权协议

六、Swagger接口文档使用Jwt鉴权

做好以上五点,webapi中就能正常使用jwt鉴权,但如果你使用Swagger测试接口,那么就要让Swagger遵守Jwt协议

所以必须在添加以下代码,注意这段代码是写在AddSwaggerGen中

 //把jwt添加到swagger中
 optinos.AddSecurityDefinition("OverallAuth.WebApi", newOpenApiSecurityScheme
{
Description
= "直接在下框中输入Bearer token(注意两者之间是一个空格)",
Name
= "Authorization",//jwt默认的参数名称 In = ParameterLocation.Header,//jwt默认存放请求头中 Type =SecuritySchemeType.ApiKey
});
//swagger遵守jwt授权协议 optinos.AddSecurityRequirement(newOpenApiSecurityRequirement
{
{
newOpenApiSecurityScheme{
Reference
= newOpenApiReference {
Type
=ReferenceType.SecurityScheme,
Id
= "OverallAuth.WebApi"}
},
new string[] { }
}
});

以上就是在webapi中使用jwt的详细代码

BGE M3-Embedding来自BAAI和中国科学技术大学,是BAAI开源的模型。相关论文在https://arxiv.org/abs/2402.03216,论文提出了一种新的embedding模型,称为M3-Embedding,它在多语言性(Multi-Linguality)、多功能性(Multi-Functionality)和多粒度性(Multi-Granularity)方面表现出色。M3-Embedding支持超过100种工作语言,支持8192长度的输入文本,同时支持密集检索(Dense Retrieval)、多向量检索(Multi-Vector Retrieval)和稀疏检索(Sparse Retrieval),为现实世界中的信息检索(IR)应用提供了统一的模型基础,通过这几种检索方式的组合,取得了良好的混合召回效果。

我们可以查看官方与openai模型的对比,整体来看,采用三种方式联合检索的BGE-M3(ALL)在三项评测中全面领先,而 BGE-M3(Dense)稠密检索在多语言、跨语言检索中具有明显优势。

BGE-M3与OpenAI-emb-3对比

BGE-M3 模型亮点

1. 多语言(Multi-Linguality)
,训练集包含100+种以上语言
2. 多功能(Multi-Functionality)
,支持稠密检索(Dense Retrieval),还支持稀疏检索(Sparse Retrieval)与多向量检索(Multi-vector Retrieval)
3. 多粒度(Multi-Granularity)
BGE-M3目前可以处理最大长度为8192 的输入文本,支持“句子”、“段落”、“篇章”、“文档”等不同粒度的输入文本

BGE-M3 训练数据

M3-Embedding模型的训练数据组成是模型效果较好的一个关键创新点,因为它旨在支持多语言性、多功能性和多粒度性,训练数据分为三部分:

M3-Embedding模型训练数据

  1. 无监督数据(Unsupervised Data)

    • 从大量多语言语料库中提取未经标记的文本数据,这些语料库包括Wikipedia、S2ORC、xP3、mC4和CC-News等。
    • 这些数据通过提取丰富的语义结构(例如标题-正文、标题-摘要、指令-输出等)来丰富模型的语义理解能力。
    • 无监督数据的规模达到了1.2亿文本对,覆盖了194种语言和2655种跨语言对应关系。
  2. 微调数据(Fine-tuning Data)

    • 来自标记语料库的高质量数据,包括英语、中文和其他语言的数据集。
    • 例如,英语数据集包括HotpotQA、TriviaQA、NQ、MS MARCO等,而中文数据集包括DuReader、T2-Ranking、NLI-zh等。
    • 这些数据集用于进一步微调模型,以提高其在特定任务和语言上的性能。
  3. 合成数据(Synthetic Data)

    • 为了解决长文档检索任务中数据不足的问题,研究者们生成了额外的多语言微调数据(称为MultiLongDoc)。
    • 通过从Wikipedia和MC4数据集中随机选择长文章,并从中随机选择段落,然后使用GPT-3.5生成基于这些段落的问题,生成的问题和所选文章构成新的文本对,增加了训练数据的多样性和覆盖范围。

这些训练数据的创新之处在于:

  • 多语言覆盖
    :M3-Embedding通过大规模的多语言无监督数据,学习不同语言之间的共同语义空间,从而支持多语言检索和跨语言检索。
  • 数据多样性
    :通过结合无监督数据、微调数据和合成数据,M3-Embedding能够捕捉到不同类型和长度的文本数据的语义信息,从而提高模型对不同输入粒度的处理能力。
  • 高质量数据整合
    :通过精心筛选和整合不同来源的数据,M3-Embedding确保了训练数据的高质量,这对于模型学习有效的文本嵌入至关重要。

通过这种创新的训练数据组成,
M3-Embedding
能够有效地学习并支持超过100种语言的文本嵌入,同时处理从短句到长达8192个词符的长文档,实现了在多语言、多功能和多粒度方面的突破。

从业界来看,
M3-Embedding

E5-mistral-7b
,都是利用GPT这样的LLM来合成了大量多语言数据,这个应该是后续的主流方案。

BGE-M3 混合检索

M3-Embedding统一了嵌入模型的三种常见检索功能,即密集检索(Dense retrieval)、词汇(稀疏)检索(Lexical retrieval)和多向量检索(Multi-vector retrieval)。以下是这些方法的公式化描述:

  • 密集检索(Dense retrieval)
    :输入查询q被转换为基于文本编码器的隐藏状态Hq,使用特殊标记“[CLS]”的归一化隐藏状态来表示查询:$e_q = \text{norm}(H_q[0])$。类似地,我们可以获取段落p的嵌入表示为 $e_p = \text{norm}(H_p[0])$。查询和段落之间的相关性得分通过两个嵌入向量 $e_p$ 和 $e_q$的内积来度量:$s_{\text{dense}} \leftarrow \langle e_p, e_q \rangle$。
    def dense_embedding(self, hidden_state, mask):
        if self.sentence_pooling_method == 'cls':
            return hidden_state[:, 0]
        elif self.sentence_pooling_method == 'mean':
            s = torch.sum(hidden_state * mask.unsqueeze(-1).float(), dim=1)
            d = mask.sum(axis=1, keepdim=True).float()
            return s / d
  • 词汇检索(Lexical Retrieval)
    :输出嵌入还被用来估计每个词项的重要性,以促进词汇检索。对于查询中的每个词项t(在我们的工作中,词项对应于一个标记),词项权重被计算为 $w_{qt} \leftarrow \text{Relu}(W_{\text{lex}} H_q[i])$,其中 $W_{\text{lex}} \in \mathbb{R}^{d \times 1}$ 是将隐藏状态映射到一个实数的矩阵。如果词项t在查询中出现多次,我们只保留其最大权重。我们以相同的方式计算段落中每个词项的权重。基于估计的词项权重,查询和段落之间的相关性得分通过查询和段落中共同出现的词项(表示为$q ∩ p$)的联合重要性来计算:$s_{\text{lex}} \leftarrow \sum_{t \in q \cap p}(w_{qt} \cdot w_{pt})$。
    def sparse_embedding(self, hidden_state, input_ids, return_embedding: bool = True):
        # sparse_linear 线性层= torch.nn.Linear(in_features=self.model.config.hidden_size, out_features=1)
        # 通过relu计算token weight
        token_weights = torch.relu(self.sparse_linear(hidden_state))
        if not return_embedding: return token_weights
        # 形状为(input_ids.size(0), input_ids.size(1), self.vocab_size)的零张量
        sparse_embedding = torch.zeros(input_ids.size(0), input_ids.size(1), self.vocab_size,
                                       dtype=token_weights.dtype,
                                       device=token_weights.device)
        # 将token_weights中的值分散scatter到sparse_embedding的相应位置,索引位置根据input_ids提供
        sparse_embedding = torch.scatter(sparse_embedding, dim=-1, index=input_ids.unsqueeze(-1), src=token_weights)
        # CLS,PAD 等无用token
        unused_tokens = [self.tokenizer.cls_token_id, self.tokenizer.eos_token_id, self.tokenizer.pad_token_id,
                         self.tokenizer.unk_token_id]
        sparse_embedding = torch.max(sparse_embedding, dim=1).values
        #  无用token weight设置为0
        sparse_embedding[:, unused_tokens] *= 0.
        return sparse_embedding
  • 多向量检索(Multi-Vector Retrieval)
    :作为密集检索的扩展,多向量方法利用整个输出嵌入来表示查询和段落:$E_q = \text{norm}(W_{\text{mul}} H_q), E_p = \text{norm}(W_{\text{mul}} H_p)$,其中 $W_{\text{mul}} \in \mathbb{R}^{d \times d}$ 是可学习的投影矩阵。按照ColBERT(Khattab和Zaharia, 2020)的方法,使用后期交互来计算细粒度的相关性得分:$s_{\text{mul}} \leftarrow \frac{1}{N} \sum_{i=1}^{N} \max_{j=1}^{M} E_q[i] \cdot E_p[j]$;N和M分别是查询和段落的长度。

由于嵌入模型的多功能性,检索过程可以在混合过程中进行。首先,可以通过每种方法单独检索候选结果(由于其高成本,可以免去多向量方法的这一步)。最终的检索结果是根据集成的相关性得分重新排序的:$s_{\text{rank}} \leftarrow s_{\text{dense}} + s_{\text{lex}} + s_{\text{mul}}$。

BGE-M3训练方式和创新点

BGE-M3采用多阶段

BGE-M3模型训练分为三个阶段:

  • 1)
    RetroMAE预训练
    ,在105种语言的网页数据和wiki数据上进行,提供一个可以支持8192长度和面向表示任务的基座模型;

  • 2)
    无监督对比学习
    ,在194种单语言和1390种翻译对数据共1.1B的文本对上进行的大规模对比学习;

  • 3)
    多检索方式统一优化
    ,在高质量多样化的数据上进行多功能检索优化,使模型具备多种检索能力。

  • 其中,一些重要的关键技术如下:

1. 自学习蒸馏

人类可以利用多种不同的方式计算结果,矫正误差。模型也可以,通过联合多种检索方式的输出,可以取得比单检索模式更好的效果。因此,BGE-M3使用了一种自激励蒸馏方法来提高检索性能。具体来说,合并三种检索模式的输出,得到新的文本相似度分数,将其作为激励信号,让各单模式学习该信号,以提高单检索模式的效果。

image.png

2. 训练效率优化

通过根据长度对文本数据进行分组,确保一个batch内文本长度相对相似,从而减少填充。为了减少文本建模时的显存消耗,将一批数据分成多个小批。对于每个小批,利用模型编码文本,收集输出的向量同时丢弃所有前向传播中的中间状态,最后汇总向量计算损失,可以显著增加训练的batch size。

Efficient Batching

3. 长文本优化

BGE-M3提出了一种简单而有效的方法:MCLS(Multiple CLS)来增强模型的能力,而无需对长文本进行微调。

MCLS方法旨在利用多个CLS令牌来联合捕获长文本的语义。为每个固定数量的令牌插入一个cls令牌,每个cls令牌可以从相邻的令牌获取语义信息,最后通过对所有cls令牌的最后隐藏状态求平均值来获得最终的文本嵌入。

MCLS

BGE-M3 实验结果

多语言检索任务,稀疏检索(Sparse)大幅超过了传统的稀疏匹配算法BM25。多向量检索(multi-vector)则获得了三种检索方式中的最佳效果。

多语言检索任务

跨语言检索能力(MKQA)

BGE-M3在跨语言任务上依然具备最佳的检索效果。稀疏检索并不擅长应对跨语言检索这种词汇重合度很小的场景。因此,稀疏检索的自身效果以及与其他方法混搭所带来的收益相对较小。

跨语言检索能力

长文档检索能力 (MLRB: Multi-Lingual Long Retrieval Benchmark)

BGE-M3可以支持长达8192的输入文档,从实验结果可以观察到,稀疏检索(Sparse)的效果要显著高于稠密检索(Dense),这说明关键词信息对于长文档检索极为重要。

长文档检索能力

BGE-M3 模型微调

需要先安装,

  • with pip
pip install -U FlagEmbedding
  • from source
git clone https://github.com/FlagOpen/FlagEmbedding.git
cd FlagEmbedding
pip install -e .

模型微调的数据集格式是json line格式文件,json格式如下:

{"query": str, "pos": List[str], "neg":List[str]}

query 是查询,pos 是正文本列表,neg 是负文本列表。

模型训练:

torchrun --nproc_per_node {number of gpus} \
-m FlagEmbedding.BGE_M3.run \
--output_dir {path to save model} \
--model_name_or_path BAAI/bge-m3 \
--train_data ./toy_train_data \
--learning_rate 1e-5 \
--fp16 \
--num_train_epochs 5 \
--per_device_train_batch_size {large batch size; set 1 for toy data} \
--dataloader_drop_last True \
--normlized True \
--temperature 0.02 \
--query_max_len 64 \
--passage_max_len 256 \
--train_group_size 2 \
--negatives_cross_device \
--logging_steps 10 \
--same_task_within_batch True \
--unified_finetuning True \
--use_self_distill True

参考文献

PrimiHub
一款由密码学专家团队打造的开源隐私计算平台,专注于分享数据安全、密码学、联邦学习、同态加密等隐私计算领域的技术和内容。

随着信息技术的飞速发展,数据已成为现代社会最宝贵的资产之一。然而,数据的快速增长也带来了安全风险,包括数据泄露、篡改和滥用等。数据加密技术作为保护数据安全的重要手段,其重要性日益凸显。

数据加密技术概述

数据加密是一种将原始数据(明文)通过算法转换成只有授权用户才能解读的格式(密文)的过程。这一过程的关键在于密钥,它是加密和解密过程中不可或缺的元素。

对称加密与非对称加密

数据加密技术主要分为两大类:对称加密和非对称加密。

对称加密

对称加密使用相同的密钥进行加密和解密。这种加密方式的优点是速度快,适合大量数据的加密。常见的对称加密算法包括AES(高级加密标准)和DES(数据加密标准)。

非对称加密

非对称加密使用一对密钥,即公钥和私钥。公钥用于加密数据,私钥用于解密。非对称加密的优点是安全性高,但由于计算复杂,通常用于加密小量数据或传输对称密钥。著名的非对称加密算法有RSA和ECC(椭圆曲线密码学)。

数据加密的作用

数据加密技术在数据安全中的作用主要体现在以下几个方面:

1. 保护数据机密性

通过加密,只有拥有正确密钥的用户才能访问数据内容,从而防止未授权的用户读取敏感信息。

2. 确保数据完整性

加密技术通常与完整性校验相结合,确保数据在传输或存储过程中未被篡改。

3. 实现数据可用性

即使在遭受攻击的情况下,加密也能确保数据可以被授权用户正常使用。

4. 符合法律法规要求

许多国家和地区的法律法规要求企业对存储的个人数据进行加密,以保护用户隐私。

实际应用举例

为了更直观地理解数据加密的作用,我们可以通过一个简单的例子来说明。

假设Alice想要发送一封包含敏感信息的电子邮件给Bob。如果Alice直接发送明文邮件,那么任何截获这封邮件的人都能看到邮件内容。但是,如果Alice使用Bob的公钥对邮件内容进行加密,那么只有Bob使用自己的私钥才能解密并阅读邮件内容。

流程图

sequenceDiagram
participant A as Alice
participant B as Bob
A->>B: 发送公钥请求
B->>A: 发送公钥
A->>B: 使用B的公钥加密邮件
B->>A: 接收加密邮件
B->>B: 使用私钥解密邮件

专业词汇解释

  • 密钥(Key)
    :用于加密和解密数据的一串字符。
  • 明文(Plaintext)
    :原始的、未经加密的数据。
  • 密文(Ciphertext)
    :加密后的数据显示的格式。

数据加密技术的发展趋势

随着计算能力的提高和新型攻击手段的出现,数据加密技术也在不断发展和完善。

量子加密

量子加密是一种新兴的加密技术,它利用量子力学的原理来实现数据的安全传输。量子加密的关键在于量子密钥分发(QKD),它能够检测任何试图窃听的行为。

同态加密

同态加密是一种允许对密文进行特定运算,而运算结果解密后与对明文进行相同运算的结果相同的加密方式。这种加密方式对于云计算等场景非常有用,因为它允许在不解密数据的情况下对数据进行处理。

安全多方计算

安全多方计算(SMC)是一种在多个不信任的方之间进行计算的技术,而无需暴露各方的数据。SMC结合了加密技术和其他密码学原语,如秘密共享和零知识证明。

结论

数据加密技术是确保数据安全的关键环节。通过加密,我们可以保护数据免受未授权访问,确保数据的完整性和可用性,并满足法律法规的要求。随着技术的发展,加密技术也在不断进步,以应对日益复杂的网络安全威胁。

PrimiHub
一款由密码学专家团队打造的开源隐私计算平台,专注于分享数据安全、密码学、联邦学习、同态加密等隐私计算领域的技术和内容。