微服务项目的登录流程

背景:

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

整体的流程图:

 在网关进行的登录验证:

@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private final JwtTool jwtTool;

    private final AuthProperties authProperties;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1:获取请求头
        final ServerHttpRequest request = exchange.getRequest();
        final String path = request.getPath().toString();
        //2:判断是否需要做拦截(登录操作肯定不能拦截)
        final boolean flag = extracted(path);
        if(flag){
            return chain.filter(exchange);
        }
        //3:获取token并解析
        String token = null;
        List<String> list = request.getHeaders().get("authorization");
        if(!CollUtils.isEmpty(list)){
           token = list.get(0);
        }
        System.out.println(token);
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token);//jwt令牌解析之后返回的Long类型的数据是当前登录用户的id
        } catch (Exception e) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(org.springframework.http.HttpStatus.valueOf(HttpStatus.HTTP_UNAUTHORIZED));
            return response.setComplete();//直接返回,并且返回的状态码是可控的。
        }
        //4:传递用户信息就是把用户的id传给下游的拦截器,然后传给微服务模块
        final String userInfo = userId.toString();
        final ServerWebExchange swe = exchange.mutate()
                .request(builder -> builder.header("user-info", userInfo))
                .build();
        //5:放行
        return chain.filter(swe);
    }

    private boolean extracted(String path) {
        for (String pathPattern : authProperties.getExcludePaths()) {
            if(antPathMatcher.match(pathPattern, path)){
                return true;
            }
        }
        return false;
    }


    @Override
    public int getOrder() {
        return -1;
    }
}
hm:
  jwt:
    location: classpath:hmall.jks
    alias: hmall
    password: hmall123
    tokenTTL: 30m
@Data
@ConfigurationProperties(prefix = "hm.auth")
@Component
public class AuthProperties {
    private List<String> includePaths;
    private List<String> excludePaths;
}

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

我们从上到下来分析: 

首先@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模块中写这个拦截器

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

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

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

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {


    //在所有的controller方法之前
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的用户信息
        String userInfo = request.getHeader("user-info");
        // 2.判断是否为空
        if (StrUtil.isNotBlank(userInfo)) {
            // 不为空,保存到ThreadLocal
            UserContext.setUser(Long.valueOf(userInfo));
        }
        // 3.放行
        return true;
    }

    //在DisPatchServlet返回给浏览器之后,主要为了保证线程安全
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContext.removeUser();
    }
}

我们来分析:

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

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

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

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

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

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

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

最后还有一个这个afterCompletion

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

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

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

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

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

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

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

在商品服务中扣除库存

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

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

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

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

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

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

private final IOrderDetailService detailService;
    private final ItemClient itemClient;
    private final CartClient cartClient;
    @Override
    @Transactional
    public Long createOrder(OrderFormDTO orderFormDTO) {
        // 1.订单数据
        Order order = new Order();
        // 1.1.查询商品
        List<OrderDetailDTO> detailDTOS = orderFormDTO.getDetails();
        // 1.2.获取商品id和数量的Map
        Map<Long, Integer> itemNumMap = detailDTOS.stream()
                .collect(Collectors.toMap(OrderDetailDTO::getItemId, OrderDetailDTO::getNum));
        Set<Long> itemIds = itemNumMap.keySet();
        // 1.3.查询商品
        List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
        if (items == null || items.size() < itemIds.size()) {
            throw new BadRequestException("商品不存在");
        }
        // 1.4.基于商品价格、购买数量计算商品总价:totalFee
        int total = 0;
        for (ItemDTO item : items) {
            total += item.getPrice() * itemNumMap.get(item.getId());
        }
        order.setTotalFee(total);
        // 1.5.其它属性
        order.setPaymentType(orderFormDTO.getPaymentType());
        order.setUserId(UserContext.getUser());
        order.setStatus(1);
        // 1.6.将Order写入数据库order表中
        save(order);

        // 2.保存订单详情
        List<OrderDetail> details = buildDetails(order.getId(), items, itemNumMap);
        detailService.saveBatch(details);

        // 3.清理购物车商品
        try {
            cartClient.removeByItemIds(itemIds);
        } catch (Exception e) {
            throw new RuntimeException("清理购物车商品失败");
        }

        // 4.扣减库存
        try {
            itemClient.deductStock(detailDTOS);
        } catch (Exception e) {
            throw new RuntimeException("库存不足!");
        }

        return order.getId();
    }

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

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

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

public class DefaultFeignConfig {

    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.FULL;
    }

    @Bean
    public RequestInterceptor UserInfoInterceptor(){
        return new RequestInterceptor(){
            @Override
            public void apply(RequestTemplate requestTemplate) {
                final Long userId = UserContext.getUser();
                if(userId!=null){
                    requestTemplate.header("user-info",userId.toString());
                }
            }
        };
    }
}

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值