苍穹外卖

1、使用JWT进行用户身份验证和授权,用自定义拦截器完成用户认证,通过ThreadLocal优化鉴权逻辑;

JWT(JSON Web Token)是一种用于在网络应用间传递信息的安全方式。它由三部分组成:头部、载荷和签名。用户在登录成功后,服务器会生成一个JWT令牌并返回给客户端。客户端在后续的请求中将该令牌携带在HTTTP的请求头Authorization中。

自定义拦截器是一种在请求到达处理程序之前或之后执行特定功能的机制。在这种情况下,自定义拦截器可以用于验证请求中的JWT令牌的有效性和完整性。如果JWT令牌有效,则允许请求继续进行处理,否则拒绝请求或返回错误响应。

优化鉴权逻辑怎样做?可以使用ThreadLocal。ThreadLocal是Java中的一个线程级别的变量,它可以在当前线程中存储数据,并且只有当前线程可以访问和修改这些数据。在这种情况下,可以使用ThreadLocal来存储解析后的JWT令牌中的用户信息,例如用户ID、角色等。这样,在后续的请求处理过程中,可以直接从ThreadLocal中获取用户信息,避免了多次解析JWT令牌的性能开销

jwt和过滤器解决了什么问题?登录校验 -> 只有登录后才能进行后续操作

那应该怎么来实现登录校验的操作呢?具体的实现思路可以分为两部分:

第一步,在员工登录成功后,需要将用户登录成功的信息保存起来,记录用户已经登录成功的标识;

第二步,在浏览器发起请求时,需要在服务端进行统一拦截,拦截后进行登录校验。

我们项目中的jwt令牌校验拦截器实现了HandleerInterceptor接口,并重写了preHandle方法(从请求头中获取令牌、校验令牌,-从token中获取用户id并放入threadlocal中-,通过放行、不通过响应401状态码)。再去WebMvcConfiguration配置类中注册自定义的jwt令牌校验拦截器,调用addInterceptors方法进行注册,就可以拦截和放行指定路径下的请求。

2、JTW

JWT令牌组成及创建?

JWT令牌由三个部分组成,三个部分之间使用英文的点来分割:

  • 第一部分:Header(头),记录令牌类型、签名算法等。

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。

  • 第三部分:Signature(签名),防止token被篡改、确保安全性。将Header、Payload加入指定密钥,通过指定签名算法计算而来。

生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码转为串。

JJWT签名是如何被计算出来的?

JWT 的签名是通过 HMAC(Hash-based Message Authentication Code)算法进行计算的。在生成 JWT 时,签名的计算过程如下:

首先,将 JWT 的头部和载荷以 Base64URL 编码的形式串联起来,中间使用一个点号(".")连接,形成未签名的部分。

其次,使用指定的算法(比如在你的代码中使用的是HMAC-SHA256算法)和密钥对上一步得到的未签名部分进行签名计算,得到签名值。

最后,将签名值添加到 JWT 中,形成最终的 JWT。

在验证 JWT 时,解析器会重复这个过程,使用相同的算法和密钥对接收到的 JWT 的头部和载荷进行签名计算,然后将计算得到的签名值与 JWT 中的签名值进行比较,以验证 JWT 的真实性。

为什么JWT令牌被篡改一点,都无法认证成功?

服务器在验证 JWT 时会对(header +"." +payload)部分 使用密钥重新签名,并与令牌中的签名进行比较。如果令牌中的任何部分被篡改,即使是微小的改动,都会导致重新计算的签名与原签名不一致,从而验证失败。

JWT令牌是如何判断时间过的,你怎么得到它设定的时间的?

JWT 令牌中通常包含一个字段用来指定令牌的过期时间(expiration time),即令牌的有效期限。在验证 JWT 令牌时,可以通过这个过期时间字段来判断令牌是否已经过期。要得到 JWT 令牌设定的时间,可以在解析 JWT 令牌后获取载荷部分(Payload)中的过期时间字段,然后将其转换为人类可读的日期时间格式。(过期时间字段通常是 Unix 时间戳格式,表示从 1970 年 1 月 1 日至当前时间的秒数。通过将这个 Unix 时间戳转换为人类可读的日期时间格式,就可以得到 JWT 令牌设定的过期时间。在 Java 中,可以使用 java.util.Date 类或者 java.time.* 包中的类来进行时间的转换和处理。)

3、ThreadLocal 在项目中的应用

