写点什么

Spring Cloud 微服务网关 Zuul 过滤链和整合 OAuth2+JWT 入门实战

  • 2023-04-12
    湖南
  • 本文字数:9811 字

    阅读完需:约 32 分钟

一、Spring Cloud Zuul 过滤链

1.1 工作原理

Zuul 的核心逻辑是由一系列的 Filter 来实现的,他们能够在进行 HTTP 请求或者相应的时候执行相关操作。


Zuul Filter 的主要特性有一下几点:

  • Filter 的类型:Filter 的类型决定了它在 Filter 链中的执行顺序。路由动作发生前、路由动作发生时,路由动作发生后,也可能是路由过程发生异常时。

  • Filter 的执行顺序:同一种类型的 Filter 可以通过 filterOrder()方法来设定执行顺序

  • Filter 的执行条件:Filter 运行所需要的条件

  • Filter 的执行效果:符合某个 Filter 执行条件,产生的执行效果


Zuul 内部提供了一个动态读取、编译和运行这些 Filter 的机制。Filter 之间不能直接通信,在请求线程中通过 RequestContext 来共享状态,它的内部是用 ThreadLocal 实现的。

上图描述了 Zuul 关于 Filter 的请求生命周期。

  • pre:在 Zuul 按照规则路由到下级服务之前执行。如果需要对请求进行预处理,比如鉴权、限流等,可在考虑在这类 Filter 中实现。

  • route:这类 Filter 是 Zuul 路由动作的执行者,是 Http 客户端构建和发送 HTTP 请求的地方。

  • post:这类 Filter 是在原服务返回结果或者异常信息发生后执行,如果需要对返回信息做一些处理,可以在此类 Filter 进行处理。

  • error:在整个生命周期内如果发生异常,则会进入 error Filter,可以做全局异常处理


其中 post Filter 抛出错误分成两种情况:

  1. 在 post Filter 抛错之前,pre、route Filter 没有抛错,此时会进入 ZuulException 的逻辑,打印堆栈信息,然后再返回 status=500 的 Error 信息

  2. 再 post Filter 跑错之前,pre、route Filter 已有跑错,此时不会打印堆栈信息,直接返回 status=500 的 error 信息。


也就是说整个责任链中重点不只是 post Filter,还可能是 error Filter。


在实际项目中,需要子实现以上类型的 Filter 来对链路进行处理,根据业务的需求,选取对应生命周期的 Filter 来达到目的。每个 Filter 之间通过 RequestContext(Zuul 包中)类来进行通信,内部采用 ThreadLocal 保存每个请求的一些信息,包括请求路由,错误信息,HttpServletRequest,HttpServletResponse,这使得一些操作十分可靠,它害扩展了 ConcurrentHashMap,目的是为了在处理过程中保存任何形式的信息。

1.2 Zuul 中的原生 Filter

Zuul Server 通过@EnableZuulProxy开启之后,搭配 Spring Boot Actuator,会多两个管控断点。

在配置文件中配置一下:

management:  endpoints:    web:      exposure:        include: 'routes,filters'
复制代码

1、/route:返回当前 Zuul Server 中已生成的映射规则,加上/details 可查看明细。例如

每个路由的详细信息

2、/filters:返回当前 Zuul Filter 中已注册生效的 Filter

从 Filter 的信息可以看到,所有已经注册生效的 Filter 的信息:Filter 实现类的路径、Filter 执行次序、是否被禁用、是否静态。而且很明显地可以看出 Zuul 内 Filter 的整个请求的生命流程,如下图:

Zuul 中各内置的 Filter:

上表为使用@EnableZuulProxy之后安装的 Filter,当使用@EnableZuulServer将会缺少 PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter。这些原生的 Filter 可以关掉,例如:在配置文件里面配置zuul.SendErrorFilter.error.disable=true

1.3 多过滤器组成过滤链

在实际中我们不仅是只定义一个过滤器,而是多个过滤器组成过滤链来完成工作,除了 Zuul 的其他网关也是有这个功能。


要在 Zuul 中自定义 Filter 子需要继承 ZuulFilter 即可。它是个抽象类,主要实现的几个方法:

  • String filterType():使用返回值定义 Filter 的类型,有 pre、route、post、error

  • int filterOrder():使用返回值设置 Filter 的执行顺序

  • boolean shouldFilter():使用返回值设置 Filter 是否执行,即所定义 Filter 的开关

  • Object run():Filter 里面的核心执行逻辑便需要写在该方法里面


自定义一个前置过滤器,如下:

