Spring Cloud——API网关服务:Spring Cloud Zuul

API网关像是整个微服务框架系统的门面一样,所有的客户端访问都需要经过它来进行调度和过滤。它实现了请求路由、负载均衡、校验过滤等功能。zuul包含了hystrix、ribbon、acturator等重要依赖。

(一)zuul实现例子

(1)服务注册中心和服务提供者

参考前一篇文章:https://blog.csdn.net/hjy132/article/details/84871891

(2)服务消费者

创建Spring Boot工程,名为zuul-web,在pom.xml中引入zuul和euraka的依赖

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>iwhale</groupId>
    <artifactId>zuul-web</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>zuul-web</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.13.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>
    </dependencies>

    <!--idea自动提示引入依赖,但是没有需要的版本,如spring-cloud-starter-eureka-server。这就是一个典型的版本老旧的问题。
    建议大家不易一个一个去定义,直接使用dependencyManagement 自动引入即可-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Edgware.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

创建应用主类,使用@EnableZuulProxy注解开启Zuul的API网关服务功能。

@EnableZuulProxy
@SpringCloudApplication
public class ZuulWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulWebApplication.class, args);
    }
}

本例使用面向服务的路由,针对服务cloudservice定义了名为cloudservice的路由映射它。同时,通过指定Eureka服务注册中心的位置,除了将自己注册成服务之外,也让zuul能够获取eureka下的服务实例清单,以实现path映射服务,再从服务中挑选实例来进行请求转发的完整路由机制。文件application.yml配置如下

server:
  port: 9884
#  context-path: /${spring.application.name}

#############################spring配置#############################
spring:
  application:
    name: zuul-web


#############################eureka配置#############################
eureka:
  client:
    serviceUrl:
      defaultZone:  http://127.0.0.1:8888/eureka/
    eureka-server-read-timeout-seconds: 60
