自学内容网 自学内容网

Java 开发常见面试题3

两个类加载器加载同一个单例类,会有几个对象 ?

如果两个不同的类加载器分别加载同一个单例类,那么会创建两个单例对象。原因如下:

类加载器的隔离性:
每个类加载器都有自己的命名空间,加载的类在JVM中是不同的,即使它们来自同一个字节码文件。
通过ClassLoader加载的类在JVM中是以<ClassLoader, Class>的组合为唯一标识的。因此,不同的类加载器加载同一个类时,这些类在JVM中被视为完全不同的类。

单例模式的限制:
单例模式通常依赖于类的静态变量来存储唯一实例。
静态变量是类级别的,每个类加载器加载的类有独立的静态变量空间。
因此,如果两个类加载器加载了同一个单例类,会导致每个类加载器各自维护一个单例实例,最终创建两个不同的实例。

以下代码展示了如何通过两个类加载器加载同一个单例类,并验证是否创建了多个实例。

public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {
        // 私有构造函数
        System.out.println("Singleton instance created");
    }

    public static Singleton getInstance() {
        return instance;
    }
}
import java.io.*;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        // 获取当前类路径下的 Singleton 类字节码文件路径
        String classFilePath = "out/production/YourProjectPath/Singleton.class";

        // 定义第一个自定义类加载器
        ClassLoader loader1 = new CustomClassLoader(classFilePath);
        // 定义第二个自定义类加载器
        ClassLoader loader2 = new CustomClassLoader(classFilePath);

        // 使用 loader1 加载 Singleton 类
        Class<?> class1 = loader1.loadClass("Singleton");
        // 使用 loader2 加载 Singleton 类
        Class<?> class2 = loader2.loadClass("Singleton");

        // 获取单例实例
        Object instance1 = class1.getMethod("getInstance").invoke(null);
        Object instance2 = class2.getMethod("getInstance").invoke(null);

        // 打印结果
        System.out.println("Instance 1: " + instance1);
        System.out.println("Instance 2: " + instance2);
        System.out.println("Are instances equal? " + (instance1 == instance2));
    }
}

// 自定义类加载器
class CustomClassLoader extends ClassLoader {
    private String classPath;

    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classBytes = loadClassBytes();
            return defineClass(name, classBytes, 0, classBytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    }

    private byte[] loadClassBytes() throws IOException {
        try (InputStream inputStream = new FileInputStream(classPath);
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            return outputStream.toByteArray();
        }
    }
}

运行代码后,输出结果类似以下内容:

Singleton instance created
Singleton instance created
Instance 1: Singleton@1d251891
Instance 2: Singleton@7c53a09e
Are instances equal? false

结论
两个实例被创建:每个类加载器加载了一个独立的Singleton类,导致创建了两个不同的实例。
实例不同:因为两个类由不同的类加载器加载,虽然类名相同,但它们在 JVM 中是不同的。

如何避免这种情况?
在一些场景中,我们希望确保真正的单例。可以通过以下方法解决:
确保只有一个类加载器加载单例类:例如,将单例类放在根类加载器(Bootstrap ClassLoader)或系统类加载器中。
通过注册中心管理实例:使用全局的注册表或容器(如Spring的单例 Bean 容器)统一管理实例。

类加载机制

单例模式

JVM 调优参数

JVM 报错信息

JVM 报错信息通常是由 Java 程序在运行时引发的错误或异常,这些信息可以帮助开发人员定位和解决问题。以下是一些常见的 JVM 报错信息类型及其含义:

1. Java 编译错误(编译时异常)
这些错误发生在代码编译期间,通常由语法错误或不合法的代码引起。

示例:

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello World")
    }
}

报错信息:

Error: ';' expected

含义:缺少分号。

2. 运行时异常(RuntimeException)
这些异常是程序在运行时因逻辑错误引发的,属于未检查异常(Unchecked Exception)。

常见类型:
NullPointerException:调用空对象的方法或访问空对象的属性。
ArrayIndexOutOfBoundsException:访问数组时索引超出范围。
ClassCastException:类型转换不正确。
ArithmeticException:除数为零等算术错误。

示例:

public class Test {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str.length());
    }
}

报错信息:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null

含义:试图调用空对象的length()方法。

3. 错误(Error)
这些通常是更严重的问题,JVM 无法处理。错误继承自java.lang.Error。

