瀏覽代碼

feature: 鉴权实现,转发处理,配置转换

Walker 1 年之前
父節點
當前提交
69c303a5f7

+ 8 - 0
pom.xml

@@ -56,6 +56,14 @@
             <groupId>org.redisson</groupId>
             <artifactId>redisson</artifactId>
         </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-json</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-jwt</artifactId>
+        </dependency>
     </dependencies>
 
 

+ 24 - 0
src/main/java/com/ywt/gateway/configuration/BizCfg.java

@@ -0,0 +1,24 @@
+package com.ywt.gateway.configuration;
+
+import com.ywt.gateway.model.AppInfo;
+import com.ywt.gateway.model.AuthInfo;
+import com.ywt.gateway.model.UpstreamInfo;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Walker
+ * Created on 2023/11/22
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "biz")
+public class BizCfg {
+    private List<AppInfo> apps = new ArrayList<>();
+    private List<AuthInfo> auths = new ArrayList<>();
+    private List<UpstreamInfo> upstreams = new ArrayList<>();
+}

+ 279 - 0
src/main/java/com/ywt/gateway/filter/AuthGatewayFilterFactory.java

@@ -0,0 +1,279 @@
+package com.ywt.gateway.filter;
+
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.hutool.json.JSONUtil;
+import cn.hutool.jwt.JWT;
+import cn.hutool.jwt.JWTPayload;
+import cn.hutool.jwt.JWTUtil;
+import cn.hutool.jwt.signers.JWTSignerUtil;
+import com.ywt.gateway.configuration.BizCfg;
+import com.ywt.gateway.decorator.RecorderServerHttpRequestDecorator;
+import com.ywt.gateway.model.*;
+import com.ywt.gateway.utils.BizUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.*;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.util.MultiValueMap;
+import reactor.core.publisher.Flux;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * 处理鉴权
+ * @author Walker
+ * Created on 2023/11/10
+ */
+@Component
+public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<HostLocationInfo> {
+    private final Logger logger = LoggerFactory.getLogger(AuthGatewayFilterFactory.class);
+    public static final String AUTH_TYPE_WEB = "web";
+    public static final String AUTH_TYPE_WECHATMP = "wechatmp";
+    public static final String AUTH_TYPE_API = "api";
+    public static final String KEY_AUTHDATA = "authdata";
+    public static final String KEY_AUTH_DATA = "auth-data";
+    public static final String KEY_CLEAN_DATA = "clean-data";
+    public static final String KEY_AUTH_PARAM = "auth-param";
+    public static final String KEY_BEARER = "Bearer ";
+    public static final String KEY_APPID = "appid";
+    public static final String KEY_EXP = "exp";
+    public static final String KEY_CHECKSUM = "checksum";
+    public static final String KEY_AUTH_APPID = "auth-appid";
+    public static final String KEY_AUTHORIZATION = "Authorization";
+    public static final String KEY_IAT = "iat";
+    public static final String POST = "post";
+    public static final String YES = "yes";
+    public static final int HTTP_STATUS_CODE_601 = 601;
+    public static final int HTTP_STATUS_CODE_602 = 602;
+
+    @Autowired
+    private BizCfg bizCfg;
+
+    public AuthGatewayFilterFactory() {
+        super(HostLocationInfo.class);
+    }
+
+    @Override
+    public GatewayFilter apply(HostLocationInfo config) {
+        return (exchange, chain) -> {
+
+            ServerHttpResponse response = exchange.getResponse();
+            ServerHttpRequest request = exchange.getRequest();
+            String requestUrl = request.getPath().toString();
+            String methodName = request.getMethodValue();
+            HttpHeaders headers = request.getHeaders();
+            String host = "";
+            if (request.getLocalAddress() != null) {
+                host = request.getLocalAddress().getHostString();
+            } else {
+                logger.warn("Could not get local address!");
+            }
+            // 获取 request body
+            AtomicReference<String> requestBody = new AtomicReference<>("");
+            // 复用现成的 decorator
+            RecorderServerHttpRequestDecorator requestDecorator = new RecorderServerHttpRequestDecorator(request);
+            Flux<DataBuffer> body = requestDecorator.getBody();
+            body.subscribe(buffer -> {
+                CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
+                requestBody.set(charBuffer.toString());
+            });
+            String bodyStr = requestBody.get();
+            try {
+                String auth = config.getAuth();
+                String protocol = config.getProtocol();
+                if (auth == null || auth.isEmpty()) {
+                    // 不需要授权
+
+                    proxyPass(config, null, headers, response);
+                    return chain.filter(exchange);
+                } else {
+                    AuthInfo authInfo = bizCfg.getAuths().stream().filter(i -> auth.equals(i.getName())).findFirst().orElse(null);
+                    if (authInfo == null) throw new HttpMsgException(HttpStatus.BAD_GATEWAY, HttpStatus.BAD_GATEWAY.value(),
+                            "No Auth");
+                    String authType = authInfo.getType();
+                    String name = authInfo.getName();
+                    MultiValueMap<String, HttpCookie> cookieMap = request.getCookies();
+                    switch (authType) {
+                        case AUTH_TYPE_WEB:
+                        case AUTH_TYPE_WECHATMP:
+                            String tokenName = String.format("t%d", BizUtil.getCRC32Checksum(name.getBytes()));
+                            String cookieName = authInfo.getCookieName();
+                            if (cookieName != null && !cookieName.isEmpty()) {
+                                tokenName = cookieName;
+                            }
+                            HttpCookie cookie = cookieMap.getFirst(tokenName);
+                            if (cookie != null) {
+                                String tokenStr = cookie.getValue();
+                                JWT jwt = JWTUtil.parseToken(tokenStr);
+                                JWTPayload payload = jwt.getPayload();
+                                String authdata = (String) payload.getClaim(KEY_AUTHDATA);
+                                if (authdata != null && !authdata.isEmpty()) {
+                                    headers.add(KEY_AUTH_DATA, authdata);
+                                }
+                                if (authInfo.getParams() != null) {
+                                    headers.add(KEY_AUTH_PARAM, JSONUtil.toJsonStr(authInfo.getParams()));
+                                }
+
+                                proxyPass(config, authInfo, headers, response);
+                                return chain.filter(exchange);
+                            } else {
+                                boolean authResponse401 = config.isAuthResponse401();
+                                // 处理授权跳转
+                                if (AUTH_TYPE_WECHATMP.equals(authType)) {
+                                    if (authResponse401) {
+                                        throw new HttpMsgException(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(), "");
+                                    } else {
+                                        String returnUrl = String.format("%s://%s%s?r=%s", protocol, host, authInfo.getUrl(),
+                                                URLEncoder.encode(String.format("%s://%s%s", protocol, host, requestUrl),
+                                                        StandardCharsets.UTF_8.name()));
+                                        String weRdtUrl = String.format("https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=STATE#wechat_redirect",
+                                                authInfo.getWeappid(), URLEncoder.encode(returnUrl, StandardCharsets.UTF_8.name()),
+                                                authInfo.getScope());
+                                        response.setStatusCode(HttpStatus.FOUND);
+                                        response.getHeaders().set(HttpHeaders.LOCATION, weRdtUrl);
+                                        return response.setComplete();
+                                    }
+                                } else {
+                                    if (authResponse401) {
+                                        throw new HttpMsgException(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(), "");
+                                    } else {
+                                        String rUrl = String.format("%s://%s%s", protocol, host, requestUrl);
+                                        String url = String.format("%s?r=%s", authInfo.getUrl(), URLEncoder.encode(rUrl,
+                                                StandardCharsets.UTF_8.name()));
+                                        response.setStatusCode(HttpStatus.FOUND);
+                                        response.getHeaders().set(HttpHeaders.LOCATION, url);
+                                        return response.setComplete();
+                                    }
+                                }
+                            }
+                        case AUTH_TYPE_API:
+                            if (POST.equalsIgnoreCase(methodName))
+                                throw new HttpMsgException(HttpStatus.METHOD_NOT_ALLOWED, HttpStatus.METHOD_NOT_ALLOWED.value(),
+                                        HttpStatus.METHOD_NOT_ALLOWED.getReasonPhrase());
+                            String authStr = Optional.ofNullable(headers.getFirst(KEY_AUTHORIZATION)).orElse("");
+                            if (!authStr.startsWith(KEY_BEARER))
+                                throw new HttpMsgException(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(),
+                                        "Auth Fail");
+
+                            // JWT 验证
+                            JWT jwt = JWTUtil.parseToken(authStr.replace(KEY_BEARER, ""));
+                            JWTPayload payload = jwt.getPayload();
+                            if (payload == null)
+                                throw new HttpMsgException(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.value(),
+                                        "Parse Payload Error");
+                            String appId = (String) payload.getClaim(KEY_APPID);
+                            if (appId == null) {
+                                throw new HttpMsgException(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.value(),
+                                        "Payload 必需包含 appid");
+                            }
+                            AppInfo appInfo = bizCfg.getApps().stream().filter(i -> appId.equals(i.getAppid())).findFirst().orElse(null);
+                            if (appInfo == null)
+                                throw new HttpMsgException(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.value(),
+                                        "不正确的 appid");
+                            String appSecret = appInfo.getAppsecret();
+                            Long exp = (Long) payload.getClaim(KEY_EXP);
+                            // 判断 token 是否过期
+                            if (exp != null && exp > 0 && (new Date()).getTime() > exp) {
+                                throw new HttpMsgException(HttpStatus.INTERNAL_SERVER_ERROR, HTTP_STATUS_CODE_602,
+                                        "Token expired");
+                            }
+                            //判断是否需要验证checksum
+                            if (appInfo.isChecksum()) {
+                                String checksum = (String) payload.getClaim(KEY_CHECKSUM);
+                                if (!DigestUtil.md5Hex(bodyStr).equals(checksum)) {
+                                    throw new HttpMsgException(HttpStatus.INTERNAL_SERVER_ERROR, HTTP_STATUS_CODE_601,
+                                            "Checksum Error");
+                                }
+                            }
+                            String authdata = (String) payload.getClaim(KEY_AUTHDATA);
+                            if (authdata != null && !authdata.isEmpty()) {
+                                headers.add(KEY_AUTH_DATA, authdata);
+                            }
+                            Map<String, String> authParamMap = new HashMap<>();
+                            authParamMap.putAll(authInfo.getParams());
+                            authParamMap.putAll(appInfo.getParams());
+                            headers.add(KEY_AUTH_PARAM, JSONUtil.toJsonStr(authParamMap));
+
+                            // 下发当前授权的 appid 至后端
+                            headers.add(KEY_AUTH_APPID, appInfo.getAppid());
+
+                            proxyPass(config, authInfo, headers, response);
+                            return chain.filter(exchange);
+                        default:
+                            throw new HttpMsgException(HttpStatus.BAD_GATEWAY, HttpStatus.BAD_GATEWAY.value(), "No Auth");
+                    }
+                }
+            } catch (HttpMsgException e) {
+                response.setStatusCode(e.getHttpStatus());
+                response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
+                DataBuffer dataBuffer = response.bufferFactory().wrap(JSONUtil.toJsonStr(new BaseResponse(e.getStatusCode(), e.getMessage())).getBytes());
+                return response.writeWith(Flux.just(dataBuffer));
+            } catch (UnsupportedEncodingException e) {
+                throw new RuntimeException(e);
+            }
+        };
+    }
+
+    private void proxyPass(HostLocationInfo locationInfo, AuthInfo authInfo, HttpHeaders headers, ServerHttpResponse response) throws HttpMsgException {
+        // 原网关的“选择后端服务”、“解析协议”部分代码不需要重编码实现,直接使用 Spring Cloud Gateway 的 uri 实现转发
+//        if (locationInfo.getServers() == null || locationInfo.getServers().isEmpty())
+//            throw new HttpMsgException(HttpStatus.BAD_GATEWAY, HttpStatus.BAD_GATEWAY.value(),
+//                    "No Server");
+        // “重定向”部分代码不需要重编码实现,直接使用 Filter 的 RedirectTo 实现
+
+        // 设置授权
+        if (authInfo == null) {
+            String refreshAuth = Optional.ofNullable(locationInfo.getRefreshAuth()).orElse("");
+            authInfo = bizCfg.getAuths().stream().filter(i -> refreshAuth.equals(i.getName())).findFirst().orElse(null);
+        }
+        if (authInfo != null && !AUTH_TYPE_API.equals(authInfo.getType())) {
+            String authDataStr = Optional.ofNullable(headers.getFirst(KEY_AUTH_DATA)).orElse("");
+            String cleanAuthStr = Optional.ofNullable(headers.getFirst(KEY_CLEAN_DATA)).orElse("");
+            String cookieName = Optional.ofNullable(authInfo.getCookieName()).orElse("");
+            String appSecret = Optional.ofNullable(authInfo.getJwtSecret()).orElse("");
+            String name = Optional.ofNullable(authInfo.getName()).orElse("");
+            // 签发JWT授权
+            if (!authDataStr.isEmpty()) {
+                Map<String, Object> payload = new HashMap<>();
+                payload.put(KEY_AUTHDATA, authDataStr);
+                payload.put(KEY_IAT, new Date().getTime());
+                String token = JWTUtil.createToken(payload, JWTSignerUtil.hs256(appSecret.getBytes()));
+                String tokenName = String.format("t%d", BizUtil.getCRC32Checksum(name.getBytes()));
+                if (!cookieName.isEmpty()) tokenName = cookieName;
+                ResponseCookie cookie = ResponseCookie.from(tokenName, token)
+                        .httpOnly(false)
+                        .path("/")
+                        .maxAge(authInfo.getMaxAge())
+                        .domain(authInfo.getCookieDomain())
+                        .build();
+                response.addCookie(cookie);
+            }
+            // 清除授权
+            if (YES.equals(cleanAuthStr)) {
+                String tokenName = String.format("t%d", BizUtil.getCRC32Checksum(name.getBytes()));
+                if (!cookieName.isEmpty()) tokenName = cookieName;
+                ResponseCookie cookie = ResponseCookie.from(tokenName, "")
+                        .httpOnly(false)
+                        .path("/")
+                        .maxAge(-1)
+                        .domain(authInfo.getCookieDomain())
+                        .build();
+                response.addCookie(cookie);
+            }
+        }
+    }
+}

