概述
Spring Cloud Netflix zuul组件是微服务架构中的网关组件,其核心功能是路由和过滤,目前公司业务就是基于Zuul搭建的网关服务,本文主要内容:Zuul的执行流程、Zuul过滤实现、Zuul安全配置
Zuul服务
Zuul执行流程
- 接收请求
- 客户端发送请求到Zuul服务器(前置经过Nginx反向代理)
- Pre-filters
- 请求首先通过Pre类型的过滤器,这些过滤器可以进行安全检查、身份验证、日志记录、选择路由等操作。
- 路由转发Routing
- 如果Pre-filter通过请求将被路由到具体的微服务实例上。Zuul 可以根据配置的路由规则将请求转发到不同的后端服务。
- 服务调用
- Zuul通过Fegin进行客户端负载均衡,选择一个服务实例进行调用
- Post-filters
- 服务处理完成后,响应会返回并经过 Post 类型的过滤器,这些过滤器可以修改返回的响应头、收集统计信息等。最终Zuul将响应发送回客户端
- Error-filters
- 如果在任何阶段发生错误,请求将被转发到 Error 类型的过滤器,进行错误处理后经过Post-filters,最终Zuul将响应发送回客户端
Zuul过滤器
pre-filter
前置过滤器在请求被路由到源服务器之前执行,通常继承ZuulFilter抽象类。实际项目中我们使用前置过滤器对于400到500的状态错误码进行过滤,进而对于从cas认证服务中获取token的特定请求进行错误日志的记录
java
package test.gateway.filter;
import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException;
import test.common.util.R;
import com.google.gson.Gson;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
/**
* @Description: 捕获400到500错误码,否则不执行该过滤器
*/
public class ErrorStatusCodeZuulFilter extends ZuulFilter {
protected static String GET_TOKEN_URI = "/cas/getToken";
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return -1; // Needs to run before SendResponseFilter which has filterOrder == 0
}
@Override
public boolean shouldFilter() {
int statusCode = Integer.parseInt(RequestContext.getCurrentContext().get("responseStatusCode").toString());
return statusCode >= 400 && statusCode <= 500;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
int statusCode = Integer.parseInt( ctx.get("responseStatusCode").toString());
String uri = ctx.get("requestURI").toString();
if (uri != null && uri.equals(GET_TOKEN_URI)) { //获取token请求失败
R<String> res = new R<>();
res.setCode(statusCode);
res.setData(null);
res.setMsg("Account or password error");
ctx.setResponseBody(new Gson().toJson(res));
return null;
}
try {
throw new Exception();
} catch (Throwable t) {
throw new ZuulRuntimeException(new ZuulException(t, statusCode, t.getMessage()));
}
}
}
routing-filter
处理将请求发送到源服务器的逻辑
post-filter
后置过滤器在请求被路由后执行,可以对响应进行修改或添加额外的HTTP等。
error-filter
当请求处理过程中发生错误时,这些过滤器会被调用,同时最终也会调用后置post类型过滤器返回
扩展-OncePerRequestFilter
OncePerRequestFilter是Spring Framework中的一个接口,它属于Servlet过滤器(Filter)的范畴,但不是Zuul中定义的特定过滤器类型。在Spring MVC中,OncePerRequestFilter确保每个请求只被处理一次。它通过同步代码块来保证,即使在支持异步处理的 Spring MVC 应用程序中,请求也不会被重复处理。项目中我们通过自定义过滤器(继承OncePerRequestFilter)处理访问公共接口且未携带token的请求,该过滤器也属于前置的过滤器,因为它通常用于在请求进入其他阶段之前执行某些操作。
Zuul安全配置
自定义WebSecurityConfig
在Spring Security的上下文中,WebSecurityConfig类通常用于配置Spring Security的安全策略。此类继承自WebSecurityConfigurerAdapter,WebSecurityConfig可以用来定义Zuul网关的安全设置,通过提供了一种自定义安全配置的方法实现,实际项目中我们对zuul进行的http请求相关安全配置:
java
package test.zuul.config;
import ...
/**
* @Description: Zuul http configuration
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 定义Zuul网关的安全设置
private final static Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
@Autowired
private FilterIgnorePropertiesConfig filterIgnorePropertiesConfig;
@Autowired
private FindByIndexNameSessionRepository<? extends Session> sessionRepository;
@Autowired
private RedisOperationsSessionRepository redisOperationsSessionRepository;
@Autowired
private AuthenticationServiceFeign authenticationService;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable(); //禁用csrf
http
.headers()
.frameOptions()
.disable();
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry =
http
.antMatcher("/**") // 允许所有请求
.authorizeRequests();
filterIgnorePropertiesConfig.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
registry
.anyRequest()
.authenticated();
http
.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login/idpbg"))
.and()
.logout().logoutUrl("/logout").addLogoutHandler(new LogoutHandler() { //登出后处理逻辑
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
if(authentication != null) {
String indexName = authentication.getName();
// 查询用户的 Session 信息
Map<String, ? extends Session> userSessions = sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, indexName);
// 删除当前用户相关session
List<String> sessionIds = new ArrayList<>(userSessions.keySet());
for (String session : sessionIds) {
redisOperationsSessionRepository.deleteById(session);
}
//删除系统当前在线用户信息...
}
Cookie cookie = new Cookie();
//重置cookie...
response.addCookie(cookie);
}
}).logoutSuccessHandler(logoutSuccessHandler()).oauth2Login().and().oauth2Client();
}
private LogoutSuccessHandler logoutSuccessHandler() { //登出成功后处理逻辑
return new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
if(authentication != null) {
if(authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
// 刪除JWT
......
}
}
new SecurityContextLogoutHandler().logout(request, null, null);
try {
response.sendRedirect("/"); //重定向到登录页面
}
} catch (IOException e) {
e.printStackTrace();
}
}
};
}
}