①你的项目中是怎么用的呢?

  • ThreadLocal 称为线程局部变量,可以为每个线程单独提供一份存储空间,它的特点是:线程之内,数据共享;线程之间,数据隔离。

  • 在我们的项目中经常使用ThreadLocal来存储用户的登录信息,具体的做法是:

每次用户访问后台都需要经过拦截器鉴权,如果鉴权通过,我们就将登录用户的信息保存到ThreadLocal中,

接下来,在项目的各个代码中就可以轻松的从ThreadLocal中获取用户信息了,

最后,当请求访问完服务离开的时候,还会再次经过拦截器,这个时候就可以清理掉ThreadLocal中的内容了。

为什么要用ThreadLocal

用ThreadLocal 为了优化鉴权逻辑。ThreadLocal是Java中的一个线程级别的变量,它可以在当前线程中存储数据,并且只有当前线程可以访问和修改这些数据。在这种情况下,可以使用ThreadLocal来存储解析后的JWT令牌中的用户信息,例如用户ID、角色等。这样,在后续的请求处理过程中,可以直接从ThreadLocal中获取用户信息,避免了多次解析JWT令牌的性能开销

③我们公司之前用这个出现过脏读问题,你变量是怎么清除。

1、当使用ThreadLocal来存储数据时,每个线程都有自己的ThreadLocal副本,这意味着不同线程中的ThreadLocal变量是相互隔离的。这种特性使得ThreadLocal非常适合在多线程环境下存储线程相关的数据,例如用户身份信息、数据库连接等。

2、然而,如果在使用ThreadLocal存储数据后没有及时清除,就可能导致脏读问题和内存泄漏。具体来说,如果在处理完某个请求或任务后没有清除ThreadLocal中的数据,那么下一个请求或任务可能会复用同一个线程,此时ThreadLocal中仍然保留着上一次请求或任务的数据,从而可能导致下一个请求或任务读取到错误的数据,称为脏读。

3、另外,如果长时间保留大量数据在ThreadLocal中而不清除,就可能导致内存泄漏问题。因为即使线程执行完毕,ThreadLocal中的数据仍然保留在内存中,不会被自动清理,如果这种情况持续发生,就会造成内存资源的浪费和性能问题。

4、因此,为了避免脏读问题和内存泄漏,需要在合适的时机手动调用ThreadLocal.remove()方法来清除ThreadLocal中的数据。这样可以确保每个请求或任务开始时ThreadLocal中不会包含上一次请求或任务的数据,从而保证了数据的独立性和正确性。

④讲讲threadlocal的底层原理。

每个线程内部都有一个ThreadLocalMap对象,用于存储线程本地变量。ThreadLocalMap是一个Entry数组,每个Entry包含了一个ThreadLocal对象和对应的值。当通过ThreadLocal对象设置或获取值时,实际上是通过ThreadLocal对象在ThreadLocalMap中寻找对应的值。由于每个线程有自己独立的ThreadLocalMap,所以不同线程之间的ThreadLocal变量不会相互干扰,保证了线程之间的数据隔离性。

总的来说,ThreadLocal通过利用每个线程内部的ThreadLocalMap来实现线程本地变量的存储和访问,从而实现了线程间数据的隔离,避免了多线程环境下的数据共享问题。

4、通过AOP切面编程自动记录和修改数据的操作时间等信息,减少重复编码;

在我们项目中,比如新增商品或者分类时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工或者编辑商品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是在我们的系统中很多表中都会有这些字段。所以,我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。

实现步骤:

  1. 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法(用一个枚举类代表新增和更新两种状态,并放入自定义注解中)

  2. 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值

  3. 在 Mapper 中对应的方法上加入 AutoFill 注解

5、使用Redis作为缓存机制,对于商品高频数据进行存储,提高系统性能和响应速度;

我们项目中有两个地方用到了Redis,分别是:店铺营业状态标识和小程序端的套餐、商品列表数据

  1. 对于店铺营业状态标识,仅仅需要在redis中保存一个0|1值即可。这里之所以选择redis而没有采用数据库来存储,就是因为这个字段太简单了,没有必要在数据库中新建一张表;而且,这个状态的访问又比较频繁,放在redis中,提高了查询速度的同时,可以减轻数据库的访问压力

  2. 对于小程序端的套餐、商品列表数据,由于小程序端以后的访问量比较大,所以采用Redis提高访问速度

具体的操作步骤就是:在查询列表的时候,先判断Redis缓存中是否有数据,如果有,直接返回给前端