+ 1 - 1
src/main/java/com/ywt/gateway/filter/GlobalCountAndLogFilter.java

@@ -248,7 +248,7 @@ public class GlobalCountAndLogFilter implements GlobalFilter, Ordered {
     }
 
     /**
-     * 打印日志
+     * 记录日志
      */
     private void commitRecord(CountRecord countRecord, RequestRecord requestRecord) {
         RDeque<String> reqDeque = redissonClient.getDeque(LOG_QUEUE_NAME);

+ 21 - 0
src/main/java/com/ywt/gateway/model/AppInfo.java

@@ -0,0 +1,21 @@
+package com.ywt.gateway.model;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Walker
+ * Created on 2023/11/10
+ */
+@Data
+public class AppInfo {
+    private String appid;
+    private String appsecret;
+    private Map<String, String> params;
+    private List<String> IPwhilelist;
+    private boolean checksum;
+
+//    private Map<String, Boolean> ipWhilelistMap;
+}

+ 28 - 0
src/main/java/com/ywt/gateway/model/AuthInfo.java

@@ -0,0 +1,28 @@
+package com.ywt.gateway.model;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Walker
+ * Created on 2023/11/10
+ */
+@Data
+public class AuthInfo {
+    private String name;
+    private String type;
+    private String jwtSecret;
+    private String weappid;
+    private String scope;
+    private String url;
+    private Map<String, String> params;
+    private String cookieName;
+
+//    private List<String> appids; // TODO: 原网关处理逻辑搞清楚
+//    private String authUrl;
+    private int maxAge;
+    private String cookieDomain;
+//    private Map<String, AppInfo> appidMap;
+}

+ 15 - 0
src/main/java/com/ywt/gateway/model/BaseResponse.java

@@ -0,0 +1,15 @@
+package com.ywt.gateway.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+/**
+ * @author Walker
+ * Created on 2023/11/10
+ */
+@Data
+@AllArgsConstructor
+public class BaseResponse {
+    private int code;
+    private String info;
+}

+ 25 - 0
src/main/java/com/ywt/gateway/model/HostLocationInfo.java

@@ -0,0 +1,25 @@
+package com.ywt.gateway.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author Walker
+ * Created on 2023/11/22
+ */
+@Data
+public class HostLocationInfo {
+    private String url;
+    private String rewriteUrl;
+    private String matchType;
+    private boolean replacePrefixUrl;
+    private String auth;
+    private String refreshAuth;
+    private boolean authResponse401;
+    private List<String> setHeaders;
+    private List<String> servers;
+    private boolean log;
+
+    private String protocol = "https";
+}

+ 44 - 0
src/main/java/com/ywt/gateway/model/HttpMsgException.java

@@ -0,0 +1,44 @@
+package com.ywt.gateway.model;
+
+import org.springframework.http.HttpStatus;
+
+/**
+ * @author Walker
+ * Created on 2023/11/10
+ */
+public class HttpMsgException extends Exception{
+    private HttpStatus httpStatus;
+    private int statusCode;
+    private String message;
+
+    public HttpMsgException(HttpStatus httpStatus, int statusCode, String message) {
+        this.httpStatus = httpStatus;
+        this.statusCode = statusCode;
+        this.message = message;
+    }
+
+    public HttpStatus getHttpStatus() {
+        return httpStatus;
+    }
+
+    public void setHttpStatus(HttpStatus httpStatus) {
+        this.httpStatus = httpStatus;
+    }
+
+    public int getStatusCode() {
+        return statusCode;
+    }
+
+    public void setStatusCode(int statusCode) {
+        this.statusCode = statusCode;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+}

+ 17 - 0
src/main/java/com/ywt/gateway/model/UpstreamInfo.java

@@ -0,0 +1,17 @@
+package com.ywt.gateway.model;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+/**
+ * @author Walker
+ * Created on 2023/11/22
+ */
+@Data
+public class UpstreamInfo {
+    private String name;
+    private List<String> groups;
+}

+ 16 - 0
src/main/java/com/ywt/gateway/utils/BizUtil.java

@@ -0,0 +1,16 @@
+package com.ywt.gateway.utils;
+
+import java.util.zip.CRC32;
+import java.util.zip.Checksum;
+
+/**
+ * @author Walker
+ * Created on 2023/11/10
+ */
+public final class BizUtil {
+    public static long getCRC32Checksum(byte[] bytes) {
+        Checksum crc32 = new CRC32();
+        crc32.update(bytes, 0, bytes.length);
+        return crc32.getValue();
+    }
+}

+ 349 - 1
src/main/resources/application.yml

@@ -2,7 +2,7 @@ server:
   port: 21545
 
 redis:
-  address: 192.168.3.202:6379
+  address: redis://192.168.3.202:6379
 
 spring:
   application:
@@ -42,5 +42,353 @@ spring:
             # 前缀过滤,默认配置下,我们的请求路径是 http://localhost:8888/business-oauth2/** 这时会路由到指定的服务
             # 此处配置去掉 1 个路径前缀,再配置上面的 Path=/api/**,就能按照 http://localhost:8888/api/** 的方式访问了
             - StripPrefix=1
+        - id: HISAPI-TAIHE-COM
+          uri: lb://his-api-server
+          predicates:
+            - Host=hisapi.taihe.com
+          filters:
+            - name: Auth
+              args:
+                url: '/'
+                #url匹配类型 regex: 正则, prefix: 前缀, full 全匹配
+                matchType: 'prefix'
+                replacePrefixUrl: false
+                auth: 'external-api'
+                authResponse401: true
+                #设置需要刷新的授权,当 auth 不为空,以 auth 的配置为主
+                refreshAuth: ''
+                setHeaders: [ ]
+        - id: YWT-TEST-TAIHE-COM
+          uri: lb://ywt-api-server-test
+          predicates:
+            - Host=hisapi.taihe.com
+          filters:
+            - name: Auth
+              args:
+                url: '/'
+                #url匹配类型 regex: 正则, prefix: 前缀, full 全匹配
+                matchType: 'prefix'
+                replacePrefixUrl: false
+                auth: 'external-api'
+                authResponse401: true
+                #设置需要刷新的授权,当 auth 不为空,以 auth 的配置为主
+                refreshAuth: ''
+                setHeaders: [ ]
+        - id: YWT-TAIHE-COM
+          uri: lb://ywt-api-server
+          predicates:
+            - Host=hisapi.taihe.com
+          filters:
+            - name: Auth
+              args:
+                url: '/'
+                #url匹配类型 regex: 正则, prefix: 前缀, full 全匹配
+                matchType: 'prefix'
+                replacePrefixUrl: true
+                auth: 'external-api'
+                authResponse401: true
+                #设置需要刷新的授权,当 auth 不为空,以 auth 的配置为主
+                refreshAuth: ''
+                setHeaders: [ ]
+
+
+biz:
+  # 配置接入方的appid
+  apps:
+    -
+      appid: 'ywt-thnet-weapp'
+      appsecret: 'ywt123456789'
+      IPwhilelist: ['127.0.0.1']
+      params:
+        p1: 'pv1'
+        p2: 'pv2'
+    -
+      appid: 'ywt-nf-weapp'
+      appsecret: 'ywt123456789'
+      # 可配置IP准入白名单,不配置此项则默认不限制
+      IPwhilelist: ['127.0.0.1']
+      # 当前appid下的特定参数
+      params:
+        terminal: '1'
+        p2: 'pv2'
+    -
+      appid: 'ywt-th-dg-client'
+      appsecret: 'ywt123456789'
+      # 可配置IP准入白名单,不配置此项则默认不限制
+      IPwhilelist: ['127.0.0.1']
+      # 当前appid下的特定参数
+      params:
+        pharmacyId: '2'
+    -
+      appid: 'taihe_hospital'
+      appsecret: 'taihe_hospital1'
+    -
+      appid: 'gk_druggist'
+      appsecret: 'gk_druggist_123456'
+      checksum: true
+    -
+      appid: 'ywt-gs-app-client'
+      appsecret: 'ywt123456789'
+      # 可配置IP准入白名单,不配置此项则默认不限制
+      IPwhilelist: ['127.0.0.1']
+      # 当前appid下的特定参数
+      params:
+        pharmacyId: '1'
+    -
+      appid: 'ywt-thnetdoc-weapp'
+      appsecret: 'ywt123456789'
+      IPwhilelist: ['127.0.0.1']
+      params:
+        terminal: '16'
+    -
+      appid: 'ywt-thnetdoc-weapp-internal-mp'
+      appsecret: 'ywt123456789'
+      IPwhilelist: ['127.0.0.1']
+      params:
+        terminal: '17'
+    -
+      appid: 'ywt-nutrimeal-merchant-client'
+      appsecret: 'ywt123456789'
+    -
+      appid: 'ywt'
+      appsecret: 'ywt123456789'
+      checksum: true
+    -
+      appid: 'ywt_auto_test'
+      appsecret: 'ywt1234567890'
+    -
+      appid: 'patient_app_android'
+      appsecret: 'patient_app_123456'
+      checksum: true
+      params:
+        terminal: '5'
+    -
+      appid: 'patient_app_ios'
+      appsecret: 'patient_app_123456'
+      checksum: true
+      params:
+        terminal: '6'
+    -
+      appid: 'ywt-yunmp-weapp'
+      appsecret: 'ywt123456789'
+      params:
+        terminal: '18'
+    -
+      appid: 'ywt-common-push'
+      appsecret: 'ywt-common-push-Ux8$6x0Dz1l'
+    -
+      # 下单服务助手小程序(太和耗材业务)
+      appid: 'order-helper-weapp'
+      appsecret: 'ywt123456789'
+      params:
+        terminal: '19'
+    -
+      # 南方医院白云分院小程序
+      appid: 'nfyybyfy-weapp'
+      appsecret: 'ywt123456789'
+      params:
+        terminal: '20'
+    -
+      # 互联网监管平台-白云
+      appid: 'by-regulatory-api'
+      appsecret: 'ywtby123456789'
+      checksum: true
+    -
+      # 视光中心客户端
+      appid: 'ywt-glasses-merchant-client'
+      appsecret: 'ywt123456789'
+    -
+      # 白云营养餐第三方商家
+      appid: 'ywt-by-open-nutrimeal151'
+      appsecret: 'ywt11108054#'
+    -
+      # 专家工作室微信小程序
+      appid: 'digital-weapp'
+      appsecret: 'ywt123456789'
+      params:
+        terminal: '26'
+    -
+      # taihe_his_push_api 太和、白云处方接入
+      appid: 'ywt-common-push-taihe'
+      appsecret: 'ywt-common-push-Ux86x0Dz1l-taihe'
+
+  #配置授权
+  auths:
+    -
+      name: 'nfth-qt'
+      type: 'web'
+      jwtSecret: 'ywt-mg-12132asddfdfddfd'
+      #取授权url
+      url: '/login'
+    -
+      #(医生端)「我的工作站」授权
+      name: 'ywt-wechat-doc-mp'
+      type: 'wechatmp'
+      jwtSecret: 'ywt-mg-12132asddfdfd'
+      weappid: 'wx245b2192979beb49'
+      scope: 'snsapi_userinfo'
+      #取授权url
+      url: '/drapi/auth/wechat'
+      params:
+        p1: 'pv1'
+        p2: 'pv2'
+    -
+      #(医生端)「南方医院太和互联网医院医生版」授权
+      name: 'ywt-wechat-thnetdoc-mp'
+      type: 'wechatmp'
+      jwtSecret: 'ywt-mg-12132asddfdfddfdfdf'
+      weappid: 'wx245b2192979beb49'
+      scope: 'snsapi_userinfo'
+      #取授权url
+      url: '/thnetdocapi/auth/wechat'
+      params:
+        terminal: '13'
+
+    -
+      #白云分院医生版授权
+      name: 'ywt-wechat-nfyybyfydoc-mp'
+      type: 'wechatmp'
+      jwtSecret: 'ywt-nfyybyfydoc-Rx$5#0'
+      weappid: 'wx245b2192979beb49'
+      scope: 'snsapi_userinfo'
+      #取授权url
+      url: '/nfyybyfydocapi/auth/wechat'
+      params:
+        terminal: '22'
+    -
+      name: 'external-api'
+      type: 'api'
+      appids: ['ywt-common-push-taihe','digital-weapp', 'ywt-thnet-weapp', 'ywt-nf-weapp', 'gk_druggist', 'ywt-thnetdoc-weapp', 'ywt-nutrimeal-merchant-client', 'ywt-glasses-merchant-client','ywt', 'ywt_auto_test', 'ywt-thnetdoc-weapp-internal-mp', 'patient_app_android', 'patient_app_ios', 'ywt-yunmp-weapp', 'ywt-common-push', 'order-helper-weapp', 'nfyybyfy-weapp', 'by-regulatory-api', 'ywt-by-open-nutrimeal151']
+    -
+      name: 'ywt-mg'
+      type: 'web'
+      jwtSecret: 'ywt-mg-12132asddfdfd'
+      #取授权url
+      url: 'https://mg-qa.ywtinfo.com/m-sysconfig/user/login'
+      # cookieDomain: 'ywtinfo.com'
+      params:
+        p1: 'pv1'
+        p2: 'pv2'
+    -
+      name: 'ywt-mg-api'
+      type: 'api'
+      appids: ['ywt-th-dg-client', 'ywt-gs-app-client', 'ywt_auto_test']
+    -
+      name: 'ywt-mg-dg-pharmacy'
+      type: 'web'
+      jwtSecret: 'ywt-mg-12132asddfdfddfd'
+      #取授权url
+      url: '/user/login'
+    -
+      #(患者端)「南方医院太和互联网医院」授权
+      name: 'ywt-wechat-thnetpatientwe-mp'
+      type: 'wechatmp'
+      jwtSecret: 'ywt-mg-12132asddfdfddfdfdf'
+      weappid: 'wx245b2192979beb49'
+      scope: 'snsapi_userinfo'
+      #取授权url
+      url: '/thnetpatientweapi/auth/wechat'
+      params:
+        terminal: '12'
+    -
+      #(患者端)「广东医务通」静默授权
+      name: 'ywt-wechat-gdywt-mp-base'
+      type: 'wechatmp'
+      jwtSecret: 'ywt-mfdfp-12132asddfdfddfdfdf'
+      weappid: 'wx245b2192979beb49'
+      scope: 'snsapi_base'
+      #取授权url
+      url: '/gdywtweapi/auth/wechat/base'
+      params:
+        terminal: '14'
+    -
+      name: 'ywt-doctor-pc-app'
+      type: 'web'
+      jwtSecret: 'ywt-mg-12132asddfdfddfdfdfbbb'
+      url: '/user/login'
+      params:
+        terminal: '15'
+    -
+      name: 'ywt-wechat-nfywt-mp-base'
+      type: 'wechatmp'
+      jwtSecret: 'ywt-mg-12132asddfdfddfdfdf'
+      weappid: 'wx245b2192979beb49'
+      scope: 'snsapi_base'
+      url: '/thnetdocapi/auth/wechat/base'
+    -
+      name: 'ywt-wechat-nfywt-mp-base-for-nfywtwe'
+      type: 'wechatmp'
+      jwtSecret: 'ywt-mg-12132asddfdfddfdfdfbbbb'
+      weappid: 'wx245b2192979beb49'
+      cookieName: 'nfywtwe_t'
+      scope: 'snsapi_base'
+      url: '/api/auth/wechat/base'
+    -
+      name: 'ywt-wechat-nfywt-mp'
+      type: 'wechatmp'
+      jwtSecret: 'ywt-mg-12132asddfdfddfdfdfbbbb'
+      weappid: 'wx245b2192979beb49'
+      scope: 'snsapi_userinfo'
+      cookieName: 'nfywtwe_t'
+      url: '/api/auth/wechat'
+    -
+      name: 'ywt-wechat-nfth-mp'
+      type: 'wechatmp'
+      jwtSecret: 'ywt-mg-12fdfdfd132asddfdfddfdfdfbbbb'
+      weappid: 'wx245b2192979beb49'
+      scope: 'snsapi_userinfo'
+      url: '/nfthapi/auth/wechat'
+      params:
+        terminal: '8'
+    -
+      name: 'ywt-wechat-druggistwe-mp'
+      type: 'wechatmp'
+      jwtSecret: 'ywt-mg-12fdfdfd1dfd32asddfdfddfdfdfbbbb'
+      weappid: 'wx245b2192979beb49'
+      scope: 'snsapi_userinfo'
+      url: '/marketingapi/auth/wechat'
+      params:
+        terminal: '9'
+    -
+      name: 'regulatory-api'
+      type: 'api'
+      appids: ['by-regulatory-api']
+
+  upstreams:
+    -
+      name: 'nfth-qt-static'
+      groups: ['http://172.18.82.224:17000']
+    -
+      name: 'nfth-qt-api'
+      groups: ['http://172.18.82.224:17001']
+    -
+      name: 'ywt-web-api'
+      groups: ['http://172.18.82.224:8090']
+    -
+      name: 'ywt-rest-api'
+      groups: ['http://172.18.82.224:8003']
+    -
+      name: 'ywt-web-static-mp'
+      groups: ['http://172.18.82.224:7023']
+    -
+      name: 'ywt-mg-server'
+      groups: ['http://127.0.0.1:8067']
+    -
+      name: 'ywt-mg-static'
+      groups: ['http://127.0.0.1:7016']
+    -
+      name: 'ywt-mg-dg-server'
+      groups: ['http://127.0.0.1:8068']
+    -
+      name: 'ywt-go-api'
+      groups: ['http://127.0.0.1:8111']
+    -
+      name: 'ywt-n-rest-api'
+      groups: ['http://127.0.0.1:8003']
+    -
+      name: 'wjcat-be'
+      groups: ['http://127.0.0.1:8000']
+
+