SpringCloud之Gateway
1.Gateway是什么?
1.1 为微服务提供简单有效的路由管理方式
1.2 词汇
(1)Route(路由) :构建网关的基础模块,由ID、目标URL、断言和过滤器等组成
id:路由唯一标识,区别于其他的route
url: 路由指向的目的地URL,客户端请求最终被转发到的微服务
order: 用于多个Route之间的排序,数值越小越靠前,匹配优先级越高
(2)Predicate(断言) :可以匹配HTTP请求中的内容(请求头和请求参数),如果请求断言匹配则进行路由
1.请求主机、路径、cookie等
Host:匹配当前请求是否来自于设置的主机。
RemoteAddr:匹配指定IP或IP段,符合条件转发。
Path(用的最多):匹配指定路径下的请求,可以是多个用逗号分隔
Method:可以设置一个或多个参数,匹配HTTP请求,比如GET、POST
Header:需要两个参数header和regexp(正则表达式),也可以理解为Key和Value,匹配请求携带信息。
Query:需要指定一个或者多个参数,一个必须参数和一个可选的正则表达式,匹配请求中是否包含第一个参数,如果有两个参数,则匹配请求中第一个参数的值是否符合正则表达式。
Cookie:需要指定两个参数,分别为name和regexp(正则表达式),也可以理解Key和Value,匹配具有给定名称且其值与
正则表达式匹配的Cookie。
2.时间日期类:
After:匹配在指定日期时间之后发生的请求。
Before:匹配在指定日期之前发生的请求。
Between:需要指定两个日期参数,设定一个时间区间,匹配此时间区间内的请求。
3.权重类等
Weight:需要两个参数group和weight(int),实现了路由权重功能,按照路由权重选择同一个分组中的路由
(3)Filter(过滤) :GateWayFilter的实例,使用过滤器,可以在请求被路由之前或者之后对请求进行修改
链接:
Spring Cloud Gateway官方文档
SpringCloud Gateway用法详解
SpringCloud GateWay 万字详解
3.Gateway如何简单运用于项目中?
3.1 创建gateway 模块
3.2 添加依赖
<dependencies>
<!--Spring Cloud & Alibaba-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- OAuth2资源服务器-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>com.ams</groupId>
<artifactId>common-base</artifactId>
<version>${ams.version}</version>
</dependency>
<dependency>
<groupId>com.ams</groupId>
<artifactId>common-redis</artifactId>
<version>${ams.version}</version>
</dependency>
</dependencies>
3.3 添加配置文件:
(1)创建连接nacos的bootstrap.yml 文件
server:
port: 9999
spring:
application:
name: ams-gateway
cloud:
nacos:
# 注册中心
discovery:
server-addr: http://192.168.2.30:8848
# 配置中心
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
shared-configs[0]:
data-id: ams-common.yaml
refresh: true
(2)在nacos中添加gateway 配置文件:ams-gateway.yaml
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:9999/ams-auth/oauth/public-key'
redis:
timeout: PT30S
database: 0
host: ${redis.host}
port: ${redis.port}
password: ${redis.password}
pool:
# 连接池最大连接数(使用负值表示没有限制)
maxactive: 64
# 连接池中的最大空闲连接
maxidle: 64
# 连接池最大阻塞等待时间(使用负值表示没有限制)
maxwait: -1
# 连接池中的最小空闲连接
minidle: 1
cloud:
gateway:
discovery:
locator:
enabled: true # 启用服务发现
lower-case-service-id: true
routes:
- id: 认证中心
uri: lb://ams-auth
predicates:
- Path=/ams-auth/**
filters:
- StripPrefix=1
- id: 系统服务
uri: lb://ams-admin
predicates:
- Path=/ams-admin/**
filters:
- StripPrefix=1
# 配置白名单路径(无需登录)
security:
ignoreUrls:
- /ams-auth/oauth/token/**
- /ams-auth/oauth/public-key
- /webjars/**
# 是否开启本地缓存
local-cache:
enabled: false
# 全局参数设置
ribbon:
ReadTimeout: 120000
ConnectTimeout: 10000
SocketTimeout: 10000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 1
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 40000
feign:
httpclient:
enabled: true
okhttp:
enabled: false
sentinel: # 开启feign对sentinel的支持
enabled: false
3.4 网关配置
(1)开启注册客户端
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApp {
public static void main(String[] args) {
SpringApplication.run(GatewayApp.class, args);
}
}
(2)资源服务配置:ResourceServerConfig
package com.ams.gateway.security;
import com.ams.common.constant.SecurityConstants;
import com.ams.common.result.ResultCode;
import com.ams.gateway.util.ResponseUtils;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import reactor.core.publisher.Mono;
/**
* @description:该组件主要是用来设置对哪些请求进行拦截,和如何解析token内容的
*/
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final ResourceServerManager resourceServerManager;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
http.oauth2ResourceServer().authenticationEntryPoint(authenticationEntryPoint());
http.authorizeExchange()
.anyExchange().access(resourceServerManager)
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler()) // 处理未授权
.authenticationEntryPoint(authenticationEntryPoint()) //处理未认证
.and().csrf().disable();
return http.build();
}
/**
* 自定义未授权响应
*/
@Bean
ServerAccessDeniedHandler accessDeniedHandler() {
return (exchange, denied) -> {
Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse()))
.flatMap(response -> ResponseUtils.writeErrorInfo(response, ResultCode.ACCESS_UNAUTHORIZED));
return mono;
};
}
/**
* token无效或者已过期自定义响应
*/
@Bean
ServerAuthenticationEntryPoint authenticationEntryPoint() {
return (exchange, e) -> {
Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse()))
.flatMap(response -> ResponseUtils.writeErrorInfo(response, ResultCode.TOKEN_INVALID_OR_EXPIRED));
return mono;
};
}
@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(SecurityConstants.AUTHORITY_PREFIX);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(SecurityConstants.JWT_AUTHORITIES_KEY);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
}
(3)资源权限校验配置:ResourceServerManager
package com.ams.gateway.security;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.ams.common.constant.GlobalConstants;
import com.ams.common.constant.SecurityConstants;
import com.ams.gateway.util.UrlPatternUtils;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @description:该组件主要用来做对资源权限的判断
*/
@Component
@RequiredArgsConstructor
@Slf4j
@ConfigurationProperties(prefix = "security")
public class ResourceServerManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private final RedisTemplate redisTemplate;
@Setter
private List<String> ignoreUrls;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
if (request.getMethod() == HttpMethod.OPTIONS) { // 预检请求放行
return Mono.just(new AuthorizationDecision(true));
}
PathMatcher pathMatcher = new AntPathMatcher();
String method = request.getMethodValue();
String path = request.getURI().getPath();
// 跳过token校验,放在这里去做是为了能够动态刷新
if (skipValid(path)) {
return Mono.just(new AuthorizationDecision(true));
}
// 如果token为空 或者token不合法 则进行拦截
String restfulPath = method + ":" + path; // RESTFul接口权限设计 @link https://www.cnblogs.com/haoxianrui/p/14961707.html
String token = request.getHeaders().getFirst(SecurityConstants.AUTHORIZATION_KEY);
if (StrUtil.isBlank(token) || !StrUtil.startWithIgnoreCase(token, SecurityConstants.JWT_PREFIX)) {
return Mono.just(new AuthorizationDecision(false));
}
// 从redis中获取资源权限
Map<String, Object> urlPermRolesRules = redisTemplate.opsForHash().entries(GlobalConstants.URL_PERM_ROLES_KEY);
List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
boolean requireCheck = false; // 是否需要鉴权,默认未设置拦截规则不需鉴权
// 获取当前资源 所需要的角色
for (Map.Entry<String, Object> permRoles : urlPermRolesRules.entrySet()) {
String perm = permRoles.getKey();
if (pathMatcher.match(perm, restfulPath)) {
List<String> roles = Convert.toList(String.class, permRoles.getValue());
authorizedRoles.addAll(Convert.toList(String.class, roles));
if (requireCheck == false) {
requireCheck = true;
}
}
}
// 如果资源不需要权限 则直接返回授权成功
if (!requireCheck) {
return Mono.just(new AuthorizationDecision(true));
}
// 判断JWT中携带的用户角色是否有权限访问
Mono<AuthorizationDecision> authorizationDecisionMono = mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authority -> {
String roleCode = authority.substring(SecurityConstants.AUTHORITY_PREFIX.length()); // 用户的角色
boolean hasAuthorized = CollectionUtil.isNotEmpty(authorizedRoles) && authorizedRoles.contains(roleCode);
return hasAuthorized;
})
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
return authorizationDecisionMono;
}
/**
* 跳过校验
*
* @param path
* @return
*/
private boolean skipValid(String path) {
for (String skipPath : ignoreUrls) {
if (UrlPatternUtils.match(skipPath, path)) {
return true;
}
}
return false;
}
}
(4)全局过滤器配置:SecurityGlobalFilter
package com.ams.gateway.security;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.ams.common.constant.SecurityConstants;
import com.ams.common.result.ResultCode;
import com.ams.gateway.util.ResponseUtils;
import com.nimbusds.jose.JWSObject;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
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.RedisTemplate;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URLEncoder;
/**
* @description:该组件是自定义的网关过滤器,主要用来将解析后的token信息存放在请求头中,转发给各个服务。
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class SecurityGlobalFilter implements GlobalFilter, Ordered {
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 不是正确的的JWT不做解析处理
String token = request.getHeaders().getFirst(SecurityConstants.AUTHORIZATION_KEY);
if (StrUtil.isBlank(token) || !StrUtil.startWithIgnoreCase(token, SecurityConstants.JWT_PREFIX)) {
return chain.filter(exchange);
}
// 解析JWT获取jti,以jti为key判断redis的黑名单列表是否存在,存在则拦截访问
token = StrUtil.replaceIgnoreCase(token, SecurityConstants.JWT_PREFIX, Strings.EMPTY);
String payload = StrUtil.toString(JWSObject.parse(token).getPayload());
request = exchange.getRequest().mutate()
.header(SecurityConstants.JWT_PAYLOAD_KEY, URLEncoder.encode(payload, "UTF-8"))
.build();
exchange = exchange.mutate().request(request).build();
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
3.5 网关流程图
4.Gateway的工作原理?
4.1 客户端向 Spring Cloud Gateway 发出请求。如果网关处理程序映射确定请求与路由匹配,则将其发送到网关 Web 处理程序。
随心所往,看见未来。Follow your heart,see night!
欢迎点赞、关注、留言,收藏及转发,一起学习、交流!