目录
1、用户登录流程
网关:网关就是网络的关口。数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。也就是一切请求都需要先经过网关
用户登录流程:用户请求用户微服务进行登录,然后自定义网关过滤器判断该请求是否需要做登录校验(一般不用,用户登录请求不拦截),然后用户提交登陆数据表单,用户微服务进行用户登录数据校验,
如果账号密码正确,用户微服务则生成一个临时token返回,前端拿到响应把token设置在session中
网关部分过滤不需要请求代码
用户登录校验代码如下:
private final PasswordEncoder passwordEncoder;
private final JwtTool jwtTool;
private final JwtProperties jwtProperties;
@Override
public UserLoginVO login(LoginFormDTO loginDTO) {
// 1.数据校验
String username = loginDTO.getUsername();
String password = loginDTO.getPassword();
// 2.根据用户名或手机号查询
User user = lambdaQuery().eq(User::getUsername, username).one();
Assert.notNull(user, "用户名错误");
// 3.校验是否禁用
if (user.getStatus() == UserStatus.FROZEN) {
throw new ForbiddenException("用户被冻结");
}
// 4.校验密码
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadRequestException("用户名或密码错误");
}
// 5.生成TOKEN
String token = jwtTool.createToken(user.getId(), jwtProperties.getTokenTTL());
// 6.封装VO返回
UserLoginVO vo = new UserLoginVO();
vo.setUserId(user.getId());
vo.setUsername(user.getUsername());
vo.setBalance(user.getBalance());
vo.setToken(token);
return vo;
}
前端代码如下:
2、网关转发请求到微服务传递用户信息
如图所示:
-
客户端请求进入网关后由
HandlerMapping
对请求做判断,找到与当前请求匹配的路由规则(Route
),然后将请求交给WebHandler
去处理。 -
WebHandler
则会加载当前路由下需要执行的过滤器链(Filter chain
),然后按照顺序逐一执行过滤器(后面称为Filter
)。 -
图中
Filter
被虚线分为左右两部分,是因为Filter
内部的逻辑分为pre
和post
两部分,分别会在请求路由到微服务之前和之后被执行。 -
只有所有
Filter
的pre
逻辑都依次顺序执行通过后,请求才会被路由到微服务。 -
微服务返回结果后,再倒序执行
Filter
的post
逻辑。 -
最终把响应结果返回。
如图中所示,最终请求转发是有一个名为NettyRoutingFilter
的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter
之前,这就符合我们的需求了!
可以看到NetttyRoutingFilter的顺序是int的最大值,因此只需要将自定义网关的顺序大小比NettyRoutingFilter小即可
实现Ordered接口,并重写getOrder方法,制定顺序
@RequiredArgsConstructor
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return 0;
}
因此,接下来我们要做的事情有:
-
改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
-
编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行
完整的自定义网关代码:
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.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.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@RequiredArgsConstructor
@Component
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、判断是否需要做登录拦截
if(isExclude(request.getPath().toString())){
//放行
return chain.filter(exchange);
}
// 3、获取token
String token=null;
HttpHeaders headers = request.getHeaders();
// JWT令牌默认保存在key为authentication的请求头中
List<String> authorization = headers.get("authorization");
if(authorization !=null && !authorization.isEmpty()){
token=authorization.get(0);
}
// 4、解析并校验token
Long userId=null;
try {
userId=jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 拦截 并返回401
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 结束响应
return response.setComplete();
}
String userInfo = userId.toString();
// http请求在转发到微服务之后就失效,由微服务调用其他微服务时需要重新在请求头中加入用户信息
// 5、传递用户信息
ServerWebExchange serverWebExchange = exchange.mutate().request(builder -> {
builder.header("user-info", userInfo);
}).build(); // 构建并返回修改后的 ServerWebExchange 实例
System.out.println("userId = " + userId);
// 6、放行
return chain.filter(serverWebExchange);
}
private boolean isExclude(String path) {
for(String pathPattern : authProperties.getExcludePaths()){
if(antPathMatcher.match(pathPattern,path)){
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
微服务拦截器:
-
实现 HandlerInterceptor 接口其中的 preHandle() 方法,在 Controller 之前执行
-
实现 afterCompletion() 方法在 Controller 执行完之后执行,清除 ThreadLocal 中的用户信息
-
注册拦截器:定义 SpringMVC 的配置类,实现 WebMvcConfigurer 中的 addInterceptors()
不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是com.hmall.common.config
,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。
基于SpringBoot的自动装配原理,我们要将其添加到resources
目录下的META-INF/spring.factories
文件中:
注意❗网关模块不能使用 / 引用 SpringMVC 相关的类,Spring Cloud Gateway 的底层用的不是 SpringMVC,而是响应式的 webflux
网关项目也引入了 common 模块,但是 common 模块中定义了 SpringMVC 的拦截器,微服务项目需要该拦截器,网关不需要该拦截器,如何次拦截器配置类在有些情况(微服务模块)生效,有些情况(网关)不生效?
解决方法:
让 SpringMVC 的配置类根据条件来加载,即判断是否有 SpringMVC(网关和其他微服务项目的差别就是是否有 SpringMVC,可以根据是否有 SpringMVC 中的核心 API DispatcherServlet)
gateway不是基于springMvc的,所以该MvcConfig不应该生效。通过使用@ConditionalOnClass(DispatcherServlet.class),表示仅对包含了springMvc的核心类(DispatcherServlet)的微服务生效
3、微服务之间传递用户信息
前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。
但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,流程如下:
下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!
由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。
微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?
这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor
public interface RequestInterceptor {
/**
* Called for every request.
* Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template);
}
们只需要实现这个接口,然后实现apply方法,利用RequestTemplate
类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。
由于FeignClient
全部都是在hm-api
模块,因此我们在hm-api
模块的com.hmall.api.config.DefaultFeignConfig
中编写这个拦截器:
在com.hmall.api.config.DefaultFeignConfig
中添加一个Bean:
import com.hmall.api.fallback.*;
import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
public class DefaultFeignConfig {
@Bean
public Logger.Level DefaultLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public RequestInterceptor userInfoRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
// 获取登录用户信息
Long userInfo = UserContext.getUser();
if (userInfo == null) {
// 没有登录用户信息,不添加header
return;
}
// 添加header
requestTemplate.header("user-info", userInfo.toString());
}
};
}
@Bean
public ItemClientFallback itemClientFallback() {
return new ItemClientFallback();
}
@Bean
public CartClientFallback cartClientFallback(){
return new CartClientFallback();
}
@Bean
public UserClientFallback userClientFallback() {
return new UserClientFallback();
}
@Bean
public TradeClientFallback tradeClientFallback(){
return new TradeClientFallback();
}
@Bean
public PayClientFallback payClientFallback(){
return new PayClientFallback();
}
}
好了,现在微服务之间通过OpenFeign调用时也会传递登录用户信