《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 segmentfault.com/a/1190000016227780 「十方」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

一切的业务开发都是基于需求的,首先看看需求:

对访问网关的请求进行token校验,只有当token校验通过时,才转发到后端服务,否则直接返回401

本文给出的示例代码适用场景:

token存放在redis中, key为用户的uid

依赖的pom.xml

<?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>com.winture</groupId>
<artifactId>api-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>api-gateway</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.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>
<spring-cloud.version>Finchley.SR1</spring-cloud.version>
</properties>

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

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

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

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</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>

在Spring Cloud Gateway中,主要有两种类型的过滤器:GlobalFilterGatewayFilter

  • GlobalFilter : 全局过滤器,对所有的路由均起作用
  • GatewayFilter : 只对指定的路由起作用

1、自定义GatewayFilter

自定义GatewayFilter又有两种实现方式,一种是直接 实现GatewayFilter接口,另一种是 继承AbstractGatewayFilterFactory类 ,任意选一种即可

1.1 实现GatewayFilter接口

package com.winture.gateway.filter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* token校验过滤器
* @Version V1.0
*/
public class AuthorizeGatewayFilter implements GatewayFilter, Ordered {

private static final String AUTHORIZE_TOKEN = "token";
private static final String AUTHORIZE_UID = "uid";

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(AUTHORIZE_TOKEN);
String uid = headers.getFirst(AUTHORIZE_UID);
if (token == null) {
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
if (uid == null) {
uid = request.getQueryParams().getFirst(AUTHORIZE_UID);
}

ServerHttpResponse response = exchange.getResponse();
if (StringUtils.isEmpty(token) || StringUtils.isEmpty(uid)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
String authToken = stringRedisTemplate.opsForValue().get(uid);
if (authToken == null || !authToken.equals(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}

return chain.filter(exchange);
}

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

}

首先从header头信息中获取uid和token信息,如果token或者uid为null,则从请求参数中尝试再次获取,如果依然不存在token或者uid,则直接返回401状态吗,同时结束请求;如果两者都存在,则根据uid从redis中读取保存的authToken,并和请求中传输的token进行比对,比对一样则继续通过过滤器链,否则直接结束请求,返回401.

如何应用 AuthorizeGatewayFilter 呢?

package com.winture.gateway;

import com.winture.gateway.filter.AuthorizeGatewayFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class GatewayApplication {

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

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes().route(r ->
r.path("/user/list")
.uri("http://localhost:8077/api/user/list")
.filters(new AuthorizeGatewayFilter())
.id("user-service"))
.build();
}
}

1.2 继承AbstractGatewayFilterFactory类

package com.winture.gateway.filter.factory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
public class AuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthorizeGatewayFilterFactory.Config> {

private static final Log logger = LogFactory.getLog(AuthorizeGatewayFilterFactory.class);

private static final String AUTHORIZE_TOKEN = "token";
private static final String AUTHORIZE_UID = "uid";

@Autowired
private StringRedisTemplate stringRedisTemplate;

public AuthorizeGatewayFilterFactory() {
super(Config.class);
logger.info("Loaded GatewayFilterFactory [Authorize]");
}

@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("enabled");
}

@Override
public GatewayFilter apply(AuthorizeGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
if (!config.isEnabled()) {
return chain.filter(exchange);
}

ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(AUTHORIZE_TOKEN);
String uid = headers.getFirst(AUTHORIZE_UID);
if (token == null) {
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
if (uid == null) {
uid = request.getQueryParams().getFirst(AUTHORIZE_UID);
}

ServerHttpResponse response = exchange.getResponse();
if (StringUtils.isEmpty(token) || StringUtils.isEmpty(uid)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
String authToken = stringRedisTemplate.opsForValue().get(uid);
if (authToken == null || !authToken.equals(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
return chain.filter(exchange);
};
}

public static class Config {
// 控制是否开启认证
private boolean enabled;

public Config() {}

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}

如何应用 AuthorizeGatewayFilterFactory 呢?

# 网关路由配置
spring:
cloud:
gateway:
routes:
- id: user-service
uri: http://localhost:8077/api/user/list
predicates:
- Path=/user/list
filters:
# 关键在下面一句,值为true则开启认证,false则不开启
# 这种配置方式和spring cloud gateway内置的GatewayFilterFactory一致
- Authorize=true

上面的两种方式都可以实现对访问网关的 特定请求 进行token校验,如果想对 所有的请求 都进行token校验,那么可以采用实现 GlobalFilter 方式。

2、自定义GlobalFilter

package com.winture.gateway.filter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* token校验全局过滤器
* @Version V1.0
*/
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
private static final String AUTHORIZE_TOKEN = "token";
private static final String AUTHORIZE_UID = "uid";

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(AUTHORIZE_TOKEN);
String uid = headers.getFirst(AUTHORIZE_UID);
if (token == null) {
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
if (uid == null) {
uid = request.getQueryParams().getFirst(AUTHORIZE_UID);
}

ServerHttpResponse response = exchange.getResponse();
if (StringUtils.isEmpty(token) || StringUtils.isEmpty(uid)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
String authToken = stringRedisTemplate.opsForValue().get(uid);
if (authToken == null || !authToken.equals(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}

return chain.filter(exchange);
}

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

如何应用 AuthorizeFilter 呢?

只需要添加 @Component 注解,不需要进行任何额外的配置,实现GlobalFilter接口,自动会对所有的路由起作用

3、总结

由于刚接触Spring Cloud Gateway,有些地方也不是特别熟悉,上面的示例代码仅仅作为参考,如果有错误的地方,还望指正。

备注

  • 运行上面的代码,需要先启动redis服务,由于没有配置redis的地址和端口,默认采用localhost和6379端口,如果不一致,请自行在application.yml文件中配置即可;
  • 网关的端口采用默认的8080;
  • 当通过网关访问/user/list时,如果token验证通过,会转发到 http://localhost:8077/api/user/list 上,这是另外的一个接口服务,自行根据实际情况修改;

参考学习:

文章目录
  1. 1. 1、自定义GatewayFilter
    1. 1.1. 1.1 实现GatewayFilter接口
    2. 1.2. 1.2 继承AbstractGatewayFilterFactory类
  2. 2. 2、自定义GlobalFilter
  3. 3. 3、总结