常见类型:
StackOverflowError:递归调用无终止条件导致栈溢出。
OutOfMemoryError:JVM 内存不足。
ClassNotFoundException:类加载失败(通过反射)。
NoClassDefFoundError:类加载失败(类在编译时存在但运行时丢失)。

示例:

public class Test {
    public static void main(String[] args) {
        recursiveMethod();
    }

    public static void recursiveMethod() {
        recursiveMethod();
    }
}

报错信息:

Exception in thread "main" java.lang.StackOverflowError

含义:方法递归调用没有结束条件,导致栈溢出。

4. 类加载错误
这些错误发生在类加载或方法解析阶段。

示例:

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("NonExistentClass");
    }
}

报错信息:

Exception in thread "main" java.lang.ClassNotFoundException: NonExistentClass

含义:尝试加载的类不存在。

5. 线程相关异常
IllegalThreadStateException:线程状态非法。
InterruptedException:线程在阻塞操作时被中断。
示例:

public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread();
        thread.start();
        thread.start(); // 重复启动
    }
}

报错信息:

Exception in thread "main" java.lang.IllegalThreadStateException

含义:线程已经启动,不能再次启动。

6. JVM 内部错误
这些是 JVM 本身的问题,通常由配置或代码问题引发。

示例:
UnsupportedClassVersionError:
当 JDK 版本低于编译版本时,运行会报错。
错误信息:

java.lang.UnsupportedClassVersionError: Test has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0

解决方法:升级运行环境的 JDK 版本。

OutOfMemoryError:
JVM 堆、非堆、或其他内存区域耗尽。
错误信息:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

GC Overhead Limit Exceeded:
GC 时间过长,JVM 认为已经无法回收足够内存。
错误信息:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

7. 网络或 I/O 错误
这些异常与网络通信或文件操作有关。

示例:

import java.io.File;
import java.io.FileInputStream;

public class Test {
    public static void main(String[] args) throws Exception {
        FileInputStream fis = new FileInputStream(new File("nonexistent.txt"));
    }
}

报错信息:

Exception in thread "main" java.io.FileNotFoundException: nonexistent.txt (No such file or directory)

含义:尝试打开的文件不存在。

JVM 报错解决建议
查看错误栈信息:
报错信息中会列出出错的类、方法、行号,可以定位问题代码。

检查 JVM 配置:
确保-Xms和-Xmx等参数配置合理。
遇到OutOfMemoryError时,可以通过调大堆大小或分析内存泄漏解决。

日志记录:
使用日志框架(如 Log4j、SLF4J)记录关键信息,帮助定位问题。

使用调试工具:
使用调试器(如 IDEA、Eclipse)或分析工具(如 VisualVM、JProfiler)定位问题。

多线程问题:
确保线程安全,避免死锁。
使用线程池代替直接创建线程。

OOM 输出报错日志

OutOfMemoryError (OOM) 错误通常会输出详细的报错日志,帮助开发者分析和定位问题。在某些情况下,还可以通过配置 JVM 参数生成更详细的诊断信息,比如堆转储文件(Heap Dump)。

默认的 OOM 日志输出
当程序抛出 java.lang.OutOfMemoryError 时,默认日志信息包含以下内容:
错误类型(java.lang.OutOfMemoryError)。
错误描述(如Java heap space或GC overhead limit exceeded)。
错误发生的线程栈(如果有)。
示例代码

import java.util.ArrayList;
import java.util.List;

public class OOMExample {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[10 * 1024 * 1024]); // 每次分配10MB
        }
    }
}

输出日志示例

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at OOMExample.main(OOMExample.java:8)

解释:
OutOfMemoryError: Java heap space:表示堆内存不足。
at OOMExample.main(OOMExample.java:8):指示问题发生在代码的第8行。

常见 OOM 类型及日志描述
java.lang.OutOfMemoryError: Java heap space

含义:堆内存不足。
原因:分配了太多的对象或对象无法被 GC 回收。
解决方法:
增大堆内存大小(-Xmx 参数)。
优化代码,减少不必要的对象分配。
分析内存泄漏问题。
java.lang.OutOfMemoryError: GC overhead limit exceeded

含义:垃圾回收占用了过多的 CPU 时间,但仍未能回收足够的内存。
解决方法:
增大堆内存。
检查是否存在内存泄漏。
java.lang.OutOfMemoryError: Metaspace

