org.springframework.cloud
spring-cloud-starter-ribbon
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-zuul
注意上面部分的依赖,因为网关也是基于Eureka的说一客户端一定要引入,否则网关根本跑不起来
另外网关默认也是基于ribbon的,所以也要引入。
入口APP
很简单:
@SpringBootApplication
@EnableZuulProxy
public class App
{
public static void main( String[] args )
{
SpringApplication.run(App.class, args);
}
}
这个是基本的配置:
server:
port: 5000 #网关5字头,第一个
指定当前eureka客户端的注册地址,也就是eureka服务的提供方,当前配置的服务的注册服务方
eureka:
instance:
hostname: 172.17.7.90
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:2000/eureka
#指定应用名称
spring:
application:
name: zuul-agw
这个和普通的基本eureka没啥区别,有了这个网关就能跑起来,其他不说了。
Zuul超时有两种方式,比较靠谱的方式用ribbon进行超时。Zuul是用ribbon做负载均衡的,所以配置了ribbon就配置了超时。
ribbon:
eureka:
enabled: true
ReadTimeout: 3500
ConnectTimeout: 3500
顺便说一下红色部分,红色部分很重要,如果使用eureka的服务列表,那么这个开关一定要打开,否则在eureka上线和下线的时候,通知不及时会导致服务异常!
zuul:
# 使用 prefix 添加前缀
prefix: /zyth
routes:
eureka:
path: /monitor-eureka/**
url: url
serviceId: oneServiceName
上面就是一个最简单的配置。
说明一下,prefix是统一前缀,就是先判断能否进入这个网关代理,有这个前缀的才进入网关代理。如果不配置,就是所有的请求都经过代理。
Routes就是具体不同的二级后缀和实际转发的配置了。
Path:是匹配的模式。
模式匹配到了,就有两个转发模式:url或者serviceId
url就会转入到指定的http地址中,而serviceId就会转入到一个具体的微服务地址中。如果url后面的值,不是http等,这个地方就会当作是一个serviceID处理,也就是说,url这里直接配置serviceId也是ok的
服务路由有两种,默认情况下,配置了eureka后,直接就是/前缀/服务id/服务端点。这样方式。
如果觉得服务前缀不爽,就要手动修改路由了,有两种方式,当然两种方式都是通过urlmapping实现的。
第一种方式:通过直接的ip地址:
网关配置
zuul:
# 使用 prefix 添加前缀
prefix: /zyth
routes:
eureka:
path: /monitor-eureka/**
url: http://${eureka.instance.hostname}:2000/
如上,是对eureka的配置,这个名字随便取
下面有paht,这个无论哪种配置都是一样的,就是使用path路径映射
然后,就是对应这个路径下的访问url了。
下面url针对集群,是可以配置多个的,用’,’隔开就行。
另外,以上就是,配置访问对eurake的配置逻辑。
第二种方式:通过配置服务id,就是注册到eureka上的服务名进行访问:
prefix: /zyth
routes:
oneservice:
path: /monitor-eureka/**
serviceId: serviceName
这种方式中serviceId就是eureka上,可以按照动态列表进行获取
另外对第一种方式的补充说明:
如果没有经过Eureka服务器,那自然就得不到Ribbon的负载均衡功能了。针对这个问题,Zuul已经帮开发者想到了解决方案,在这种情况下开发者只需要禁用Ribbon与Eureka的自动集成设置,采用手工设置方式开启即可,配置如下:
zuul:
routes:
myroutes1:
path: /mypath/**
serviceId: myserverId
myserverId:
ribbon:
listOfServers: localhost:8080, localhost:8081
ribbon:
eureka:
enabled: false
默认路由就是zuul啥都不配的情况下,默认就是微服务名称转向对应的微服务即:
[servicename]:[servicename]
这是一个巨坑,很多时候,我们要显示配置,微服务内部服务是不暴露出来的。这个需要手动配置关闭:
zuul:
ignored-services: '*'
ignoredPatterns: /**/channel/pay/**,/**/channel/transfer/**
第二个配置项,和安全相关,就是直接屏蔽掉某些不该有的pattern
跨域实际就是在响应的时候统一加上跨域头部。
实际代码可以是在setting中加入如下:
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
/* String curOrigin = request.getHeader("Origin");
System.out.println("###跨域过滤器->当前访问来源->"+curOrigin+"###"); */
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "*");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
}
说明,这个应该可以统一用网关的拦截器做的,还没有验证过,后面可以验证一下。
Zuul的拦截器,很多门道,基础就是继承一个类,实现拦截:ZuulFilter
public String filterType() 这个函数返回拦截器类型,可以之前拦截,可以之后拦截,主要看返回的是什么类型
public int filterOrder() 这个函数是返回拦截器的执行顺序,可见是可以支持多个拦截器的
public boolean shouldFilter() 这个函数是返回是否要执行本拦截
public Object run() throws ZuulException 真正的拦截器逻辑代码
这个要只要继承就可以使用,但是如果要加入到spring的注入中,就要增加要给注解,简单的就是用:
@Component
RequestContext ctx = RequestContext.getCurrentContext();
直接看代码吧
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String url = request.getRequestURI();
log.info(String.format("LoginCheck(%s)->开始校验",url));
当该请求非法,就要拦截,拦截是在run函数里面执行的。Run函数返回null就行,无论成功还是失败。
但真正的拦截就要靠如下代码:
//登录验证失败了
ctx.set(GWConst.FILTER_RESULT_NAME_LASTRESULT,false);
ctx.setSendZuulResponse(false);
ctx.set("sendForwardFilter.ran", true);
ctx.setResponseBody(JSON.toJSONString(GWConst.LOGINCHECK_FAIL_NOLOGIN));
log.info(String.format("LoginCheck(%s)->token(%s),rbacuui(%s)校验失败",url,token,rbacUserUUID));
return null;
ctx.setSendZuulResponse(false);表示要拦截,不转向到后面微服务代码了。所以反之如果成功的话这里要设置成true
ctx.set(“sendForwardFilter.ran”, true);设置到ctx上面,标识已经拦截了,其他filter看到这个标记可以不拦截,当然这个业务自己实现的。
ctx.set(GWConst.FILTER_RESULT_NAME_LASTRESULT,false);这个就比较有门道了,这里设置了一个失败标识。为什么呢?原因是,第一个setSendZuulResponse设置成false后,是不会分发请求了,但是后续的其他filter会继续执行的。所以这里设置了一个我们自己定义的一个特殊标识,其他filter中,可以在shouldFilter山上中取这个标识并判断是否需要继续filter了
ctx.setResponseBody就是设置返回的结果了。
一般我们的业务逻辑就是判断用户是否可以登录,如果可以登录,我们就会放置一些信息给后面的服务器,给后面服务器信息,最好就是在head上增加内容了。
//其他情况下是成功的,需要进行转换
ctx.addZuulRequestHeader(GWConst.FILTER_RESULT_NAME_HEAD_TOKEN, token);
ctx.addZuulRequestHeader(GWConst.FILTER_RESULT_NAME_HEAD_RBACUSERUUID, rbacUserUUID);
参数拦截后,可能要根据拦截对象将整个请求参数修改掉。
这里分两种情况:get请求和post请求。如果参数是写在url上的get请求,直接用ctx的方法即可。
否则要重写整个requestbody。
private void resetParameter(RequestContext ctx, Map<String, List<String>> paramList) {
String method = ctx.getRequest().getMethod();
if ("get".equalsIgnoreCase(method)) {
ctx.setRequestQueryParams(paramList);
}
else if("POST".equalsIgnoreCase(method)){
String charSet = ctx.getRequest().getCharacterEncoding();
try{
StringBuilder postParamString = new StringBuilder();
boolean isFirst = true;
for (String name : paramList.keySet()) {
for (String value : paramList.get(name)) {
if (isFirst) {
isFirst = false;
}
else {
postParamString.append('&');
}
postParamString.append(name).append('=').append(URLEncoder.encode(value, charSet));
}
}
byte[] paramBytes = postParamString.toString().getBytes(charSet);
ctx.setRequest(new HttpServletRequestWrapper(ctx.getRequest()){
@Override
public ServletInputStream getInputStream() throws IOException{
return new ServletInputStreamWrapper(paramBytes);
}
@Override
public int getContentLength(){
return paramBytes.length;
}
@Override
public long getContentLengthLong(){
return paramBytes.length;
}
});
}catch (IOException e){
e.printStackTrace();
}
}
}
在yml的配置文件中增加:
spring:
http:
encoding:
charset: UTF-8
enabled: true
force: true
或者每一个下发的时候,增加:
ctx.getResponse().setCharacterEncoding("UTF-8");
package com.qfkj.setting;
import com.alibaba.fastjson.JSON;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.qfkj.base.constants.GWConst;
import com.qfkj.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Component
@Slf4j
public class LoginCheck extends ZuulFilter {
@Value("service.base.copyrightEn")
private String copyrightEn;
@Value("service.base.profileActive")
private String profileActive;
@Value("${service.mode.check}")
private boolean isCheck;
@Value("${service.mode.defalutToken}")
private String defalutToken;
@Value("${service.mode.defalutRbacUserUUID}")
private String defalutRbacUserUUID;
@Value("${service.sigprefix}")
private String sigprefix;
@Value("${service.cookiesName}")
private String cookiesName;
@Value("${service.tokenRedisKey}")
private String tokenRedisKey;
@Autowired
private RedisUtil redisUtil;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String url = request.getRequestURI();
if (url.indexOf("swagger") > 0) {
log.info(String.format("shouldFilter(%s)->不用校验",url));
return false;
}
if (url.indexOf("webjars") > 0) {
log.info(String.format("shouldFilter(%s)->不用校验",url));
return false;
}
if (url.indexOf("/csrf") > 0) {
log.info(String.format("shouldFilter(%s)->不用校验",url));
return false;
}
if (url.indexOf("/api-docs") > 0) {
log.info(String.format("shouldFilter(%s)->不用校验",url));
return false;
}
if (url.lastIndexOf("/") == url.length()-1) {
log.info(String.format("shouldFilter(%s)->不用校验",url));
return false;
}
log.info(String.format("shouldFilter(%s)->开始校验",url));
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String url = request.getRequestURI();
log.info(String.format("LoginCheck(%s)->开始校验",url));
//判断当前的系统模式,是否是全场模式
String token = this.defalutToken;
String rbacUserUUID = this.defalutRbacUserUUID;
if (this.isCheck) {
//获取cookies
String cookiesname = this.sigprefix + '-' + this.profileActive + '-' + this.cookiesName;
Cookie[] cookies = request.getCookies();
if (null != cookies && cookies.length > 0) {
for (Cookie cookie : cookies){
if (cookiesname.equals(cookie.getName())) {
token = cookie.getValue();
}
}
}
if (token!= null && !this.defalutToken.equals(token)) {
//从redis中获取token
String key = this.sigprefix + '-' + this.profileActive + '-' + this.tokenRedisKey + token;
rbacUserUUID = (String) this.redisUtil.get(key);
}
log.info(String.format("LoginCheck(%s)->token(%s),rbacuui(%s)",url,token,rbacUserUUID));
if (token == null || this.defalutToken.equals(token) || defalutRbacUserUUID == null) {
//登录验证失败了
ctx.set(GWConst.FILTER_RESULT_NAME_LASTRESULT,false);
ctx.setSendZuulResponse(false);
ctx.set("sendForwardFilter.ran", true);
ctx.setResponseBody(JSON.toJSONString(GWConst.LOGINCHECK_FAIL_NOLOGIN));
log.info(String.format("LoginCheck(%s)->token(%s),rbacuui(%s)校验失败",url,token,rbacUserUUID));
return null;
}
}
//其他情况下是成功的,需要进行转换
ctx.addZuulRequestHeader(GWConst.FILTER_RESULT_NAME_HEAD_TOKEN, token);
ctx.addZuulRequestHeader(GWConst.FILTER_RESULT_NAME_HEAD_RBACUSERUUID, rbacUserUUID);
ctx.setSendZuulResponse(true);
log.info(String.format("LoginCheck(%s)->token(%s),rbacuui(%s)校验成功",url,token,rbacUserUUID));
return null;
}
}
这个实际上是根据某些拦截的结果,动态的转到后面不同的服务的方案。
比如,用户登录了,就转到user/login这个服务。否则就转到user/guest这个服务上。
直接看代码吧:
@Component
public class ForwardFilter extends ZuulFilter{
private Logger logger= LoggerFactory.getLogger(ForwardFilter.class);
@Override
public String filterType() {
//注意,重要,重要,要动态修改路由,这个值必须是ROUTE_TYPE
return FilterConstants.ROUTE_TYPE;
}
@Override
public int filterOrder() {
// filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
return 0;
}
@Override
public boolean shouldFilter() {
// 是否执行该过滤器,此处为true,说明需要过滤
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
//获取请求的URI //测试访问:http://localhost:6001/testforward/hello?name=zs&token=1
String url=request.getRequestURI();//
if(url.indexOf("testforward")>-1){
try {
//[1]:设置RouteHost::说明Host我没试过,因为指定了host相当于违背了微服务了,负载均衡等要自己手动处理,一般我不这么干
URI uri1=new URI("http://127.0.0.1:8001/");
ctx.setRouteHost(uri1.toURL());
//[2]:设置URI
url=url.substring(url.indexOf("testforward")+12,url.length());
ctx.put(FilterConstants.REQUEST_URI_KEY,url);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
这里有一个很重要的代码:
public String filterType() {
//注意,重要,重要,要动态修改路由,这个值必须是ROUTE_TYPE
return FilterConstants.ROUTE_TYPE;
}
这个部分参见Spring-Cloud-Config部分说明。
这里注意的是,ruul的路由配置,可能自己一行实现了@ @RefreshScope注解,路由刷新,用curl -X POST http://localhost:4001/actuator/refresh访问一次即可
一般而言,zuul中大部分都是路由配置服务,但也可以直接编写普通的Controller进行访问。Controller编写后,起Mapping可以直接生效,无需在进行路由映射配置。
值得注意的时候,路由映射的优先级会大于Controller,换句话说,通过路由配置,可以直接将其结果转换拦截掉。
在zuul中可以配置forward的
### 网关配置
zuul:
# 路由信息配置
routes:
demo-local:
# 访问的路径,此处要以 '/do/' 开头
path: /local1/**
# 访问的 url,forward:向本地转发
url: forward:/local2
而controller如下:
/**
* @Author:大漠知秋
* @Description:网关本地的 Controller
* @CreateDate:1:33 PM 2018/10/30
*/
@RestController
@RequestMapping(
value = "/local2",
produces = MediaType.APPLICATION_JSON_UTF8_VALUE
)
public class LocalController {
@RequestMapping(value = "/testOne")
public String testOne() {
return "testOne";
}
}
如上,正常访问/local2/testOne来访问服务。
但有上面的拦截和forward,那么,就可以通过/local1/testOne来访问