一.网关传递用户

我们将用户信息保存到请求头中,并通过http请求传递给微服务。但是在微服务中我们可能会多实例部署,那这样的话我们还要在每个业务中都编写接收用户信息请求头的逻辑。这样的话十分麻烦。有没有一个方法能在业务执行之前就获取用户信息。那什么东西可以在所有controller接口执行之前执行——SpringMVC中的拦截器。我们可以在微服务中定义一个拦截器。在拦截器中获取用户信息,将其保存在ThreadLocal中,方便后续所有业务使用。
但是请求是网关自己发送的,我们如何对其请求头进行处理?使用ServerWebExchange类中提供的API:nutate()。

// 5.将信息加入请求头,并传递给下一个微服务
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info",userInfo))
.build();
要携带的请求头的名字可以随便写,不过要和微服务约定好。值为userInfo。
这样就会返回一个新的exchange,将其返回到filter方法里。
package com.hmall.gateway.filter;
import cn.hutool.core.text.AntPathMatcher;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.utils.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
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.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
@RequiredArgsConstructor
public class AutoGlobalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
// 获取jwt令牌并进行校验
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取request
ServerHttpRequest request = exchange.getRequest();
// 2.判断是否需要拦截
if (isExclude(request.getPath().toString())) {
return chain.filter(exchange);
}
// 3.获取token
String token = null;
HttpHeaders headers = request.getHeaders(); // 拿到所有的请求头,实际上是一个map
List<String> list = headers.get("authorization"); // 获取请求头中key为authorization的值,请求头一个头对应的值不一定只有一个,因此需要用List来接收
if (list != null && !list.isEmpty()) {
token = list.get(0); // 实际上请求头中key为authorization的值只可能有一个,就是token
}
Long userId = null;
// 4.解析并校验token
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) { // 如果没能成功获取用户信息,则抛出异常
// 给前端抛出一个响应状态码:401 代表未授权,拦截前端发起的请求
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete(); // response.setComplete()返回值是Mono<Void>
}
// 5.将信息加入请求头,并传递给下一个微服务
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info",userInfo))
.build();
// 6.放行
return chain.filter(swe);
}
@Override
public int getOrder() {
return 0;
}
// 判断是否需要拦截, 根据配置文件来判断,如果是需要拦截的请求,则返回true, 否则返回false
private boolean isExclude(String path) {
for (String pathPattern : authProperties.getExcludePaths()) {
if (antPathMatcher.match(pathPattern,path)) {
return true;
}
}
return false;
}
}
二.定义拦截器
我们后面有很多微服务,难道每个微服务再写一个拦截器吗?那就太麻烦了。因此我们将拦截器定义在hm-common中。

UserInfoInterceptor.java
package com.hmall.common.interceptor;
import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.context.annotation.Configuration;
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 userInfo = request.getHeader("user-info");
// 2.判断是否获取了用户,如果有,存入ThreadLocal
if (StrUtil.isNotBlank(userInfo)) {
UserContext.setUser(Long.valueOf(userInfo));
}
// 3.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理用户
UserContext.removeUser();
}
}
这里我们不做拦截,一律放行,因为只要到了拦截器这里,就证明登录成功了。有用户信息了。
UserContext.java
package com.hmall.common.utils;
public class UserContext {
private static final ThreadLocal<Long> tl = new ThreadLocal<>();
/**
* 保存当前登录用户信息到ThreadLocal
* @param userId 用户id
*/
public static void setUser(Long userId) {
tl.set(userId);
}
/**
* 获取当前登录用户信息
* @return 用户id
*/
public static Long getUser() {
return tl.get();
}
/**
* 移除当前登录用户信息
*/
public static void removeUser(){
tl.remove();
}
}
使用UserContext将用户信息存入ThreadLocal中。
接着为了使拦截器生效,我们来配置拦截器。
package com.hmall.common.config;
import com.hmall.common.interceptor.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());
}
}
这是一个配置类,配置类要想生效要被spring扫描包扫描到。这个config类所在的包是com.hmall.common。而微服务所在包是com.hmall.cart等等。因此该config类无法被扫描到。因此无法生效。
所以我们要使用自动装配在其中定义

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig,\
com.hmall.common.config.JsonConfig
另外要注意:在网关的依赖中也引入了common依赖,而MvcConfig所实现的WebMvcConfigurer是属于SpringMVC包下的,但是网关底层不是SpringMVC,而是SpringCloud。因此他找不到WebMvcConfigurer,会报错。
解决办法:既然common又被网关引用了,又被微服务引用了,但是我们只想让他在微服务中生效。因此我们在配置类上加上条件。使其判定:有springmvc的api才生效,没有不生效。
因此加上springmvc的核心api:DispatcherServlet。
@ConditionalOnClass(DispatcherServlet.class)
到此,成功实现微服务获取用户信息。