从RSA角度出发解析JWT原理

从RSA角度出发解析JWT原理

在今天的数字化世界中,安全地传递信息变得越来越重要。JSON Web TokenJWT)作为一种流行的开放标准,为简化服务器与客户端之间的安全交流提供了一种高效的方式。本文旨在深入解析JWT的工作原理,并通过示例演示如何使用JWT进行安全通信。我们将从JWT的基本组成部分讲起,探讨公密钥加密的原理,解释为什么JWT是安全的,以及如何验证JWT的有效性。

一、JWT介绍

1. JWT组成部分

JWT由三个部分组成,它们分别是头部(Header)、载荷(Payload)、和签名(Signature)。通过将这三部分用点(.)连接起来,形成了一个完整的JWT字符串。例如:xxxxx.yyyyy.zzzzz

2. 头部(Header)

头部通常由两部分组成:令牌类型(typ)和签名算法(alg)。例如:

language-json
1
2
3
4
5
{

"kid": "33bd1cad-62a6-4415-89a6-c2c816f3d3b1",
"alg": "RS256"
}
  • kid (Key ID):密钥标识符,用于指明验证JWT签名时使用的密钥。在含有多个密钥的系统中,这可以帮助接收者选择正确的密钥进行验证。这里的值33bd1cad-62a6-4415-89a6-c2c816f3d3b1是一个UUID,唯一标识了用于签名的密钥。
  • alg (Algorithm):指明用于签名的算法,这里是RS256,表示使用RSA签名算法和SHA-256散列算法。这种算法属于公钥/私钥算法,意味着使用私钥进行签名,而用公钥进行验证。

3. 载荷(Payload)

载荷包含了所要传递的信息,这些信息以声明(claims)的形式存在。声明可以是用户的身份标识,也可以是其他任何必要的信息。载荷示例:

language-json
1
2
3
4
5
6
7
8
9
10
{

"sub": "XcWebApp",
"aud": "XcWebApp",
"nbf": 1707373072,
"iss": "http://localhost:63070/auth",
"exp": 1707380272,
"iat": 1707373072,
"jti": "62e885c5-6b3f-49a2-aa10-b2e872a52b33"
}
  • sub (Subject):主题,标识了这个JWT的主体,通常是指用户的唯一标识。这里XcWebApp可能是一个应用或用户标识。
  • aud (Audience):受众,标识了这个JWT的预期接收者。这里同样是XcWebApp,意味着这个JWT是为XcWebApp这个应用或服务生成的。
  • nbf (Not Before):生效时间,这个时间之前,JWT不应被接受处理。这里的时间是Unix时间戳格式,表示JWT生效的具体时间。
  • iss (Issuer):发行者,标识了这个JWT的发行方。这里是http://localhost:63070/auth,表明JWT由本地的某个认证服务器发行。
  • exp (Expiration Time):过期时间,这个时间之后,JWT不再有效。同样是Unix时间戳格式,表示JWT过期的具体时间。
  • iat (Issued At):发行时间,JWT创建的时间。这提供了JWT的时间信息,也是Unix时间戳格式。
  • jti (JWT ID):JWT的唯一标识符,用于防止JWT被重放(即两次使用同一个JWT)。这里的值是一个UUID,确保了JWT的唯一性。

4. 签名(Signature)

签名是对头部和载荷的加密保护,确保它们在传输过程中未被篡改。根据头部中指定的算法(例如HS256),使用私钥对头部和载荷进行签名。

二、深入理解JWT签名验证

1. 签名生成

  1. 哈希处理 :首先对JWT的头部和载荷部分(经过Base64编码并用.连接的字符串)进行哈希处理,生成一个哈希值。这个步骤是为了确保数据的完整性,即使是微小的改动也会导致哈希值有很大的不同。
  2. 私钥加密哈希值 :然后使用发行者的私钥对这个哈希值进行加密,生成的结果就是JWT的签名部分。这个加密的哈希值(签名)附加在JWT的后面。

