springcloud gateway路由、拦截最佳实践
springcloud gateway路由、拦截最佳实践先上pom.xmllog4j2.yml配置application.yml 配置AdminAuthorizeGatewayFilterFactory.java 拦截器网关入口类GatewayApplication.javaAutoConfiguration.java 必须定义RouteDefinitionLocator 这个bean启动测试先.
springcloud gateway路由、拦截最佳实践
先上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">
<parent>
<artifactId>mall-cloud</artifactId>
<groupId>com.zmg</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zmg-gateway</artifactId>
<packaging>jar</packaging>
<dependencies>
<!-- log related -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<!-- exclude掉spring-boot的默认log配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency> <!-- 引入log4j2依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency> <!-- 加上这个才能辨认到log4j2.yml文件 -->
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency> <!-- 引入log4j-web -->
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-web</artifactId>
</dependency>
<!-- end of log related -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.isomorphism</groupId>
<artifactId>token-bucket</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>com.zmg</groupId>
<artifactId>zmg-basic</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
<exclusion>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
</exclusion>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.zmg</groupId>
<artifactId>zmg-auth</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>dev</id>
<properties>
<profileActive>dev</profileActive>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>sit</id>
<properties>
<profileActive>sit</profileActive>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<profileActive>test</profileActive>
</properties>
</profile>
<profile>
<id>prd</id>
<properties>
<profileActive>prd</profileActive>
</properties>
</profile>
</profiles>
<build>
<finalName>zmg-gateway</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
注意其中的 exclusion 去除的log组件是为了正常使用log4j2的相关配置输出日志。
log4j2.yml配置
Configuration:
status: INFO
Properties: # 定义全局变量
Property: # 缺省配置(用于开发环境)。其他环境需要在VM参数中指定,如下:
#测试:-Dlog.level.console=warn
#生产:-Dlog.level.console=warn
- name: log.level.console
value: DEBUG
- name: log.level.file
value: INFO
- name: LOG_HOME
value: /home/data/logs
- name: PROJECT_NAME
value: zmg-gateway
- name: CONAOLE_LOG_PATTERN
value: "%highlight{%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%5p} [%t] %highlight{%c{1.}.%M(%L)} : %m%n}"
- name: FILE_LOG_PATTERN
value: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %X{pid} [%15.15t] %c.%M(%L) : %m%n"
Appenders:
Console: #输出到控制台
name: CONSOLE
target: SYSTEM_OUT
PatternLayout:
pattern: ${CONAOLE_LOG_PATTERN}
RollingFile: # 输出到文件,超过128MB归档
name: ROLLING_FILE
fileName: ${LOG_HOME}/${PROJECT_NAME}/${PROJECT_NAME}.log
filePattern: ${LOG_HOME}/${PROJECT_NAME}/${PROJECT_NAME}_%d{yyyy-MM-dd}_%i.log.gz
PatternLayout:
pattern: ${FILE_LOG_PATTERN}
Policies:
TimeBasedTriggeringPolicy:
interval: "1"
modulate: true
SizeBasedTriggeringPolicy:
size: "50MB"
# DefaultRolloverStrategy:
# max: 1000
Loggers:
Root:
level: ${log.level.file}
AppenderRef:
- ref: CONSOLE
- ref: ROLLING_FILE
Logger: # 为com.zmg包配置特殊的Log级别,方便调试
- name: com.zmg
additivity: false
level: ${log.level.console}
AppenderRef:
- ref: CONSOLE
- ref: ROLLING_FILE
日志使用了2个appenders,一个控制台输出,一个文件滚动存储。
控制的日志输出采用了高亮显示 %highlight{…}
application.yml 配置
server:
port: 7005
spring:
profiles:
active: @profileActive@
application:
name: zmg-gateway
redis:
database: 1
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: zmg*123
timeout: 2000
jedis:
pool:
max-active: 20
sleuth:
enabled: true
http:
legacy:
enabled: true
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: zmg-sys
uri: lb://zmg-sys
order: 2001
predicates:
- Path=/api/zmg/sys/**
filters:
- StripPrefix=3
- adminAuthorize=true
- id: zmg-org
uri: lb://zmg-org
order: 2002
predicates:
- Path=/api/zmg/org/**
filters:
- StripPrefix=3
- adminAuthorize=true
- id: zmg-gds
uri: lb://zmg-gds
order: 2003
predicates:
- Path=/api/zmg/gds/**
filters:
- StripPrefix=3
- adminAuthorize=true
- id: zmg-odr
uri: lb://zmg-odr
order: 2004
predicates:
- Path=/api/zmg/odr/**
filters:
- StripPrefix=3
- adminAuthorize=true
eureka:
instance:
statusPageUrlPath: /actuator/info
healthCheckUrlPath: /actuator/health
home-page-url-path: /
# docker 部署开启后将IP修改为部署所在服务器的外网IP
prefer-ip-address: true
ip-address: 127.0.0.1
instance-id: zmg-gateway
client:
serviceUrl:
defaultZone: http://${EUREKA_HOST:localhost}:${EUREKA_PORT:7001}/eureka/
client:
healthcheck:
enabled: true
#请求和响应GZIP压缩支持
feign:
httpclient:
enabled: false
okhttp:
enabled: true
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 2048
response:
enabled: true
logging:
level:
com.zmg.cloud.gateway: info
management:
endpoints:
web:
exposure:
include: '*'
security:
enabled: false
gate:
ignore:
# 开发不进行权限校验的路径
startWith: /auth/jwt
auth:
serviceId: zmg-sys
user:
token-header: Authorization
authorize:
protected-patterns: /protected/*
关键看routes配置,以某个被路由的微服务zmg-sys为例
zmg-sys 微服务的内部地址为 lb://zmg-sys
路由断言的路径为 /api/zmg/sys/**
使用的拦截器为adminAuthorize,从第3个通配符后截取路径转发给对应微服务
AdminAuthorizeGatewayFilterFactory.java 拦截器
根据springcloud gateway的命名规则,adminAuthorize即对应 AdminAuthorizeGatewayFilterFactory 类,springcloud gateway启动时会以此名称去扫描所有带有spring相关注解的类,加载到容器中。
/**
* zmg权限验签网关过滤器
*
* @author 张军平
*/
@Slf4j
@Component
public class AdminAuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<AdminAuthorizeGatewayFilterFactory.Config> {
public static final String ENABLE_KEY = "enabled";
@Value("${auth.user.token-header}")
private String tokenHeader;
@Value("${auth.authorize.protected-patterns:/protected/*}")
private List<String> protectedPatterns;
@Autowired
private UserAuthUtil userAuthUtil;
@Autowired
private AuthFeign authFeign;
public AdminAuthorizeGatewayFilterFactory() {
super(AdminAuthorizeGatewayFilterFactory.Config.class);
}
@Override
public String name() {
return "adminAuthorize";
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(ENABLE_KEY);
}
@Override
public GatewayFilter apply(AdminAuthorizeGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
// log.info("/// zmg Authorize filter start... ");
if (!config.isEnabled()) {
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
String uri = request.getURI().getPath();
// log.info("// request uri:{}", uri);
// log.info("// protectedPatterns:{}", protectedPatterns);
if (!StringUtils.urlMatches(uri, protectedPatterns)) {
return chain.filter(exchange);
}
String token = getToken(request);
ServerHttpRequest.Builder mutate = request.mutate();
if (null == token || token.trim().equals("")) {
return getVoidMono(exchange,
ResultSupport.error(CommonConstants.USER_TOKEN_NULL_CODE, "未认证的请求,请先登录认证!"));
} else {
IJWTInfo userInfo = null;
try {
userInfo = userAuthUtil.getInfoFromToken(token);
String userId = userInfo.getId();
// log.info("// userId : {}", userId);
// mutate.header("userId", userId);
} catch (Exception ex) {
log.error("用户Token过期异常", ex);
return getVoidMono(exchange,
ResultSupport.error(CommonConstants.USER_TOKEN_EXPIRED_CODE, "Token已过期,请重新登录!"));
}
if (!isAuthorized(uri, userInfo)) {
String msg = String.format("该账号[%s]无此权限[%s]!", userInfo.getAccount(), uri);
return getVoidMono(exchange,
ResultSupport.error(CommonConstants.USER_TOKEN_FORBIDDEN_CODE, msg));
}
}
// log.info("/// zmg Authorize filter end... ");
return chain.filter(exchange.mutate().request(mutate.build()).build());
};
}
private boolean isAuthorized(String uri, IJWTInfo userInfo) {
if (null == userInfo || null == userInfo.getId()) {
return false;
}
// log.info("// uri:{}", uri);
boolean pass = authFeign.isAdminAuthorized(uri, Long.parseLong(userInfo.getId()));
// log.info("// pass:{}", pass);
return pass;
}
private String getToken(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(tokenHeader);
if (null == token || token.trim().equals("")) {
MultiValueMap<String, HttpCookie> cookies = request.getCookies();
if (null != cookies) {
HttpCookie cookie = cookies.getFirst(tokenHeader);
if (null != cookie) {
token = cookie.getValue();
}
}
}
return token;
}
/**
* 网关抛异常
* @param serverWebExchange
* @param result
* @return
*/
@NotNull
private Mono<Void> getVoidMono(ServerWebExchange serverWebExchange, Result result) {
serverWebExchange.getResponse().setStatusCode(HttpStatus.OK);
byte[] bytes = JacksonUtils.writeAsString(result).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = serverWebExchange.getResponse().bufferFactory().wrap(bytes);
return serverWebExchange.getResponse().writeWith(Flux.just(buffer));
}
public static class Config {
/**
* 是否开启zmg认证
*/
private boolean enabled;
public Config() {
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}
其中,
String token = getToken(request);
这行代码获取http 请求头中的 token,通常为了方便后台开发测试,我这个方法会在请求头无token时额外会去cookie中获取token(当然登录时也会存储对应cookie)。
然后,对token进行验签,得到用户的userId。
最后,调用权限验证的feign进行用户权限验证
网关入口类GatewayApplication.java
/**
* Description: 网关中心
*
* @author jacky
* @create 2019/11/01
**/
@Slf4j
@SpringCloudApplication
@EnableEurekaClient
@EnableFeignClients({"com.zmg.cloud.gateway.feign", "com.zmg.auth.feign"})
@EnableScheduling
@ComponentScan(basePackages = {"com.zmg.cloud.gateway","com.zmg.auth.runner"})
@RestController
public class GatewayApplication {
@Value("${spring.application.name}")
protected String applicationName;
@GetMapping(value = {"", "/"})
public String root() {
String welcomeMsg = String.format("Welcome, this is %s application!", this.applicationName);
return welcomeMsg;
}
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
log.info("zmg网关中心启动成功!");
}
}
在入口类上指定feign的包路径及spring扫描的包路径即可。
AutoConfiguration.java 必须定义RouteDefinitionLocator 这个bean
@Configuration
public class AutoConfiguration {
@Bean
public UserAuthConfig userAuthConfig() {
return new UserAuthConfig();
}
@Bean
public UserAuthUtil userAuthUtil() {
return new UserAuthUtil();
}
@Bean
public RouteDefinitionLocator discoveryClientRouteDefinitionLocator(DiscoveryClient discoveryClient,
DiscoveryLocatorProperties properties) {
return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);
}
};
另外,UserAuthConfig、UserAuthUtil 这两个bean是使用jwt方式产生token需要用到的类,略过。
启动测试
gateway启动成功。
用户登录,该用户无商品发布权限。
无权限用户操作相关模块,会被拦截并提示无权限操作。
更多推荐
所有评论(0)