如果没有,再去查询数据库,并将查询结果保存到redis中,并返回给前端

为了保证Redis和数据库中数据的实时一致性,在对数据库相关数据进行增删改操作时,需要同时清理Redis中数据。

SpringCache在项目中的应用

SpringCache是Spring提供的一个缓存框架,它可以通过简单的注解实现缓存的操作,我们常用的注解有下面几个:

  • @EnableCaching: 开启基于注解的缓存

  • @CachePut: 一般用在查询方法上,表示将方法的返回值放到缓存中

  • @Cacheable: 一般用在查询方法上,表示在方法执行前先查看缓存中是否有数据,如果有直接返回;如果没有,再调用方法体查询数据并将返回结果放到缓存中;他有两个关键属性:

    • value: 缓存的名称,每个缓存名称下面可以有多个key

    • key: 缓存的key,支持Spring的表达式语言SPEL语法

  • @CacheEvict: 一般用在增删改方法上 ,用于清理指定缓存,可以根据key清理,也可以清理整个value下的缓存

SpringCache还有一个有点,就是可以随意切换底层的缓存软件,比如:Redis、EHCache等等

本项目中商品和套餐列表的缓存用到了SpringCache。实现步骤:导入Spring Cache和Redis相关maven坐标、在启动类上加入@EnableCaching注解,开启缓存注解功能、在用户端接口SetgoodsController的 list 方法上加入@Cacheable注解、在管理端接口SetgoodsController的 save、delete、update、startOrStop等方法上加入CacheEvict注解。

6、通过WebSocket进行客户端和服务器的长连接,向前端传信息实现来单提醒和用户催单功能;

①WebSocket和HTTP协议:

  • HTTP的通信是短连接;是单向的,要先请求后响应,类似于对讲机

  • WebSocket通信是长连接;双向的、实时的,客户端和服务端可以同时发消息,类似于手机通话

  • HTTP和WebSocket底层都是TCP连接

我们在项目中大部分场景下都是使用HTTP协议,只有在高实时场景(视频弹幕、体育实况更新)下,使用WebSocket(服务器长期维护长连接需要一定的成本、各个浏览器支持程度不一、WebSocket 是长连接,受网络限制比较大,需要处理好重连)

②项目在提醒商家接单时,用户催单发送提醒时使用了webSocket

  1. 通过WebSocket实现客户端页面和服务端保持长连接状态

  2. 当客户下单后,调用WebSocket的相关API实现服务端向客户端推送消息

  3. 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报

  4. 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content

7、SpringTask在项目中的应用

SpringTask是Spring框架提供的一种任务调度工具,可以按照约定的时间自动执行某个代码逻辑。它的一个关键注解是@Scheduled,此注解标注在方法上,用于设置方法的调用时机。(应用场景:信用卡每月还款提醒、入职纪念日为用户发送通知)

在我们的项目中,超时订单的状态改变用到了SpringTask,通过@Scheduled注解下的cron属性定义任务的触发时间:(在自定义定时任务类中实现订单状态定时处理)

  • 每隔1分钟检查是否有超过15分钟未支付的订单,如果有就将订单取消

  • 每天凌晨1点检查前一天是否有派送中的订单,如果有将订单状态改成已完成

8、使用Swagger进行前后端联调,进行接口测试。

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。它是在开发阶段使用的框架,帮助后端开发人员做后端的接口测试。它的主要作用是:

  1. 使得前后端分离开发更加方便,有利于团队协作

  2. 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担

  3. 功能测试

Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。

knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!

目前,一般都使用knife4j框架。

在我们的项目中,首先,在pom.xml中添加依赖;然后,在配置类WebMvcConfiguration.java中加入 knife4j 相关配置(也就是通过knife4j生成接口文档、再设置静态资源映射,否则接口文档页面无法访问);最后,通过接口文档访问路径 http://ip:port/doc.htmlhttp://localhost:8080/doc.html进行访问测试。

当然,通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性:

注解说明
@Api 用在类上,例如Controller,表示对类的说明
@ApiModel 用在类上,例如entity、DTO、VO
@ApiModelProperty 用在属性上,描述属性信息
@ApiOperation 用在方法上,例如Controller中的方法,说明方法的用途、作用
通过 Swagger 就可以生成接口文档,那么我们就不需要 Yapi 了?

1、Yapi 是设计阶段使用的工具,管理和维护接口

2、Swagger 在开发阶段使用的框架,帮助后端开发人员做后端的接口测试

posted @   守漠待花开  阅读(413)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示