2. 签名验证

  1. 哈希处理 :接收方收到JWT后,会独立地对其头部和载荷部分进行同样的哈希处理,生成一个哈希值。
  2. 公钥解密签名 :接收方使用发行者的公钥对签名(加密的哈希值)进行解密,得到另一个哈希值。
  3. 哈希值比较比较这两个哈希值。如果它们相同,就证明了JWT在传输过程中未被篡改,因为只有相应的私钥能够生成一个对应的、能够通过公钥解密并得到原始哈希值的签名。

3. 为什么JWT是安全的

由于破解RSA算法的难度极高,没有私钥就无法生成有效的签名,因此无法伪造签名。这就是为什么使用公钥进行签名验证是安全的原因。

三、如何验证JWT是否有效

对于一个JWT,我们可以使用Base64解码,获取前两部分信息,可以进行token是否过期等验证,具体取决于前两部分具体内容,这是可以人为设置的。
对于签名部分,可以用公钥去验证其有效性(即是否被纂改)。

四、 Why JWT?

为什么是JWT?
首先是不可伪造的安全性,其次,因为你会发现,只要有公钥就可以验证JWT这种token,这也就意味着对于微服务来说,任意微服务都有能力自行验证JWT,而不需要额外的验证模块。这种自校验没有用到网络通信,性能十分好。同时,JWT有时间限制,一定程度上也提高了最坏情况的安全性。

SpringCloud + Nacos环境下抽取Feign独立模块并支持MultipartFile

SpringCloud + Nacos环境下抽取Feign独立模块并支持MultipartFile

一、前提条件和背景

1. 前提

已经部署好Nacos,本文以192.168.101.65:8848为例。

2. 背景

有两个微服务mediacontent,都已经注册到Nacos
后者通过引用Feign实现远程调用前者。
两个微服务都被分为3个子模块:api、service、model,对应三层架构。

请根据自身情况出发阅读本文。

二、Feign模块

1. 依赖引入

首先需要Feign依赖和扩展。

language-xml
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
<!--   openfeign     -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<!--feign支持Multipart格式传参-->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.8.0</version>
</dependency>

需要测试依赖(可选),为了MockMultipartFile类才引入的,非必需功能。

language-xml
1
2
3
4
5
6
7
<!--    测试    -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.1.2</version>
<scope>compile</scope>
</dependency>

其次需要涉及到的微服务的数据模型,根据个人情况而定。

如果只想要它们的数据模型,而不引入不必要的依赖,可以使用通配符*全部过滤掉。

language-xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--   数据模型pojo     -->
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-plus-media-model</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-plus-content-model</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

2. application.yaml配置

填入以下内容,大抵为超时熔断处理。(可选),甚至可以留空。

language-yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
feign:
hystrix:
enabled: true
circuitbreaker:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 30000
ribbon:
ConnectTimeout: 60000
ReadTimeout: 60000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 1

3. 扩展支持MultipartFile

新建一个配置类,如下,
主要是Encoder feignEncoder()使得Feign支持MultipartFile类型传输。
MultipartFile getMultipartFile(File file)是一个工具方法,和配置无关。

language-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
@Configuration
public class MultipartSupportConfig {


@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;

@Bean
@Primary//注入相同类型的bean时优先使用
@Scope("prototype")
public Encoder feignEncoder() {

return new SpringFormEncoder(new SpringEncoder(messageConverters));
}

//将file转为Multipart
public static MultipartFile getMultipartFile(File file) {

try {

byte[] content = Files.readAllBytes(file.toPath());
MultipartFile multipartFile = new MockMultipartFile(file.getName(),
file.getName(), Files.probeContentType(file.toPath()), content);
return multipartFile;
} catch (IOException e) {

e.printStackTrace();
XueChengPlusException.cast("File->MultipartFile转化失败");
return null;
}
}
}