含义:元空间(Metaspace)内存不足。
原因:加载了太多的类或动态生成类。
解决方法:
增大元空间大小(-XX:MaxMetaspaceSize 参数)。
检查是否有类加载器泄漏。
java.lang.OutOfMemoryError: Direct buffer memory

含义:直接内存不足。
原因:使用了大量的直接内存(如ByteBuffer.allocateDirect)。
解决方法:
增大直接内存大小(-XX:MaxDirectMemorySize 参数)。
避免频繁分配直接内存。
java.lang.OutOfMemoryError: Unable to create new native thread

含义:系统无法分配新线程。
原因:线程数超过操作系统限制或内存不足。
解决方法:
减少线程创建。
增加系统资源(如堆内存或线程栈内存)。

如何增强 OOM 日志输出
可以通过配置 JVM 参数,获取更详细的诊断信息。

  1. 启用堆转储文件
    在 OOM 发生时生成堆转储文件,便于分析内存问题。
    JVM 参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof

-XX:+HeapDumpOnOutOfMemoryError:启用堆转储。
-XX:HeapDumpPath=path:指定堆转储文件的保存路径。
当 OOM 发生时,JVM 会生成一个堆转储文件(如heapdump.hprof),可以使用工具(如 VisualVM、Eclipse MAT)分析内存使用情况。

2. 启用 GC 日志
记录垃圾回收的详细信息,便于分析 GC 性能和内存问题。
JVM 参数(以 JDK 11 为例):

-Xlog:gc*:file=gc.log:time,uptime,level,tags

日志示例:

[5.468s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 20M->8M(32M) 10.745ms

通过分析 GC 日志,可以判断是否是频繁的垃圾回收导致 OOM。

3. 设置内存警告阈值
启用 JVM 内存使用警告阈值:

-XX:OnOutOfMemoryError="kill -9 %p"

如何分析 OOM
堆转储分析:
使用工具如 VisualVM 或 Eclipse MAT 打开堆转储文件,查看内存中存在哪些对象占用了大量空间。
GC 日志分析:
检查垃圾回收频率和内存回收效果,判断是否需要调整堆大小或使用不同的 GC 策略。
代码分析:
检查循环中分配大量对象、缓存未清理、线程池未正确回收等潜在问题。

堆内存溢出报错日志

堆内存溢出(OutOfMemoryError: Java heap space) 会在 Java 程序发生错误时输出报错日志。这个错误通常发生在 JVM 堆内存不足时,无法为新的对象分配内存。

堆内存溢出的输出日志
当程序触发堆内存溢出时,JVM 会输出类似以下的错误日志:

示例日志:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:166)
    at java.util.ArrayList.add(ArrayList.java:450)
    at java.util.ArrayList.add(ArrayList.java:463)
    at OOMExample.main(OOMExample.java:10)

java.lang.OutOfMemoryError: Java heap space:这是最常见的错误信息,表示堆内存空间已满,无法分配新的对象。
at OOMExample.main(OOMExample.java:10):指出出错的代码位置(第10行)。
这个日志信息可以帮助开发人员定位问题代码及内存不足的情况。

如何增强堆内存溢出的日志信息
为了更好地分析堆内存溢出问题,可以启用一些 JVM 参数,输出更详细的日志和诊断信息:

1. 生成堆转储文件(Heap Dump)
当发生 OutOfMemoryError 时,JVM 可以生成一个堆转储文件(.hprof),该文件包含所有对象的详细信息,有助于分析内存泄漏或过多的对象分配。
JVM 参数:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof

-XX:+HeapDumpOnOutOfMemoryError:启用堆转储,当堆内存溢出时生成堆转储文件。
-XX:HeapDumpPath=./heapdump.hprof:指定堆转储文件的路径。

2. 启用 GC 日志
通过记录垃圾回收日志,可以更好地了解内存回收的情况,以便进一步分析堆内存溢出的根本原因。
JVM 参数(JDK 9 及以上):

-Xlog:gc*:file=gc.log:time,uptime,level,tags

-Xlog:gc*:启用 GC 日志,记录垃圾回收的详细信息。
file=gc.log:将日志输出到指定的文件中。

日志内容可能如下:

