Browse Source

首次提交

lym 6 days ago
parent
commit
3c6dfb944e

+ 87 - 0
forest-gateway/pom.xml

@@ -0,0 +1,87 @@
+<?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>forest-mall-cloud</artifactId>
+        <groupId>com.hwrj.cloud</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.hwrj.cloud.gateway</groupId>
+    <artifactId>forest-gateway</artifactId>
+    <packaging>jar</packaging>
+    <dependencies>
+        <dependency>
+            <groupId>com.hwrj.cloud.common</groupId>
+            <artifactId>forest-common</artifactId>
+            <version>${forest.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-web</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.github.pagehelper</groupId>
+                    <artifactId>pagehelper-spring-boot-starter</artifactId>
+
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.tomcat.embed</groupId>
+                    <artifactId>tomcat-embed-core</artifactId>
+                </exclusion>
+            </exclusions>
+
+
+        </dependency>
+        <!-- gateway -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-gateway</artifactId>
+
+        </dependency>
+        <!-- nacos discovery-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+        <!-- spring boot admin client -->
+        <dependency>
+            <groupId>de.codecentric</groupId>
+            <artifactId>spring-boot-admin-starter-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <!-- nacos config -->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 18 - 0
forest-gateway/src/main/java/com/hwrj/cloud/gateway/MallGatewayApplication.java

@@ -0,0 +1,18 @@
+package com.hwrj.cloud.gateway;
+
+import com.hwrj.cloud.gateway.config.IgnoreUrlsConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+
+
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+@EnableConfigurationProperties(IgnoreUrlsConfig.class)
+public class MallGatewayApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(MallGatewayApplication.class, args);
+        System.out.println("---------------------路由启动成功!---------------------");
+    }
+}

+ 40 - 0
forest-gateway/src/main/java/com/hwrj/cloud/gateway/config/AbstractExceptionHandler.java

@@ -0,0 +1,40 @@
+package com.hwrj.cloud.gateway.config;
+
+import com.hwrj.cloud.common.exception.ApiException;
+import org.springframework.cloud.gateway.support.NotFoundException;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+public class AbstractExceptionHandler {
+    private static long DEFAULT_ERROR_CODE = 500;
+
+    protected String formatMessage(Throwable ex) {
+
+        String errorMessage = null;
+        if (ex instanceof NotFoundException) {
+            String reason = ((NotFoundException) ex).getMessage();
+            errorMessage = reason;
+        } else if (ex instanceof ResponseStatusException) {
+            ResponseStatusException responseStatusException = (ResponseStatusException) ex;
+            errorMessage = responseStatusException.getMessage();
+        } else if (ex instanceof ApiException) {
+            ApiException exception = (ApiException) ex;
+            errorMessage = exception.getMessage();
+            DEFAULT_ERROR_CODE = exception.getErrorCode().getCode();
+        } else {
+            errorMessage = ex.getMessage();
+        }
+        return errorMessage;
+    }
+
+    protected Map<String, Object> buildErrorMap(String errorMessage) {
+        Map<String, Object> resMap = new HashMap<>();
+        resMap.put("code", DEFAULT_ERROR_CODE);
+        resMap.put("message", errorMessage);
+        resMap.put("data", null);
+        return resMap;
+    }
+}

+ 35 - 0
forest-gateway/src/main/java/com/hwrj/cloud/gateway/config/GatewayExceptionConfig.java

@@ -0,0 +1,35 @@
+package com.hwrj.cloud.gateway.config;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.codec.ServerCodecConfigurer;
+import org.springframework.web.reactive.result.view.ViewResolver;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @Deacription TODO
+ * @Author jianhua.hong
+ * @Date 2020/4/2 10:45
+ **/
+@Configuration
+public class GatewayExceptionConfig {
+
+    @Primary
+    @Bean
+    @Order(Ordered.HIGHEST_PRECEDENCE)
+    public ErrorWebExceptionHandler errorWebExceptionHandler(ObjectProvider<List<ViewResolver>> viewResolversProvider,
+                                                             ServerCodecConfigurer serverCodecConfigurer) {
+        GatewayExceptionHandler gatewayExceptionHandler = new GatewayExceptionHandler();
+        gatewayExceptionHandler.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList));
+        gatewayExceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
+        gatewayExceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
+        return gatewayExceptionHandler;
+    }
+}
+