4. 将media-api注册到feign

新建一个类,如下。
@FeignClient(value)要和服务名称对上,即media模块spring.application.name=media-api
@FeignClient(path)要和服务前缀路径对上,即media模块server.servlet.context-path=/media
然后MediaClient中的方法定义尽量和media模块对应的controller函数保持一致。

language-java
1
2
3
4
5
6
7
8
9
10
11
@FeignClient(value = "media-api", path = "/media")
public interface MediaClient {


@RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(
@RequestPart("filedata") MultipartFile file,
@RequestParam(value = "objectName", required = false) String objectName
);
}

三、Media模块

被调用方media模块无需做什么修改。

四、Content模块

测试在content-api上操作。

1. 引入依赖

content模块需要引入刚才feign模块的依赖。

language-xml
1
2
3
<dependency>
<!-- 根据自身情况引入 -->
</dependency>

2. 启用FeignClient

在启动类上加上@EnableFeignClients注解。

3. 测试

新建测试类,如下

language-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
package com.xuecheng.content.service.jobhandler;

import com.xuecheng.feign.client.MediaClient;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

@SpringBootTest
class CoursePublishTaskTest {

@Autowired
MediaClient mediaClient;

@Test
void generateCourseHtml() {

MultipartFile file = new MockMultipartFile(
"filedata",
"filename.txt",
"text/plain",
"Some dataset...".getBytes()
);
UploadFileResultDto upload = mediaClient.upload(file, "/static-test/t-1");
System.out.println(upload);
}
}

启动Media模块,启动测试方法,
具体的Debug和检验,可以通过Media模块对应的controller函数打印日志,检查是否通过MediaClient 被触发。

五、需要澄清的几点

  1. Feign模块不需要注册到Nacos且不需要服务发现
    正确。feign-client模块只是一个包含Feign客户端接口的库,它自身并不是一个独立的微服务。因此,它不需要注册到Nacos,也不需要服务发现功能。这个模块只是被其他微服务模块(如content模块)作为依赖引入。这样做的主要目的是为了代码的重用和解耦,允许任何微服务通过引入这个依赖来调用其他服务。

  2. 只有调用者(如content模块)需要使用@EnableFeignClients注解,被调用者(如media模块)不需要
    正确。@EnableFeignClients注解是用来启用Feign客户端的,它告诉Spring Cloud这个服务将会使用Feign来进行远程服务调用。因此,只有需要使用Feign客户端的服务(在这个例子中是content模块)需要添加这个注解。而被调用的服务(如media模块),只需作为普通的Spring Boot应用运行,提供REST API即可,无需使用@EnableFeignClients

  3. 如何在服务间共享数据模型(如DTOs)而不引入不必要的依赖。
    解决这个问题的一种方法是创建一个共享的库或模块,这个库包含所有服务共享的数据模型。另一种使用依赖剥离,使用通配符(*)可以排除pom.xml中特定依赖的所有传递性依赖。

SpringCloud Gateway(4.1.0) 返回503:原因分析与解决方案

SpringCloud Gateway(4.1.0) 返回503:原因分析与解决方案

一、环境版本

Version
spring-cloud-dependencies 2023.0.0
spring-cloud-starter-gateway 4.1.0
Nacos v2.3.0

二、原因分析

Spring Cloud Gateway 的早期版本中,Ribbon 被用作默认的负载均衡器。随着Spring Cloud的发展,RibbonSpring Cloud LoadBalancer 替代。在过渡期间,为了兼容,Spring Cloud 同时支持了 RibbonSpring Cloud LoadBalancer。然而,从 Spring Cloud 2020.0.0 版本开始,Ribbon 被废弃,Spring Cloud LoadBalancer 成为了推荐的负载均衡方案。

