先上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微服务的路由设置
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启动成功。

用户登录,该用户无商品发布权限。
在这里插入图片描述
无权限用户操作相关模块,会被拦截并提示无权限操作。
在这里插入图片描述

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