[5.468s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 20M->8M(32M) 10.745ms

通过分析这些日志,可以观察到是否有频繁的垃圾回收,或者是否存在内存泄漏导致堆内存溢出。

3. 设置内存警告
如果希望在堆内存即将溢出时收到警告,可以设置内存警告阈值。

-XX:OnOutOfMemoryError="kill -9 %p"

当 OOM 发生时,JVM 会触发指定的动作(在此示例中会终止进程)。

如何分析堆内存溢出问题
查看堆转储文件(Heap Dump): 使用工具如 VisualVM 或 Eclipse MAT(Memory Analyzer Tool) 打开堆转储文件。这些工具可以帮助你查看内存中的对象,分析哪些对象占用了大量内存,以及是否存在内存泄漏。
分析 GC 日志: 分析 GC 日志可以帮助你了解垃圾回收的频率和效率。频繁的 Full GC 或长时间停顿的 GC 可能是堆内存溢出的原因。
代码检查:
对象泄漏:检查是否有长生命周期的对象不被及时释放。
缓存管理:检查是否有大对象或缓存未被清理。
无限增长的数据结构:如无限增长的列表、队列等。

如何预防堆内存溢出
优化内存使用:
避免不必要的对象创建。
使用合适的集合类,如选择合适的大小和类型。
使用对象池来重复使用对象,减少垃圾回收的压力。
增加堆内存大小:
如果机器内存充足,可以通过以下参数增大堆内存:

-Xms512m -Xmx2g

-Xms:指定初始堆内存大小。
-Xmx:指定最大堆内存大小。

定期清理缓存:
定期清理不再需要的缓存或对象。
使用内存分析工具:
使用 VisualVM 或 JProfiler 等工具监控应用程序的内存使用情况,发现潜在的内存泄漏。

Arraylist 和 linkedlist 的扩容机制有什么不同

1. ArrayList 的扩容机制
底层原理:ArrayList 是基于动态数组实现的。
扩容触发条件:当向 ArrayList 添加元素时,如果底层数组的容量不足,就会触发扩容。
扩容策略:
默认初始容量为 10(JDK 1.8+)。
扩容时,新的容量为原容量的 1.5 倍:newCapacity = oldCapacity + (oldCapacity >> 1)。
如果扩容后的容量仍不足以容纳新元素,直接调整为能够容纳新元素的最小所需容量。
扩容后,会将旧数组中的数据复制到新数组中。
性能特点:
扩容涉及数组的重新分配和数据拷贝,代价较高。
适合频繁读取、随机访问的场景。

2. LinkedList 的扩容机制
底层原理:LinkedList 是基于双向链表实现的。
扩容触发条件:LinkedList 不需要像 ArrayList 那样手动扩容,因为链表结构的特点允许动态增加或删除节点,不涉及容量限制。
扩容策略:没有扩容机制,只需在需要时动态分配节点。
性能特点:
增删操作性能较好,因为不需要移动大量元素。
由于链表节点的动态分配,会增加内存开销(包括每个节点的对象头和指针存储)。
适合频繁增删操作但随机访问较少的场景。

使用 map,如果想保证添加顺序应该用什么集合?

在 Java 中,如果需要使用 Map 并同时保证 元素的添加顺序,应该使用 LinkedHashMap。它是 Java 提供的一种按插入顺序维护元素的集合类型。

1. 为什么选择 LinkedHashMap?
底层实现:
LinkedHashMap 继承自 HashMap,并在内部通过一个 双向链表 来维护键值对的插入顺序。
每次插入新元素时,它会将新元素添加到链表尾部,同时保留 HashMap 的快速查找能力(时间复杂度为 O(1))。
特性:
插入顺序:默认情况下,LinkedHashMap 按照键值对插入的顺序存储。
访问顺序(可选):可以通过构造函数设置为 按访问顺序维护元素(如 LRU 缓存)。

2. 使用示例

import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapExample {
    public static void main(String[] args) {
        // 创建一个 LinkedHashMap(默认按插入顺序)
        Map<String, String> map = new LinkedHashMap<>();

        // 插入元素
        map.put("1", "Apple");
        map.put("2", "Banana");
        map.put("3", "Cherry");

        // 遍历元素(按插入顺序)
        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // 输出:
        // 1: Apple
        // 2: Banana
        // 3: Cherry
    }
}

3. 按访问顺序维护
如果需要按 访问顺序(如最近使用)来维护顺序,可以在创建 LinkedHashMap 时使用以下构造方法:

LinkedHashMap<K, V> map = new LinkedHashMap<>(initialCapacity, loadFactor, true);

第三个参数 true 表示启用 按访问顺序。
示例:

import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapAccessOrderExample {
    public static void main(String[] args) {
        // 按访问顺序排序
        LinkedHashMap<String, String> map = new LinkedHashMap<>(16, 0.75f, true);

        // 插入元素
        map.put("1", "Apple");
        map.put("2", "Banana");
        map.put("3", "Cherry");

        // 访问一个元素
        map.get("1");

        // 遍历元素(注意 "1" 已经被访问过)
        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // 输出:
        // 2: Banana
        // 3: Cherry
        // 1: Apple
    }
}

4. LinkedHashMap 与其他 Map 的对比

特性HashMapLinkedHashMapTreeMap
顺序无序插入顺序或访问顺序自然顺序或自定义顺序
实现基于哈希表基于哈希表 + 双向链表基于红黑树
查找效率O(1)O(1)O(log n)
适用场景快速查找,但无序保证插入顺序或访问顺序有序存储,适合范围查询

es 支持事务吗 ?

Elasticsearch(ES)本身不支持传统关系型数据库的 多表事务,因为它是一个分布式的全文搜索和分析引擎,主要设计目标是高性能搜索和实时数据分析,而不是关系型数据库那样的事务处理。

不过,Elasticsearch 提供了一些机制来实现 类似事务的保证,但这些机制并不是完全的 ACID 事务。以下是详细分析:

1. Elasticsearch 的事务特性
单文档级的事务支持:
Elasticsearch 在单个文档的操作中提供原子性。
操作(如 index、delete)针对单个文档是 原子性的,要么成功,要么失败。
如果你只涉及单个文档的修改,ES 可以保证操作的完整性。

分布式特性与多文档操作:
对多个文档的修改(如批量 bulk 操作)时,Elasticsearch 不能保证事务一致性。
如果 bulk 中的一部分操作失败,已经完成的操作不会自动回滚。

不支持隔离级别:
Elasticsearch 是一个最终一致性系统,无法提供关系型数据库中的隔离级别(如 READ COMMITTED、SERIALIZABLE 等)。
数据在主分片和副本分片之间的同步可能会有短暂延迟。

2. Elasticsearch 提供的解决方案
虽然不支持完整的事务,Elasticsearch 提供了以下功能来满足一定的事务性需求:
(1) 乐观并发控制(Optimistic Concurrency Control, OCC)
用于防止更新冲突:
Elasticsearch 每个文档都有一个 _version 字段。
更新文档时可以指定预期的 _version 值,只有在 _version 匹配时更新才能成功。
示例:

POST /index/_update/1
{
  "doc": {
    "field": "newValue"
  },
  "if_seq_no": 4,
  "if_primary_term": 1
}

这确保了只有当文档处于特定版本时,修改才会被应用。

(2) 批量操作(Bulk API)
Bulk API 允许批量插入、更新或删除多个文档,从而提高性能。
然而,Bulk 操作并不是事务性操作,部分失败不会触发回滚。

(3) 写一致性(Write Consistency)
Elasticsearch 确保写操作至少写入主分片和一些副本分片后才认为成功。
可以通过设置 wait_for_active_shards 参数来指定成功写入所需的副本数。

(4) 数据回滚模拟
通过 手动补偿事务 的方式模拟回滚。例如,记录失败的操作并重新执行,或者通过日志回放恢复一致性。

(5) 基于外部服务的事务管理
如果需要更复杂的事务逻辑,可以结合消息队列(如 Kafka)或分布式事务协调器(如 Saga 模式、两阶段提交(2PC))来实现跨文档的事务管理。

3. Elasticsearch 事务的替代实现
如果必须要事务支持,可以考虑以下替代方案:

(1) 使用 Elasticsearch + 数据库
在系统中同时使用关系型数据库(如 MySQL、PostgreSQL)来管理事务,而 Elasticsearch 仅用作索引和搜索引擎。
数据的最终一致性可以通过以下方式实现:
异步同步:数据库的变更通过工具(如 Logstash 或 Kafka)实时同步到 Elasticsearch。
双写机制:应用程序同时写入数据库和 Elasticsearch。

(2) Elasticsearch + 分布式事务框架
使用分布式事务框架(如 Seata 或 TCC 模式)协调 Elasticsearch 和其他存储之间的事务。
例如,在写入数据库和 Elasticsearch 时,分别用预留、提交和回滚步骤控制数据一致性。

(3) 使用 Elastic 提供的 CCR(Cross-Cluster Replication)和 ILM(Index Lifecycle Management)
Elastic 提供了跨集群复制(CCR)功能和索引生命周期管理(ILM),可以用来实现数据的高可用和版本管理,但仍然不具备完整事务能力。

4. 总结
单文档操作:Elasticsearch 支持原子性。
多文档操作:Elasticsearch 不支持事务一致性。
如果需要事务能力,可以:
使用关系型数据库处理事务,ES 作为辅助搜索引擎。
借助分布式事务框架或手动补偿来模拟事务。
推荐场景:Elasticsearch 更适合于对事务一致性要求不高但需要高性能搜索和分析的场景,例如日志处理、全文检索和实时分析系统。如果你的场景对事务一致性要求较高,建议结合数据库或其他机制共同使用。

jwt 和 session 的区别 ?

在 Web 应用开发中,JWT(JSON Web Token) 和 Session 都是用于实现用户身份认证的两种常见机制。它们的原理和特点有很大区别。

1. 定义与机制

对比项JWTSession
定义JSON Web Token 是一种自包含的令牌,包含用户信息和签名,用于客户端与服务器之间的无状态认证。Session 是在服务器端维护用户状态的机制,通常通过会话 ID 标识用户会话。
存储位置存储在客户端(如浏览器的 localStorage、sessionStorage 或 Cookie)。会话数据存储在服务器端,客户端通过 Cookie 或 URL 参数传递会话 ID。

2. 数据存储与安全

对比项JWTSession
数据存储JWT 将用户信息编码为 Token 存储在客户端。信息可以包括用户标识、角色、权限等。Session 数据存储在服务器端(如内存、数据库、Redis 等)。
安全性如果不使用 HTTPS,JWT 可能会被窃取。需要设置短期有效期、签名验证、加密以提高安全性。Session 数据本身保存在服务器,比较安全,但需要防范会话劫持(Session Hijacking)。
数据大小JWT 通常较大(包含用户信息和签名),每次请求都会发送给服务器。Session ID 较小,网络传输开销低。

3. 性能与扩展性

对比项JWTSession
性能JWT 是无状态的,服务器无需存储会话信息,因此性能较好,适合分布式系统。Session 需要服务器维护会话信息,内存占用较大,对分布式支持较差。
扩展性客户端持有 Token,适合微服务和分布式架构。多服务器下需要通过共享存储(如 Redis)实现 Session 同步。

4. 认证流程
JWT 认证流程
用户登录,服务器验证用户凭证。
服务器生成一个包含用户信息的 JWT,并签名后返回给客户端。
客户端将 JWT 存储(如 localStorage 或 Cookie)。
后续请求,客户端将 JWT 附加到请求头(如 Authorization: Bearer )。
服务器通过解码和验证 JWT 来完成认证,无需维护会话状态。

Session 认证流程
用户登录,服务器验证用户凭证。
服务器生成一个唯一的 Session ID,并将会话信息存储在服务器端。
服务器将 Session ID 返回给客户端(通常通过 Cookie)。
后续请求,客户端携带 Session ID,服务器通过该 ID 查找会话数据完成认证。

5. 适用场景

对比项JWTSession
适用场景1. 分布式系统或微服务架构。2. 无状态认证需求(如第三方 API)。1. 单服务器架构或会话信息需要频繁变更的系统。2. 对安全性要求较高的场景(如需要控制用户会话)。

6. 优缺点对比

对比项JWTSession
优点- 无需服务器维护状态,易于扩展。- 支持跨域认证和第三方登录。- 会话数据存储在服务器端,更加安全。- 可灵活修改会话信息。
缺点- Token 长度大,增加网络开销。- Token 过期后需重新登录或手动刷新。- 需要维护服务器状态,占用内存,影响性能。- 不易支持分布式架构。

7. 总结:如何选择?
选择 JWT 的场景:
系统是分布式架构或微服务架构,服务器需要无状态认证。
需要客户端保存和传递用户的认证信息。
需要支持跨域或第三方登录。

选择 Session 的场景:
系统是单体架构,或者服务器容易管理会话状态。
数据对安全性要求高,不希望客户端存储敏感信息。
用户会话需要频繁变更(如动态更新权限)。

在实际开发中,很多场景会结合两者的优点,例如使用 Session 管理登录状态,而 JWT 用于访问微服务的 API。

在项目中的 jwt 密码是怎么存储的?

在项目中,JWT 密钥(通常是用来签名和验证 JWT 的密钥)非常敏感,需要妥善存储以防止泄露,从而保障系统的安全性。以下是一些最佳实践来存储和管理 JWT 密钥:

1. JWT 密钥类型
对称密钥(HMAC 签名算法,如 HS256、HS512):
使用一个共享的密钥(secret key)来签名和验证 JWT。

非对称密钥(RSA 或 ECDSA 签名算法,如 RS256、ES256):
使用 私钥 签名 JWT,公钥 验证 JWT。
私钥需要严格保密,公钥可以公开。

2. JWT 密钥的存储方式
(1) 配置文件存储(适用于开发和测试环境)
存储位置:
将密钥存储在配置文件中(如 application.yml、application.properties 或 .env 文件)。
示例:

jwt:
  secret: "your-very-secret-key"

对于非对称密钥,可以存储私钥文件路径:

jwt:
  private-key-path: "/path/to/private_key.pem"
  public-key-path: "/path/to/public_key.pem"

安全性:
配置文件应设置严格的访问权限。
不要将敏感配置文件直接提交到代码仓库(如 Git),可通过 .gitignore 忽略这些文件。
使用环境变量覆盖配置文件中的密钥。

(2) 使用环境变量(推荐)
存储方式:
将密钥存储在环境变量中,避免直接写入配置文件或代码。
示例(Linux/Mac):

export JWT_SECRET="your-very-secret-key"

Spring Boot 中可以通过以下方式加载环境变量:

jwt:
  secret: ${JWT_SECRET}

安全性:
环境变量只存储在服务器运行环境中,避免硬编码和暴露在代码仓库中。
适合云环境和容器化部署(如 Docker、Kubernetes)。

(3) 使用密钥管理服务(KMS)或专用密钥存储
云提供商密钥管理服务:
AWS:AWS KMS(Key Management Service)
Azure:Azure Key Vault
GCP:Google Cloud KMS

本地密钥管理工具:
HashiCorp Vault

优点:
自动化管理密钥的生命周期(如轮换、过期)。
提供加密密钥的存储,防止泄露。
支持访问控制和审计。

使用方式:
应用程序通过 API 从 KMS 获取密钥。
示例(Spring Boot 使用 AWS KMS):

jwt:
  secret: ${KMS_SECRET}

(4) 加密存储密钥

存储方式:
如果需要在文件系统中存储密钥,可以对密钥进行加密存储。
使用对称加密算法(如 AES)加密密钥文件,并通过主密钥解密。

示例:
加密后的密钥存储在文件中。
程序启动时解密该文件,加载密钥。

安全性:
确保主密钥的安全(通常通过环境变量或硬件安全模块存储)。

3. 密钥的安全管理策略
(1) 密钥轮换
定期更换 JWT 密钥,防止长期使用导致的安全问题。
需要支持密钥的多版本机制:
可以维护一个密钥列表,支持旧密钥验证,使用新密钥签名。

(2) 最小权限访问
确保只有应用程序需要访问密钥的部分才能读取密钥。
配置文件、环境变量、密钥存储服务的访问权限应严格限制。

(3) 禁止硬编码
绝不要将密钥直接写入代码中。
通过配置文件、环境变量或密钥管理工具加载密钥。

(4) HTTPS 加密传输
使用 HTTPS 保护密钥在网络中的传输,防止密钥被窃听。

(5) 审计与监控
对密钥的访问进行审计。
检测和监控任何异常的密钥访问行为。

4. 总结
开发环境:可通过配置文件或环境变量存储密钥。
生产环境:推荐使用环境变量或密钥管理服务(如 AWS KMS)。
遵循密钥管理的最佳实践:限制访问、定期轮换、禁止硬编码和使用加密传输。
这种方法能最大程度地降低密钥泄露风险,提升系统的安全性。


原文地址:https://blog.csdn.net/qq_41893505/article/details/145160528

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!