+ 116 - 0
forest-gateway/src/main/java/com/hwrj/cloud/gateway/config/GatewayExceptionHandler.java

@@ -0,0 +1,116 @@
+package com.hwrj.cloud.gateway.config;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.codec.HttpMessageReader;
+import org.springframework.http.codec.HttpMessageWriter;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.util.Assert;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.server.RequestPredicates;
+import org.springframework.web.reactive.function.server.RouterFunctions;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import org.springframework.web.reactive.result.view.ViewResolver;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @Deacription TODO
+ * @Author jianhua.hong
+ * @Date 2020/4/2 10:36
+ **/
+@Slf4j
+public class GatewayExceptionHandler extends AbstractExceptionHandler implements ErrorWebExceptionHandler {
+
+    private static final String TRACE_ID = "traceId";
+
+    private static final String JHJCN_BUSI_NOT_FOUND = "Unable to find instance for jhjcn-business";
+
+    private static final String JHJCN_BUSI_NOT_FOUND_ZH = "未xxx微服务,请检查服务是否可用";
+
+    private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
+
+
+    private List<HttpMessageWriter<?>> messageWriters = Collections.emptyList();
+
+
+    private List<ViewResolver> viewResolvers = Collections.emptyList();
+
+
+    private ThreadLocal<Map<String, Object>> exceptionHandlerResult = new ThreadLocal<>();
+
+
+    public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
+        Assert.notNull(messageReaders, "'messageReaders' must not be null");
+        this.messageReaders = messageReaders;
+    }
+
+
+    public void setViewResolvers(List<ViewResolver> viewResolvers) {
+        this.viewResolvers = viewResolvers;
+    }
+
+
+    public void setMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
+        Assert.notNull(messageWriters, "'messageWriters' must not be null");
+        this.messageWriters = messageWriters;
+    }
+
+    @Override
+    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
+        String errorMessage = super.formatMessage(ex);
+        if (errorMessage.equals(JHJCN_BUSI_NOT_FOUND)) {
+            errorMessage = JHJCN_BUSI_NOT_FOUND_ZH;
+        }
+        Map<String, Object> errorMap = super.buildErrorMap(errorMessage);
+        ServerHttpRequest request = exchange.getRequest();
+        String traceId = request.getHeaders().getFirst(TRACE_ID);
+        log.error("GatewayExceptionHandler request info [traceId={}] result error=[{}]", traceId, JSONObject.toJSONString(errorMap));
+        if (exchange.getResponse().isCommitted()) {
+            return Mono.error(ex);
+        }
+        exceptionHandlerResult.set(errorMap);
+        ServerRequest newRequest = ServerRequest.create(exchange, this.messageReaders);
+        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse).route(newRequest)
+                .switchIfEmpty(Mono.error(ex))
+                .flatMap((handler) -> handler.handle(newRequest))
+                .flatMap((response) -> write(exchange, response));
+
+    }
+
+
+    protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
+        Map<String, Object> result = exceptionHandlerResult.get();
+        return ServerResponse.status(HttpStatus.OK)
+                .contentType(MediaType.APPLICATION_JSON_UTF8)
+                .body(BodyInserters.fromObject(result));
+    }
+
+
+    private Mono<? extends Void> write(ServerWebExchange exchange,
+                                       ServerResponse response) {
+        exchange.getResponse().getHeaders()
+                .setContentType(response.headers().getContentType());
+        return response.writeTo(exchange, new ResponseContext());
+    }
+
+    private class ResponseContext implements ServerResponse.Context {
+
+        @Override
+        public List<HttpMessageWriter<?>> messageWriters() {
+            return GatewayExceptionHandler.this.messageWriters;
+        }
+
+        @Override
+        public List<ViewResolver> viewResolvers() {
+            return GatewayExceptionHandler.this.viewResolvers;
+        }
+    }
+}

