全局异常处理

为了统一处理异常,可以有个全局异常处理的类。用@RestControllerAdvice修饰。


@RestControllerAdvice:只能修饰类。

注解将作用在所有注解了@RequestMapping的控制层(控制器)的方法上。

该注解包含了@ControllerAdvice和@ResponseBody。而@ControllerAdvice又只包含了@Component。


@ExceptionHandler:只能修饰方法。

用于指定捕获异常后的处理方法。

@ExceptionHandler的参数为需要被捕获处理的异常类(需要该类继承Throwable)。如果为空,则默认为@ExceptionHandler修饰的方法的参数列表中列出的任何异常。

@ExceptionHandler通常与@RestControllerAdvice配合使用时,用于全局处理控制层(控制器)中出现的异常。


/**
 * 全局异常处理器。
 * RestControllerAdvice和ControllerAdvice是全局接口异常处理的类。
 * 当发生异常没有捕获时,便会触发这个异常。
 *
 * @author zhengqingquan
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * BusinessException为自定义的业务异常。
     * 这个方法只去捕获BusinessException异常。
     *
     * @return
     */
    @ExceptionHandler(BusinessException.class)
    public void BusinessExceptionHandler(BusinessException e) {
        log.error("businessException" + e.getMessage(), e);
    }

    /**
     * 运行时异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public void RuntimeExceptionHandler(RuntimeException e) {
        log.error("runtimeException", e);
    }

}

一个线程池优化的例子。

假设有这样一个需求:查询商品的基本信息、查询商品的图片列表、查询商品的描述信息,最后再一块返回。

如果使用同步的方式,代码就是按照固定的顺序执行。

public Map<String, Object> detail(long goodsId) {
    //创建一个map
        
    //step1:查询商品基本信息,放入map
    //step2:查询商品图片列表,返回一个集合放入map
    //step3:查询商品描述信息,放入map

    return map;
}

假设上面每个步骤耗时200ms,此方法总共耗时至少600毫秒。其他还涉及到网络传输耗时,估计总共会在700ms左右。但此接口是有优化空间的。

看一下上面的逻辑,整个过程是按顺序执行的,实际上3个查询之间是没有任何依赖关系,所以说3个查询可以同时执行,那对这3个步骤采用多线程并行执行。

比如下面的方式。

package com.example.demo;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

public class DemoApplication {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long starTime = System.currentTimeMillis();
        Map<String, String> map = new DemoApplication().funcabc();
        System.out.println(map);
        System.out.println("耗时(ms):" + (System.currentTimeMillis() - starTime));
    }

    //创建个线程池
    ExecutorService executorService = Executors.newFixedThreadPool(10);

    public String funcA() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(200);
        return "执行方法funcA";
    }

    public String funcB() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(200);
        return "执行方法funcB";
    }

    public String funcC() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(200);
        return "执行方法funcC";
    }

    public Map<String, String> funcabc() throws ExecutionException, InterruptedException {
        HashMap<String, String> result = new HashMap<>();

        Future<String> resultA = executorService.submit(this::funcA);
        Future<String> resultB = executorService.submit(this::funcB);
        Future<String> resultC = executorService.submit(this::funcC);

        result.put("funca", resultA.get());
        result.put("funcb", resultB.get());
        result.put("funcc", resultC.get());

        return result;
    }
}

上面的代码使用了异步和线程池的方式,最后通过Future获取异步执行结果。

最后执行的结果为:

{funca=执行方法funcA, funcb=执行方法funcB, funcc=执行方法funcC}
耗时(ms):227

ArrayList扩容机制

最开始的ArrayList()会创建长度为0的数组。

如果指定了长度ArrayList(num)会创建指定长度的数组。

如果使用了集合作为参数ArrayList(Collection)那么会使用集合的大小作为初始容量。


往ArrayList中添加元素有两种方式,一种是ArrayList.add()另一种是ArrayList.addAll()


对于ArrayList.add()来说,往数组里添加第一个元素时,会发现容量不够了,因为发现容量是0,这时就进行第一次扩容。首次扩容为10。

默认扩容是上一次容量的1.5倍。其实1.5倍也不是很准确,它是根据数组的长度进行移位来确定扩容倍速的。比如数组长度为15,0000 1111,>>移位后0000 0111,为7.然后再加上原始容量15,得到22。

对于ArrayList.addAll()来说,第一次扩容会扩容成10。当添加的元素超过10时,第一次扩容会扩容成添加元素个数的值(比如添加11个元素,第一次扩容就会扩容成11)。

用addAll,当原始容量不够时,会将默认长度(10)和添加元素个数进行比较,两者取最大值作为扩容长度。


/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

上面的代码是在源码中的,表示默认容量是10。

JVM的内存模型

JVM的内存模型分为一下几个区域:

  • 程序计数器:
  • Java虚拟机栈:
  • 本地方法栈:类似于Java虚拟机栈,面向的是 Native 本地方法(如JNI)
  • 堆:也叫Java堆,唯一的目的就是存放对象实例。
  • 方法区:

  • 常量池表:用于存放编译期生成的各种字面量与符号引用。
  • 运行常量池:属于方法区的一部分,用于存储编译期间生成的各种字面量和符号引用,这些内容在类加载后进入常量池中。

其中程序计数器、虚拟机栈、本地方法栈是线程私有的。而堆、方法区、运行时常量池是线程共享的。

2023.03.04这两天的面试题

Java的基本数据类型。

基本数据类型是四类八种:

  • 字符类型:char
  • 布尔类型:boolean
  • 数值类型:byte(1字节)、short(2字节)、int(4字节)、long(8字节)
  • 浮点型:float、double。

扩展——引用数据类型非常多:数组、类、接口、枚举等。

引用数据类型也按照GC来划分:强引用、弱引用、软引用、虚引用。

数据流的种类。

按照流的方向分为:输入流、输出流

按照实现功能分为:节点流、处理流

按照数据处理的单位分为:字节流、字符流

说几个你知道的异常类

空指针异常、MMO异常、下标越界异常、字符串与数字的转换异常、文件未找到异常等。

扩展——异常的关系,分为error和exception。而exception还细分为runtimeExcepton和其它。

runtimeException叫做运行异常,其它的叫编译异常。编译异常需要在源代码中检查,因此也叫做受检异常。runtimeException也叫做非受检异常。error也是为非受检异常。

知道hashmap和hashtable么,这两个有什么区别?

hashmap是线程不安全、hashtable是线程安全的。

hashtable是早期的Java集合,实现的是其他接口,并继承其他的类。但hashmap是实现了Map接口。

hashtable不允许有Null键或者Null值,而hashmap允许有一个Null键或多个Null值。

还有一些迭代器的区别和性能的区别。因为线程安全,hashtable在竞争条件下的性能会受到影响。

还有很多其他的。

说说set和list的异同点。

Set和List是Java集合框架中两种常见的接口,它们在数据存储和访问方式上有一些异同点。

相同点:

  1. 都是集合接口:Set和List都是Java集合框架中的接口,用于表示一组对象的集合。
  2. 支持存储多个元素:Set和List都可以存储多个元素,可以根据需要进行动态添加或删除元素。

不同点:

  1. 元素的顺序:List中的元素是有序的,每个元素都有一个对应的索引值,可以根据索引值访问和操作元素。而Set中的元素是无序的,不保证元素的存储顺序。
  2. 元素的唯一性:List允许存储重复的元素,同一个元素可以多次出现在List中。而Set中不允许存储重复的元素,每个元素在Set中只能出现一次。
  3. 实现类:Java提供了多个Set和List的实现类。常见的Set实现类有HashSet、TreeSet和LinkedHashSet,而常见的List实现类有ArrayList、LinkedList和Vector。
  4. 检索和遍历:由于List中的元素是有序的且有索引,可以通过索引值直接访问和操作元素,适用于根据位置进行快速检索。而Set中的元素是无序的,不能通过索引值访问元素,需要使用迭代器或者增强型for循环进行遍历。
  5. 性能:由于List中的元素是有序的且有索引,对于索引访问的操作性能较好。而Set需要维护元素的唯一性,可能需要进行更多的判断和比较操作,性能上相对较低。

综上所述,Set和List在元素的顺序、唯一性、实现类、检索和遍历方式以及性能等方面存在一些异同点。根据具体的需求和场景,选择合适的集合类型可以提高代码的效率和可读性。

spring的两大特点

DI和AOP、依赖注入和面向切面编程。

说说你知道的几个数据库引擎

在MySQL workbench中有九种。

  • InnoDB
  • MyISAM
  • MEMORY
  • archive
  • 。。。

MySQL支持几种存储引擎

redis的五种数据类型

  • String
  • List
  • Hash
  • Set
  • Zset

几种返回的响应状态码。

五大类。

  • 信息响应 (100 – 199)
  • 成功响应 (200 – 299)
  • 重定向消息 (300 – 399)
  • 客户端错误响应 (400 – 499)
  • 服务端错误响应 (500 – 599)

几种请求类型。

一共九种。

HTTP1.0 定义了三种请求方法: GET,POST 和 HEAD 方法。

HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。

有什么鉴权模块。

JSON Web Token(JWT)

乐观锁和悲观锁

乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)是在并发控制中使用的两种不同的策略,用于管理多个线程或进程同时访问和修改共享数据的情况。

  1. 乐观锁: 乐观锁的基本思想是,假设并发访问的线程之间不会产生冲突,因此不会阻塞任何线程。线程在读取数据时并不立即加锁,而是在更新数据时检查是否有其他线程已经修改了相同的数据。如果没有冲突,线程可以继续执行更新操作。如果发现冲突,乐观锁机制会触发回滚或重试操作。

乐观锁的实现通常通过版本号或时间戳来实现。在每次更新操作时,会比较当前数据的版本号或时间戳与之前读取时的值是否一致,如果一致则可以进行更新,否则认为发生了冲突。

  1. 悲观锁: 悲观锁的基本思想是,假设并发访问的线程之间会产生冲突,因此对数据进行加锁,以防止其他线程对数据的修改。线程在读取和更新数据之前,会先获取锁,确保其他线程无法同时访问和修改该数据。只有当前线程释放了锁之后,其他线程才能获得锁并进行操作。

悲观锁的实现通常依靠数据库的锁机制或并发控制工具来实现,例如使用数据库中的行级锁或使用 synchronized 关键字来保证数据的独占访问。

选择乐观锁还是悲观锁取决于具体的应用场景和需求。乐观锁适用于读操作较多、冲突较少的情况,可以提高并发性能。悲观锁适用于写操作较多、冲突较多的情况,可以确保数据的一致性和完整性。

用什么自动化部署方式。CICD

Jinkins、Docker、GitLab CI/DI、云服务提供商的自动化部署。提交代码以后可以直接部署。

如何解决脏数据的问题。

脏数据(Dirty Data)是指在数据库或数据集中存在错误、不一致或无效的数据。这些数据可能是由于人为错误、系统故障、数据传输问题或其他原因导致的。

脏数据有:无效数据、冗余数据、不一致数据、缺失数据。

内存数据跟磁盘数据内容不一致称为脏数据。

例如设置了缓存失效时间(比如1小时),那么在这一小时内,如果有人更新了DB的数据,只要缓存不失效,Redis就不会主动读取DB并更新数据,那么用户看到的其实都是旧的数据,与DB不一致,此谓缓存脏读。

找到更新数据的入口,每次更新DB都同步或异步更新缓存。

我记得好像有不同的更新方式。

如何处理脏数据?

数据清洗,异常值检测、数据验证和修复,数据合并和去重。

  1. 数据清洗:通过数据清洗过程,识别和纠正脏数据。这包括删除重复数据、修复格式错误、填补缺失值、校验数据一致性等操作。
  2. 异常值检测:检测和处理异常值,这些值可能是数据采集或输入过程中的错误或异常情况。可以使用统计方法、数据规则或机器学习算法来识别和处理异常值。
  3. 数据验证和修复:实施数据验证规则和约束,确保数据的准确性和完整性。定期进行数据验证,并根据需要修复或纠正错误的数据。
  4. 数据合并和去重:如果存在多个数据源或数据副本,进行数据合并和去重操作,以确保数据的一致性和唯一性。

如何避免脏数据?

数据采集和输入验证、数据库约束和规则、数据访问控制、数据培训和文档化、数据监控和审核。

  1. 数据采集和输入验证:在数据采集和输入过程中进行验证,包括验证数据格式、范围、类型和约束等。使用输入验证、正则表达式、预定义规则等来确保有效数据的录入。
  2. 数据库约束和规则:定义和应用数据库约束和规则,包括主键、外键、唯一性约束、检查约束等,以确保数据的完整性和一致性。
  3. 数据访问控制:实施适当的数据访问控制机制,限制对数据的访问和修改,防止非授权的数据修改或错误操作。
  4. 数据培训和文档化:提供数据培训和文档,确保数据录入和处理人员了解数据的规则、格式和要求,并正确操作数据。
  5. 数据监控和审核:定期监控和审查数据,发现和解决潜在的脏数据问题。使用数据质量工具或建立监控系统来识别异常和错误数据。

可以最大程度地减少脏数据的发生,并提高数据的准确性、一致性和可靠性。

说说spring对你来说厉害的地方。

轻量级和非侵入性:Spring 框架采用轻量级的设计原则,不需要强制性地继承或实现特定的类或接口。开发人员可以根据需要选择使用 Spring 提供的功能,而无需修改现有的应用程序架构。

容器管理:Spring 容器提供了对象的创建、配置和管理。通过配置文件或注解,开发人员可以声明和组织对象及其依赖关系,实现松耦合的组件化开发。

丰富的集成支持:Spring 提供了广泛的集成支持,可以轻松地集成和使用其他流行的框架和技术,如数据库访问、Web 开发、安全性、消息传递等。

开放源代码和活跃的社区支持:Spring 是一个开源框架,具有庞大和活跃的开发者社区。这意味着开发人员可以充分利用社区提供的资源、文档和支持来解决问题和获取帮助。

如何使用mybatis

如何优化(从数据库、缓存、多线程方向)

数据库连接的开销非常大、如何优化连接池。

操作数据库需要和数据库建立连接,拿到连接之后才能操作数据库,用完之后销毁。数据库连接的创建和销毁其实是比较耗时的,真正和业务相关的操作耗时是比较短的。每个数据库操作之前都需要创建连接,为了提升系统性能,后来出现了数据库连接池,系统启动的时候,先创建很多连接放在池子里面,使用的时候,直接从连接池中获取一个,使用完毕之后返回到池子里面,继续给其他需要者使用,这其中就省去创建连接的时间,从而提升了系统整体的性能。

如何解决负载均衡。

  • DNS实现负载均衡
  • 硬件负载均衡
  • 软件负载均衡

缓存?

cookie和localStorage和sessionStorage

这三个都是和客户端那边存储信息相关的。

  • cookie,主要用来保存登录信息。
  • localStroage,生命周期是永久的。
  • sessionStroage,生命周期与浏览器相关。
  1. Cookie:
    • Cookie 是由服务器发送给浏览器并存储在客户端的小型文本文件。它通常用于跟踪用户会话、实现用户认证、存储用户偏好等。
    • Cookie 在每次HTTP请求中都会被发送到服务器,因此可用于在客户端和服务器之间共享数据。
    • Cookie 可以设置过期时间,在过期之前会一直保留在客户端。可以设置持久性 Cookie,使其在浏览器关闭后仍然存在,或设置会话 Cookie,使其在用户会话结束时过期。
  2. localStorage:
    • localStorage 是HTML5引入的持久性存储机制,允许在浏览器中存储键值对的数据。
    • localStorage 存储的数据没有过期时间,除非主动清除或通过代码删除,否则会一直保留在客户端。
    • localStorage 只能通过JavaScript访问,数据在同一域名下的页面间共享。
  3. sessionStorage:
    • sessionStorage 也是HTML5引入的存储机制,类似于localStorage,但是数据在浏览器会话结束后会被清除。
    • sessionStorage 适用于存储临时性的会话数据,它只在当前会话期间有效,当用户关闭浏览器选项卡或窗口时,数据会被删除。

总结:

  • Cookie 是最早的用于在客户端存储数据的机制,适用于跟踪用户和存储简单的数据,但每次请求都会发送到服务器。
  • localStorage 提供了持久性存储,适用于在同一域名下的页面间共享数据。
  • sessionStorage 提供了临时性存储,数据在会话结束后被清除,适用于会话期间的数据共享。

跨域是什么,如何解决。

出现了以下情况中的任意一种,那么它就是跨域请求:

  • 协议不同,如 http 和 https;
  • 域名不同;
  • 端口不同。
  • JSONP(JSON with Padding)
  • CORS(Cross-Origin Resource Sharing)
  • 反向代理,反向代理是将客户端的请求转发到真正的服务端,从而解决跨域问题。
  • WebSocket

在 Spring  Boot 中跨域问题有很多种解决方案,比如以下 5 个:

  • 使用 @CrossOrigin 注解实现跨域;
  • 通过配置文件实现跨域;
  • 通过 CorsFilter 对象实现跨域;
  • 通过 Response 对象实现跨域;
  • 通过实现 ResponseBodyAdvice 实现跨域。

当然如果你愿意的话,还可以使用过滤器来实现跨域,但它的实现和第 5 种实现类似

对不起,没面试成功是我还是太菜了。

线程问题。

守护线程和用户线程的区别是什么?

  • 用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
  • 守护线程:运行在后台,为其他前台线程服务。也可以说守护线程是JVM中非守护线程的“佣人”。一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作。(属于是不求同年同月生,但求同年同月死)

如果Main函数所在的线程就是一个用户线程,Main函数启动的同时在JVM内部同时还启动了好多守护线程,比如垃圾回收线程,也比如Windows 下监听Ctrl+Break的守护进程。

需要注意以下几点:

  • 设置守护线程的方法setDaemon(true)必须要在线程启动start()前执,否则会抛出IllegalThreadStateException 异常。
  • 在守护线程中产生的新线程也是守护线程。
  • 守护线程中不能依靠finally块的内容来确保执行关闭或清理资源的逻辑。因为一旦所有用户线程结束运行,守护线程会随JVM一起结束工作,所以守护线程中的finally语句块可能无法被执行。
  • 因此不是所有的任务都可以分配给守护线程来执行的,比如读写操作或者计算逻辑。因为用户进程结束,自然读写操作就会被中断,而计算逻辑也得不出结果。

线程的上下文切换是什么?

多线程会共同使用一组计算机上的CPU,而线程数大于给程序分配的CPU 数量时,
为了让各个线程都有执行的机会,就需要轮转使用CPU。不同的线程切换使用CPU
发生的切换数据等就是上下文切换。

JVM的垃圾回收算法

JVM有四种垃圾回收算法:

  • 标记清除算法(Mark-Sweep)
  • 复制算法(copying)
  • 标记整理算法(Mark-Compact)
  • 分代收集算法(Generational)

标记清除算法(Mark-Sweep):

标记清除算法分为两个阶段:

1、通过可达性分析,标记出所有需要回收的对象。

2、回收被标记的对象所占用的空间。

优点:实现简单、不需要对象进行移动。

缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。

复制算法(copying):

为了解决清除算法效率不高的问题,产生了复制算法。

把内存空间划分为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用的区域,把存活对象复制到另一个区域中,最后将当前使用的区域的可回收的对象进行回收。

优点:按顺序分配内存即可,实现简单、运行高效、不用考虑内存碎片。

缺点:可用的内存大小缩为原来的一半,对象存活率高时会频繁进行复制。

标记整理算法(Mark-Compact):

标记整理算法,与标记清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使用它们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。

优点:解决了标记清理算法存在的内存碎片问题。

缺点:仍需要进行局部对象移动,一定程度上降低了效率。

分代收集算法(Generational):

当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法是根据对象的存活周期将内存划分为几块。一般包括新生代(年轻代)、老年代和永久代。新生代基本采用复制算法,老年代和永久代采用标记整理算法。

在新生代中使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。而标记清除算法在老年代中的效率不高,在内存回收后容易产生大量内存碎片。因此老年代使用标记整理算法。

函数式编程

  • 函数可以作为一等公民
    • 函数可以赋值给变量(函数表达式写法)
    • 函数可以在变量之间来回传递
    • 函数可以作为另一个函数的参数
    • 函数作为另一个函数的返回值
    • 函数存储在另一个数据结构中
  • 函数式编程: 通常我们对函数作为头等公民的编程方式, 称之为函数式编程
 // 1. 函数的声明(推荐)
  function foo() {
    console.log("foo");
  }

  // 2. 函数的表达式
  var bar = function () {
    console.log("bar");
  };

  // 3. 函数可以作为一等公民
  // 函数可以赋值给变量(函数表达式写法)
  var bar1 = function () {
    console.log("bar");
  };
  // 函数可以在变量之间来回传递
  var bar2 = bar1;
  bar2(); // bar

  // 函数可以作为另一个函数的参数
  function foo1(fn) {
    fn();
  }
  foo1(bar1); // bar

  // 函数作为另一个函数的返回值
  function foo2() {
    return bar;
  }
  foo2()(); //bar

  // 函数存储在另一个数据结构中
  var obj = {
    name: "zgc",
    eating: function () {
      console.log("eating");
    },
  };

  obj.eating(); // eating

  // 4. 函数式编程: 通常我们对函数作为头等公民的编程方式, 称之为函数式编程

滨海扇贝

大家好,欢迎来到我的博客,这里是扇贝。

我出生在泉州。以现在城市化的进程来看,泉州只能算是一个滨海小城。泉州的基础设施和城市职能都不算高,但我很喜欢这里。这里清晨的空气通常很潮湿,闻起来甜甜的。晚上的风吹得也很舒服。

我最喜欢的小说是小川糸的《山茶文具店》。我没什么大理想,只想偏安一隅。像书里的主角那样,成为一个温柔有温度的人,能找到自己想守护的东西,也有能力去保护它。

在准备写这篇文章的时候,我想我应该写一些自己的功绩。但以它人的眼光来看,这些功绩又显得苍白且不值一提。如果要说有什么值得骄傲的事情,也就是在这里我还可以写点东西。如果有什么觉得很厉害的话,我翻译并分享了一本国外关于插件的书叫《Beginning Lua with World of Warcraft Addons》。

重新审视自己,我从小学到大学毕业,再到现在,好像都没有什么值得吹嘘的东西。大多只是平平稳稳地走了过来。等反应过来的时候已经成为了一名程序员。

在我接触到编程的时候,我尝试着写过插件,写过一些桌面小程序、写过一些web小项目。大多都是基于自己的需求去开发。有了自己的博客,有了自己的Github,上面也上传了我以前写的东西。但这些远远不够,以前是我眼光太窄,只看自己想看到的东西。现在才发现,我必须看到时代发展需要的东西。

要学的东西很多,我希望我能坚持下去。

至于我为什么叫扇贝,这主要是沿用以前玩《魔兽世界》取的血精灵术士角色名称——嚣张的扇贝。取名时参考了我高中朋友的建议——用一个形容词加一个看似无关的东西组成一个名字。那段时间在游戏中结识的朋友在生活上给了我很大的帮助,在遇见他们的那段时间我度过了很多开心时光,我永远会喜欢并感激他们。

JWT学习笔记

JWT介绍

JWT标准:RFC 7519: JSON Web Token (JWT) (rfc-editor.org)

jwt.io 官网:https://jwt.io/

百度:JWT(JSON WEB Token)的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT:Json Web Token,是基于Json的一个公开规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,他的两大使用场景是:认证数据交换

  • 认证:认证是JWT的最常用场景。只要用户完成登录,其随后的请求都会包含JWT,以允许用户访问经由当前JWT授权的路由、服务或者是资源。由于开销小且能够被简单应用在跨域访问上,JWT在分布式站点上所支持的单点登录(SSO)已经是当前它被广泛应用的一个特性。
  • 信息交换:JWT是一种在各参与方之间安全传递信息的良好方法。由于JWT可以被签名(例:使用公钥/秘钥对),因而可用于确认发送者自称的身份。除此之外,由于signature使用header和payload进行计算,也可以验证内容没有被篡改。

Session方案

**是什么要用token?**Session 存放在服务器端,Session ID无法实现共享

Token+Redis方案

解决Session集群无法共享的问题–>将token存到redis中

  • Token 类似于 Session ID
  • Token 依赖于 Redis 真实token存放value值

使用Token缺点:每次都需要根据token查询真实内容,对服务器端(比如Redis)压力就比较大。

JWT方案

JWT和token 最大的区别:

token依赖于Redis,token存放value数据比较安全;而JWT不需要依赖于服务器端,将数据信息内容直接存放在客户端(浏览器)

传统的token

传统的Token,例如:用户登录成功生成对应的令牌,key为令牌 value:userid,隐藏了数据真实性 ,同时将该token存放到redis中,返回对应的真实令牌给客户端存放。

客户端每次访问后端请求的时候,会传递该token在请求中,服务器端接收到该token之后,从redis中查询如果存在的情况下,则说明在有效期内,如果在Redis中不存在的情况下,则说明过期或者token错误。

JWT组成的部分

  1. Header(头) 作用:记录令牌类型、签名算法等 例如:{“alg”:”HS256″,”type”,”JWT}
  2. Payload(有效载荷)作用:携带一些用户信息 例如{“userId”:”1″,”username”:”mayikt”}
  3. Signature(签名)作用:防止Token被篡改、确保安全性 例如 计算出来的签名,一个字符串