Ruoyi框架学习笔记-Spring Cloud Gateway+认证中心
<p>[TOC]</p>
<h1>1. Spring Cloud Gateway和认证中心简介</h1>
<p>Spring Cloud Gateway 是由 WebFlux + Netty + Reactor 实现的响应式的 API 网关。</p>
<p>Spring Cloud Gateway 旨在为微服务架构提供一种简单且有效的 API 路由的管理方式,并基于 Filter 的方式提供网关的基本功能,例如说安全认证、监控、限流等等。</p>
<p>Spring Cloud Gateway 定位于取代 Netflix Zuul,成为 Spring Cloud 生态系统的新一代网关。目前看下来非常成功,老的项目的网关逐步从 Zuul 迁移到 Spring Cloud Gateway,新项目的网关直接采用 Spring Cloud Gateway。相比 Zuul 来说,Spring Cloud Gateway 提供更优秀的性能,更强大的有功能。</p>
<h1>2. 依赖引入、配置文件与整体架构</h1>
<h2>2.1 引入依赖:</h2>
<pre><code class="language-java">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;
&lt;parent&gt;
&lt;artifactId&gt;...&lt;/artifactId&gt;
&lt;groupId&gt;...&lt;/groupId&gt;
&lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;
&lt;/parent&gt;
&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
&lt;artifactId&gt;...&lt;/artifactId&gt;
&lt;properties&gt;
&lt;spring.boot.version&gt;2.2.4.RELEASE&lt;/spring.boot.version&gt;
&lt;spring.cloud.version&gt;Hoxton.SR1&lt;/spring.cloud.version&gt;
&lt;spring.cloud.alibaba.version&gt;2.2.0.RELEASE&lt;/spring.cloud.alibaba.version&gt;
&lt;/properties&gt;
&lt;!--
引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
--&gt;
&lt;dependencyManagement&gt;
&lt;dependencies&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
&lt;version&gt;${spring.boot.version}&lt;/version&gt;
&lt;type&gt;pom&lt;/type&gt;
&lt;scope&gt;import&lt;/scope&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-dependencies&lt;/artifactId&gt;
&lt;version&gt;${spring.cloud.version}&lt;/version&gt;
&lt;type&gt;pom&lt;/type&gt;
&lt;scope&gt;import&lt;/scope&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-alibaba-dependencies&lt;/artifactId&gt;
&lt;version&gt;${spring.cloud.alibaba.version}&lt;/version&gt;
&lt;type&gt;pom&lt;/type&gt;
&lt;scope&gt;import&lt;/scope&gt;
&lt;/dependency&gt;
&lt;/dependencies&gt;
&lt;/dependencyManagement&gt;
&lt;dependencies&gt;
&lt;!-- 引入 Spring Cloud Gateway 相关依赖,使用它作为网关,并实现对其的自动配置 --&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-starter-gateway&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;/dependencies&gt;
&lt;/project&gt;</code></pre>
<h2>2.2 配置文件</h2>
<p>创建 application.yaml 配置文件,添加 Spring Cloud Gateway 相关配置。配置如下:</p>
<pre><code class="language-yml">server:
port: 8080
spring:
application:
# 应用名称
name: ruoyi-gateway
profiles:
# 环境配置
active: dev
cloud:
# Spring Cloud Gateway 配置项,对应 GatewayProperties 类
gateway:
# 路由配置项,对应 RouteDefinition 数组
routes:
- id: ruoyi-system # 路由的编号
uri: http://localhost:9201/ # 路由到的目标地址
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/system/**
filters:
- StripPrefix=1</code></pre>
<p>① <code>server.port</code> 配置项,设置网关的服务器端口。</p>
<p>② <code>spring.cloud.gateway</code> 配置项,Spring Cloud Gateway 配置项,对应 GatewayProperties 类。</p>
<p>这里我们主要使用了 <code>routes</code> 路由配置项,对应 RouteDefinition 数组。路由(Route)是 Gateway 中最基本的组件之一,由一个 ID、URI、一组谓语(Predicate)、过滤器(Filter)组成。一个请求如果满足某个路由的所有谓语,则匹配上该路由,最终过程如下图:<img src="https://static.iocoder.cn/images/Spring-Cloud-Gateway/2020_01_10/03.png" alt="路由过程" /></p>
<ul>
<li>ID:编号,路由的唯一标识。</li>
<li>
<p>URI:路由指向的目标 URI,即请求最终被转发的目的地。
> 例如说,这里配置的 <code>http://localhost:9201/</code> ,就是被转发的地址。</p>
</li>
<li>
<p>Predicate:谓语,作为路由的匹配条件。Gateway 内置了多种 Predicate 的实现,提供了多种请求的匹配条件,比如说基于请求的 Path、Method 等等。
> 例如说,这里配置的 <code>Path</code>匹配请求的 Path 地址。</p>
</li>
<li>
<p>Filter:过滤器,对请求进行拦截,实现自定义的功能。Gateway 内置了多种 Filter 的实现,提供了多种请求的处理逻辑,比如说限流、熔断等等。</p>
<p>> 例如说,这里配置的 <code>StripPrefix</code>会将请求的 Path 去除掉前缀。假设我们请求 <a href="http://127.0.0.1:8080/system/test时">http://127.0.0.1:8080/system/test时</a>:
> <em> 如果<strong>有</strong>配置 <code>StripPrefix</code> 过滤器,则转发到的最终 URI 为 <a href="http://localhost:9201/test,正确返回">http://localhost:9201/test,正确返回</a>
> </em> 如果<strong>未</strong>配置 <code>StripPrefix</code> 过滤器,转发到的最终 URI 为 <a href="http://localhost:9201/system/test,错误返回">http://localhost:9201/system/test,错误返回</a> 404</p>
</li>
</ul>
<p><strong>集成Nacos作为注册中心和配置中心后的网关模块配置</strong>
bootstrap.yml:</p>
<pre><code class="language-yml"># Tomcat
server:
port: 8080
# Spring
spring:
application:
# 应用名称
name: ruoyi-gateway
profiles:
# 环境配置
active: dev
main:
allow-circular-references: true
allow-bean-definition-overriding: true
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: 127.0.0.1:8843
config:
# 配置中心地址
server-addr: 127.0.0.1:8843
# 配置文件格式
file-extension: yml
# 共享配置
+
shared-configs:
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}</code></pre>
<p>nacos中配置文件ruoyi-gateway-dev.yml:</p>
<pre><code class="language-yml">spring:
...
...
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
# 认证中心
- id: ruoyi-auth
uri: lb://ruoyi-auth
predicates:
- Path=/auth/**
filters:
# 验证码处理
- CacheRequestFilter
- ValidateCodeFilter
- StripPrefix=1</code></pre>
<h2>2.3 Spring Cloud Gateway整体架构</h2>
<p>Gateway中包含3大组件:Route、Predicate、Filter,它们在整体工作流程中的作用,如下图所示:
<img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=9d3dcb0146f8122506dbed52a25670b6&amp;file=file.png" alt="" /></p>
<ul>
<li>
<p>① Gateway 接收客户端请求。</p>
</li>
<li>
<p>② 请求与 Predicate 进行匹配,获得到对应的 Route。匹配成功后,才能继续往下执行。</p>
</li>
<li>
<p>③ 请求经过 Filter 过滤器链,执行前置(prev)处理逻辑。</p>
<p>> 例如说,修改请求头信息等。</p>
</li>
<li>
<p>④ 请求被 Proxy Filter 转发至目标 URI,并最终获得响应。</p>
<p>> 一般来说,目标 URI 是被代理的微服务,如果是在 Spring Cloud 架构中。</p>
</li>
<li>
<p>⑤ 响应经过 Filter 过滤器链,执行后置(post)处理逻辑。</p>
</li>
<li>⑥ Gateway 返回响应给客户端。</li>
</ul>
<h1>3. Route Predicate</h1>
<p>Gateway 内置了多种 Route Predicate 实现,将请求匹配到对应的 Route 上。并且,多个 Route Predicate 是可以组合实现,满足我们绝大多数的路由匹配规则。而 Predicate 的创建,实际是通过其对应的 Predicate Factory 工厂来完成。关系图如下:
<img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=bfa535e93085b09da105d59604800546&amp;file=file.png" alt="" /></p>
<p>一般情况下,我们主要使用 Path 和 Host 两个 Predicate,甚至只使用 Path。</p>
<h1>4. Route Filter</h1>
<p>Gateway 内置了多种 Route Filter 实现,对请求进行拦截,实现自定义的功能,例如说限流、熔断等等功能。并且,多个 Route Filter 是可以组合实现,满足我们绝大多数的路由的处理逻辑。
而 Filter 的创建,实际是通过其对应的 Filter Factory 工厂来完成。关系图如下:
<img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=228e98a01590e252cda625328ddbe864&amp;file=file.png" alt="" /></p>
<p>一般情况下,我们在 Gateway 上会去做的拓展,主要集中在 Filter 上,RuoYi-Cloud中自定义了3个Route Filter:BlackListUrlFilter、CacheRequestFilter和ValidateCodeFilter</p>
<h2>4.1 黑名单过滤器-BlackListUrlFilter</h2>
<p>过滤器效果:访问和blacklistUrl匹配的请求路径,请求将被拦截
类定义如下:</p>
<pre><code class="language-java">/**
* 黑名单过滤器
* 继承了 AbstractGatewayFilterFactory 抽象类,并将泛型参数 &lt;C&gt; 设置为我们定义的 BlackListUrlFilter.Config 配置类。
* 这样,Gateway 在解析配置时,会转换成 Config 对象。
* 注意:在 AuthGatewayFilterFactory 构造方法中,也需要传递 Config 类给父构造方法,保证能够正确创建 Config 对象。
*
* @author ruoyi
*/
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory&lt;BlackListUrlFilter.Config&gt;
{
/**
* 和blacklistUrl匹配的请求路径,将被拦截
* @param config 1
* @return: org.springframework.cloud.gateway.filter.GatewayFilter
* @author: gefeng
* @date: 2022/6/20 15:41
*/
@Override
public GatewayFilter apply(Config config)
{
return (exchange, chain) -&gt; {
String url = exchange.getRequest().getURI().getPath();
if (config.matchBlacklist(url))
{
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), &quot;请求地址不允许访问&quot;);
}
return chain.filter(exchange);
};
}
public BlackListUrlFilter()
{
super(Config.class);
}
public static class Config
{
private List&lt;String&gt; blacklistUrl; //配置的url黑名单
private List&lt;Pattern&gt; blacklistUrlPattern = new ArrayList&lt;&gt;(); //配置的url黑名单对应的匹配模式
public boolean matchBlacklist(String url)
{
return !blacklistUrlPattern.isEmpty() &amp;&amp; blacklistUrlPattern.stream().anyMatch(p -&gt; p.matcher(url).find());
}
public List&lt;String&gt; getBlacklistUrl()
{
return blacklistUrl;
}
public void setBlacklistUrl(List&lt;String&gt; blacklistUrl)
{
this.blacklistUrl = blacklistUrl;
this.blacklistUrlPattern.clear();
this.blacklistUrl.forEach(url -&gt; {
this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll(&quot;\\*\\*&quot;, &quot;(.*?)&quot;), Pattern.CASE_INSENSITIVE));
});
}
}
}</code></pre>
<p>对应配置如下:</p>
<pre><code class="language-yml">spring:
...
...
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
- name: BlackListUrlFilter
args:
blacklistUrl: #对应BlackListUrlFilter.Config中属性名
- /user/list</code></pre>
<h2>4.2 缓存请求过滤器-CacheRequestFilter</h2>
<p>类定义如下:</p>
<pre><code class="language-java">/**
* 获取body请求数据(解决流不能重复读取问题)
*
* @author ruoyi
*/
@Component
public class CacheRequestFilter extends AbstractGatewayFilterFactory&lt;CacheRequestFilter.Config&gt;
{
public CacheRequestFilter()
{
super(Config.class);
}
@Override
public String name()
{
return &quot;CacheRequestFilter&quot;;
}
@Override
public GatewayFilter apply(Config config)
{
CacheRequestGatewayFilter cacheRequestGatewayFilter = new CacheRequestGatewayFilter();
Integer order = config.getOrder();
if (order == null)
{
return cacheRequestGatewayFilter;
}
return new OrderedGatewayFilter(cacheRequestGatewayFilter, order);
}
public static class CacheRequestGatewayFilter implements GatewayFilter
{
@Override
public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
// GET DELETE 不过滤
HttpMethod method = exchange.getRequest().getMethod();
if (method == null || method.matches(&quot;GET&quot;) || method.matches(&quot;DELETE&quot;))
{
return chain.filter(exchange);
}
return DataBufferUtils.join(exchange.getRequest().getBody()).map(dataBuffer -&gt; {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
}).defaultIfEmpty(new byte[0]).flatMap(bytes -&gt; {
DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest())
{
@Override
public Flux&lt;DataBuffer&gt; getBody()
{
if (bytes.length &gt; 0)
{
return Flux.just(dataBufferFactory.wrap(bytes));
}
return Flux.empty();
}
};
return chain.filter(exchange.mutate().request(decorator).build());
});
}
}
@Override
public List&lt;String&gt; shortcutFieldOrder()
{
return Collections.singletonList(&quot;order&quot;);
}
static class Config
{
private Integer order;
public Integer getOrder()
{
return order;
}
public void setOrder(Integer order)
{
this.order = order;
}
}
}</code></pre>
<h2>4.3 验证码过滤器-ValidateCodeFilter</h2>
<p>类定义如下:</p>
<pre><code class="language-java">/**
* 验证码过滤器:过滤/auth/login, /auth/register请求
*
* @author ruoyi
*/
@Component
public class ValidateCodeFilter extends AbstractGatewayFilterFactory&lt;Object&gt;
{
private final static String[] VALIDATE_URL = new String[] { &quot;/auth/login&quot;, &quot;/auth/register&quot; };
@Autowired
private ValidateCodeService validateCodeService;
@Autowired
private CaptchaProperties captchaProperties;
private static final String CODE = &quot;code&quot;;
private static final String UUID = &quot;uuid&quot;;
@Override
public GatewayFilter apply(Object config)
{
return (exchange, chain) -&gt; {
ServerHttpRequest request = exchange.getRequest();
// 非登录/注册请求或验证码关闭,不处理
if (!StringUtils.containsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled())
{
return chain.filter(exchange);
}
try
{
String rspStr = resolveBodyFromRequest(request);
JSONObject obj = JSONObject.parseObject(rspStr);
validateCodeService.checkCaptcha(obj.getString(CODE), obj.getString(UUID));
}
catch (Exception e)
{
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
}
return chain.filter(exchange);
};
}
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest)
{
// 获取请求体
Flux&lt;DataBuffer&gt; body = serverHttpRequest.getBody();
AtomicReference&lt;String&gt; bodyRef = new AtomicReference&lt;&gt;();
body.subscribe(buffer -&gt; {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
return bodyRef.get();
}
}</code></pre>
<h1>5. Global Filter</h1>
<p>在Gateway中,有两类过滤器,除了Route Filter路由过滤器,还有Global Filter全局过滤器,对应GlobalFilter接口。两者基本是等价的,差别在于 Route Filter 不是全局,而是可以配置到指定路由上。接口对比如下图:
<img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=a48e9731609ec50cd22a8a20101a306a&amp;file=file.png" alt="" /></p>
<p>绝大多数情况下,在 Route Filter 能满足我们的拓展需求的情况下,优先使用它。并且如果想要作用到所有路由上,可以通过 spring.cloud.gateway.default-filters 配置项来设置。相同排序值,<code>default-filters</code> 排在 <code>routes[n].filters</code> 前面。</p>
<h2>5.1 网关认证鉴权过滤器-AuthFilter</h2>
<pre><code class="language-java">/**
* 网关鉴权(全局过滤器)
*
* @author ruoyi
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered
{
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
// 排除过滤的 uri 地址,nacos自行添加
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Autowired
private RedisService redisService;
/**
* 过滤所有请求
* 1. 获取请求url路径,完成认证登录的用户请求才可通过
* 2. 从请求头中获取jwt令牌从请求头中获取(Authorization:Bearer jwt令牌)并校验令牌
* 3. 从jwt令牌中获取userKey,userid,username
* 4. 根据userKey组成redis中的tokenKey,判断是否已登录(redis中tokenKey是否存在)
* 5. 设置用户信息到请求头
* 6. 内部请求来源参数清除(将外部请求头中from-source参数清除)
* @param exchange 1
* @param chain 2
* @return: reactor.core.publisher.Mono&lt;java.lang.Void&gt;
* @author: gefeng
* @date: 2022/6/20 11:54
*/
@Override
public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
// 跳过不需要验证的路径(白名单中url不需要认证)
if (StringUtils.matches(url, ignoreWhite.getWhites()))
{
return chain.filter(exchange);
}
String token = getToken(request);
if (StringUtils.isEmpty(token))
{
return unauthorizedResponse(exchange, &quot;令牌不能为空&quot;);
}
Claims claims = JwtUtils.parseToken(token);
if (claims == null)
{
return unauthorizedResponse(exchange, &quot;令牌已过期或验证不正确!&quot;);
}
String userkey = JwtUtils.getUserKey(claims);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
//redis中不存在该tokenKey的值
// tokenKey=CacheConstants.LOGIN_TOKEN_KEY+claims中SecurityConstants.USER_KEY的值
if (!islogin)
{
return unauthorizedResponse(exchange, &quot;登录状态已过期&quot;);
}
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
//若jwt令牌中没有user_id或username
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))
{
return unauthorizedResponse(exchange, &quot;令牌验证失败&quot;);
}
// 设置用户信息到请求头
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
// 内部请求来源参数清除(将外部请求头中from-source参数清除)
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value)
{
if (value == null)
{
return;
}
String valueStr = value.toString();
String valueEncode = ServletUtils.urlEncode(valueStr);
mutate.header(name, valueEncode);
}
private void removeHeader(ServerHttpRequest.Builder mutate, String name)
{
mutate.headers(httpHeaders -&gt; httpHeaders.remove(name)).build();
}
private Mono&lt;Void&gt; unauthorizedResponse(ServerWebExchange exchange, String msg)
{
log.error(&quot;[鉴权异常处理]请求路径:{}&quot;, exchange.getRequest().getPath());
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
}
/**
* 获取缓存key
*/
private String getTokenKey(String token)
{
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
/**
* 获取请求token:从请求头中获取(Authorization:Bearer token)
*/
private String getToken(ServerHttpRequest request)
{
String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
// 如果前端设置了令牌前缀,则裁剪掉前缀
if (StringUtils.isNotEmpty(token) &amp;&amp; token.startsWith(TokenConstants.PREFIX))
{
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
}
return token;
}
/**
* 越小的值,优先级越高,越大的值优先级越低
* @return: int
* @author: gefeng
* @date: 2022/5/17 21:44
*/
@Override
public int getOrder()
{
return -200;
}
}</code></pre>
<h2>5.2 跨站脚本过滤器-XssFilter</h2>
<p>RuoYi-Cloud自定义XssFilter实现GlobalFilter, Ordered接口,定义如下</p>
<pre><code class="language-java">/**
* 跨站脚本过滤器
*
* @author ruoyi
*/
@Component
@ConditionalOnProperty(value = &quot;security.xss.enabled&quot;, havingValue = &quot;true&quot;)
public class XssFilter implements GlobalFilter, Ordered
{
// 跨站脚本的 xss 配置,nacos自行添加
@Autowired
private XssProperties xss;
@Override
public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
ServerHttpRequest request = exchange.getRequest();
// GET DELETE 不过滤
HttpMethod method = request.getMethod();
if (method == null || method.matches(&quot;GET&quot;) || method.matches(&quot;DELETE&quot;))
{
return chain.filter(exchange);
}
// 非json类型,不过滤
if (!isJsonRequest(exchange))
{
return chain.filter(exchange);
}
// excludeUrls 不过滤
String url = request.getURI().getPath();
if (StringUtils.matches(url, xss.getExcludeUrls()))
{
return chain.filter(exchange);
}
ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
return chain.filter(exchange.mutate().request(httpRequestDecorator).build());
}
private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange)
{
ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest())
{
@Override
public Flux&lt;DataBuffer&gt; getBody()
{
Flux&lt;DataBuffer&gt; body = super.getBody();
return body.buffer().map(dataBuffers -&gt; {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
String bodyStr = new String(content, StandardCharsets.UTF_8);
// 防xss攻击过滤
bodyStr = EscapeUtil.clean(bodyStr);
// 转成字节
byte[] bytes = bodyStr.getBytes();
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
});
}
@Override
public HttpHeaders getHeaders()
{
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
// 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, &quot;chunked&quot;);
return httpHeaders;
}
};
return serverHttpRequestDecorator;
}
/**
* 是否是Json请求
*
* @param exchange HTTP请求
*/
public boolean isJsonRequest(ServerWebExchange exchange)
{
String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
}
@Override
public int getOrder()
{
return -100;
}
}</code></pre>
<h1>6. 请求限流</h1>
<h2>6.1 简介</h2>
<p>Gateway 内置 RequestRateLimiterGatewayFilterFactory 提供请求限流的功能。该 Filter 是基于 Token Bucket Algorithm(令牌桶算法)实现的限流,同时搭配上 Redis 实现分布式限流。令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
<img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=083120bde275145aa4b0bb6fda54ec76&amp;file=file.png" alt="" /></p>
<h2>6.2 配置</h2>
<p>路由配置如下:</p>
<pre><code class="language-yml">spring:
redis:
host: localhost
port: 6379
password:
cloud:
gateway:
routes:
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
key-resolver: &quot;#{@pathKeyResolver}&quot; # 使用 SpEL 表达式按名称引用 bean</code></pre>
<ul>
<li>
<p><code>redis-rate-limiter.replenishRate</code>:令牌桶的每秒放的数量。我们可以近似理解为是每秒平均的请求数。假设在令牌桶为空的情况下,一秒最多放这么多令牌,所以最大请求书当然也是这么多。</p>
</li>
<li><code>redis-rate-limiter.burstCapacity</code>:令牌桶的最大令牌数。我们可以近似理解为是每秒最大的请求数。因此每请求一次,都会从桶里获取掉一块令牌。实际上,在令牌桶满的情况下,每秒最大的请求数是 <code>burstCapacity + replenishRate</code>。</li>
</ul>
<p>限流规则配置类定义如下:</p>
<pre><code class="language-java">/**
* 限流规则配置类
*/
@Configuration
public class KeyResolverConfiguration
{
@Bean
public KeyResolver pathKeyResolver()
{
return exchange -&gt; Mono.just(exchange.getRequest().getURI().getPath());
}
}</code></pre>
<h1>7. 基于Sentinel实现服务容错</h1>
<h2>7.1 简介</h2>
<p>Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级流量控制产品,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。Sentinel 提供了 sentinel-spring-cloud-gateway-adapter 子项目,已经对 Gateway 进行适配,所以我们只要引入它,基本就完成了 Gateway 和 Sentinel 的整合,贼方便。
<img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=2da9f0c3fbc270d150040cae573905e9&amp;file=file.png" alt="" /></p>
<h2>7.2 实现Sentinel限流</h2>
<p>ruoyi-gateway引入依赖</p>
<pre><code class="language-xml">&lt;!-- SpringCloud Alibaba Sentinel --&gt;
&lt;dependency&gt;
&lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-starter-alibaba-sentinel&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;!-- SpringCloud Alibaba Sentinel Gateway --&gt;
&lt;dependency&gt;
&lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-alibaba-sentinel-gateway&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<p>ruoyi-sentinel引入依赖</p>
<pre><code class="language-xml">&lt;!-- springcloud alibaba sentinel --&gt;
&lt;dependency&gt;
&lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-starter-alibaba-sentinel&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;!-- SpringBoot Web --&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;!-- spring cloud openfeign 依赖 --&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-starter-openfeign&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<p>ruoyi-gateway和ruoyi-sentnel模块的配置文件:</p>
<pre><code class="language-yml"># Spring
spring:
cloud:
nacos:
...
...
sentinel:
enabled: true # 是否开启。默认为 true 开启
# 取消控制台懒加载
eager: true
transport:
# 控制台地址
dashboard: 127.0.0.1:8718
filter:
url-patterns: /* # 拦截请求的地址。默认为 /*
# nacos配置持久化
datasource:
ds1:
nacos:
server-addr: 127.0.0.1:8843
dataId: sentinel-ruoyi-gateway
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow</code></pre>
<p>1. <code>enabled</code> 配置项,设置是否开启 Sentinel,默认为 <code>true</code> 开启,所以一般不用主动设置。如果胖友关闭 Sentinel 的功能,例如说在本地开发的时候,可以设置为 <code>false</code> 关闭。</p>
<p>2. <code>eager</code> 配置项,设置是否饥饿加载,默认为 <code>false</code> 关闭。默认情况下,Sentinel 是延迟初始化,在首次使用到 Sentinel 才进行初始化。通过设置为 <code>true</code> 时,在项目启动时就会将 Sentinel 直接初始化,完成向 Sentinel 控制台进行注册。</p>
<p>3. <code>transport.dashboard</code> 配置项,设置 Sentinel 控制台地址。</p>
<p>4. <code>filter.url-patterns</code> 配置项,设置拦截请求的地址,默认为 <code>/*</code>,只能拦截根目录的请求。</p>
<ol>
<li><code>feign:sentinel:enabled: true</code>配置项,设置为 true,开启 Sentinel 对 Feign 的支持。</li>
</ol>
<p>这里的配置集成了nacos配置中心,指定了sentinel-ruoyi-gateway.json配置文件,该配置文件定义了各个服务的限流规则将在下节介绍。</p>
<p>ruoyi-sentinel中定义RemoteUserService,实现对服务ruoyi-system声明式调用:</p>
<pre><code class="language-java">/**
* 用户服务
*
* @author ruoyi
*/
@FeignClient(contextId = &quot;remoteUserService&quot;, value = &quot;ruoyi-system&quot;, fallbackFactory = RemoteUserFallbackFactory.class)
public interface RemoteUserService
{
/**
* 通过用户名查询用户信息
*
* @param username 用户名
* @return 结果
*/
@GetMapping(value = &quot;/user/info/{username}&quot;)
public Object getUserInfo(@PathVariable(&quot;username&quot;) String username);
}</code></pre>
<p>通过 <code>@FeignClient</code> 注解,声明一个 FeignClient 客户端。</p>
<p>① <code>name</code> 属性,设置 FeignClient 客户端的名字。</p>
<p>② <code>url</code> 属性,设置调用服务的地址。因为我们没有引入注册中心,所以我们直接设置稍后启动的服务 <code>demo-provider</code> 的地址。</p>
<p>③ <code>fallbackFactory</code> 属性,设置 fallback 工厂类。fallback 的作用是,用于在 HTTP 调动失败而抛出异常的时候,提供 fallback 处理逻辑。</p>
<p>> 友情提示:Feign 和 Sentinel 进行整合的时候,fallback 并<strong>不是必须条件</strong>,主要看是否想要提供 fallback 处理逻辑。</p>
<p>创建RemoteUserFallbackFactory,用于创建RemoteUserService的工厂类。代码如下:</p>
<pre><code class="language-java">/**
* 用户服务降级处理
*
* @author ruoyi
*/
@Component
public class RemoteUserFallbackFactory implements FallbackFactory&lt;RemoteUserService&gt;
{
private static final Logger log = LoggerFactory.getLogger(RemoteUserFallbackFactory.class);
@Override
public RemoteUserService create(Throwable throwable)
{
log.error(&quot;用户服务调用失败:{}&quot;, throwable.getMessage());
return new RemoteUserService()
{
@Override
public Object getUserInfo(String username)
{
return &quot;{\&quot;code\&quot;:\&quot;500\&quot;,\&quot;msg\&quot;: \&quot;用户服务熔断降级处理\&quot;}&quot;;
}
};
}
}</code></pre>
<p>create方法中提供了具体的处理逻辑,要实现 RemoteUserService 接口,这样每个实现方法,能够一一对应,进行 fallback 处理逻辑。</p>
<h2>7.3 网关限流规则</h2>
<p>nacos的sentinel-ruoyi-gateway.json配置文件中定义如下:</p>
<pre><code class="language-json">[
{
&quot;resource&quot;: &quot;ruoyi-auth&quot;,
&quot;count&quot;: 500,
&quot;grade&quot;: 1,
&quot;limitApp&quot;: &quot;default&quot;,
&quot;strategy&quot;: 0,
&quot;controlBehavior&quot;: 0
},
{
&quot;resource&quot;: &quot;ruoyi-system&quot;,
&quot;count&quot;: 1000,
&quot;grade&quot;: 1,
&quot;limitApp&quot;: &quot;default&quot;,
&quot;strategy&quot;: 0,
&quot;controlBehavior&quot;: 0
},
{
&quot;resource&quot;: &quot;ruoyi-gen&quot;,
&quot;count&quot;: 200,
&quot;grade&quot;: 1,
&quot;limitApp&quot;: &quot;default&quot;,
&quot;strategy&quot;: 0,
&quot;controlBehavior&quot;: 0
},
{
&quot;resource&quot;: &quot;ruoyi-job&quot;,
&quot;count&quot;: 300,
&quot;grade&quot;: 1,
&quot;limitApp&quot;: &quot;default&quot;,
&quot;strategy&quot;: 0,
&quot;controlBehavior&quot;: 0
}
]</code></pre>
<p>限流规则对应着GatewayFlowRule,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
GatewayFlowRule 的字段解释如下:</p>
<ul>
<li><code>resource</code>:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。</li>
<li><code>resourceMode</code>:规则是针对 API Gateway 的 route 还是用户在 Sentinel 中定义的 API 分组,默认是 route。</li>
<li><code>grade</code>:限流阈值类型(QPS 或并发线程数)</li>
<li><code>count</code>:限流阈值</li>
<li><code>limitApp</code>: 流控针对的调用来源,若为 default 则不区分调用来源</li>
<li><code>strategy</code>: 调用关系限流策略</li>
<li><code>controlBehavior</code>: 流量控制效果(直接拒绝、Warm Up、匀速排队)</li>
<li><code>intervalSec</code>:统计时间窗口,单位是秒,默认是 1 秒。</li>
<li><code>controlBehavior</code>:流量整形的控制效果,目前支持快速失败和匀速排队两种模式,默认是快速失败。</li>
<li><code>burst</code>:应对突发请求时额外允许的请求数目。</li>
<li><code>maxQueueingTimeoutMs</code>:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。</li>
<li><code>paramItem</code>:参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段:
<ul>
<li><code>parseStrategy</code>:从请求中提取参数的策略,目前支持提取来源 IP、Host、任意 Header 和任意 URL 参数四种策略。</li>
<li><code>fieldName</code>:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称。</li>
<li><code>pattern</code>:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流控;若为空则统计该请求属性的所有值。</li>
<li><code>matchStrategy</code>:参数值的匹配策略,目前支持精确匹配、子串匹配和正则匹配三种策略。</li>
</ul></li>
</ul>
<h2>7.4 Sentinel分组限流</h2>
<p>可在GatewayConfig中定义如下代码:</p>
<pre><code class="language-java">/**
* 网关限流规则
*/
private void initGatewayRules()
{
Set&lt;GatewayFlowRule&gt; rules = new HashSet&lt;&gt;();
rules.add(new GatewayFlowRule(&quot;system-api&quot;)
.setCount(3) // 限流阈值
.setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
rules.add(new GatewayFlowRule(&quot;code-api&quot;)
.setCount(5) // 限流阈值
.setIntervalSec(60));
// 加载网关限流规则
GatewayRuleManager.loadRules(rules);
// 加载限流分组
initCustomizedApis();
}
/**
* 限流分组
*/
private void initCustomizedApis()
{
Set&lt;ApiDefinition&gt; definitions = new HashSet&lt;&gt;();
// ruoyi-system 组
ApiDefinition api1 = new ApiDefinition(&quot;system-api&quot;).setPredicateItems(new HashSet&lt;ApiPredicateItem&gt;()
{
private static final long serialVersionUID = 1L;
{
// 匹配 /user 以及其子路径的所有请求
add(new ApiPathPredicateItem().setPattern(&quot;/system/user/**&quot;)
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}
});
// ruoyi-gen 组
ApiDefinition api2 = new ApiDefinition(&quot;code-api&quot;).setPredicateItems(new HashSet&lt;ApiPredicateItem&gt;()
{
private static final long serialVersionUID = 1L;
{
// 只匹配 /job/list
add(new ApiPathPredicateItem().setPattern(&quot;/code/gen/list&quot;));
}
});
definitions.add(api1);
definitions.add(api2);
// 加载限流分组
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}</code></pre>
<p>实现对ruoyi-system 组和ruoyi-gen 组的不同限流规则。</p>
<h2>7.5 Sentinel自定义异常</h2>
<ul>
<li>方案一:yml配置</li>
<li>方案二:GatewayConfig注入Bean
<pre><code class="language-java">
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelFallbackHandler sentinelGatewayExceptionHandler()
{
return new SentinelFallbackHandler();
}</code></pre></li>
</ul>
<p>SentinelFallbackHandler.java</p>
<pre><code class="language-java">/**
* 自定义限流异常处理
*
* @author ruoyi
*/
public class SentinelFallbackHandler implements WebExceptionHandler
{
private Mono&lt;Void&gt; writeResponse(ServerResponse response, ServerWebExchange exchange)
{
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), &quot;请求超过最大数,请稍候再试&quot;);
}
@Override
public Mono&lt;Void&gt; handle(ServerWebExchange exchange, Throwable ex)
{
if (exchange.getResponse().isCommitted())
{
return Mono.error(ex);
}
if (!BlockException.isBlockException(ex))
{
return Mono.error(ex);
}
return handleBlockedRequest(exchange, ex).flatMap(response -&gt; writeResponse(response, exchange));
}
private Mono&lt;ServerResponse&gt; handleBlockedRequest(ServerWebExchange exchange, Throwable throwable)
{
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
}
}</code></pre>
<h1>8. 网关流控实现原理</h1>
<p>当通过 <code>GatewayRuleManager</code> 加载网关流控规则(<code>GatewayFlowRule</code>)时,无论是否针对请求属性进行限流,Sentinel 底层都会将网关流控规则转化为热点参数规则(<code>ParamFlowRule</code>),存储在 <code>GatewayRuleManager</code> 中,与正常的热点参数规则相隔离。转换时 Sentinel 会根据请求属性配置,为网关流控规则设置参数索引(<code>idx</code>),并同步到生成的热点参数规则中。</p>
<p>外部请求进入 API Gateway 时会经过 Sentinel 实现的 filter,其中会依次进行 <strong>路由/API 分组匹配</strong>、<strong>请求属性解析</strong>和<strong>参数组装</strong>。Sentinel 会根据配置的网关流控规则来解析请求属性,并依照参数索引顺序组装参数数组,最终传入 <code>SphU.entry(res, args)</code> 中。Sentinel API Gateway Adapter Common 模块向 Slot Chain 中添加了一个 <code>GatewayFlowSlot</code>,专门用来做网关规则的检查。<code>GatewayFlowSlot</code> 会从 <code>GatewayRuleManager</code> 中提取生成的热点参数规则,根据传入的参数依次进行规则检查。若某条规则不针对请求属性,则会在参数最后一个位置置入预设的常量,达到普通流控的效果。</p>
<p><img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=d66b7db41e3e177004ca7bc5c18443cb&amp;file=file.png" alt="" /></p>
<h1>9. 认证中心</h1>
<h2>9.1 简介</h2>
<ul>
<li>
<p>什么是认证中心
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。</p>
</li>
<li>为什么要使用认证中心
登录请求后台接口,为了安全认证,所有请求都携带token信息进行安全认证,比如使用vue、react后者h5开发的app,用于控制可访问系统的资源。</li>
</ul>
<h2>9.2 登录认证</h2>
<p>顾名思义,就是对系统登录用户的进行认证过程。<code>TokenController</code>控制器<code>login</code>方法会进行用户验证,如果验证通过会保存登录日志并返回<code>token</code>,同时缓存中会存入<code>login_tokens:xxxxxx</code>(包含用户、权限信息)。</p>
<p>用户登录接口地址 <code>http://localhost:9200/login</code>,
请求头<code>Content-Type - application/json</code>,请求方式<code>Post</code>:</p>
<pre><code class="language-json">{
&quot;username&quot;: &quot;admin&quot;,
&quot;password&quot;: &quot;admin123&quot;
}</code></pre>
<p>响应结果:</p>
<pre><code class="language-json">{
&quot;code&quot;: 200,
&quot;data&quot;: {
&quot;access_token&quot;: &quot;f840488c-68a9-4272-acc9-c34d3b66a943&quot;,
&quot;expires_in&quot;: 43200
}
}</code></pre>
<h2>9.3 刷新令牌</h2>
<p>顾名思义,就是对系统操作用户的进行缓存刷新,防止过期。TokenController控制器refresh方法会在用户调用时更新令牌有效期。刷新令牌接口地址 <a href="http://localhost:9200/refresh">http://localhost:9200/refresh</a>
请求头Authorization - f840488c-68a9-4272-acc9-c34d3b66a943,请求方式Post
响应结果:</p>
<pre><code class="language-json">{
&quot;code&quot;: 200,
}</code></pre>
<p>刷新后有效期为默720(分钟)。</p>
<h2>9.4 系统退出</h2>
<p>顾名思义,就是对系统登用户的退出过程。TokenController控制器logout方法会在用户退出时删除缓存信息同时保存用户退出日志。系统退出接口地址 <a href="http://localhost:9200/logout,请求头Authorization">http://localhost:9200/logout,请求头Authorization</a> - f840488c-68a9-4272-acc9-c34d3b66a943,请求方式Delete</p>
<pre><code class="language-json">{
&quot;username&quot;: &quot;admin&quot;,
&quot;password&quot;: &quot;admin123&quot;
}</code></pre>
<p>响应结果:</p>
<pre><code class="language-json">{
&quot;code&quot;: 200,
}</code></pre>