+ 21 - 0
forest-gateway/src/main/java/com/hwrj/cloud/gateway/config/IgnoreUrlsConfig.java

@@ -0,0 +1,21 @@
+package com.hwrj.cloud.gateway.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 用于配置不需要保护的资源路径
+ * Created by macro on 2018/11/5.
+ */
+@Getter
+@Setter
+@ConfigurationProperties(prefix = "secure.ignored")
+public class IgnoreUrlsConfig {
+
+    private List<String> urls = new ArrayList<>();
+
+}

+ 83 - 0
forest-gateway/src/main/java/com/hwrj/cloud/gateway/filter/AuthGlobalFilter.java

@@ -0,0 +1,83 @@
+package com.hwrj.cloud.gateway.filter;
+
+
+import com.hwrj.cloud.common.api.ResultCode;
+import com.hwrj.cloud.common.exception.ApiException;
+import com.hwrj.cloud.gateway.config.IgnoreUrlsConfig;
+import com.hwrj.cloud.gateway.util.JwtTokenUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+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.HttpMethod;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.PathMatcher;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+
+@Component
+@Slf4j
+public class AuthGlobalFilter implements GlobalFilter, Ordered {
+
+    @Autowired
+    private IgnoreUrlsConfig ignoreUrlsConfig;
+
+    @Autowired
+    private JwtTokenUtil jwtTokenUtil;
+
+    @Autowired
+    private StringRedisTemplate stringRedisTemplate;
+
+    @Value("${jwt.tokenHeader}")
+    private String tokenHeader;
+    @Value("${redis.database}")
+    private String REDIS_DATABASE;
+    @Value("${redis.key.token}")
+    private String REDIS_KEY_TOKEN;
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        ServerHttpRequest request = exchange.getRequest();
+        //防止 OPTIONS 请求直接放行
+        if (request.getMethod().equals(HttpMethod.OPTIONS)) {
+            return chain.filter(exchange);
+        }
+        //白名单请求直接放行
+        PathMatcher pathMatcher = new AntPathMatcher();
+        for (String path : ignoreUrlsConfig.getUrls()) {
+            if (pathMatcher.match("/**" + path, request.getPath().toString())) {
+                return chain.filter(exchange);
+            }
+        }
+        // token 验证
+        String token = request.getHeaders().getFirst(tokenHeader);
+        if (StringUtils.isBlank(token)){
+            log.error("token = {}",token);
+            throw new ApiException(ResultCode.UNAUTHORIZED);
+        }
+        String username = jwtTokenUtil.getUserNameFromToken(token);
+        // 待抽离
+        String key = REDIS_DATABASE + ":" + REDIS_KEY_TOKEN + ":" + username;
+        String resultToken = stringRedisTemplate.opsForValue().get(key);
+        if (StringUtils.isBlank(resultToken)) {
+            log.error("resultToken = {}",resultToken);
+            throw new ApiException(ResultCode.UNAUTHORIZED);
+        }
+        log.info("resultToken = {}",resultToken);
+        return chain.filter(exchange);
+    }
+
+    @Override
+    public int getOrder() {
+        return Ordered.HIGHEST_PRECEDENCE;
+    }
+
+
+}

+ 81 - 0
forest-gateway/src/main/java/com/hwrj/cloud/gateway/util/JwtTokenUtil.java

@@ -0,0 +1,81 @@
+package com.hwrj.cloud.gateway.util;
+
+
+import com.hwrj.cloud.common.api.ResultCode;
+import com.hwrj.cloud.common.exception.ApiException;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * JwtToken生成的工具类
+ * JWT token的格式:header.payload.signature
+ * header的格式(算法、token的类型):
+ * {"alg": "HS512","typ": "JWT"}
+ * payload的格式(用户名、创建时间、生成时间):
+ * {"sub":"wang","created":1489079981393,"exp":1489684781}
+ * signature的生成算法:
+ * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
+ * Created by macro on 2018/4/26.
+ */
+@Component
+public class JwtTokenUtil {
+    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
+
+    @Value("${jwt.secret}")
+    private String secret;
+
+    @Value("${jwt.tokenHeader}")
+    private String tokenHeader;
+    @Value("${jwt.tokenHead}")
+    private String tokenHead;
+
+
+
+    /**
+     * 从token中获取JWT中的负载
+     */
+    private Claims getClaimsFromToken(String token) {
+        Claims claims = null;
+        try {
+            claims = Jwts.parser()
+                    .setSigningKey(secret)
+                    .parseClaimsJws(token)
+                    .getBody();
+        } catch (Exception e) {
+            LOGGER.info("JWT格式验证失败:{}", token);
+        }
+        return claims;
+    }
+
+
+    /**
+     * 从token中获取登录用户名
+     */
+    public String getUserNameFromToken(String token) {
+        LOGGER.info("开始验证JWT:{}", token);
+        if (!token.startsWith(this.tokenHead)) {
+            throw new ApiException(ResultCode.UNAUTHORIZED);
+        }
+        String authToken = token.substring(this.tokenHead.length());
+        String username;
+        try {
+            Claims claims = getClaimsFromToken(authToken);
+            username = claims.getSubject();
+        } catch (Exception e) {
+            username = null;
+        }
+        return username;
+    }
+
+
+
+
+
+
+
+
+}

+ 35 - 0
forest-gateway/src/main/resources/bootstrap.yml

@@ -0,0 +1,35 @@
+server:
+  port: 9098
+spring:
+  profiles:
+    # dev 默认为开发环境 , prod 线上环境
+    active: prod
+  application:
+    name: forest-gateway
+  cloud:
+    nacos:
+      discovery:
+        server-addr: 127.0.0.1:8898
+      config:
+        server-addr: 127.0.0.1:8898
+        prefix: forest-gateway
+        file-extension: yml
+        # 公共配置文件
+        shared-dataids: forest-common.yml
+    gateway:
+      discovery:
+        locator:
+          # 是否与服务发现组件进行结合,通过 serviceId 转发到具体的服务实例。默认为false
+          enabled: true
+          lower-case-service-id: true #使用小写service-id
+      # 处理响应头重复
+      globalcors:
+        corsConfigurations:
+          '[/**]':
+            #这里有个allowCredentials: true这个东西是设置允许访问携带cookie的,这点一定要和前端对应!
+            allowCredentials: true
+            #可以填写多个域名用","隔开 例如 "*"代表允许所有
+            allowedOrigins: "*"
+            allowedMethods: "*"
+            allowedHeaders: "*"
+            allow-credentials: true

+ 55 - 0
forest-gateway/src/main/resources/logback-spring.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <contextName>febs</contextName>
+    <springProperty scope="context" name="springAppName" source="spring.application.name"/>
+    <property name="log.path" value="log/forest-gateway" />
+    <property name="log.maxHistory" value="15" />
+    <property name="log.colorPattern" value="%magenta(%d{yyyy-MM-dd HH:mm:ss}) %highlight(%-5level) %boldCyan([${springAppName:-},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]) %yellow(%thread) %green(%logger) %msg%n"/>
+    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5level [${springAppName:-},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}] %thread %logger %msg%n"/>
+
+    <!--输出到控制台-->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${log.colorPattern}</pattern>
+        </encoder>
+    </appender>
+
+    <!--输出到文件-->
+    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/info/info.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <MaxHistory>${log.maxHistory}</MaxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>INFO</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/error/error.%d{yyyy-MM-dd}.log</fileNamePattern>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <root level="debug">
+        <appender-ref ref="console" />
+    </root>
+
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+</configuration>