1.微服务获取用户信息
现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?
由于网关发送请求到微服务依然采用的是Http
请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。
据图流程图如下:
因此,接下来我们要做的事情有:
-
改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
-
编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行
1.1.保存用户到请求头
首先,我们修改登录校验拦截器的处理逻辑,保存用户信息到请求头中:
package com.hmall.gateway.filters;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.utils.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取到request
ServerHttpRequest request = exchange.getRequest();
//2.判断是否需要拦截
String path = request.getPath().toString();
if(isExcluded(path)){
//如果不需要拦截,放行
return chain.filter(exchange);
}
//3.从请求头获取到token
List<String> headers = request.getHeaders().get("authorization");
String token = null;
if(headers != null && !headers.isEmpty()){
token = headers.get(0);
}
//4.解析token,获取到用户id
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
//token有问题,拦截
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);//设置401未登录状态码
return response.setComplete();//直接结束
}
//5.TODO 传递用户信息到微服务
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate().request(builder -> builder.header("user-info",userInfo)).build();
//6.放行
return chain.filter(swe);
}
private boolean isExcluded(String path) {
//匹配,如果成功匹配则true
for (String pathPattern : authProperties.getExcludePaths()) {
if (antPathMatcher.match(pathPattern, path)){
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
1.2.拦截器获取用户信息
在hm-common中已经有一个用于保存登录用户的ThreadLocal工具:
接下来,我们只需要编写拦截器,获取用户信息并保存到UserContext
,然后放行即可。
由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在hm-common
中,并写好自动装配。这样微服务只需要引入hm-common
就可以直接具备拦截器功能,无需重复编写。
我们在hm-common
模块下定义一个拦截器:
package com.hmall.common.interceptor;
import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//此拦截器不作拦截,而是微服务想获取到网关或者上游微服务传递的请求头(用户信息)
//1.获取到请求头(用户信息)
String header = request.getHeader("user-info");
//2.判断是否为空,有则获取,存入ThreadLocal
if(StrUtil.isNotBlank(header)){
UserContext.setUser(Long.valueOf(header));
}
//3.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.removeUser();
}
}
接着在hm-common
模块下编写SpringMVC
的配置类,配置登录拦截器:
package com.hmall.common.config;
import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是com.hmall.common.config
,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。
基于SpringBoot的自动装配原理,我们要将其添加到resources
目录下的META-INF/spring.factories
文件中:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig
1.3.恢复购物车代码
之前我们无法获取登录用户,所以把购物车服务的登录用户写死了,现在需要恢复到原来的样子。
找到cart-service
模块的com.hmall.cart.service.impl.CartServiceImpl
: