面试项目技术点
1.1SpringSecurity认证流程
1.1.1注册
用户输入信息注册,使用PasswordEncoder
的实现类BCryptPasswordEncoder
对密码进行加密存储到数据库中。
BCrypt
是一种基于哈希+随机盐值的方式进行加密。
1.1.2登录认证
登录的业务包括:用于输入用户名+密码–>登录
- SpringSecurity的核心是Filter Chain(过滤器链)
- 当用户登录发送请求时,首先会被
UsernamePasswordAuthenticationFilter
过滤器拦截请求,并根据用户名和密码,封装Authentication
对象。- 然后通过认证管理器
AuthenticationManager
对Authentication
对象进行认证。- 认证的过程就是调用
UserDetailsService
接口的实现类当中的loadUserByUsername
方法。- 在
loadUserByUsername
方法当中可以根据用户名查询数据库查询到对应的用户信息和权限信息,最后封装为UserDetails
对象返回。- 通过
DaoAuthenticationProvider
对象将UserDetails
对象进行处理。包括通过PasswordEncode
对比UserDetails
中的密码与Authentication
中密码是否一致,如果不一致则认证失败。如果认证成功,填充用户信息和授权信息,封装为Authentication
对象返回。- 将从
Authentication
对象中获取到用户信息和权限信息存入redis缓存。- 并根据用户主键生成Token加入返回给客户端浏览器。
- 用户登录成功。
下面是DaoAuthencitionProvider
使用PasswordEncoder
对用户密码进行验证。
1.1.3后续请求认证
- 创建token拦截器。
- 从请求头当中获取到token,如果不存在token则放行(1.不需要认证的请求 2.需要认证但是未携带token)。
- 存在token,根据用户id查询redis缓存中对应的用户信息和权限信息,将其存入到SpringSecurity的上下文对象
SecurityContextHolder
当中。后续的授权就是FilterSecurityInterceptor
从SecurityContextHolder
获取到对应的权限信息。
1.2SpringSecurity鉴权流程
鉴权就是使用的是SpringSecurity
的过滤器链当中的FilterSecurityInterceptor
从SecurityContextHolder
获取到对应的权限信息。开启鉴权的方案有两种。
方法级安全管控
- 在SpringSecurity的配置类中添加注解
@EnableGlobaMethodSecurity
开启方法级安全管控。@EnableGlobalMethodSecurity
注解的属性prePostEnabled = true
会解锁@PreAuthorize
和@PostAuthorize
两个注解。**@PreAuthorize 注解会在方法执行前进行验证,而 @PostAuthorize 注解会在方法执行后进行验证。** - 其中对应的属性
hasAuthority
就可以设置对应的权限字符串。
- 在SpringSecurity的配置类中添加注解
配置文件配置接口鉴权
- 在
SpringSecurity
的配置类(实现了WebSecurityConfigurerAdapter
接口)中的configure
中配置对应接口的鉴权规则。
- 在
1.3自定义失败处理
认证授权失败都是由SpringSecurity
的过滤器链当中的ExceptionTranslationFilter
所捕获。
- 认证异常:实现
AuthenticationEntryPoint
接口来实现自定义认证异常处理。 - 授权异常:实现
AccessDeniedHandler
接口来处理授权异常处理。
2.下单业务
- 负责订单模块下单业务,采用
令牌机制
保证下单接口的幂等性,采用延迟队列完成定时关单。
2.1接口幂等性
接口幂等性概述
接口幂等性就是用户的一次请求和多次请求产生的结果是相同的,不会因为多次点击差生不同的结果。
哪些情况需要保证接口幂等性
- 用户的多次点击
- 页面回退再提交
如何保证接口幂等性
令牌机制
服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。
如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。
但是令牌机制存在一些问题,在高并发情况下应该确保查询缓存、对比令牌和删除缓存是原子性的,可以采用Lua脚本来实现。
锁机制
数据库悲观锁
数据库乐观锁机制
分布式锁
采用分布式锁的方案就是,当执行一个下单的请求的时候,会获取到分布式锁,同一订单只能被处理一次,从而确保了下单接口的幂等性。在释放锁之前,其他请求将会被阻塞,等待当前请求完成。
数据库层面的唯一约束
- 唯一约束
- 主键约束
下面的业务就是当响应订单页的时候,返回前端防重令牌
:
2.2锁定库存
下单成功后,需要去扣减库存,但是不能直接扣减库存,因为在用户下单之后,未付款之前,用户可能会取消订单或者是未付款直至订单超时。
所以应该先锁定库存,确保本次下单有足够的库存。
锁定库存操作采用乐观锁
机制,stock - stock_locked >= #{count}
部分用于确保在更新数据时,所需的库存仍然充足。这样的确可以作为一种乐观锁的实现,因为它在更新之前检查了条件,以确保库存不会因为并发操作而不正确地减少。如果库存不足,更新操作就会失败,从而避免了超卖的问题。
并将锁定的库存信息放入到消息队列当中,用于当订单取消
、订单未支付
时解锁库存。
2.3可靠消息+最终一致性解决分布式事务
可靠消息+最终一致性解决方案是对BASE理论的实现,就是基于消息队列来完成的,适用于高并发的场景。
在业务处理服务中,事务未提交之前,将消息数据存入到消息队列当中,消息队列对消息数据进行处理,这种方式不能够保证数据的强一致性,但是只要能够保证消息的可靠消费,就能够保证最终一致性,并且这种方式是异步的,能够提升系统的并发性能。
上图中,当关单消费者关单后,将对应的订单消息发送到库存解锁队列
当中,由对应的解锁库存消费者
解锁库存,即如果库存工作单处于锁定状态
就进行解锁。
目的是为了防止由于超时关单处理过慢,库存解锁
先行 执行,发现订单状态为未支付
,就不做处理,导致库存不能够解锁。
具体流程图如下:
2.4.1延迟队列定时关单
延迟队列
就是基于死信队列
实现的
- 一共有一个交换机和两个队列,其中一个是延迟队列,一个是处理死信消息的队列
- 交换机分别和两个队列绑定,并且在延迟队列当中配置
死信交换机
、死信routing-key
和消息的TTL
- 创建订单后,将订单的信息发送到交换机,然后路由到对应的
延迟队列
,当消息过期之后,发送到死信交换机(就是当前交换机),并路由到对应的处理死信消息的队列
,最后由对应的消费者进行关单。
2.4.2延迟队列解锁库存
下单成功后会锁定库存,在锁定库存成功后,将锁定的库存信息(库存工作单:订单号、商品列表和商品锁定的库存
)持久化并放入到延迟队列当中。
库存解锁的场景
- 下单成功,但是订单过期被系统自动取消,或被用户手动取消
- 下单成功,库存锁定成功,但是接下来的业务调用失败,导致订单回滚
消费者监听库存的延迟队列:
- 首先判断库存工作单是否存在,不存在,则不用释放库存,因为是库存服务自己回滚
- 判断库存工作单中的订单是否存在,不存在则表示订单在调用其他服务时出错回滚,释放库存
- 判断订单的状态,订单已支付,不用释放库存,订单已取消(自动取消or手动取消),释放库存
3.项目模块
- 网关模块
- 曲谱模块
- 用户模块
- 检索模块
- 第三方服务模块
- 消息模块
- 订单模块
- 购物车模块
- 商品模块