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,接口通了