public class CustomPreFilter extends ZuulFilter {    @Override    public String filterType() {        return "pre";    }
@Override public int filterOrder() { return 0; }
@Override public boolean shouldFilter() { return true; }
@Override public Object run() throws ZuulException { LOG.info("This is custom pre filter..."); return null; }}
复制代码

FirstPreFilter注入到 Spring Bean 容器

@Configurationpublic class ZuulFilterConfig {
@Bean public CustomPreFilter customPreFilter() { return new CustomPreFilter(); }}
复制代码

然后启动分别启动eurekazuulservice-a,访问http://localhost:88/servicea/add?a=1&b=2。观察网关的日志输出


INFO 20260 --- [ XNIO-1 task-1] c.m.better.zuul.filter.CustomPreFilter : This is custom pre filter...


到这可以看到定义一个 Zuul 过滤器其实很简单,对于微服务网关来说不仅是 Zuul,其他的微服务网关也是,很大部分的开发工作都是开发各种过滤器来达到我们目的。现在来实现一个简单的参数校验功能:


FirstPreFilter:

public class FirstPreFilter extends ZuulFilter {    private Logger log = LoggerFactory.getLogger(FirstPreFilter.class);
@Override public String filterType() { // 自定义的过滤器类型为前置过滤器 return PRE_TYPE; }
@Override public int filterOrder() { // 自定义过滤器的执行次序 return 2; }
@Override public boolean shouldFilter() { return true; }
@Override public Object run() throws ZuulException { log.info("first pre filter..."); // 拿到请求上下文 RequestContext requestContext = RequestContext.getCurrentContext(); // 拿到HttpServletRequest HttpServletRequest request = requestContext.getRequest(); // 获取传入的参数值 String a = request.getParameter("a"); if (StringUtils.isBlank(a)) { // 禁止路由,也就是不允许访问下游服务 requestContext.setSendZuulResponse(false); // 设置响应结果,供PostFilter使用,参数是字符串,序列化一下返回对象也行。 ObjectMapper mapper = new ObjectMapper(); Map<String, Object> map = new HashMap<>(); map.put("code", -1); map.put("msg", "参数a不能为空"); String result = null; try { result = mapper.writeValueAsString(map); } catch (JsonProcessingException e) { e.printStackTrace(); } requestContext.setResponseBody(result); // parameter-check-success保存于上下文,作为同类型下游Filter的执行开关 requestContext.set("parameter-check-success", false); return null; } // 设置避免报空 requestContext.set("parameter-check-success", true); return null; }}
复制代码

SecondPreFilter:

public class SecondPreFilter extends ZuulFilter {    private Logger log = LoggerFactory.getLogger(SecondPreFilter.class);
@Override public String filterType() { return PRE_TYPE; }
@Override public int filterOrder() { return 3; }
@Override public boolean shouldFilter() { RequestContext requestContext = RequestContext.getCurrentContext(); // 参数a是否检验成功,不成功那就没必要继续执行下去 return (boolean) requestContext.get("parameter-check-success"); }
@Override public Object run() throws ZuulException { log.info("second pre filter..."); // 拿到请求上下文 RequestContext requestContext = RequestContext.getCurrentContext(); // 拿到HttpServletRequest HttpServletRequest request = requestContext.getRequest(); // 获取传入的参数值 String b = request.getParameter("b"); if (StringUtils.isBlank(b)) { // 禁止路由,也就是不允许访问下游服务 requestContext.setSendZuulResponse(false); // 设置响应结果,供PostFilter使用,参数是字符串,序列化一下返回对象也行。 ObjectMapper mapper = new ObjectMapper(); Map<String, Object> map = new HashMap<>(); map.put("code", -1); map.put("msg", "参数b不能为空"); String result = null; try { result = mapper.writeValueAsString(map); } catch (JsonProcessingException e) { e.printStackTrace(); } requestContext.setResponseBody(result); // parameter-check-success保存于上下文,作为同类型下游Filter的执行开关 requestContext.set("parameter-check-success", false); return null; } return null; }}
复制代码

CustomPostFilter:

public class CustomPostFilter extends ZuulFilter {
private static final Logger LOG = LoggerFactory.getLogger(CustomPostFilter.class);
@Override public String filterType() { return POST_TYPE; }
@Override public int filterOrder() { return 0; }
@Override public boolean shouldFilter() { return true; }
@Override public Object run() throws ZuulException { System.out.println("这是PostFilter!"); // 从RequestContext获取上下文 RequestContext requestContext = RequestContext.getCurrentContext(); // 处理返回中文乱码 requestContext.getResponse().setCharacterEncoding("UTF-8"); // 获取上下文中保存的responseBody String responseBody = requestContext.getResponseBody(); // 如果responseBody不为空,则说明流程有异常发生 if (null != responseBody) { //设定返回状态码 requestContext.setResponseStatusCode(500); //替换响应报文 requestContext.setResponseBody(responseBody); } return null; }}
复制代码

这整个小功能实现下来,体验到了 Zuul 中过滤器的执行顺序,以及通过RequestContext来获取HttpServletRequest得到请求信息。

二、Spring Cloud Zuul 整合 OAuth2+JWT 入门实战

作为一个微服务网关,一般我们会在网关上进行鉴权,对于网关后面众多的无状态服务常用的授权和认证便是基于 OAuth2。

2.1 什么是 OAuth2 和 JWT

OAuth2 是 OAuth 协议的第二个版本,是对授权认证比较成熟地面向资源的授权协议,在业界中广泛应用。出了定义了常用的用户名密码登录之后,还可以使用第三方一个用登录。例如在某些网站上可以使用 QQ、微信、Github 等进行登录。其主要流程如下:


至于 JWT 则是一种使用 JSON 格式来规约 Token 和 Session 的协议。因为传统的认证方式中会产生一个凭证,比如 Session 会话是保存在服务端,然后依赖于 Cookie 返回给客户端,Session 是有状态的。但是对于众多的微服务来说又是无状态,便诞生像 JWT 这样的解决方案。


JWT 通常有三部分组成:

  • Header:头部,指定 JWT 使用的签名算法

  • Payload:载荷,包含一些自定义或非自定义的认证信息

  • Signature:签名,将头部和载荷用.连接之后,使用头部的签名算法生成的签名信息并拼接到末尾


OAuth2 + JWT 就是服务端使用 OAuth2 的方式进行认证,然后颁发一个 Token,而这个 Token 使用 JWT。客户端拿着这个 Token,便可以访问系统,一般我们会给这个 Token 设置一个有效期,因为服务端并不会保存这个 Token。OAuth2 的实现有很多,这里使用 Spring 社区的基于Spring Security实现的 OAuth2

2.2 Zuul + OAuth2 + JWT 入门实操

2.2.1 修改 cloud-zuul-gateway

在 Zuul 网关中我们需要对接口的请求进行保护,判断是否登录鉴权。如果未登录需要重定向到登录页面,登录成功由认证服务器颁发 JWT Token;把 JWT Token 放到请求头传递到下游服务器。


引入 Maven 依赖:

<dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-security</artifactId></dependency><dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-oauth2</artifactId></dependency>
复制代码

配置文件:

  • 首先定义了service-a服务的路由规则

  • 注册中心 Eureka 的地址

  • 验证授权端点:http://localhost:7788/uaa/oauth/authorize

  • Token 的颁发端点:http://localhost:7788/uaa/oauth/token

  • 默认是使用 HS256 加密算法,密钥是hahaha。加密算法的话建议使用安全性更高的非堆成加密

server:  port: 88spring:  application:    name: zuul-gatewayeureka:  client:    serviceUrl:      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8671}/eureka/  instance:    prefer-ip-address: truezuul:  routes:    service-a:      path: /servicea/**      serviceId: service-asecurity:  oauth2:    client:      access-token-uri: http://localhost:7788/uaa/oauth/token #令牌端点      user-authorization-uri: http://localhost:7788/uaa/oauth/authorize #授权端点      client-id: zuul-gateway #OAuth2客户端ID      client-secret: my-secret #OAuth2客户端密钥    resource:      jwt:        key-value: hahaha #使用对称加密方式,默认算法为HS256
复制代码

WebSecurity 的配置:主要是声明

@Configuration@Order(101)public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login", "/servicea/**") .permitAll() .anyRequest() .authenticated() .and() .csrf() .disable(); }}
复制代码

在启动类上添加@EnableOAuth2Sso注解

@SpringBootApplication@EnableZuulProxy@EnableDiscoveryClient@EnableOAuth2Ssopublic class ZuulServerApplication {    public static void main(String[] args) {        SpringApplication.run(ZuulServerApplication.class, args);    }}
复制代码
2.2.2 编写认证服务器 cloud-auth-server

创建cloud-auth-server来基于 OAuth2 实现我们的认证服务器。依赖如下:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <parent>        <artifactId>cloud-zuul-practice-intermediate</artifactId>        <groupId>com.msr.better</groupId>        <version>1.0</version>    </parent>    <modelVersion>4.0.0</modelVersion>
<artifactId>cloud-auth-server</artifactId>
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
复制代码

配置文件application.yml

spring:  application:    name: cloud-auth-serverserver:  port: 7788  servlet:    contextPath: /uaaeureka:  client:    serviceUrl:      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8671}/eureka/  instance:    prefer-ip-address: true
复制代码

认证服务器配置:继承AuthorizationServerConfigurerAdapter编写认证授权服务器配置。主要是指定 clientId、密钥、以及权限定义和作用域声明,指定JwtTokenStore,类似的实现 Spring Security 还有RedisTokenStore等。

@Configuration@EnableAuthorizationServerpublic class AuthServerConfig extends AuthorizationServerConfigurerAdapter {    @Autowired    private AuthenticationManager authenticationManager;
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients .inMemory() .withClient("zuul-gateway") .secret("my-secret") .scopes("write", "read").autoApprove(true) .authorities("WRIGTH_READ", "WRIGTH_WRITE") .authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code") .redirectUris("http://localhost:88/login"); }
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(jwtTokenStore()) .tokenEnhancer(jwtTokenConverter()) .authenticationManager(authenticationManager); }
@Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtTokenConverter()); }
@Bean protected JwtAccessTokenConverter jwtTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("hahaha"); return converter; }}
复制代码

Web Security 相关配置:声明 guest 用户,密码为 guest,拥有 READ 权限。admin 用户,密码为 admin,拥有 READ、WRITE 权限。


AuthenticationManager是认证管理器,需要注入到 Spring 容器中。passwordEncoder()声明密码的加密方式,在 Spring Security 中要求需要对密码进行加密,因此需要向 Spring 容器中注入。但是这里使用了内存的方式存放用户信息,而且密码是原值保存,所以使用NoOpPasswordEncoder,即不做加密处理。

@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean public static NoOpPasswordEncoder passwordEncoder() { return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance(); }
@Bean(name = BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("guest").password("guest").authorities("READ") .and() .withUser("admin").password("admin").authorities("READ", "WRITE"); }}
复制代码

认证服务器启动类:

@SpringBootApplication@EnableDiscoveryClientpublic class AuthApplication {
public static void main(String[] args) { SpringApplication.run(AuthApplication.class, args); }}
复制代码
2.2.3 cloud-service-a 服务整合资源服务器

service-a 的编写相对简单,在 Spring Security OAuth2 中,每个服务都是一个资源服务器,拥有者该服务的资源。


引入依赖:

<dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-security</artifactId></dependency><dependency>	<groupId>org.springframework.cloud</groupId>	<artifactId>spring-cloud-starter-oauth2</artifactId></dependency>
复制代码

配置文件:

server:  port: 8080spring:  application:    name: service-aeureka:  client:    serviceUrl:      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8671}/eureka/  instance:    prefer-ip-address: true
复制代码

编写资源服务器:

@Configurationpublic class ServiceAResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override public void configure(HttpSecurity http) throws Exception { http.csrf() .disable() .authorizeRequests() .antMatchers("/**").authenticated() .antMatchers(HttpMethod.GET, "/servicea/test") .hasAuthority("WRIGHT_READ"); }
@Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("WRIGHT") .tokenStore(jwtTokenStore()); }
@Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter(); tokenConverter.setSigningKey("hahaha"); return tokenConverter; }
@Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); }}
复制代码

编写ClientController

@RestController@RequestMappingpublic class ClientController {
@GetMapping("/test") public String test(HttpServletRequest request, HttpServletResponse response) { System.out.println("================header================"); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String key = headerNames.nextElement(); System.out.println(key + ": " + request.getHeader(key)); } System.out.println("================header================"); return "hello word!"; }}
复制代码

servicea 的启动类:启用资源服务器@EnableResourceServer

@SpringBootApplication@EnableDiscoveryClient@EnableResourceServerpublic class ServiceAApplication {
public static void main(String[] args) { SpringApplication.run(ServiceAApplication.class, args); }}
复制代码
2.2.4 测试

先启动注册中心 Eureka、然后启动 Zuul 网关、serivce-a、auth-server。


请求访问:

http://localhost:88/service/test

OAuth2 + JWT 实战小总结

这里关于 Zuul 整合 OAuth2 + JWT 的介绍就到这,后面会写一篇详细的Spring Security实现的 OAuth2 文章。本文这里用到的认证服务器和资源服务器是较为早期的写法了,前年Spring Security开了一个新项目专门来编写认证服务器。


作者:LoveLifsSuper

链接:https://juejin.cn/post/7220253332421541944

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
Spring Cloud微服务网关Zuul过滤链和整合OAuth2+JWT入门实战_Java_做梦都在改BUG_InfoQ写作社区