• 微服务项目的登录流程


    背景:

    项目背景是黑马商城,通过黑马商城这个微服务项目将前端发送登录请求,到API网关进行登录校验并获取登录用户ID,再将用户ID传递到微服务板块中的MVC拦截器,并且在微服务板块的各个模块中用openfeign的拦截器结合具体的需求来实现功能

    整体的流程图:

     在网关进行的登录验证:

    1. @Component
    2. @RequiredArgsConstructor
    3. @EnableConfigurationProperties(AuthProperties.class)
    4. public class AuthGlobalFilter implements GlobalFilter, Ordered {
    5. private final JwtTool jwtTool;
    6. private final AuthProperties authProperties;
    7. private AntPathMatcher antPathMatcher = new AntPathMatcher();
    8. @Override
    9. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    10. //1:获取请求头
    11. final ServerHttpRequest request = exchange.getRequest();
    12. final String path = request.getPath().toString();
    13. //2:判断是否需要做拦截(登录操作肯定不能拦截)
    14. final boolean flag = extracted(path);
    15. if(flag){
    16. return chain.filter(exchange);
    17. }
    18. //3:获取token并解析
    19. String token = null;
    20. List<String> list = request.getHeaders().get("authorization");
    21. if(!CollUtils.isEmpty(list)){
    22. token = list.get(0);
    23. }
    24. System.out.println(token);
    25. Long userId = null;
    26. try {
    27. userId = jwtTool.parseToken(token);//jwt令牌解析之后返回的Long类型的数据是当前登录用户的id
    28. } catch (Exception e) {
    29. ServerHttpResponse response = exchange.getResponse();
    30. response.setStatusCode(org.springframework.http.HttpStatus.valueOf(HttpStatus.HTTP_UNAUTHORIZED));
    31. return response.setComplete();//直接返回,并且返回的状态码是可控的。
    32. }
    33. //4:传递用户信息就是把用户的id传给下游的拦截器,然后传给微服务模块
    34. final String userInfo = userId.toString();
    35. final ServerWebExchange swe = exchange.mutate()
    36. .request(builder -> builder.header("user-info", userInfo))
    37. .build();
    38. //5:放行
    39. return chain.filter(swe);
    40. }
    41. private boolean extracted(String path) {
    42. for (String pathPattern : authProperties.getExcludePaths()) {
    43. if(antPathMatcher.match(pathPattern, path)){
    44. return true;
    45. }
    46. }
    47. return false;
    48. }
    49. @Override
    50. public int getOrder() {
    51. return -1;
    52. }
    53. }
    1. hm:
    2. jwt:
    3. location: classpath:hmall.jks
    4. alias: hmall
    5. password: hmall123
    6. tokenTTL: 30m
    1. @Data
    2. @ConfigurationProperties(prefix = "hm.auth")
    3. @Component
    4. public class AuthProperties {
    5. private List<String> includePaths;
    6. private List<String> excludePaths;
    7. }

    在网关层做登录校验并且往下游传递用户信息

    我们从上到下来分析: 

    首先@EnableConfigurationProperties这个注解就是用来将配置文件中的信息绑定到对呀的java类上,这里其实也得稍微说一下

    在springboot项目中,你想将配置文件绑定到这个Java对象上,首先你得先确认你这个对象是一个bean对象,将这个对象注册成bean对象的方法有两种@Compont或者在启动类加上面@Scan注解

    等你注册成bean对象之后,你还要用@Configuration("prefix = --")这种注解来将指定的配置文件绑定到这个java对象上。

    还有一种方式是专门针对需要读取配置文件的bean对象的@EnableConfigurationProperties

    我们点进去看也能发现,这个EnableConfigurationProperties自动将这个类注册成了bean对象

    上面的内容总结起来就是说,你要想你的这个类和配置文件中的信息绑定

    你可以先将这个类注册成bean对象用@Component注解,或者在启动类上加@Scan扫描

    注册之后用@ConfigurationProperties (prefix=“xxx”)的方式

    还有一种方式就是直接在这个类上加@ConfigurationProperties 注解即可,并且也要指定前缀。

     我们根据这个配置文件的信息就是说,有些路径需要校验,有些不需要,

    这个很好想,登录校验这种路径肯定不能校验

    JWT登录校验流程-CSDN博客

    具体流程可以看这篇博客。

    接着我们看到这个过滤器:

    首先获取这个请求头,从这个请求头中拿到我们需要的信息

    关于官网的一些概念在这篇博客API网关理解-CSDN博客

    接着就是判断是否需要拦截,这一步上面说过了

    如果不要拦截,直接return chain.filter(exchange)放到下一个拦截器

    下一步就是需要解析token令牌

    这里取出来的步骤还有点区别

    就是这里的请求requet.getHeaders()之后返回的是一个HttpHeaders,这个我们点进去:

    是一个哈希表,所以我们在取一次get("authorization),再取出一个列表的第一个元素就是这个token了。

    接着我们用我们封装好的jwttool进行解析。

    如果说这个解析有误:

    我们可以return response.setComplete();//直接返回。

    用这行代码返回,返回的状态码是可控的

    我们接着走。

    如果解析没有问题,我们就需要将用户的id传给下游的拦截器

    用这个ServerWebExchange的mutate方法可以照着自己的规则创建一个新的 ServerWebExchange 实例,以便在不修改原始交换对象的情况下构造所需的请求或响应。

    最后放行即可。

    在微服务模块的创建MVC创建拦截器获取用户信息

     这里的过程就叫做获取用户id了

    这个的拦截器已经不是平常我们经常用来登录校验的拦截器了

    这也是我自己一开始没想清楚的点:

    这里的拦截器的作用是如果你从网关来的时候带着用户id,我就取出来,没有我也不拦截你,这就和传统的拦截器进行校验有很大区别

    想清楚了这个拦截器的作用

    我们下一步要想

    我们这个拦截器要在哪里写?

    首先我们知道我们这个项目有很多模块,每一个模块都复制粘贴一个相同的拦截器

    那很不现实

    所以我们的解决办法是在common模块中写这个拦截器

    因为我们知道我们所有的模块都会引入这个模块

    这有点类似于抽线公共模块的感觉了

    想清楚了我们直接来看代码:

    1. @Slf4j
    2. public class LoginInterceptor implements HandlerInterceptor {
    3. //在所有的controller方法之前
    4. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    5. // 1.获取请求头中的用户信息
    6. String userInfo = request.getHeader("user-info");
    7. // 2.判断是否为空
    8. if (StrUtil.isNotBlank(userInfo)) {
    9. // 不为空,保存到ThreadLocal
    10. UserContext.setUser(Long.valueOf(userInfo));
    11. }
    12. // 3.放行
    13. return true;
    14. }
    15. //在DisPatchServlet返回给浏览器之后,主要为了保证线程安全
    16. @Override
    17. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    18. UserContext.removeUser();
    19. }
    20. }

    我们来分析:

    我们先从请求头中取用户信息

    我们想我们在网关那一层是怎么存进去的

    1. final ServerWebExchange swe = exchange.mutate()
    2. .request(builder -> builder.header("user-info", userInfo))
    3. .build();

    存在了请求头为user-info里面

    所以我们现在取得话也要照着这个取

    取出来之后,我们仔细看这个判断逻辑

    判断你是否为空,不空我就保存,你为空,我直接给你放行

    最后还有一个这个afterCompletion

    这个方法就是在DisPatchServlet返回给浏览器之前,进行一个用户信息得清理

    主要也是为了保证线程的安全,避免用户的上下文信息被污染。

    用OpenFeign在微服务模块中传递用户信息:

    想说明这个功能,我们来一个具体的业务场景

    我们来看这个具体的流程:

    我们在购物车中执行了下单操作之后,我们的后台处理流程

    首先需要在交易服务中保存订单

    在商品服务中扣除库存

    最后再购物车服务中清理购物车。

    这个时候我们的第一反应可能是我们直接用openfeign的方式远程调用即可

    但是我们还需要想一个问题,就是我们执行这些操作之前,我们是不是都需要获取用户的id啊

    我们清理购物车肯定只能清理自己的购物车

    根据上面的业务流程,我们就可以知道,我们需要在模块之间传递用户的信息

    我们先看一下创建订单的代码。

    1. private final IOrderDetailService detailService;
    2. private final ItemClient itemClient;
    3. private final CartClient cartClient;
    4. @Override
    5. @Transactional
    6. public Long createOrder(OrderFormDTO orderFormDTO) {
    7. // 1.订单数据
    8. Order order = new Order();
    9. // 1.1.查询商品
    10. List<OrderDetailDTO> detailDTOS = orderFormDTO.getDetails();
    11. // 1.2.获取商品id和数量的Map
    12. Map<Long, Integer> itemNumMap = detailDTOS.stream()
    13. .collect(Collectors.toMap(OrderDetailDTO::getItemId, OrderDetailDTO::getNum));
    14. Set<Long> itemIds = itemNumMap.keySet();
    15. // 1.3.查询商品
    16. List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
    17. if (items == null || items.size() < itemIds.size()) {
    18. throw new BadRequestException("商品不存在");
    19. }
    20. // 1.4.基于商品价格、购买数量计算商品总价:totalFee
    21. int total = 0;
    22. for (ItemDTO item : items) {
    23. total += item.getPrice() * itemNumMap.get(item.getId());
    24. }
    25. order.setTotalFee(total);
    26. // 1.5.其它属性
    27. order.setPaymentType(orderFormDTO.getPaymentType());
    28. order.setUserId(UserContext.getUser());
    29. order.setStatus(1);
    30. // 1.6.将Order写入数据库order表中
    31. save(order);
    32. // 2.保存订单详情
    33. List<OrderDetail> details = buildDetails(order.getId(), items, itemNumMap);
    34. detailService.saveBatch(details);
    35. // 3.清理购物车商品
    36. try {
    37. cartClient.removeByItemIds(itemIds);
    38. } catch (Exception e) {
    39. throw new RuntimeException("清理购物车商品失败");
    40. }
    41. // 4.扣减库存
    42. try {
    43. itemClient.deductStock(detailDTOS);
    44. } catch (Exception e) {
    45. throw new RuntimeException("库存不足!");
    46. }
    47. return order.getId();
    48. }

    上面说了要想实现在模块间传递用户信息

    我们需要用到oepnfeign的拦截器:feign.RequestInterceptor

    我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。

    1. public class DefaultFeignConfig {
    2. @Bean
    3. public Logger.Level feignLogLevel(){
    4. return Logger.Level.FULL;
    5. }
    6. @Bean
    7. public RequestInterceptor UserInfoInterceptor(){
    8. return new RequestInterceptor(){
    9. @Override
    10. public void apply(RequestTemplate requestTemplate) {
    11. final Long userId = UserContext.getUser();
    12. if(userId!=null){
    13. requestTemplate.header("user-info",userId.toString());
    14. }
    15. }
    16. };
    17. }
    18. }

    由于FeignClient全部都是在hm-api模块,因此我们在hm-api模块的com.hmall.api.config.DefaultFeignConfig中编写这个拦截器:

    知道了这个oepnfeign的拦截器之后,我们代码的逻辑就很简单了。 

  • 相关阅读:
    rocksdb中测试工具Benchmark.sh用法(基准、性能测试)
    (01)ORB-SLAM2源码无死角解析-(55) 闭环线程→闭环检测:寻找闭环候选关键帧 LoopClosing::DetectLoop()
    06-Linux用户管理
    003编程语言中的类型
    详解 OpenDAL |Data Infra 研究社第三期
    代码随想录动态规划——背包问题总结篇
    猿创征文|大厂说的 代码门禁如何实现?
    Java面试题
    分类之混淆矩阵(Confusion Matrix)
    LiveQing视频点播流媒体RTMP推流服务功能-支持视频点播分屏大屏展示视频轮巡分组播放RMP推流直播大屏展示
  • 原文地址:https://blog.csdn.net/m0_73966521/article/details/141092691