zuul:
  routes:
    cloudservice:
      path: /cloudservice/**
      serviceId: cloudservice

路由通配符:

(3)测试

分别启动注册中心,两个服务cloudservice(端口分别为9881和9882)和zuul-web,根据映射关系向网关发起请求:http://localhost:9884/cloudservice/hello1?name=hjo,以访问接口/hello1,并输入参数,不断刷新,可以发现以交替方式访问两个cloudservice服务

(二)请求过滤

zuul允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,只需要继承ZuulFilter抽象类并定义它的4个抽象方法就可以完成对请求的拦截和过滤。Spring Cloud Zuul中实现的过滤器必选包含4个基本重写方法:过滤请求、执行顺序,执行条件、具体操作,如下。

  • filterType:过滤器类型,决定过滤器在请求的哪个生命周期中执行,如“pre”,代表会在请求被路由之前执行。
  • filterOrder:过滤器执行顺序,当存在多个过滤器时,需要根据该方法返回的值来一次执行
  • shouldFilter:判断该过滤器是否需要被执行,直接返回true,表示对所有请求都生效,实际中,可以利用该函数来指定过滤器的有效范围
  • run:过滤器的具体逻辑

4种不同的过滤器类型:

  • pre:在请求被路由之前调用,目的是做一些前置加工,比如请求的校验等
  • routing:在路由请求时调用,具体是将请求转发到具体服务实例,当服务实例返回请求结果时,该阶段完成
  • post:在routing和error过滤器之后被调用,这些过滤器不仅可以获取到请求信息,还可以获取到服务实例的返回信息,所以可以对返回结果进行一些加工或者转换。
  • error:处理请求时发生错误时被调用,但最终还是流向到post过滤器

下例的过滤器,实现了在请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,没有就拒绝访问,返回401错误。

public class AccessFilter  extends ZuulFilter {

    private static Logger log=LoggerFactory.getLogger(AccessFilter.class);

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx=RequestContext.getCurrentContext();
        HttpServletRequest request=ctx.getRequest();
        log.info("send{} request to {}",request.getMethod(),request.getRequestURL().toString());

        Object accessToken=request.getParameter("accessToken");
        if(accessToken==null){
            log.warn("access token is empty");
            ctx.setSendZuulResponse(false);//另zuul过滤该请求,不对其进行路由
            ctx.setResponseStatusCode(401);//设置返回的错误码
            return null;
        }
        log.info("access token ok");
        return null;
    }
}

过滤器需要为其创建具体的Bean才能启动,在主类中增加内容:

@EnableZuulProxy
@SpringCloudApplication
public class ZuulWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulWebApplication.class, args);
    }
    
    @Bean
    public AccessFilter accessFilter(){
        return new AccessFilter();
    }
}

重新启动zuul-web,访问请求中还有accessToken参数

添加以下内容可关闭该过滤器:

zuul:
  AccessFilter:
    pre:
      disable: true #关闭AccessFilter过滤器

(三)重试机制

(1)引入spring-retry依赖

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

(2)application.yml配置

zuul:
  retryable: true #开启重试机制

ribbon:
  MaxAutoRetries: 2 #对当前实例的重试次数
  MaxAutoRetriesNextServer: 1 #切换实例的重试次数

(3)对9881端口的cloudservice服务进行修改,然后重新启动

@RequestMapping(value = "/hello1",method = RequestMethod.GET)
    public String hello(@RequestParam String name) throws Exception{
        ServiceInstance instance=client.getLocalServiceInstance();
        System.out.println("request is coming..");
        //测试超时
        Thread.sleep(8000);
        return "ServiceId:"+instance.getServiceId()+
                ", port:"+instance.getPort()+",  参数为:"+name;
    }

测试可看出,当轮询到9881端口时,访问了3次9881服务(睡眠8s达到Zuul的转发超时情况,Zuul默认连接超时未2s、read超时时间为5s),最后切换到9882端口的服务

重试机制需要慎重使用,使用的接口需要保证幂等性(即对接口的多次调用所产生的结果和调用一次是一致的),举个栗子,在系统中,调用方A调用系统B的接口进行用户的扣费操作时,由于网络不稳定,A重试了N次该请求,那么不管B是否接收到多少次请求,都应该保证只会扣除该用户一次费用。

(四)zuul权限集成——OAuth2.0+JWT

OAuth2.0是业界对于“授权-认证”比较成熟的面向资源的授权协议,而JWT(JSON Web Token)是一种使用Json格式来规范Token或者Session的协议。JWT由三部分组成:

  • Header头部:JWT使用的签名算法
  • Payload载荷:包含一些自定义与非自定义的认证信息
  • Signature签名:将头部与载荷使用“.”连接之后,使用头部的签名算法生成签名信息并拼装到末尾

OAuth2.0+JWT的意义在于,使用OAuth2.0协议的思想拉取认证生成的Token,使用JWT瞬时保存这个Token,在客户端与资源端进行对称或非对称加密,使得这个规约具有定时、定量的授权认证功能,从而免去Token存储所带来的安全或系统扩展问题。

实践说明:zuul-web请求时,需判断是否登录授权,如果未登录,则跳转到auth-server的登录界面(这里使用Spring Security OAuth的默认登录界面,也可以重写相关代码定制页面),登录成功后auth-server颁发jwt token,zuul-web在访问下游服务时将jwt token放入header中即可。

(1)zuul-web模块

pom.xml需要引入Oauth2与Security的依赖

<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>

文件application.yml添加以下配置

security:
  basic:
    enabled: false
  oauth2:
    client:
      access-token-uri: http://localhost:7777/auth-server/oauth/token #令牌端点
      user-authorization-uri: http://localhost:7777/auth-server/oauth/authorize #授权端点
      client-id: zuul_web #Oauth2客户端ID
      client-secret: secret #Oauth2客户端密钥
    resource:
      jwt:
        key-value: springcloud123 #使用对称加密方法,默认算法为HS256

启动类,重写WebSecurityConfigurerAdapter适配器的configure(HttpSecurity http)方法,声明需要鉴权的url信息。

@EnableZuulProxy
@SpringCloudApplication
@EnableOAuth2Sso
public class ZuulWebApplication extends WebSecurityConfigurerAdapter {
    public static void main(String[] args) {
        SpringApplication.run(ZuulWebApplication.class, args);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests()
                .antMatchers("/login","/cloudservice/**")
                .permitAll().anyRequest()
                .authenticated().and()
                .csrf().disable();
    }
}

(2)auth-server模块

创建auth-server工程,该模块作为认证授权中心,会颁发jwt token凭证

pom.xml如下,引入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">
    <modelVersion>4.0.0</modelVersion>
    <groupId>iwhale</groupId>
    <artifactId>auth-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>auth-server</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.13.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
    </dependencies>

    <!--idea自动提示引入依赖,但是没有需要的版本,如spring-cloud-starter-eureka-server。这就是一个典型的版本老旧的问题。
    建议大家不易一个一个去定义,直接使用dependencyManagement 自动引入即可-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Edgware.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置文件application.yml如下

server:
  port: 7777
  context-path: /${spring.application.name}

#############################spring配置#############################
spring:
  application:
    name: auth-server


#############################eureka配置#############################
eureka:
  client:
    serviceUrl:
      defaultZone:  http://127.0.0.1:8888/eureka/
    eureka-server-read-timeout-seconds: 60

认证授权服务适配器类OauthConfiguration.java,该类用于指定客户端ID、密钥,以及权限定义与作用域声明,指定TokenStore为JWT。

@Configuration
@EnableAuthorizationServer
public class OauthConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
        clients.inMemory()
                .withClient("zuul_web")   //需要与zuul-web模块yml里的client-id一致
                .secret("secret")
                .scopes("WRIGTH","read")
                .autoApprove(true)
                .authorities("WRIGTH_READ","WRIGTH_WRITE")
                .authorizedGrantTypes("implicit","refresh_token","password","authorization_code");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
        endpoints.tokenStore(jwtTokenStore())
                .tokenEnhancer(jwtTokenConverter())
                .authenticationManager(authenticationManager);
    }

    @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtTokenConverter(){
        JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
        converter.setSigningKey("springcloud123");
        return converter;
    }
}

启动类如下,声明用户admin具有读写权限,用户guest具有读权限;authenticationManagerBean()方法用于手动注入AuthenticationManager;passwordEncoder()用于声明用户名和密码的加密方式。

@EnableDiscoveryClient
@SpringBootApplication
public class AuthServerApplication extends WebSecurityConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(AuthServerApplication.class, args);
    }

    @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("WRIGTH_READ")
        .and()
        .withUser("admin").password("admin").authorities("WRIGTH_READ","WRIGTH_WRITE");
    }

    public static NoOpPasswordEncoder passwordEncoder(){
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }
}

(3)cloud_service模块

在pom.xml文件添加以下依赖

<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>

启动类如下,需要按照规则解析jwt token

@EnableDiscoveryClient
@SpringBootApplication
@EnableResourceServer
public class CloudServiceApplication extends ResourceServerConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(CloudServiceApplication.class, args);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception{
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/**").authenticated()
                .antMatchers(HttpMethod.GET,"test")
                .hasAuthority("WRIGHT_READ");
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception{
        resources.resourceId("WRIGTH")
                .tokenStore(jwtTokenStore());
    }

    @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtTokenConverter(){
        JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
        converter.setSigningKey("springcloud123");
        return converter;
    }
}

创建一个Controller类

@RestController
public class OauthController{

    @RequestMapping("/test")
    public String test(HttpServletRequest request) {
        System.out.println("-----------header------------");
        Enumeration headerNames=request.getHeaderNames();
        while(headerNames.hasMoreElements()){
            String key=(String) headerNames.nextElement();
            System.out.println(key+":"+request.getHeader(key));
        }
        return "helloooooooooo!";
    }

}

(4)测试

先后启动eruke,cloud_service,auth-server,zuul-web

访问url:http://localhost:9884/cloudservice/test,结果如下,由于还没有登录授权,该接口是调不通的

访问url:http://localhost:9884,跳转到auth-server默认的登录页面,使用用户名admin登录

再访问url:http://localhost:9884/cloudservice/test,接口通了

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值