Spring Cloud GateWay基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty.
Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
提前声明:Spring Cloud Gateway 底层使用了高性能的通信框架Netty
SpringCloud官方,对SpringCloud Gateway 特征介绍如下:
基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
集成 Hystrix 断路器
集成 Spring Cloud DiscoveryClient
Predicates 和 Filters 作用于特定路由,易于编写的 Predicates 和 Filters
具备一些网关的高级功能:动态路由、限流、路径重写
从以上的特征来说,和Zuul的特征差别不大。SpringCloud Gateway和Zuul主要的区别,还是在底层的通信框架上。
简单说明一下上文中的三个术语:
**(**1) Filter(过滤器):
和Zuul的过滤器在概念上类似,可以使用它拦截和修改请求,并且对上游的响应,进行二次处理。过滤器为org.springframework.cloud.gateway.filter.GatewayFilter类的实例。
(2)Route(路由):
网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。
**(**3)Predicate(断言):
这是一个 Java 8 的 Predicate,可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。断言的输入类型是一个 ServerWebExchange。
Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型。
大家知道,servlet由servlet container进行生命周期管理。container启动时构造servlet对象并调用servlet init()进行初始化;container关闭时调用servlet destory()销毁servlet;container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()。
弊端:servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的,但是一旦并发上升,线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单的业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势。
所以Springcloud Zuul 是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet),并由该servlet阻塞式处理处理。所以Springcloud Zuul无法摆脱servlet模型的弊端。虽然Zuul 2.0开始,使用了Netty,并且已经有了大规模Zuul 2.0集群部署的成熟案例,但是,Springcloud官方已经没有集成改版本的计划了。
Webflux模式替换了旧的Servlet线程模型。用少量的线程处理request和response io操作,这些线程称为Loop线程,而业务交给响应式编程框架处理,响应式编程是非常灵活的,用户可以将业务中阻塞的操作提交到响应式框架的work线程中执行,而不阻塞的操作依然可以在Loop线程中进行处理,大大提高了Loop线程的利用率。
Webflux虽然可以兼容多个底层的通信框架,但是一般情况下,底层使用的还是Netty,毕竟,Netty是目前业界认可的最高性能的通信框架。而Webflux的Loop线程,正好就是著名的Reactor 模式IO处理模型的Reactor线程,如果使用的是高性能的通信框架Netty,这就是Netty的EventLoop线程。
由于在网关层面拦截token之后,需要检验并且解析,并且将解析后的token(包含用户信息)传递给其他微服务,实现微服务之间的调用
AuthLoginGlobalFilter全局认证过滤器,实现GlobalFilter接口,进行请求的过滤.
@Slf4j
@PropertySource(value = "classpath:loginfilter.properties")
@Component
public class AuthLoginGlobalFilter implements GlobalFilter, Ordered {
@Value("#{'${jwt.ignoreUrls}'.split(',')}")
List<String> ignoreUrls;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* @Description 全局校验过滤器
* @author Liu_gx
* @date 2021/1/8 17:29
* @param exchange
* @param chain
* @return
*/
/*@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request=exchange.getRequest();
if(ignoreUrls!=null&&ignoreUrls.contains(request.getURI().getPath())){
return chain.filter(exchange);
}
//获取token
String authoriztion=request.getHeaders().getFirst(Constants.LOGIN_TOKEN);
//获取服务名称
String serverName = getServerNameByPath(request.getURI().getPath());
//获取具体的请求服务实例
AbstractRequestUrlResolver serverInstance = AbstractRequestUrlResolver.getServerInstance(serverName);
if(null == serverInstance){
return onError(exchange,new ResponseEntity("fail", HttpStatus.BAD_REQUEST.value(), "暂不支持该登录类型", Collections.EMPTY_MAP));
}
serverInstance.setRedisTemplate(redisTemplate);
//token如果为null,app端需要校验JwtUser,放行
if(StringUtils.isEmpty(authoriztion)){
//是否需要强制校验token
if(serverInstance.tokenNullIsCheckToken()){
return onError(exchange,serverInstance.setResponseEntity("fail", HttpStatus.UNAUTHORIZED.value(), "尚未登录", Collections.EMPTY_MAP));
}else {
ServerHttpRequest shr=request.mutate().header(Constants.JWT_USERINFO, JSON.toJSONString( new JwtUserInfo())).build();
return chain.filter(exchange.mutate().request(shr).build());
}
}
//校验token
ResponseEntity responseEntity = serverInstance.checkAuthToken(authoriztion);
if(null != responseEntity && responseEntity.getResult().equals("fail")){
return onError(exchange,responseEntity);
}
ServerHttpRequest shr=request.mutate().header(Constants.JWT_USERINFO, JSON.toJSONString(responseEntity.getData())).build();
return chain.filter(exchange.mutate().request(shr).build());
}*/
private static final String X_CLIENT_TOKEN_USER = "x-client-token-user";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request=exchange.getRequest();
if(ignoreUrls!=null&&ignoreUrls.contains(request.getURI().getPath())){
return chain.filter(exchange);
}
//获取token
String authoriztion=request.getHeaders().getFirst(Constants.LOGIN_TOKEN);
//获取服务名称
String serverName = getServerNameByPath(request.getURI().getPath());
//获取具体的请求服务实例
AbstractRequestUrlResolver serverInstance = AbstractRequestUrlResolver.getServerInstance(serverName);
if(null == serverInstance){
return onError(exchange,new ResponseEntity("fail", HttpStatus.BAD_REQUEST.value(), "暂不支持该登录类型", Collections.EMPTY_MAP));
}
serverInstance.setRedisTemplate(redisTemplate);
//token如果为null,app端需要校验JwtUser,放行
if(StringUtils.isEmpty(authoriztion)){
//是否需要强制校验token
if(serverInstance.tokenNullIsCheckToken()){
return onError(exchange,serverInstance.setResponseEntity("fail", HttpStatus.UNAUTHORIZED.value(), "尚未登录", Collections.EMPTY_MAP));
}else {
ServerHttpRequest shr=request.mutate().header(X_CLIENT_TOKEN_USER, JSON.toJSONString( new JwtUserInfo())).build();
return chain.filter(exchange.mutate().request(shr).build());
}
}
//校验token
ResponseEntity responseEntity = serverInstance.checkAuthToken(authoriztion);
if(null != responseEntity && responseEntity.getResult().equals("fail")){
return onError(exchange,responseEntity);
}
JSONObject jsonObject = (JSONObject) JSON.toJSON(responseEntity.getData());
ServerHttpRequest shr=request.mutate().header(X_CLIENT_TOKEN_USER, JSON.toJSONString(responseEntity.getData())).build();
return chain.filter(exchange.mutate().request(shr).build());
}
/**
* @Description 获取请求的服务名称
* @author Liu_gx
* @date 2021/1/8 17:28
* @param path 请求路径
* @return
*/
private String getServerNameByPath(String path){
return getServerNameByPath(path,
(pathUrl)->{
return pathUrl.replace("/api/", "");
},(pathUrl)->{
return pathUrl.substring(0, pathUrl.indexOf("/"));
});
}
private String getServerNameByPath(String str, Function<String,String> fun1,Function<String,String> fun2){
return fun1.andThen (fun2).apply (str);
}
/**
* @Description 异常返回信息
* @author Liu_gx
* @date 2021/1/8 17:29
* @param exchange
* @param rd
* @return
*/
private Mono<Void> onError(ServerWebExchange exchange,ResponseEntity rd){
ServerHttpResponse response=exchange.getResponse();
response.setStatusCode(HttpStatus.valueOf(rd.getRescode()));
response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
ObjectMapper objectMapper=new ObjectMapper();
String rs="";
try {
rs=objectMapper.writeValueAsString(rd);
} catch (JsonProcessingException e) {
log.error("occur Exception:"+e);
}
DataBuffer buffer= response.bufferFactory().wrap(rs.getBytes());
return response.writeWith(Flux.just(buffer));
}
@Override
public int getOrder() {
return 0;
}
}
HandlerMethodArgumentResolver是用来处理方法参数的解析器,包含以下2个方法:
知识储备已到位,接下来着手实现,主要分为三步走:
在Common公用模块自定义参数解析器LoginUserHandlerResolver实现HandlerMethodArgumentResolver(方法参数解析器)
@Configuration
public class LoginUserHandlerResolver implements HandlerMethodArgumentResolver
{
private static final String CURRENT_ID = "jwtUserInfo";
@Override
public boolean supportsParameter(MethodParameter parameter)
{
return parameter.hasParameterAnnotation(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest nativeWebRequest, WebDataBinderFactory factory) throws Exception
{
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
// 获取用户ID
String userid = String.valueOf(request.getHeader(CURRENT_ID));
if (userid == null){
return null;
}
return userid;
}
}
定义@LoginUser注解,绑定用户信息
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser
{}
将自定义解析器注册到spring容器中
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Value("${file.userfiles-path}")
private String filePath;
/**
* 登录校验拦截器
*
* @return
*/
@Bean
public AuthenticationInterceptor loginRequiredInterceptor() {
return new AuthenticationInterceptor();
}
/**
* CurrentUser 注解参数解析器
*
* @return
*/
@Bean
public CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver() {
return new CurrentUserMethodArgumentResolver();
}
/**
* 参数解析器
*
* @param argumentResolvers
*/
@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(currentUserMethodArgumentResolver());
super.addArgumentResolvers(argumentResolvers);
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginRequiredInterceptor())
.addPathPatterns(Constants.BASE_API_PATH + "/**")
.excludePathPatterns(Constants.BASE_API_PATH + "/login");
super.addInterceptors(registry);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/META-INF/resources/")
.addResourceLocations("classpath:/static/page/")
.addResourceLocations("classpath:/static/templates/")
.addResourceLocations("file:" + filePath);
}
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.QuoteFieldNames,
SerializerFeature.WriteEnumUsingToString,
SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteDateUseDateFormat,
SerializerFeature.DisableCircularReferenceDetect);
fastJsonConfig.setSerializeFilters((ValueFilter) (o, s, source) -> {
if (null != source && (source instanceof Long || source instanceof BigInteger) && source.toString().length() > 15) {
return source.toString();
} else {
return null == source ? EMPTY : source;
}
});
//处理中文乱码问题
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastConverter.setSupportedMediaTypes(fastMediaTypes);
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(fastConverter);
}
}
获取用户id
ResponseEntity<CentreListResponse<EfficiencyNeedhandleVo>> queryListByDayTwo(@LoginUser String userId, @RequestBody @Valid EfficiencyNeedhandleQueryForm from) throws UserCenterException {
System.out.println(userId);
}
主要分为三步走:
定义用户上下文
public class UserContextHolder {
private ThreadLocal<Map<String, String>> threadLocal;
private UserContextHolder() {
this.threadLocal = new ThreadLocal<>();
}
/**
* 创建实例
*
* @return
*/
public static UserContextHolder getInstance() {
return SingletonHolder.sInstance;
}
/**
* 静态内部类单例模式
* 单例初使化
*/
private static class SingletonHolder {
private static final UserContextHolder sInstance = new UserContextHolder();
}
/**
* 用户上下文中放入信息
*
* @param map
*/
public void setContext(Map<String, String> map) {
threadLocal.set(map);
}
/**
* 获取上下文中的信息
*
* @return
*/
public Map<String, String> getContext() {
return threadLocal.get();
}
/**
* 获取上下文中的用户名
*
* @return
*/
public String getUsername() {
return Optional.ofNullable(threadLocal.get()).orElse(Maps.newHashMap()).get("id");
}
/**
* 清空上下文
*/
public void clear() {
threadLocal.remove();
}
}
定义用户请求拦截器UserInterceptor实现HandlerInterceptor,并且将拿到的用户信息存储到上下文UserContextHolder中
@Slf4j
public class UserInterceptor implements HandlerInterceptor {
/**
* 服务间调用token用户信息,格式为json
* {
* "user_name":"必须有"
* "自定义key:"value"
* }
*/
public static final String X_CLIENT_TOKEN_USER = "x-client-token-user";
/**
* 服务间调用的认证token
*/
public static final String X_CLIENT_TOKEN = "x-client-token";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从网关获取并校验,通过校验就可信任x-client-token-user中的信息
checkToken(request.getHeader(X_CLIENT_TOKEN));
String userInfoString = StringUtils.defaultIfBlank(request.getHeader(X_CLIENT_TOKEN_USER), "{}");
UserContextHolder.getInstance().setContext(new ObjectMapper().readValue(userInfoString, Map.class));
return true;
}
private void checkToken(String token) {
//TODO 从网关获取并校验,通过校验就可信任x-client-token-user中的信息
log.debug("//TODO 校验token:{}", token);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
UserContextHolder.getInstance().clear();
}
}
将自定义拦截器注册到spring容器中
@Configuration
public class WebServerMvcConfigurerAdapter implements WebMvcConfigurer {
@Bean
public HandlerInterceptor userInterceptor() {
return new UserInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor());
}
}
获取用户信息
ResponseEntity<CentreListResponse<EfficiencyNeedhandleVo>> queryListByDayTwo(@RequestBody @Valid EfficiencyNeedhandleQueryForm from) throws UserCenterException {
String username = UserContextHolder.getInstance().getUsername();
System.out.println(username);
}