背景:
项目背景是黑马商城,通过黑马商城这个微服务项目将前端发送登录请求,到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
注解即可,并且也要指定前缀。
我们根据这个配置文件的信息就是说,有些路径需要校验,有些不需要,
这个很好想,登录校验这种路径肯定不能校验
具体流程可以看这篇博客。
接着我们看到这个过滤器:
首先获取这个请求头,从这个请求头中拿到我们需要的信息
关于官网的一些概念在这篇博客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的拦截器之后,我们代码的逻辑就很简单了。