在这个版本变动中,为了提供更大的灵活性,spring-cloud-starter-loadbalancer 被标记为了可选依赖,不再默认包含在 Spring Cloud Gateway 中。因此,在使用 4.1.0 版本的 Spring Cloud Gateway 并需要服务发现和负载均衡功能时,如果没有显式包含这个依赖,就会导致无法处理 lb://URI,从而返回503错误。

三、解决方案

要解决这个问题,您需要在您的项目的 POM 文件中显式添加 spring-cloud-starter-loadbalancer 依赖:

language-xml
1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.1.0</version>
</dependency>

添加后,确保重启应用程序以使配置生效。这样,Spring Cloud Gateway 就能够正确处理基于服务发现的负载均衡,从而避免503错误。

通过理解 Spring Cloud 的历史演变和适应其依赖管理的变化,我们可以更好地维护和优化我们的微服务架构。

SpringCloud + Nacos配置文件加载顺序和优先级详解

SpringCloud + Nacos配置文件加载顺序和优先级详解

在微服务架构中,合理地管理和理解配置文件的加载顺序与优先级对于确保应用的稳定性和灵活性至关重要。特别是在使用 Spring Cloud Alibaba Nacos 作为配置中心的场景下,这一点显得尤为重要。本文将基于一个具体的 bootstrap.yml 配置示例,深入探讨这些概念,并介绍如何通过 Nacos 配置实现本地配置的优先级设置。

一、加载顺序与优先级

1. 示例配置

首先,我们看一下示例的 bootstrap.yml 配置:

language-yaml
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
spring:
application:
name: content-api
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project
config:
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
extension-configs:
- data-id: content-service-${
spring.profiles.active}.yaml
group: xuecheng-plus-project
refresh: true
shared-configs:
- data-id: swagger-${
spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: logging-${
spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
profiles:
active: dev

2. 配置文件分类

Spring Cloud Alibaba Nacos 环境中,我们主要遇到以下类型的配置文件:

  1. 本地配置文件

    • bootstrap.yml / bootstrap.yaml
    • application.yml / application.yaml
  2. Nacos 配置中心的配置文件

    • 共享配置文件 (shared-configs)
    • 扩展配置文件 (extension-configs)
    • 项目应用名配置文件 (${spring.application.name}.yaml / .properties)

3. 加载顺序

  1. **bootstrap.yml / bootstrap.yaml**:首先加载,用于配置应用的启动环境。
  2. Nacos 配置中心的配置文件
    • 先加载 共享配置文件 (shared-configs)
    • 然后是 扩展配置文件 (extension-configs)
    • 最后是 项目应用名配置文件 (${spring.application.name}.yaml / .properties)
  3. **application.yml / application.yaml**:在 Nacos 配置加载之后。

4. 优先级

  1. 项目应用名配置文件:具有最高优先级。
  2. 扩展配置文件:次之,覆盖共享配置。
  3. 共享配置文件:优先级低于扩展配置。
  4. **本地 application.yml / application.yaml**:优先级低于所有从 Nacos 加载的配置。
  5. **本地 bootstrap.yml / bootstrap.yaml**:优先级最低。

二、本地配置优先的设置

Nacos 中,可以通过特定的配置来设置本地配置优先。这可以在 bootstrap.ymlapplication.yml 文件中设置:

language-yaml
1
2
3
4
spring:
cloud:
config:
override-none: true

override-none 设置为 true 时,本地配置文件 (application.yml / application.yaml) 将具有最高的优先级,即使这些配置在 Nacos 中也有定义。这种设置适用于需要在不同环境中覆盖远程配置中心配置的场景。

结论

了解和正确应用 Spring Cloud Alibaba Nacos 中配置文件的加载顺序和优先级,对于确保微服务的正确运行至关重要。此外,通过配置 override-nonetrue,可以灵活地实现本地配置优先的需求,进一步增强了配置管理的灵活性。这些特性使得 Spring Cloud Alibaba Nacos 成为管理微服务配置的强大工具。