liyan 3 år sedan
förälder
incheckning
fa1145ff4d
100 ändrade filer med 3959 tillägg och 0 borttagningar
  1. 17 0
      README.md
  2. 142 0
      build.gradle.kts
  3. 116 0
      config/menu/items.json
  4. 22 0
      config/menu/menus.json
  5. 91 0
      gateway/build.gradle.kts
  6. 52 0
      gateway/src/main/kotlin/gaf3/core/gateway/GatewayApplication.kt
  7. 41 0
      gateway/src/main/kotlin/gaf3/core/gateway/GatewayController.kt
  8. 60 0
      gateway/src/main/kotlin/gaf3/core/gateway/bean/FactoryBean.kt
  9. 59 0
      gateway/src/main/kotlin/gaf3/core/gateway/bean/GlobalFilterBean.kt
  10. 90 0
      gateway/src/main/kotlin/gaf3/core/gateway/bean/WebFilterBean.kt
  11. 48 0
      gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/ForwardGatewayFilterFactory.kt
  12. 63 0
      gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/HostToTenantGatewayFilterFactory.kt
  13. 184 0
      gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/JwtParserGatewayFilterFactory.kt
  14. 96 0
      gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/PeekRequestBodyGatewayFilterFactory.kt
  15. 110 0
      gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/PeekResponseBodyGatewayFilterFactory.kt
  16. 39 0
      gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/SetRequestHeaderExGatewayFilterFactory.kt
  17. 93 0
      gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/SetRequestParameterGatewayFilterFactory.kt
  18. 41 0
      gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/VerifyCodeGatewayFilterFactory.kt
  19. 128 0
      gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/WeixinTokenGatewayFilterFactory.kt
  20. 23 0
      gateway/src/main/kotlin/gaf3/core/gateway/handler/predicate/ExcludesRoutePredicateFactory.kt
  21. 101 0
      gateway/src/main/kotlin/gaf3/core/gateway/handler/predicate/JwtRoutePredicateFactory.kt
  22. 31 0
      gateway/src/main/kotlin/gaf3/core/gateway/webfilter/ApiAccessFilter.kt
  23. 42 0
      gateway/src/main/kotlin/gaf3/core/gateway/webfilter/HostToAppFilter.kt
  24. 47 0
      gateway/src/main/resources/application-routes.yml
  25. 7 0
      gateway/src/main/resources/application-services.yml
  26. 45 0
      gateway/src/main/resources/application.yml
  27. 7 0
      gradle.properties
  28. BIN
      gradle/wrapper/gradle-wrapper.jar
  29. 5 0
      gradle/wrapper/gradle-wrapper.properties
  30. 185 0
      gradlew
  31. 89 0
      gradlew.bat
  32. BIN
      libs/oscarHibernate5.jar
  33. BIN
      libs/oscarJDBC16.jar
  34. 20 0
      platform/build.gradle.kts
  35. 1 0
      publish.sh
  36. 197 0
      services/build.gradle.kts
  37. 17 0
      services/service-all/src/main/kotlin/gaf3/core/services/GafCoreServicesConfiguration.kt
  38. 25 0
      services/service-all/src/main/kotlin/gaf3/core/services/GafServicesApplication.kt
  39. 28 0
      services/service-all/src/main/resources/application.yml
  40. 3 0
      services/service-all/src/main/resources/data-mysql.sql
  41. 32 0
      services/service-api/src/main/kotlin/gaf3/core/services/ServiceApiApplication.kt
  42. 35 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/CodeService.kt
  43. 14 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/LogService.kt
  44. 37 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/UserService.kt
  45. 12 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/domain/AcctForm.kt
  46. 21 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/domain/CodeForm.kt
  47. 16 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/domain/CodeItem.kt
  48. 9 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/domain/LogData.kt
  49. 30 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/domain/LogForm.kt
  50. 17 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/domain/LoginForm.kt
  51. 18 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/domain/PassForm.kt
  52. 14 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/domain/UserForm.kt
  53. 19 0
      services/service-api/src/main/kotlin/gaf3/core/services/api/domain/UserInfo.kt
  54. 21 0
      services/service-api/src/main/resources/application.yml
  55. 30 0
      services/service-auth/src/main/kotlin/gaf3/core/services/auth/GafAuthConfiguration.kt
  56. 22 0
      services/service-auth/src/main/kotlin/gaf3/core/services/auth/GafAuthConfigure.kt
  57. 19 0
      services/service-auth/src/main/kotlin/gaf3/core/services/auth/GafAuthServiceController.kt
  58. 17 0
      services/service-auth/src/main/kotlin/gaf3/core/services/auth/ServiceAuthApplication.kt
  59. 5 0
      services/service-auth/src/main/kotlin/gaf3/core/services/auth/domain/AuthToken.kt
  60. 13 0
      services/service-auth/src/main/kotlin/gaf3/core/services/auth/domain/LoginForm.kt
  61. 93 0
      services/service-auth/src/main/kotlin/gaf3/core/services/auth/service/GafAuthService.kt
  62. 24 0
      services/service-auth/src/main/resources/application.yml
  63. 11 0
      services/service-bff/README.md
  64. 21 0
      services/service-bff/src/main/kotlin/gaf3/core/services/bff/BffServiceConfiguration.kt
  65. 31 0
      services/service-bff/src/main/kotlin/gaf3/core/services/bff/BffServiceController.kt
  66. 22 0
      services/service-bff/src/main/kotlin/gaf3/core/services/bff/ServiceBffApplication.kt
  67. 7 0
      services/service-bff/src/main/kotlin/gaf3/core/services/bff/dao/GafBffUserDeptDAO.kt
  68. 7 0
      services/service-bff/src/main/kotlin/gaf3/core/services/bff/dao/GafBffUserRoleDAO.kt
  69. 46 0
      services/service-bff/src/main/kotlin/gaf3/core/services/bff/entity/BffBindItem.kt
  70. 32 0
      services/service-bff/src/main/kotlin/gaf3/core/services/bff/entity/BffUserDept.kt
  71. 33 0
      services/service-bff/src/main/kotlin/gaf3/core/services/bff/entity/BffUserRole.kt
  72. 34 0
      services/service-bff/src/main/kotlin/gaf3/core/services/bff/service/BffUserDeptService.kt
  73. 46 0
      services/service-bff/src/main/kotlin/gaf3/core/services/bff/service/BffUserRoleService.kt
  74. 18 0
      services/service-bff/src/main/resources/application.yml
  75. 14 0
      services/service-bind/README.md
  76. 46 0
      services/service-bind/src/main/kotlin/gaf3/core/services/bind/BindServiceController.kt
  77. 19 0
      services/service-bind/src/main/kotlin/gaf3/core/services/bind/ServiceBindApplication.kt
  78. 18 0
      services/service-bind/src/main/kotlin/gaf3/core/services/bind/dao/GafBindItemDAO.kt
  79. 3 0
      services/service-bind/src/main/kotlin/gaf3/core/services/bind/domain/BatchBindForm.kt
  80. 8 0
      services/service-bind/src/main/kotlin/gaf3/core/services/bind/domain/BatchBindResult.kt
  81. 3 0
      services/service-bind/src/main/kotlin/gaf3/core/services/bind/domain/BindForm.kt
  82. 48 0
      services/service-bind/src/main/kotlin/gaf3/core/services/bind/entity/GafBindItem.kt
  83. 92 0
      services/service-bind/src/main/kotlin/gaf3/core/services/bind/service/BindService.kt
  84. 16 0
      services/service-bind/src/main/resources/META-INF/orm.xml
  85. 18 0
      services/service-bind/src/main/resources/application.yml
  86. 7 0
      services/service-code/README.md
  87. 71 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/CodeServiceController.kt
  88. 18 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/CodeStoreController.kt
  89. 16 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/GafCodeConfiguration.kt
  90. 31 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/GafCodeConfigure.kt
  91. 17 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/ServiceCodeApplication.kt
  92. 35 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/dao/GafCodeItemDAO.kt
  93. 21 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/domain/CodeForm.kt
  94. 14 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/domain/CodeItem.kt
  95. 45 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/entity/GafCodeItem.kt
  96. 151 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/service/CodeService.kt
  97. 12 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/service/GafCodeStore.kt
  98. 21 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/service/impl/AutoCodeStore.kt
  99. 24 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/service/impl/DBCodeStore.kt
  100. 0 0
      services/service-code/src/main/kotlin/gaf3/core/services/code/service/impl/FileCodeStore.kt

+ 17 - 0
README.md

@@ -0,0 +1,17 @@
+# GAF3 (for cloud)
+GAF3核心工程 gaf-core
+## services
+核心服务 
+## shared
+共享库
+
+# change log
+## 3.0.1101 (2020-11-01)
+1. JPA扩展方法新的exists接口: 
+    exists(pair: Pair<String, Any>, exact: Boolean = true): Boolean
+
+## 3.1.604
+1. JPA扩展方法,调整模糊查询处理策略
+
+## 3.1.622
+1. 增加UKey证书登录处理

+ 142 - 0
build.gradle.kts

@@ -0,0 +1,142 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.springframework.boot.gradle.tasks.bundling.BootJar
+
+group = "cc-lotus.gaf3"
+version = "3.1.714"
+extra["springCloudVersion"] = "Hoxton.SR5"
+extra["jjwtVersion"] = "0.10.5"
+
+val mavenUser: String? by project
+val mavenPassword: String? by project
+private val repoConf: String = System.getProperty("repoPath") ?: "/var/repo"
+val repoPath: String = file("$rootDir").toPath().root.resolve(repoConf).toString()
+
+plugins {
+    id("java")
+    id("maven-publish")
+    id("io.spring.dependency-management") version "1.0.10.RELEASE"
+    id("org.springframework.boot") version "2.3.3.RELEASE" apply false
+    id("org.jetbrains.dokka") version "0.10.1"
+    kotlin("jvm") version "1.3.72" apply false
+    kotlin("plugin.spring") version "1.3.72" apply false
+    kotlin("plugin.jpa") version "1.3.72" apply false
+}
+
+dependencyManagement {
+    imports {
+        mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
+    }
+}
+
+repositories {
+    // 阿里云镜像
+    maven { url = uri("https://maven.aliyun.com/repository/public") }
+    maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
+    maven { url = uri("https://maven.aliyun.com/repository/spring") }
+    maven { url = uri("https://maven.aliyun.com/repository/spring-plugin") }
+    maven {
+        name = "localRepo"
+        url = uri("file://$repoPath")
+    }
+    maven {
+        name = "cc-lotus"
+        url = uri("http://maven.cc-lotus.info/repository/maven-public/")
+    }
+    // mavenCentral()
+}
+
+configure(subprojects.filter { it.name != "platform" }) {
+    apply(plugin = "java")
+    apply(plugin = "maven-publish")
+    apply(plugin = "org.jetbrains.kotlin.jvm")
+    apply(plugin = "org.jetbrains.dokka")
+
+    java {
+        disableAutoTargetJvm()
+        // withSourcesJar()
+        // withJavadocJar()
+    }
+
+    tasks.withType<JavaCompile> {
+        options.encoding = "UTF-8"
+        sourceCompatibility = "1.8"
+        targetCompatibility = "1.8"
+    }
+
+    tasks.withType<KotlinCompile> {
+        kotlinOptions {
+            freeCompilerArgs = listOf("-Xjsr305=strict")
+            jvmTarget = "1.8"
+        }
+    }
+
+    tasks.withType<Javadoc> {
+        options.encoding = "UTF-8"
+    }
+
+    repositories {
+        maven {
+            name = "localRepo"
+            url = uri("file://$repoPath")
+        }
+        maven {
+            name = "cc-lotus"
+            url = uri("http://maven.cc-lotus.info/repository/maven-public/")
+        }
+        // 阿里云镜像
+        maven { url = uri("https://maven.aliyun.com/repository/public") }
+        maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
+        maven { url = uri("https://maven.aliyun.com/repository/spring") }
+        maven { url = uri("https://maven.aliyun.com/repository/spring-plugin") }
+        // mavenCentral()
+    }
+    publishing {
+//        publications {
+//            create<MavenPublication>("maven") {
+//                groupId = "cc-lotus.gaf3"
+//                artifactId = tasks.jar.get().archiveBaseName.get()
+//                version = "3.0.0"
+//                from(components["java"])
+//            }
+//        }
+        repositories {
+            maven {
+                name = "localRepo"
+                url = uri("file://$repoPath")
+            }
+        }
+        repositories {
+            maven {
+                name = "cc-lotus"
+                url = uri("http://maven.cc-lotus.info/repository/maven-releases/")
+                //认证用户和密码
+                credentials {
+                    username = mavenUser // """${property("mavenUser")}"""
+                    password = mavenPassword // """${property("mavenPassword")}"""
+                }
+            }
+        }
+    }
+}
+
+task<Copy>("dist") {
+    into("$buildDir/dist")
+    project(":gateway") {
+        from(tasks.withType<BootJar>())
+    }
+}
+
+tasks {
+//    processResources {
+//         expand(project.properties)
+//    }
+    val dokka by getting(org.jetbrains.dokka.gradle.DokkaTask::class) {
+        outputFormat = "html"
+        outputDirectory = "$buildDir/dokka"
+    }
+}
+
+//task("printRepo") {
+//    println(org.jetbrains.kotlin.org.jline.utils.OSUtils.IS_WINDOWS)
+//    println(repoPath)
+//}

+ 116 - 0
config/menu/items.json

@@ -0,0 +1,116 @@
+[
+  {
+    "id": "user",
+    "title": "用户管理",
+    "path": "/xms",
+    "icon": "account",
+    "module": "@user",
+    "children": [
+      {
+        "id":"user-info",
+        "title": "用户信息",
+        "path": "/xms/user",
+        "icon": "account"
+      },
+      {
+        "title": "机构管理",
+        "path": "/xms/org",
+        "icon": "dept"
+      },
+      {
+        "title": "群组管理",
+        "path": "/xms/group",
+        "icon": "users"
+      },
+      {
+        "title": "注册审核",
+        "path": "/xms/register",
+        "icon": "audit"
+      },
+      {
+        "title": "证书管理",
+        "path": "/xms/cert",
+        "icon": "cert"
+      },
+      {
+        "title": "黑名单管理",
+        "path": "/xms/blacklist",
+        "icon": "user1"
+      }
+    ]
+  },
+  {
+    "title": "授权管理",
+    "path": "/xms",
+    "icon": "auth",
+    "module": "@empower",
+    "children": [
+      {
+        "title": "机构授权",
+        "path": "/xms/orgEmpower",
+        "icon": "dept"
+      },
+      {
+        "title": "群组授权",
+        "path": "/xms/groupEmpower",
+        "icon": "users"
+      }
+    ]
+  },
+  {
+    "title": "应用管理",
+    "path": "/xms",
+    "icon": "column",
+    "module": "@app",
+    "children": [
+      {
+        "title": "应用信息",
+        "path": "/xms/app",
+        "icon": "bill"
+      },
+      {
+        "title": "资源管理",
+        "path": "/xms/resources",
+        "icon": "tags"
+      },
+      {
+        "title": "角色管理",
+        "path": "/xms/roles",
+        "icon": "tag"
+      }
+    ]
+  },
+  {
+    "title": "系统管理",
+    "path": "/gaf",
+    "icon": "system",
+    "module": "@gaf",
+    "children": [
+      {
+        "title": "系统用户",
+        "path": "/gaf/user",
+        "icon": "account"
+      },
+      {
+        "title": "用户部门",
+        "path": "/gaf/dept",
+        "icon": "dept"
+      },
+      {
+        "title": "数据字典",
+        "path": "/gaf/dict",
+        "icon": "dict"
+      },
+      {
+        "title": "菜单管理",
+        "path": "/gaf/menu",
+        "icon": "menu"
+      },
+      {
+        "title": "日志审计",
+        "path": "/gaf/log",
+        "icon": "log"
+      }
+    ]
+  }
+]

+ 22 - 0
config/menu/menus.json

@@ -0,0 +1,22 @@
+[
+  {
+    "title": "用户管理",
+    "path": "/xms",
+    "module": "@user"
+  },
+  {
+    "title": "授权管理",
+    "path": "/xms",
+    "module": "@empower"
+  },
+  {
+    "title": "应用管理",
+    "path": "/xms",
+    "module": "@app"
+  },
+  {
+    "title": "系统管理",
+    "path": "/gaf",
+    "module": "@gaf"
+  }
+]

+ 91 - 0
gateway/build.gradle.kts

@@ -0,0 +1,91 @@
+import org.springframework.boot.gradle.tasks.bundling.BootJar
+
+group = "cc-lotus.gaf3"
+version = rootProject.version
+
+plugins {
+    id("java")
+    id("maven-publish")
+    id("io.spring.dependency-management")
+    id("org.springframework.boot")
+    kotlin("jvm")
+    kotlin("plugin.spring")
+    kotlin("plugin.jpa")
+}
+
+dependencies {
+    implementation(project(path = ":shared:cloud"))
+    implementation(project(path = ":shared:util"))
+    implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
+    implementation("org.springframework.boot:spring-boot-starter-webflux")
+    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+    implementation("org.springframework.boot:spring-boot-configuration-processor")
+    implementation("org.springframework.cloud:spring-cloud-starter-gateway")
+    implementation("org.springframework.cloud:spring-cloud-gateway-webflux")
+    implementation("org.springframework.boot:spring-boot-starter-data-redis")
+    implementation("io.jsonwebtoken:jjwt-api")
+    implementation("io.jsonwebtoken:jjwt-impl")
+    implementation("io.jsonwebtoken:jjwt-jackson")
+    implementation("com.alibaba:fastjson")
+    implementation("commons-io:commons-io")
+    implementation(kotlin("reflect"))
+    implementation(kotlin("stdlib-jdk8"))
+    implementation(project(path = ":services:service-verify", configuration = "lib"))
+    implementation(project(path = ":services:service-token", configuration = "lib"))
+}
+
+dependencyManagement {
+    imports {
+        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
+    }
+}
+
+tasks.named<BootJar>(name = "bootJar") {
+    archiveBaseName.set("gaf3-${project.name}")
+}
+
+tasks.register<Jar>(name = "libJar") {
+    archiveBaseName.set("gaf3-${project.name}-lib")
+    from(project.the<SourceSetContainer>()["main"].output)
+    include("gaf3/**")
+    exclude("gaf3/core/gateway/bean/**")
+    exclude {
+        it.name.endsWith("Application.class")
+    }
+    exclude {
+        it.name.endsWith("ApplicationKt.class")
+    }
+    exclude {
+        it.name.endsWith("Application${'$'}Companion.class")
+    }
+}
+tasks.build { dependsOn(tasks.named("libJar")) }
+
+configurations {
+    create("lib")
+}
+
+artifacts {
+    add("lib", tasks["libJar"])
+}
+
+tasks {
+    processResources {
+        filesMatching("application-services.yml") {
+            // expand("db.user" to dbUser)
+            expand(project.properties)
+        }
+    }
+}
+publishing {
+    publications {
+        create<MavenPublication>("maven") {
+            artifact(tasks["libJar"])
+            artifactId = "gaf-core-${project.name}"
+            // from(components["java"])
+        }
+    }
+}
+
+
+

+ 52 - 0
gateway/src/main/kotlin/gaf3/core/gateway/GatewayApplication.kt

@@ -0,0 +1,52 @@
+package gaf3.core.gateway
+
+import gaf3.core.cloud.GafCloudConfiguration
+import gaf3.core.gateway.filter.factory.JwtParserGatewayFilterFactory
+import gaf3.core.jpa.GafJpaConfiguration
+import org.slf4j.LoggerFactory
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.cloud.gateway.webflux.ProxyExchange
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.http.HttpStatus
+import org.springframework.http.server.reactive.ServerHttpResponse
+import org.springframework.stereotype.Controller
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.ResponseBody
+import reactor.core.publisher.Mono
+
+
+@SpringBootApplication
+@Configuration
+@Controller
+@Import(GafCloudConfiguration::class, GafJpaConfiguration::class)
+@ComponentScan(basePackages = ["gaf3.core.services"], basePackageClasses = [GatewayApplication::class])
+class GatewayApplication {
+
+    @RequestMapping(value = ["/loopback"])
+    @ResponseBody
+    fun loopback(res: ServerHttpResponse, proxy: ProxyExchange<ByteArray>): Mono<String> {
+        val url = proxy.path()
+        println(url)
+        res.statusCode = HttpStatus.UNAUTHORIZED
+        return Mono.just("No Authorization!")
+    }
+
+    @RequestMapping(value = ["/proxy/**"])
+    fun forward(res: ServerHttpResponse, proxy: ProxyExchange<ByteArray>): String {
+        val url = proxy.path("/proxy")
+        log.debug("forward to {}", url);
+        return "forward:$url";
+    }
+
+    companion object {
+        internal val log = LoggerFactory.getLogger(GatewayApplication::class.java)
+    }
+}
+
+fun main(args: Array<String>) {
+    runApplication<GatewayApplication>(*args)
+    JwtParserGatewayFilterFactory.doInit()
+}

+ 41 - 0
gateway/src/main/kotlin/gaf3/core/gateway/GatewayController.kt

@@ -0,0 +1,41 @@
+package gaf3.core.gateway
+
+import org.springframework.core.io.ClassPathResource
+import org.springframework.core.io.Resource
+import org.springframework.http.HttpStatus
+import org.springframework.http.server.reactive.ServerHttpResponse
+import org.springframework.stereotype.Controller
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.ResponseBody
+import reactor.core.publisher.Mono
+
+@Controller
+class GatewayController {
+    @RequestMapping(value = ["/401"])
+    @ResponseBody
+    fun status401(res: ServerHttpResponse): Mono<String> {
+        res.statusCode = HttpStatus.UNAUTHORIZED
+        return Mono.just("No Authorization!")
+    }
+
+    @GetMapping(path = ["/mock/menu/items"])
+    @ResponseBody
+    fun items(res: ServerHttpResponse): Resource {
+        res.headers["content-type"] = "text/plain; charset=utf-8"
+        return ClassPathResource("/menu/items_res.json")
+    }
+    @GetMapping(path = ["/mock/menu/menus"])
+    @ResponseBody
+    fun menus(res: ServerHttpResponse): Resource {
+        res.headers["content-type"] = "application/json; charset=utf-8"
+        return ClassPathResource("/menu/menus_res.json")
+    }
+    @GetMapping(path = ["/mock/code/{type}/items"])
+    @ResponseBody
+    fun codes(@PathVariable type: String, res: ServerHttpResponse): Resource {
+        res.headers["content-type"] = "text/plain; charset=utf-8"
+        return ClassPathResource("/code/${type}.json")
+    }
+}

+ 60 - 0
gateway/src/main/kotlin/gaf3/core/gateway/bean/FactoryBean.kt

@@ -0,0 +1,60 @@
+package gaf3.core.gateway.bean
+
+import gaf3.core.gateway.filter.factory.*
+import gaf3.core.gateway.handler.predicate.JwtRoutePredicateFactory
+import gaf3.core.services.verify.service.VerifyCodeService
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+@Configuration
+class FactoryBean {
+    @Bean
+    fun setRequestHeaderExGatewayFilterFactory(): SetRequestHeaderExGatewayFilterFactory {
+        return SetRequestHeaderExGatewayFilterFactory()
+    }
+
+    @Bean
+    fun jwtParserGatewayFilterFactory(): JwtParserGatewayFilterFactory {
+        return JwtParserGatewayFilterFactory()
+    }
+
+    @Bean
+    fun hostToTenantGatewayFilterFactory(): HostToTenantGatewayFilterFactory {
+        return HostToTenantGatewayFilterFactory()
+    }
+
+    @Bean
+    fun weixinTokenGatewayFilterFactory(): WeixinTokenGatewayFilterFactory {
+        return WeixinTokenGatewayFilterFactory()
+    }
+
+    @Bean
+    fun setRequestParameterGatewayFilterFactory(): SetRequestParameterGatewayFilterFactory {
+        return SetRequestParameterGatewayFilterFactory()
+    }
+
+    @Bean
+    fun forwardGatewayFilterFactory(): ForwardGatewayFilterFactory {
+        return ForwardGatewayFilterFactory()
+    }
+
+    @Bean
+    fun verifyCodeGatewayFilterFactory(service: VerifyCodeService): VerifyCodeGatewayFilterFactory {
+        return VerifyCodeGatewayFilterFactory(service)
+    }
+
+    @Bean
+    fun jwtRoutePredicateFactory(): JwtRoutePredicateFactory {
+        return JwtRoutePredicateFactory()
+    }
+
+    @Bean
+    fun peekResGatewayFilterFactory(): PeekResponseBodyGatewayFilterFactory {
+        return PeekResponseBodyGatewayFilterFactory()
+    }
+
+    @Bean
+    fun peekReqGatewayFilterFactory(): PeekRequestBodyGatewayFilterFactory {
+        return PeekRequestBodyGatewayFilterFactory()
+    }
+}

+ 59 - 0
gateway/src/main/kotlin/gaf3/core/gateway/bean/GlobalFilterBean.kt

@@ -0,0 +1,59 @@
+package gaf3.core.gateway.bean
+
+import org.slf4j.LoggerFactory
+import org.springframework.cloud.gateway.filter.GlobalFilter
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.annotation.Order
+import java.net.URI
+import java.util.regex.Pattern
+
+@Configuration
+class GlobalFilterBean {
+    //    @Bean
+    //    @Order(-1)
+    @Suppress("UseExpressionBody")
+    fun hostToApp(): GlobalFilter {
+        return GlobalFilter { exchange, chain ->
+            log.debug("Set X-App use Host")
+            val host = exchange.request.headers.getFirst("Host")
+            //			host = exchange.getRequest().getHeaders().getFirst("X-Forwarded-Host");
+            //			if(StringUtil.isNullOrEmpty(host))
+            //				host = exchange.getRequest().getHeaders().getFirst("Host");
+            val p = Pattern.compile("^([a-z0-9]+)\\.")
+            val m = p.matcher(host!!.toLowerCase())
+            if (!m.matches()) {
+                return@GlobalFilter chain.filter(exchange.mutate().build())
+            }
+            var app : String = m.group(1)
+            if (app !in arrayOf("hr", "wx", "plat")) {
+                app = "www" // 默认应用
+            }
+            val request = exchange.request.mutate()
+                    .headers { httpHeaders ->
+                        httpHeaders[HEADER_APP] = app
+                    }.build()
+            log.debug("Set X-Tenant use Host: {}", app)
+            chain.filter(exchange.mutate().request(request).build())
+        }
+    }
+
+    @Bean
+    @Order(-1)
+    fun debugLog(): GlobalFilter {
+        return GlobalFilter { exchange, chain ->
+            val header = exchange.request.headers[HEADER_APP] ?: listOf("")
+            val app1 = header[0]
+            log.debug("[debug log] Host from header: {}", app1)
+            val requestUrl = exchange.getRequiredAttribute<URI>(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR)
+            log.debug("[debug log] Uri from exchange: {}", requestUrl)
+            chain.filter(exchange)
+        }
+    }
+
+    companion object {
+        const val HEADER_APP = "X-App"
+        internal val log = LoggerFactory.getLogger(GlobalFilterBean::class.java)
+    }
+}

+ 90 - 0
gateway/src/main/kotlin/gaf3/core/gateway/bean/WebFilterBean.kt

@@ -0,0 +1,90 @@
+package gaf3.core.gateway.bean
+
+import org.slf4j.LoggerFactory
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.annotation.Order
+import org.springframework.http.HttpStatus
+import org.springframework.web.server.WebFilter
+
+@Configuration
+class WebFilterBean {
+//    @Bean
+//    @Order(-1)
+    fun apiAccessFilter(): WebFilter {
+        return WebFilter { exchange, chain ->
+            log.debug("[ApiAccess] start...")
+            val path = exchange.request.path.value()
+            log.debug("[ApiAccess] path : {}", path)
+            if(path.startsWith("/api")) {
+                chain.filter(exchange.mutate().build())
+            } else {
+                ServerWebExchangeUtils.setResponseStatus(exchange, HttpStatus.FORBIDDEN)
+                exchange.response.setComplete()
+            }
+        }
+    }
+
+//    @Bean
+//    @Order(-1)
+//    fun hostToAppFilter(): WebFilter {
+//        return WebFilter { exchange, chain ->
+//            log.debug("[HostToApp] start...")
+//            // val host = exchange.request.headers.getFirst("Host")
+//            var host = exchange.request.headers.getFirst("X-Forwarded-Host")
+//            if (host == null || StringUtils.isEmpty(host))
+//                host = exchange.request.headers.getFirst("Host")
+//            host = host!!.toLowerCase()
+//            val p = Pattern.compile("^([a-z0-9]+)\\.smart.*")
+//            val m = p.matcher(host)
+//            if (!m.matches()) {
+//                HostToAppFilter.log.debug("Not matches: {}", host)
+//                return@WebFilter chain.filter(exchange.mutate().build())
+//            }
+//            var app: String = m.group(1)
+//            if (app !in arrayOf("hr", "wx", "plat")) {
+//                app = "www" // 默认应用
+//            }
+//            val request = exchange.request.mutate()
+//                    .headers { httpHeaders ->
+//                        httpHeaders[HEADER_APP] = app
+//                    }.build()
+//            log.debug("[HostToApp] Set X-App : {}", app)
+//            chain.filter(exchange.mutate().request(request).build())
+//        }
+//    }
+
+    @Bean
+    @Order(-1000)
+    fun cleanHeaderFilter(): WebFilter {
+        return WebFilter { exchange, chain ->
+            val request = exchange.request.mutate()
+                    .headers { httpHeaders ->
+                        httpHeaders.remove(HEADER_APP)
+                        httpHeaders.remove(HEADER_ROLE)
+                        httpHeaders.remove(HEADER_TAGS)
+                        httpHeaders.remove(HEADER_USERID)
+                        httpHeaders.remove(HEADER_USERNAME)
+//                        httpHeaders.remove(HEADER_CORPID)
+//                        httpHeaders.remove(HEADER_CORPNAME)
+//                        httpHeaders.remove(HEADER_SCHID)
+//                        httpHeaders.remove(HEADER_SCHNAME)
+                    }.build()
+            log.debug("[CleanHeader] ok")
+            chain.filter(exchange.mutate().request(request).build())
+        }
+    }
+    companion object {
+        const val HEADER_APP = "X-App"
+        const val HEADER_ROLE = "X-Role"
+        const val HEADER_TAGS = "X-Tags"
+        const val HEADER_USERID = "X-UserID"
+        const val HEADER_USERNAME = "X-UserName"
+//        const val HEADER_CORPID = "X-CorpID"
+//        const val HEADER_CORPNAME = "X-CorpName"
+//        const val HEADER_SCHID = "X-SchID"
+//        const val HEADER_SCHNAME = "X-SchName"
+        internal val log = LoggerFactory.getLogger("GatewayWebFilter")
+    }
+}

+ 48 - 0
gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/ForwardGatewayFilterFactory.kt

@@ -0,0 +1,48 @@
+package gaf3.core.gateway.filter.factory
+
+import org.slf4j.LoggerFactory
+import org.springframework.cloud.gateway.filter.GatewayFilter
+import org.springframework.cloud.gateway.filter.OrderedGatewayFilter
+import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils
+import java.net.URI
+
+/**
+ * @author dyg
+ */
+class ForwardGatewayFilterFactory : AbstractGatewayFilterFactory<ForwardGatewayFilterFactory.Config>(Config::class.java) {
+
+    override fun shortcutFieldOrder(): List<String> {
+        return arrayListOf(PATH_KEY, ORDER_KEY)
+    }
+
+    override fun apply(config: Config): GatewayFilter {
+        return OrderedGatewayFilter({ exchange, chain ->
+            // 提取Path
+            val path = config.path ?: exchange.request.path
+            val to = "forward://$path"
+            val uri = URI(to)
+            exchange.attributes[ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR] = uri
+//            val exchange = exchange.mutate()
+//                    .request(exchange.request.mutate().path(to).build())
+//                    .build()
+
+            log.debug("Forward to: {}", path)
+            chain.filter(exchange)
+        }, config.order)
+    }
+
+    class Config {
+        var path: String? = null
+        var order = RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 1
+    }
+
+    companion object {
+
+        internal val log = LoggerFactory.getLogger(ForwardGatewayFilterFactory::class.java)
+
+        const val PATH_KEY = "path"
+        const val ORDER_KEY = "order"
+    }
+}

+ 63 - 0
gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/HostToTenantGatewayFilterFactory.kt

@@ -0,0 +1,63 @@
+package gaf3.core.gateway.filter.factory
+
+import org.slf4j.LoggerFactory
+import org.springframework.cloud.gateway.filter.GatewayFilter
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
+import org.springframework.core.style.ToStringCreator
+import org.springframework.validation.annotation.Validated
+import java.util.regex.Pattern
+import javax.validation.constraints.NotEmpty
+
+/**
+ * @author dyg
+ */
+class HostToTenantGatewayFilterFactory : AbstractGatewayFilterFactory<HostToTenantGatewayFilterFactory.Config>(Config::class.java) {
+
+    override fun shortcutFieldOrder(): List<String> {
+        return arrayListOf(PATTERN_KEY)
+    }
+
+    override fun apply(config: Config): GatewayFilter {
+        return GatewayFilter { exchange, chain ->
+            // 提取Host
+            val host = exchange.request.headers.getFirst("Host")
+            //			host = exchange.getRequest().getHeaders().getFirst("X-Forwarded-Host");
+            //			if(StringUtil.isNullOrEmpty(host))
+            //				host = exchange.getRequest().getHeaders().getFirst("Host");
+
+            val p = Pattern.compile(config.pattern!!)
+            val m = p.matcher(host!!)
+            if (m.matches()) {
+                val tenant = m.group(1)
+                val request = exchange.request.mutate()
+                        .headers { httpHeaders ->
+                            if (tenant != null)
+                                httpHeaders[HEADER_TENANT] = tenant
+                        }.build()
+                log.debug("Set X-Tenant use Host: {}", tenant)
+                return@GatewayFilter chain.filter(exchange.mutate().request(request).build())
+            }
+            chain.filter(exchange.mutate().build())
+        }
+    }
+
+    @Validated
+    class Config {
+        @NotEmpty
+        var pattern: String? = null
+
+        override fun toString(): String {
+            return ToStringCreator(this)
+                    .append("pattern", pattern)
+                    .toString()
+        }
+    }
+
+    companion object {
+
+        internal val log = LoggerFactory.getLogger(HostToTenantGatewayFilterFactory::class.java)
+
+        const val HEADER_TENANT = "X-Tenant"
+        const val PATTERN_KEY = "pattern"
+    }
+}

+ 184 - 0
gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/JwtParserGatewayFilterFactory.kt

@@ -0,0 +1,184 @@
+package gaf3.core.gateway.filter.factory
+
+import io.jsonwebtoken.ExpiredJwtException
+import io.jsonwebtoken.Jwts
+import io.jsonwebtoken.MalformedJwtException
+import io.jsonwebtoken.SignatureAlgorithm
+import io.jsonwebtoken.impl.crypto.MacSigner
+import io.jsonwebtoken.security.Keys
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.cloud.gateway.filter.GatewayFilter
+import org.springframework.cloud.gateway.filter.OrderedGatewayFilter
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setResponseStatus
+import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.util.StringUtils
+import java.io.UnsupportedEncodingException
+import java.net.URLEncoder
+import java.util.*
+
+/**
+ * 用户认证Token过滤器,通过Authorization或Cookie读取认证Jwt
+ * @author dyg
+ */
+class JwtParserGatewayFilterFactory : AbstractGatewayFilterFactory<JwtParserGatewayFilterFactory.Config>(Config::class.java) {
+
+    @Value("\${jwt.secret}")
+    private val secret: String? = null
+
+    override fun shortcutFieldOrder(): List<String> {
+        return arrayListOf(ISSUER_KEY, IGNORE_KEY, REDIRECT_KEY, ORDER_KEY)
+    }
+
+    override fun apply(config: Config): GatewayFilter {
+        return OrderedGatewayFilter( label@ { exchange, chain ->
+            // 检查是否忽略uri
+            val uri = exchange.request.uri
+            if (config.ignore != null && uri.path.matches(config.ignore!!.toRegex())) {
+                return@label chain.filter(exchange.mutate().build())
+            }
+            val url = exchange.request.uri.toString()
+            val method = exchange.request.methodValue
+
+            val accepts : List<MediaType>? = exchange.request.headers.accept
+            var isHtml = false
+            if (accepts!!.isNotEmpty()) {
+                val accept = exchange.request.headers.accept[0]
+                isHtml = accept.includes(MediaType.TEXT_HTML)
+            }
+
+            // 优先读取Head中的Authorization
+            val values = exchange.request.headers[HEADER_AUTH]
+            var token: String? = if (values != null && values.size > 0) values[0] else null
+            if (token != null && token.startsWith("Bearer ")) {
+                token = token.substring(7)
+            }
+
+            // 读取cookie中的token
+            if (token == null) {
+                val cookie = exchange.request.cookies.getFirst(COOKIE_TOKEN)
+                token = cookie?.value
+            }
+
+            if (token == null || StringUtils.isEmpty(token) || "null".equals(token, ignoreCase = true)
+                    || "undefined".equals(token, ignoreCase = true)) {
+                log.debug("Jwt not found [{} {}]", method, url)
+                token = null
+            }
+
+            if (token != null) {
+                try {
+                    val key = secret!!.toByteArray().copyOf(32)
+                    val parser = Jwts.parser().setSigningKey(key)
+                    if(config.issuer != null) {
+                        parser.requireIssuer(config.issuer)
+                    }
+                    val jws = parser.parseClaimsJws(token)
+
+                    val claims = jws.body
+                    exchange.attributes[JWT_CLAIMS_ATTRIBUTE] = claims
+                    log.debug("Jwt claims: {}", claims)
+                    val issuer : String? = claims.issuer
+                    val subject : String? = claims.subject
+                    val tokens = subject!!.split("@".toRegex(), 2).toTypedArray()
+                    // val userid = tokens[0]
+                    val tenant = if (tokens.size > 1) tokens[1] else null
+
+                    val request = exchange.request.mutate().headers { httpHeaders ->
+                        httpHeaders.set(HEADER_PLATFORM, issuer)
+                        httpHeaders.set(HEADER_USERID, subject)
+                        if (tenant != null)
+                            httpHeaders.set(HEADER_TENANT, tenant)
+                        if (claims.get("role", String::class.java) != null)
+                            httpHeaders.set(HEADER_ROLE, claims.get("role", String::class.java))
+                        val tags = claims.get("tags", ArrayList::class.java)
+                        if (tags != null) {
+                            httpHeaders.set(HEADER_TAGS, tags.joinToString(","))
+                        }
+                    }.build()
+                    return@label chain.filter(exchange.mutate().request(request).build())
+                } catch (ex: ExpiredJwtException) {
+                    log.warn("Jwt token expired [{} {}]", method, url)
+                } catch (ex: MalformedJwtException) {
+                    log.warn("Jwt token is invalid: MalformedJwtException [{} {}]", method, url)
+                    if (log.isDebugEnabled) {
+                        log.debug("token is: {}", token)
+                        ex.printStackTrace()
+                    }
+                }
+
+            }
+
+            if (config.redirect != null && isHtml) {
+                // 重定向到授权地址
+                var redirect_uri = config.redirect
+                redirect_uri += when ("?" in redirect_uri!!) {
+                    true -> "&"
+                    false -> "?"
+                }
+                redirect_uri += try {
+                    println(uri.toString())
+                    "redirect_uri=" + URLEncoder.encode(uri.toString(), "UTF-8")
+                } catch (e: UnsupportedEncodingException) {
+                    log.warn("URL编码错误", e)
+                    "redirect_uri=$uri"
+                }
+
+                setResponseStatus(exchange, HttpStatus.TEMPORARY_REDIRECT)
+                val response = exchange.response
+                response.headers.set(HttpHeaders.LOCATION, redirect_uri)
+            } else {
+                setResponseStatus(exchange, HttpStatus.UNAUTHORIZED)
+            }
+
+            exchange.response.setComplete()
+        }, config.order)
+
+    }
+
+    // @Validated
+    class Config {
+        // @NotEmpty
+        var ignore: String? = null
+        // @NotEmpty
+        var redirect: String? = null
+        var order = -1
+        var issuer: String? = null
+
+    }
+
+    companion object {
+
+        internal val log = LoggerFactory.getLogger(JwtParserGatewayFilterFactory::class.java)
+
+        const val HEADER_AUTH = "Authorization"
+        const val HEADER_TENANT = "X-Tenant"
+        const val HEADER_USERID = "X-Userid"
+        const val HEADER_ROLE = "X-Role"
+        const val HEADER_TAGS = "X-Tags"
+        const val HEADER_PLATFORM = "X-Platform"
+        const val JWT_CLAIMS_ATTRIBUTE = "NafGateway.jwtClaims"
+        const val COOKIE_TOKEN = "token"
+
+        const val IGNORE_KEY = "ignore"
+        const val REDIRECT_KEY = "redirect"
+        const val ORDER_KEY = "order"
+        const val ISSUER_KEY = "issuer"
+
+        fun doInit() {
+            @Suppress("SpellCheckingInspection")
+            val secret = "12345678"
+            val key = secret.toByteArray().copyOf(32)
+            MacSigner(SignatureAlgorithm.HS256, key) // 这个操作比较耗时
+
+            val cal = Calendar.getInstance()
+            cal.add(Calendar.HOUR_OF_DAY, 1)
+            val signed = Jwts.builder().setIssuer("master").setSubject("platform").setExpiration(cal.time)
+                    .claim("userid", "admin").claim("name", "管理员").signWith(Keys.hmacShaKeyFor(key)).compact()
+            Jwts.parser().setSigningKey(key).parseClaimsJws(signed)
+        }
+    }
+}

+ 96 - 0
gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/PeekRequestBodyGatewayFilterFactory.kt

@@ -0,0 +1,96 @@
+package gaf3.core.gateway.filter.factory
+
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.cloud.gateway.filter.GatewayFilter
+import org.springframework.cloud.gateway.filter.GatewayFilterChain
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
+import org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType
+import org.springframework.core.Ordered
+import org.springframework.core.io.buffer.DataBuffer
+import org.springframework.http.MediaType
+import org.springframework.http.server.reactive.ServerHttpRequestDecorator
+import org.springframework.web.server.ServerWebExchange
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.nio.charset.Charset
+
+
+class PeekRequestBodyGatewayFilterFactory
+    : AbstractGatewayFilterFactory<PeekRequestBodyGatewayFilterFactory.Config>(Config::class.java) {
+
+    @Value("\${gateway.request.body.peek-size:2000}")
+    var peekSize: Int = 2000
+
+    override fun shortcutFieldOrder(): List<String> {
+        return arrayListOf(MEDIA_TYPES_KEY)
+    }
+
+    override fun shortcutType(): ShortcutType {
+        return ShortcutType.GATHER_LIST
+    }
+
+    override fun apply(factoryConfig: Config): GatewayFilter {
+        return PeekRequestGatewayFilter(factoryConfig)
+    }
+
+    class Config { //Put the configuration properties for your filter here
+        var mediaTypes: Array<String>? = null
+            get() = if (field?.size ?: 0 > 0)
+                field
+            else
+                arrayOf(MediaType.APPLICATION_JSON_VALUE)
+    }
+
+    inner class PeekRequestGatewayFilter(private val factoryConfig: Config) : GatewayFilter, Ordered {
+        override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
+
+            return chain.filter(exchange.mutate()
+                    .request(PeekedServerHttpRequest(exchange, factoryConfig))
+                    .build())
+                    .then(Mono.fromRunnable {
+                        log.debug("content-type: {}", exchange.request.headers.contentType)
+                        val snapshot = exchange.attributes[ORIGINAL_REQUEST_BODY_SNAPSHOT] as? String
+                        log.debug("peek request body snapshot:\n{}", snapshot)
+                    })
+        }
+
+        override fun getOrder(): Int {
+            return -1
+        }
+    }
+
+    inner class PeekedServerHttpRequest(private val exchange: ServerWebExchange, private val factoryConfig: Config) : ServerHttpRequestDecorator(exchange.request) {
+
+        override fun getBody(): Flux<DataBuffer> {
+            val flux = Flux.from(delegate.body)
+                    .replay()
+                    .autoConnect()
+
+            flux.take(1).subscribe { buffer: DataBuffer ->
+                val contentType: MediaType? = headers.contentType
+                val supported = factoryConfig.mediaTypes?.any { mediaType ->
+                    runCatching {
+                        MediaType.parseMediaType(mediaType).isCompatibleWith(contentType)
+                    }.getOrElse { err ->
+                        log.warn("Invalid Media Type: $mediaType", err)
+                        false
+                    }
+                } ?: false
+                if(supported) {
+                    val snapshot = buffer.toString(Charset.defaultCharset()).take(peekSize)
+                    exchange.attributes[ORIGINAL_REQUEST_BODY_SNAPSHOT] = snapshot
+                }
+            }
+            return flux
+        }
+
+    }
+
+    companion object {
+        const val MEDIA_TYPES_KEY = "mediaTypes"
+        const val ORIGINAL_REQUEST_BODY_SNAPSHOT = "ORIGINAL_REQUEST_BODY_SNAPSHOT"
+
+        internal val log = LoggerFactory.getLogger(PeekRequestBodyGatewayFilterFactory::class.java)
+    }
+}

+ 110 - 0
gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/PeekResponseBodyGatewayFilterFactory.kt

@@ -0,0 +1,110 @@
+package gaf3.core.gateway.filter.factory
+
+import org.reactivestreams.Publisher
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.cloud.gateway.filter.GatewayFilter
+import org.springframework.cloud.gateway.filter.GatewayFilterChain
+import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR
+import org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType
+import org.springframework.core.Ordered
+import org.springframework.core.io.buffer.DataBuffer
+import org.springframework.http.HttpHeaders
+import org.springframework.http.MediaType
+import org.springframework.http.server.reactive.ServerHttpResponseDecorator
+import org.springframework.web.server.ServerWebExchange
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.nio.charset.Charset
+
+
+class PeekResponseBodyGatewayFilterFactory
+    : AbstractGatewayFilterFactory<PeekResponseBodyGatewayFilterFactory.Config>(Config::class.java) {
+
+    @Value("\${gateway.response.body.peek-size:2000}")
+    var peekSize: Int = 2000
+
+    override fun shortcutFieldOrder(): List<String> {
+        return arrayListOf(MEDIA_TYPES_KEY)
+    }
+
+    override fun shortcutType(): ShortcutType {
+        return ShortcutType.GATHER_LIST
+    }
+
+    override fun apply(factoryConfig: Config): GatewayFilter {
+        return PeekResponseGatewayFilter(factoryConfig)
+    }
+
+    class Config { //Put the configuration properties for your filter here
+        var mediaTypes: Array<String>? = null
+            get() = if(field?.size ?: 0 > 0)
+                field
+            else
+                arrayOf(MediaType.APPLICATION_JSON_VALUE,
+                        MediaType.APPLICATION_XML_VALUE,
+                        MediaType.TEXT_HTML_VALUE,
+                        MediaType.TEXT_MARKDOWN_VALUE,
+                        MediaType.TEXT_PLAIN_VALUE,
+                        MediaType.TEXT_XML_VALUE)
+    }
+
+    inner class PeekResponseGatewayFilter(private val factoryConfig: Config): GatewayFilter, Ordered {
+        override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
+
+            return chain.filter(exchange.mutate()
+                    .request { it.header(HttpHeaders.ACCEPT_ENCODING, "identity").build() }
+                    .response(PeekedServerHttpResponse(exchange, factoryConfig)).build())
+                    .then(Mono.fromRunnable {
+                        log.debug("content-type: {}", exchange.response.headers.contentType)
+                        val snapshot = exchange.attributes[ORIGINAL_RESPONSE_BODY_SNAPSHOT] as? String
+                        log.debug("peek response body snapshot:\n{}", snapshot)
+                    })
+        }
+
+        override fun getOrder(): Int {
+            return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1
+        }
+    }
+
+    inner class PeekedServerHttpResponse(private val exchange: ServerWebExchange, private val factoryConfig: Config) : ServerHttpResponseDecorator(exchange.response) {
+
+        override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
+            val flux = Flux.from(body)
+                    .replay()
+                    .autoConnect()
+
+            flux.take(1).subscribe { buffer: DataBuffer ->
+                val contentType: MediaType? = exchange.response.headers.contentType ?:
+                    exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR)
+                val supported = factoryConfig.mediaTypes?.any { mediaType ->
+                    runCatching {
+                        MediaType.parseMediaType(mediaType).isCompatibleWith(contentType)
+                    }.getOrElse { err ->
+                        log.warn("Invalid Media Type: $mediaType", err)
+                        false
+                    }
+                } ?: false
+                if(supported) {
+                    val snapshot = buffer.toString(Charset.defaultCharset()).take(peekSize)
+                    exchange.attributes[ORIGINAL_RESPONSE_BODY_SNAPSHOT] = snapshot
+                }
+            }
+            return delegate.writeWith(flux)
+        }
+
+        override fun writeAndFlushWith(
+                body: Publisher<out Publisher<out DataBuffer>>): Mono<Void> {
+            return writeWith(Flux.from(body).flatMapSequential { p: Publisher<out DataBuffer>? -> p })
+        }
+    }
+
+    companion object {
+        const val MEDIA_TYPES_KEY = "mediaTypes"
+        const val ORIGINAL_RESPONSE_BODY_SNAPSHOT = "ORIGINAL_RESPONSE_BODY_SNAPSHOT"
+
+        internal val log = LoggerFactory.getLogger(PeekResponseBodyGatewayFilterFactory::class.java)
+    }
+}

+ 39 - 0
gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/SetRequestHeaderExGatewayFilterFactory.kt

@@ -0,0 +1,39 @@
+package gaf3.core.gateway.filter.factory
+
+import org.springframework.cloud.gateway.filter.GatewayFilter
+import org.springframework.cloud.gateway.filter.GatewayFilterChain
+import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE
+import org.springframework.web.server.ServerWebExchange
+import org.springframework.web.util.UriTemplate
+import org.springframework.web.util.pattern.PathPattern
+
+/**
+ * @author dyg
+ */
+class SetRequestHeaderExGatewayFilterFactory : AbstractNameValueGatewayFilterFactory() {
+
+    override fun apply(config: NameValueConfig): GatewayFilter {
+        return GatewayFilter { exchange: ServerWebExchange, chain: GatewayFilterChain ->
+            val uriTemplate = UriTemplate(config.value)
+
+            val variables: PathPattern.PathMatchInfo? = exchange.getAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE)
+            val uriVariables: Map<String, String>
+
+            if (variables != null) {
+                uriVariables = variables.uriVariables
+            } else {
+                uriVariables = emptyMap()
+            }
+
+            val uri = uriTemplate.expand(uriVariables)
+            val newPath = uri.rawPath
+
+            val request = exchange.request.mutate()
+                    .headers { httpHeaders -> if (newPath != null) httpHeaders[config.name] = newPath }
+                    .build()
+
+            chain.filter(exchange.mutate().request(request).build())
+        }
+    }
+}

+ 93 - 0
gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/SetRequestParameterGatewayFilterFactory.kt

@@ -0,0 +1,93 @@
+package gaf3.core.gateway.filter.factory
+
+import io.jsonwebtoken.Claims
+import org.slf4j.LoggerFactory
+import org.springframework.cloud.gateway.filter.GatewayFilter
+import org.springframework.cloud.gateway.filter.GatewayFilterChain
+import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory
+import org.springframework.web.server.ServerWebExchange
+import org.springframework.web.util.UriComponentsBuilder
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+
+/**
+ * @author dyg
+ */
+class SetRequestParameterGatewayFilterFactory : AbstractNameValueGatewayFilterFactory() {
+
+    override fun apply(config: NameValueConfig): GatewayFilter {
+        return GatewayFilter { exchange: ServerWebExchange, chain: GatewayFilterChain ->
+            val uri = exchange.request.uri
+            var query: String? = uri.rawQuery
+            if (query != null) {
+                // 清空已有参数
+                val regex = String.format("(?i)%s=([^&]*)", config.name)
+                query = query.replace(regex.toRegex(), "").replace("&&".toRegex(), "&")
+                if (query.startsWith("&")) {
+                    query = query.substring(1)
+                } else if (query.endsWith("&")) {
+                    query = query.substring(0, query.length - 1)
+                }
+            } else {
+                query = ""
+            }
+
+            // 处理参数值, 支持Jwt Claim变量 {jwt:xxx}
+            var value: String? = config.value
+            if (value != null && value.isNotEmpty()) {
+                value = patternReplace(value, P_JWTVAR,
+                        exchange.attributes[JwtParserGatewayFilterFactory.JWT_CLAIMS_ATTRIBUTE] as Claims?)
+                value = patternReplace(value, P_WXVAR,
+                        exchange.attributes[WeixinTokenGatewayFilterFactory.WEIXIN_CLAIMS_ATTRIBUTE] as Claims?)
+            }
+
+            // 生成新的query
+            var part = ""
+            if (value != null && value.isNotEmpty()) {
+                part = String.format("%s=%s", config.name, value)
+            }
+            if (part.isNotEmpty() && query.isNotEmpty()) {
+                query += "&$part"
+            } else if (query.isEmpty()) {
+                query = part
+            }
+
+            try {
+                val newUri = UriComponentsBuilder.fromUri(uri).replaceQuery(query).build(true).toUri()
+
+                val request = exchange.request.mutate().uri(newUri).build()
+
+                return@GatewayFilter chain.filter(exchange.mutate().request(request).build())
+            } catch (ex: RuntimeException) {
+                throw IllegalStateException("Invalid URI query: \"$query\"")
+            }
+        }
+    }
+
+    private fun patternReplace(value: String?, p: Pattern, claims: Claims?): String? {
+        if(value == null) return value
+        val m: Matcher = p.matcher(value)
+        if (m.matches()) {
+            val str = try {
+                val key: String = m.group(1)
+                claims?.let { it[key, String::class.java] }
+            } catch (e: Throwable) {
+                log.warn("extract {} fail: {}", value, e.message)
+                if (log.isDebugEnabled) e.printStackTrace()
+                null
+            } ?: ""
+            return value.replace(p.toRegex(), str)
+        }
+        return value
+    }
+
+    @Suppress("SpellCheckingInspection")
+    companion object {
+
+        internal val log = LoggerFactory.getLogger(SetRequestParameterGatewayFilterFactory::class.java)
+        internal val P_JWTVAR = Pattern.compile("\\{jwt:(.*)}")
+        internal val P_WXVAR = Pattern.compile("\\{wx:(.*)}")
+    }
+
+}

+ 41 - 0
gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/VerifyCodeGatewayFilterFactory.kt

@@ -0,0 +1,41 @@
+package gaf3.core.gateway.filter.factory
+
+import gaf3.core.exception.BusinessError
+import gaf3.core.services.verify.service.VerifyCodeService
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.configurationprocessor.json.JSONObject
+import org.springframework.cloud.gateway.filter.GatewayFilter
+import org.springframework.cloud.gateway.filter.OrderedGatewayFilter
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
+import org.springframework.http.HttpStatus
+import reactor.core.publisher.Mono
+import java.awt.image.DataBuffer
+import java.nio.charset.StandardCharsets
+
+class VerifyCodeGatewayFilterFactory(@Autowired val service: VerifyCodeService) : AbstractGatewayFilterFactory<VerifyCodeGatewayFilterFactory.Config>(Config::class.java) {
+
+    override fun apply(config: Config): GatewayFilter {
+        return OrderedGatewayFilter({ exchange, chain ->
+            val queryParams = exchange.request.queryParams
+            val state = queryParams["state"]?.firstOrNull()
+            val code = queryParams["code"]?.firstOrNull()
+            if(state.isNullOrBlank() || code.isNullOrBlank() || !service.verify(state, code)) {
+                exchange.response.statusCode = HttpStatus.OK
+                var msg = JSONObject()
+                msg.put("errcode", BusinessError.ERR_VERIFYCODE_INVALID)
+                msg.put("errmsg", "验证码无效")
+                var bits: ByteArray = msg.toString().toByteArray(StandardCharsets.UTF_8)
+                var buffer = exchange.response.bufferFactory().wrap(bits)
+                exchange.response.headers.add("Content-Type", "application/json;charset=UTF-8")
+                exchange.response.setStatusCode(HttpStatus.BAD_REQUEST)
+                exchange.response.writeWith(Mono.just(buffer))
+                // exchange.response.setComplete()
+            }else
+                chain.filter(exchange)
+        }, config.order)
+    }
+
+    class Config {
+        var order: Int = 0
+    }
+}

+ 128 - 0
gateway/src/main/kotlin/gaf3/core/gateway/filter/factory/WeixinTokenGatewayFilterFactory.kt

@@ -0,0 +1,128 @@
+package gaf3.core.gateway.filter.factory
+
+import io.jsonwebtoken.ExpiredJwtException
+import io.jsonwebtoken.Jwts
+import io.jsonwebtoken.MalformedJwtException
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.cloud.gateway.filter.GatewayFilter
+import org.springframework.cloud.gateway.filter.OrderedGatewayFilter
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setResponseStatus
+import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.util.StringUtils
+import java.io.UnsupportedEncodingException
+import java.net.URLEncoder
+
+/**
+ * 微信认证Token过滤器,通过Cookie读取认证Jwt
+ * @author dyg
+ */
+class WeixinTokenGatewayFilterFactory : AbstractGatewayFilterFactory<JwtParserGatewayFilterFactory.Config>(JwtParserGatewayFilterFactory.Config::class.java) {
+
+    @Value("\${jwt.secret}")
+    private val secret: String? = null
+
+    override fun shortcutFieldOrder(): List<String> {
+        return arrayListOf(REDIRECT_KEY, IGNORE_KEY, ORDER_KEY)
+    }
+
+    override fun apply(config: JwtParserGatewayFilterFactory.Config): GatewayFilter {
+        return OrderedGatewayFilter(label@{ exchange, chain ->
+            // 检查是否忽略uri
+            val uri = exchange.request.uri
+            if (config.ignore != null && uri.path.matches(config.ignore!!.toRegex())) {
+                return@label chain.filter(exchange.mutate().build())
+            }
+            val url = exchange.request.uri.toString()
+            val method = exchange.request.methodValue
+
+            val accepts: List<MediaType>? = exchange.request.headers.accept
+            var isHtml = false
+            if (accepts != null && accepts.isNotEmpty()) {
+                val accept = exchange.request.headers.accept[0]
+                isHtml = accept.includes(MediaType.TEXT_HTML)
+            }
+
+            val cookie = exchange.request.cookies.getFirst(COOKIE_TOKEN)
+            var token: String? = cookie?.value
+            if (token == null || StringUtils.isEmpty(token)
+                    || "null".equals(token, ignoreCase = true) || "undefined".equals(token, ignoreCase = true)) {
+                log.warn("Weixin Jwt not found [{} {}]", method, url)
+                token = null
+                // throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED,
+                // "用户未认证");
+            }
+
+            if (token != null) {
+                try {
+                    val key = secret!!.toByteArray().copyOf(32)
+                    val jws = Jwts.parser().requireIssuer("weixin")
+                            .setSigningKey(key).parseClaimsJws(token)
+
+                    val claims = jws.body
+                    exchange.attributes[WEIXIN_CLAIMS_ATTRIBUTE] = claims
+                    log.debug("Weixin claims: {}", claims)
+                    val subject = claims.subject
+                    val issuer = claims.issuer
+                    if ("weixin".equals(issuer, ignoreCase = true)) {
+                        val request = exchange.request.mutate().headers { httpHeaders -> httpHeaders.set(HEADER_OPENID, subject) }.build()
+                        return@label chain.filter(exchange.mutate().request(request).build())
+                    } else {
+                        log.warn("Jwt issuer invalid: {} [{} {}]", issuer, method, url)
+                    }
+                } catch (ex: ExpiredJwtException) {
+                    log.warn("Weixin Jwt expired [{} {}]", method, url)
+                } catch (ex: MalformedJwtException) {
+                    log.warn("Weixin Jwt is invalid: MalformedJwtException [{} {}]", method, url)
+                    if (log.isDebugEnabled) {
+                        log.debug(token)
+                        ex.printStackTrace()
+                    }
+                }
+
+            }
+
+            if (config.redirect != null && isHtml) {
+                // 重定向到授权地址
+                var redirect_uri = config.redirect
+                redirect_uri += when (redirect_uri!!.contains("?")) {
+                    true -> "&"
+                    false -> "?"
+                }
+                redirect_uri += try {
+                    println(uri.toString())
+                    "redirect_uri=" + URLEncoder.encode(uri.toString(), "UTF-8")
+                } catch (e: UnsupportedEncodingException) {
+                    log.warn("URL编码错误", e)
+                    "redirect_uri=$uri"
+                }
+
+                setResponseStatus(exchange, HttpStatus.TEMPORARY_REDIRECT)
+                val response = exchange.response
+                response.headers.set(HttpHeaders.LOCATION, redirect_uri)
+            } else {
+                setResponseStatus(exchange, HttpStatus.UNAUTHORIZED)
+            }
+
+            exchange.response.setComplete()
+        }, config.order)
+
+    }
+
+    companion object {
+
+        internal val log = LoggerFactory.getLogger(WeixinTokenGatewayFilterFactory::class.java)
+
+        const val COOKIE_TOKEN = "wxtoken"
+        const val HEADER_OPENID = "X-OpenID"
+        const val WEIXIN_CLAIMS_ATTRIBUTE = "NafGateway.weixinClaims"
+
+        const val IGNORE_KEY = "ignore"
+        const val REDIRECT_KEY = "redirect"
+        const val ORDER_KEY = "order"
+    }
+
+}

+ 23 - 0
gateway/src/main/kotlin/gaf3/core/gateway/handler/predicate/ExcludesRoutePredicateFactory.kt

@@ -0,0 +1,23 @@
+package gaf3.core.gateway.handler.predicate
+
+import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate
+import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
+import org.springframework.web.server.ServerWebExchange
+import java.util.function.Predicate
+
+class ExcludesRoutePredicateFactory: PathRoutePredicateFactory() {
+
+    override fun apply(config: Config): Predicate<ServerWebExchange> {
+        val p: Predicate<ServerWebExchange> = super.apply(config)
+        return object : GatewayPredicate {
+            override fun test(exchange: ServerWebExchange): Boolean {
+                return !p.test(exchange)
+            }
+
+            override fun toString(): String {
+                return String.format("Excludes: %s, match trailing slash: %b",
+                        config.patterns, config.isMatchOptionalTrailingSeparator)
+            }
+        }
+    }
+}

+ 101 - 0
gateway/src/main/kotlin/gaf3/core/gateway/handler/predicate/JwtRoutePredicateFactory.kt

@@ -0,0 +1,101 @@
+package gaf3.core.gateway.handler.predicate
+
+import io.jsonwebtoken.Claims
+import io.jsonwebtoken.ExpiredJwtException
+import io.jsonwebtoken.Jwts
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory
+import org.springframework.util.StringUtils
+import org.springframework.web.server.ServerWebExchange
+import java.util.function.Predicate
+
+/**
+ * @author dyg
+ */
+@Suppress("unused")
+class JwtRoutePredicateFactory : AbstractRoutePredicateFactory<JwtRoutePredicateFactory.Config>(Config::class.java) {
+
+    @Value("\${jwt.secret}")
+    private val secret: String? = null
+
+    override fun shortcutFieldOrder(): List<String> {
+        return arrayListOf(NAME_KEY, VALUE_KEY)
+    }
+
+    override fun apply(config: Config): Predicate<ServerWebExchange> {
+        return Predicate { exchange: ServerWebExchange ->
+
+            val url = exchange.request.uri.toString()
+            val method = exchange.request.methodValue
+            var claims = exchange.attributes[JWT_CLAIMS_ATTRIBUTE] as Claims?
+            val failure = exchange.attributes[JWT_PARSE_FAILURE] as Boolean?
+            if(failure == true) {
+                return@Predicate false
+            }
+
+            if (claims == null) {
+                val values = exchange.request.headers[HEADER_KEY]
+                if (values == null || values.size == 0) {
+                    return@Predicate config.name == null
+                }
+                var token: String = values[0] ?: ""
+                if (token.startsWith("Bearer ")) {
+                    token = token.substring(7)
+                }
+                if (StringUtils.isEmpty(token) || "null".equals(token, ignoreCase = true) || "undefined".equals(token, ignoreCase = true)) {
+                    log.debug("Jwt not found [{} {}]", method, url)
+                    return@Predicate false
+                }
+                try {
+                    val key = secret!!.toByteArray().copyOf(32)
+                    val jws = Jwts.parser().setSigningKey(key)
+                            .parseClaimsJws(token)
+
+                    claims = jws.body
+                } catch (ex: ExpiredJwtException) {
+                    log.warn("Jwt token expired [{} {}]", method, url)
+                    exchange.attributes[JWT_PARSE_FAILURE] = true
+                    return@Predicate false
+                } catch (ex: Throwable) {
+                    log.warn("Jwt token is invalid: {} [{} {}]", ex.message, method, url)
+                    if (log.isDebugEnabled) {
+                        log.debug("token is: {}", token)
+                        ex.printStackTrace()
+                    }
+                    exchange.attributes[JWT_PARSE_FAILURE] = true
+                    return@Predicate false
+                }
+                exchange.attributes[JWT_CLAIMS_ATTRIBUTE] = claims
+            }
+
+            when {
+                config.name == null || config.value == null -> false
+                "issuer".equals(config.name!!, ignoreCase = true) -> config.value!!.equals(claims?.issuer
+                        ?: "", ignoreCase = true)
+                "subject".equals(config.name!!, ignoreCase = true) -> config.value!!.equals(claims?.subject
+                        ?: "", ignoreCase = true)
+                else -> {
+                    val `val` = claims?.get(config.name) as String?
+                    `val` != null && `val`.equals(config.value!!, ignoreCase = true)
+                }
+            }
+        }
+    }
+
+    class Config {
+        var name: String? = null
+        var value: String? = null
+    }
+
+    companion object {
+        internal val log = LoggerFactory.getLogger(JwtRoutePredicateFactory::class.java)
+
+        const val NAME_KEY = "name"
+        const val VALUE_KEY = "value"
+        const val HEADER_KEY = "Authorization"
+        const val JWT_CLAIMS_ATTRIBUTE = "NafGateway.jwtClaims"
+        const val JWT_PARSE_FAILURE = "NafGateway.jwtFailure"
+    }
+
+}

+ 31 - 0
gateway/src/main/kotlin/gaf3/core/gateway/webfilter/ApiAccessFilter.kt

@@ -0,0 +1,31 @@
+package gaf3.core.gateway.webfilter
+
+import org.slf4j.LoggerFactory
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils
+import org.springframework.http.HttpStatus
+import org.springframework.web.server.ServerWebExchange
+import org.springframework.web.server.WebFilter
+import org.springframework.web.server.WebFilterChain
+import reactor.core.publisher.Mono
+
+class ApiAccessFilter @JvmOverloads constructor(val includes: Array<String> = emptyArray(), val excludes: Array<String> = emptyArray()): WebFilter {
+    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain) : Mono<Void>{
+        log.debug("[ApiAccess] start...")
+        val path = exchange.request.path.value()
+        log.debug("[ApiAccess] path : {}", path)
+        return if(path.startsWith("/api")) {
+            chain.filter(exchange.mutate().build())
+        } else if ((includes.isNotEmpty() && !includes.any { path.startsWith(it) })
+                || excludes.any { path.startsWith(it) }) {
+            chain.filter(exchange.mutate().build())
+        } else {
+            ServerWebExchangeUtils.setResponseStatus(exchange, HttpStatus.FORBIDDEN)
+            exchange.response.setComplete()
+        }
+    }
+
+    companion object {
+        internal val log = LoggerFactory.getLogger(ApiAccessFilter::class.java)
+    }
+
+}

+ 42 - 0
gateway/src/main/kotlin/gaf3/core/gateway/webfilter/HostToAppFilter.kt

@@ -0,0 +1,42 @@
+package gaf3.core.gateway.webfilter
+
+import org.slf4j.LoggerFactory
+import org.springframework.web.server.ServerWebExchange
+import org.springframework.web.server.WebFilter
+import org.springframework.web.server.WebFilterChain
+import reactor.core.publisher.Mono
+import gaf3.core.gateway.bean.GlobalFilterBean
+import java.util.regex.Pattern
+
+class HostToAppFilter: WebFilter {
+    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain) : Mono<Void>{
+        log.debug("Set X-App use Host")
+        val host = exchange.request.headers.getFirst("Host")
+        //			host = exchange.getRequest().getHeaders().getFirst("X-Forwarded-Host");
+        //			if(StringUtil.isNullOrEmpty(host))
+        //				host = exchange.getRequest().getHeaders().getFirst("Host");
+        val p = Pattern.compile("^([a-z0-9]+)\\.smart.*")
+        val m = p.matcher(host!!.toLowerCase())
+        if (!m.matches()) {
+            log.debug("Not matches: {}", host.toLowerCase())
+            return chain.filter(exchange.mutate().build())
+        }
+        var app : String = m.group(1)
+        if (app !in arrayOf("hr", "wx", "plat")) {
+            app = "www" // 默认应用
+        }
+        exchange.attributes[GlobalFilterBean.HEADER_APP] = app
+
+        val request = exchange.request.mutate()
+                .headers { httpHeaders ->
+                    httpHeaders[GlobalFilterBean.HEADER_APP] = app
+                }.build()
+        log.debug("Set X-Tenant : {}", app)
+        return chain.filter(exchange.mutate().request(request).build())
+    }
+
+    companion object {
+        internal val log = LoggerFactory.getLogger(HostToAppFilter::class.java)
+    }
+
+}

+ 47 - 0
gateway/src/main/resources/application-routes.yml

@@ -0,0 +1,47 @@
+# 默认路由规则
+---
+
+spring:
+  profiles: routes
+  cloud:
+    gateway:
+      default-filters:
+        - AddResponseHeader=Cache-Control, no-cache
+        - AddResponseHeader=Pragma, no-cache
+        - AddResponseHeader=Expires, -1
+        - PeekRequestBody
+      routes:
+        # 验证码接口
+        - id: gaf_verify
+          uri: ${uri.gaf}
+          predicates:
+            - Path=/api/gaf/verify/**
+          filters:
+            - RewritePath=/api/gaf/(?<segment>.*), /gaf/$\{segment}
+            - Forward
+        # 登录接口验证
+        - id: login_verify
+          uri: ${uri.xms}
+          predicates:
+            - Path=/api/gaf/verify/**
+          filters:
+            - VerifyCode
+            - RewritePath=/api/gaf/(?<segment>.*), /gaf/$\{segment}
+        # API接口
+        - id: xms_api
+          uri: ${uri.xms}
+          predicates:
+            - Path=/api/xms/**
+            - Jwt=issuer, gaf
+          filters:
+            - RewritePath=/api/(?<segment>.*), /$\{segment}
+        # == 默认处理 ==
+        - id: api_default
+          uri: forward:///401 # default for unauthorized
+          order: 1000
+          predicates:
+            - Path=/api/**
+          filters:
+            - SetStatus=401
+
+

+ 7 - 0
gateway/src/main/resources/application-services.yml

@@ -0,0 +1,7 @@
+# 服务配置
+---
+gaf:
+  auth:
+    jwt-secret: "GafJwtSecret!@#"
+    jwt-issuer: gaf
+    jwt-validity: 1h

+ 45 - 0
gateway/src/main/resources/application.yml

@@ -0,0 +1,45 @@
+# 全局配置
+#spring:
+#  resources:
+#    static-locations: file:www/,classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
+jwt.secret: "GafJwtSecret!@#"
+api.host: 127.0.0.1
+uri:
+  gaf: http://${api.host}:8001
+  xms: http://${api.host}:9080
+
+server:
+  port: 18070
+gaf.cache:
+  verify:
+    useRedis: false
+    validity: 300s
+  token:
+    useRedis: false
+    validity: 5h
+
+spring:
+  profiles:
+    active: local
+
+---
+spring:
+  profiles: local
+  profiles.include: routes, services
+
+api.host: 172.17.116.7 #192.168.1.170
+
+logging.level.gaf3.core.*: DEBUG
+
+---
+spring:
+  profiles: dev
+  profiles.include: routes, services, log
+
+logging.level.gaf3.core.*: DEBUG
+#debug: true
+
+---
+spring:
+  profiles: prod
+  profiles.include: routes, services, log

+ 7 - 0
gradle.properties

@@ -0,0 +1,7 @@
+dbDialect = org.hibernate.dialect.MySQL5Dialect
+# dbUrl = jdbc:mysql://localhost:3306/gaf3?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
+dbUrl = jdbc:mysql://172.17.116.7:3308/xms?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
+dbUser = root
+dbPwd = 123456
+dbDriver = com.mysql.cj.jdbc.Driver
+systemProp.repoPath = /var/repo

BIN
gradle/wrapper/gradle-wrapper.jar


+ 5 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 185 - 0
gradlew

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 89 - 0
gradlew.bat

@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

BIN
libs/oscarHibernate5.jar


BIN
libs/oscarJDBC16.jar


+ 20 - 0
platform/build.gradle.kts

@@ -0,0 +1,20 @@
+plugins {
+    `java-platform`
+}
+javaPlatform.allowDependencies()
+
+dependencies {
+    // The platform declares constraints on all components that
+
+    // require alignment
+    constraints {
+
+        api("org.bouncycastle:bcprov-jdk15on:1.69")
+        api("org.bouncycastle:bcpkix-jdk15on:1.69")
+        api("com.alibaba:fastjson:1.2.72")
+        api("commons-io:commons-io:2.6")
+        api("io.jsonwebtoken:jjwt-api:${property("jjwtVersion")}")
+        api("io.jsonwebtoken:jjwt-impl:${property("jjwtVersion")}")
+        api("io.jsonwebtoken:jjwt-jackson:${property("jjwtVersion")}")
+    }
+}

+ 1 - 0
publish.sh

@@ -0,0 +1 @@
+./gradlew publishAllPublicationsToLocalRepoRepository

+ 197 - 0
services/build.gradle.kts

@@ -0,0 +1,197 @@
+import org.springframework.boot.gradle.tasks.bundling.BootJar
+
+group = "cc-lotus.gaf3"
+version = rootProject.version
+
+plugins {
+    id("java")
+    id("io.spring.dependency-management")
+    id("org.springframework.boot") apply false
+    kotlin("jvm")
+    kotlin("plugin.spring")
+    kotlin("plugin.jpa") apply false
+}
+
+java {
+    disableAutoTargetJvm()
+    // withSourcesJar()
+    // withJavadocJar()
+}
+
+repositories {
+    maven {
+        name = "aliyun"
+        url = uri("http://maven.aliyun.com/nexus/content/groups/public/")
+    }
+    // mavenCentral()
+}
+
+allprojects {
+    apply(plugin = "java")
+
+    tasks {
+        processResources {
+            // expand("db.user" to dbUser)
+            // expand 在处理奇数个中文的时候会出现乱码情况
+            // 所以这里只处理application.yml,忽略其他资源文件,避免处理后产生乱码
+            filesMatching("application.yml") {
+                expand(project.properties)
+            }
+            // expand(project.properties)
+        }
+    }
+
+}
+
+subprojects {
+    group = "cc-lotus.gaf3"
+    version = rootProject.version
+
+    apply(plugin = "org.springframework.boot")
+    apply(plugin = "io.spring.dependency-management")
+    apply(plugin = "org.jetbrains.kotlin.jvm")
+    apply(plugin = "org.jetbrains.kotlin.plugin.spring")
+    apply(plugin = "org.jetbrains.kotlin.plugin.jpa")
+
+    dependencies {
+        api(platform(project(":platform")))
+        api(project(path = ":shared:cloud"))
+        api(project(path = ":shared:util"))
+        api(kotlin("reflect"))
+        api(kotlin("stdlib-jdk8"))
+        implementation("org.springframework.boot:spring-boot-starter-webflux")
+        implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+        implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
+        implementation("org.springframework.boot:spring-boot-configuration-processor")
+        implementation("com.alibaba:fastjson")
+        implementation("commons-io:commons-io")
+        implementation("io.jsonwebtoken:jjwt-api")
+        implementation("io.jsonwebtoken:jjwt-impl")
+        implementation("io.jsonwebtoken:jjwt-jackson")
+        implementation("javax.validation:validation-api")
+        runtimeOnly("org.hibernate.validator:hibernate-validator")
+        runtimeOnly(fileTree("$rootDir/libs") { include("*.jar") })
+    }
+    dependencyManagement {
+        imports {
+            mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
+        }
+    }
+
+//    tasks.register<Jar>(name = "libJar") {
+//        archiveBaseName.set("gaf3-${project.name}-lib")
+//        from(sourceSets["main"].output)
+//        include("gaf3/**")
+//        exclude {
+//            it.name.endsWith("Application.class")
+//        }
+//        exclude {
+//            it.name.endsWith("ApplicationKt.class")
+//        }
+//        exclude {
+//            it.name.endsWith("Application${'$'}Companion.class")
+//        }
+//    }
+//    tasks.build { dependsOn(tasks.named("libJar")) }
+
+    configurations {
+        create("lib")
+    }
+
+    artifacts {
+        add("lib", tasks.jar)
+    }
+
+    tasks.withType<Jar> {
+        archiveBaseName.set("gaf3-${project.name}")
+    }
+    tasks.named<BootJar>(name = "bootJar") {
+        archiveClassifier.set("application")
+    }
+    tasks.jar {
+        enabled = true
+        // archiveAppendix.set("lib")
+        exclude {
+            it.name.endsWith("Application.class")
+        }
+        exclude {
+            it.name.endsWith("ApplicationKt.class")
+        }
+        exclude {
+            it.name.endsWith("Application${'$'}Companion.class")
+        }
+    }
+}
+
+configure(subprojects.filter { it.name != "service-api" && it.name != "service-verify" && it.name != "service-token" }) {
+    dependencies {
+        implementation("org.springframework.boot:spring-boot-starter-data-jpa")
+        implementation("org.springframework.boot:spring-boot-starter-webflux")
+        implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+        runtimeOnly("mysql:mysql-connector-java")
+        testImplementation("org.springframework.boot:spring-boot-starter-test")
+        // "testImplementation"("io.projectreactor:reactor-test")
+        // testCompile("junit", "junit", "4.12")
+    }
+}
+
+project("service-all") {
+    dependencies {
+        implementation(project(path = ":services:service-auth", configuration = "lib"))
+        implementation(project(path = ":services:service-bind", configuration = "lib"))
+        implementation(project(path = ":services:service-role", configuration = "lib"))
+        implementation(project(path = ":services:service-code", configuration = "lib"))
+        implementation(project(path = ":services:service-user", configuration = "lib"))
+        implementation(project(path = ":services:service-log", configuration = "lib"))
+        implementation(project(path = ":services:service-dept", configuration = "lib"))
+        implementation(project(path = ":services:service-menu", configuration = "lib"))
+        implementation(project(path = ":services:service-bff", configuration = "lib"))
+    }
+}
+project("service-bff") {
+    dependencies {
+        api(project(path = ":shared:cloud"))
+        api(project(path = ":shared:util"))
+        api(project(path = ":services:service-bind", configuration = "lib"))
+        api(project(path = ":services:service-user", configuration = "lib"))
+        api(project(path = ":services:service-dept", configuration = "lib"))
+        api(project(path = ":services:service-role", configuration = "lib"))
+    }
+}
+
+project("service-auth") {
+    dependencies {
+        implementation(project(path = ":services:service-bff", configuration = "lib"))
+        api(project(path = ":services:service-user", configuration = "lib"))
+        api(project(path = ":services:service-role", configuration = "lib"))
+        api(project(path = ":services:service-bind", configuration = "lib"))
+    }
+}
+
+project(":services") {
+    tasks.jar {
+        archiveBaseName.set("gaf3-core-${project.name}")
+        subprojects {
+            from(tasks.withType<JavaCompile>())
+            from(tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>())
+            include("gaf3/**")
+            exclude {
+                it.name.endsWith("Application.class")
+            }
+            exclude {
+                it.name.endsWith("ApplicationKt.class")
+            }
+            exclude {
+                it.name.endsWith("Application${'$'}Companion.class")
+            }
+        }
+    }
+    publishing {
+        publications {
+            create<MavenPublication>("maven") {
+                artifactId = "gaf-core-${project.name}"
+                from(components["java"])
+            }
+        }
+    }
+}

+ 17 - 0
services/service-all/src/main/kotlin/gaf3/core/services/GafCoreServicesConfiguration.kt

@@ -0,0 +1,17 @@
+package gaf3.core.services
+
+import gaf3.core.cloud.GafCloudConfiguration
+import gaf3.core.jpa.GafJpaConfiguration
+import gaf3.core.services.code.GafCodeConfiguration
+import org.springframework.boot.autoconfigure.domain.EntityScan
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories
+
+@Configuration
+@Import(GafCloudConfiguration::class, GafJpaConfiguration::class, GafCodeConfiguration::class)
+@EntityScan(basePackages = ["gaf3.core.services"])
+@EnableJpaRepositories(basePackages = ["gaf3.core.services"])
+@ComponentScan(basePackages = ["gaf3.core.services"])
+class GafCoreServicesConfiguration

+ 25 - 0
services/service-all/src/main/kotlin/gaf3/core/services/GafServicesApplication.kt

@@ -0,0 +1,25 @@
+package gaf3.core.services
+
+import gaf3.core.cloud.GafCloudConfiguration
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.Import
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@SpringBootApplication
+@Import(GafCloudConfiguration::class)
+@EnableJpaRepositories(basePackages=["gaf3.core.services"])
+@RestController
+class GafServicesApplication {
+
+	@RequestMapping("/all/hello")
+	fun hello(name: String?) : String {
+		return """ hello, ${name ?: "all"}!"""
+	}
+}
+
+fun main(args: Array<String>) {
+	runApplication<GafServicesApplication>(*args)
+}

+ 28 - 0
services/service-all/src/main/resources/application.yml

@@ -0,0 +1,28 @@
+gaf:
+  auth:
+    jwt-secret: "GafJwtSecret!@#"
+    jwt-issuer: xms-gaf
+    jwt-validity: 1h
+
+spring:
+  datasource:
+    username: ${dbUser}
+    password: ${dbPwd}
+    url: ${dbUrl}
+    driver-class-name: ${dbDriver}
+    initialization-mode: never
+    platform: mysql
+  jpa:
+    database-platform: ${dbDialect}
+    show-sql: true
+    hibernate:
+      naming:
+        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
+        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
+      ddl-auto: none
+  main:
+    allow-bean-definition-overriding: true
+server:
+  port: 8080
+logging.level.gaf3.core.*: DEBUG
+#debug: true

+ 3 - 0
services/service-all/src/main/resources/data-mysql.sql

@@ -0,0 +1,3 @@
+#insert into gaf_code_item (id, item_type, code, alias, status, name, created_at) value ('0', '00', '01', 'demo', '0', '测试', now());
+insert into gaf_user_info (user_id, name, status, created_at, updated_at) values ('00000000-0000-0000-0000-000000000000', '系统管理员', '0', now(), now());
+insert into gaf_user_acct ( user_id, account, secret, status, expired, created_at, updated_at) values ('00000000-0000-0000-0000-000000000000', 'admin', '123456', '0', -1, now(), now());

+ 32 - 0
services/service-api/src/main/kotlin/gaf3/core/services/ServiceApiApplication.kt

@@ -0,0 +1,32 @@
+package gaf3.core.services
+
+import gaf3.core.cloud.GafCloudConfiguration
+import gaf3.core.data.PagedData
+import gaf3.core.services.api.UserService
+import gaf3.core.services.api.domain.UserInfo
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.cloud.openfeign.EnableFeignClients
+import org.springframework.context.annotation.Import
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@SpringBootApplication
+@RestController
+@EnableFeignClients
+@Import(GafCloudConfiguration::class)
+class ServiceApiApplication {
+
+	@Autowired
+	var service: UserService? = null
+
+	@RequestMapping("/demo")
+	fun demo() : PagedData<UserInfo>? {
+		return this.service?.findUsers()
+	}
+}
+
+fun main(args: Array<String>) {
+	runApplication<ServiceApiApplication>(*args)
+}

+ 35 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/CodeService.kt

@@ -0,0 +1,35 @@
+package gaf3.core.services.api
+
+import gaf3.core.cloud.feign.GafFeignConfiguration
+import gaf3.core.data.JsonResult
+import gaf3.core.data.PagedData
+import gaf3.core.services.api.domain.*
+import org.springframework.cloud.openfeign.FeignClient
+import org.springframework.web.bind.annotation.*
+
+@FeignClient(name = "\${feign.name}", url = "\${feign.url}", configuration = [GafFeignConfiguration::class] )
+interface CodeService {
+    @GetMapping(path = ["/gaf/code/categories"])
+    fun listCategories(): PagedData<CodeItem>
+
+    @GetMapping(path = ["/gaf/code/{pid}/items"])
+    fun listCodes(@PathVariable pid: String): PagedData<CodeItem>
+
+    @GetMapping(path = ["/gaf/code/items"])
+    fun findItems(filter: CodeItem? = null, skip: Int = 0, limit: Int = 100): PagedData<CodeItem>
+
+    @PostMapping(path = ["/gaf/code/items"])
+    fun create(@RequestParam pid: String, @RequestBody data: CodeForm): CodeItem
+
+    @DeleteMapping(path = ["/gaf/code/items/{id}"])
+    fun delete(@PathVariable id: String): JsonResult<Void>
+
+    @PostMapping(path = ["/gaf/code/items/{id}"])
+    fun update(@PathVariable id: String, @RequestBody data: CodeForm): CodeItem
+
+    @GetMapping(path = ["/gaf/code/items/{id}"])
+    fun fetch(@PathVariable id: String): CodeItem?
+
+    @GetMapping(path = ["/gaf/code/{pid}/{code}"])
+    fun fetchByCode(@PathVariable pid: String, @PathVariable code: String): CodeItem?
+}

+ 14 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/LogService.kt

@@ -0,0 +1,14 @@
+package gaf3.core.services.api
+
+import gaf3.core.cloud.feign.GafFeignConfiguration
+import gaf3.core.data.JsonResult
+import gaf3.core.data.PagedData
+import gaf3.core.services.api.domain.*
+import org.springframework.cloud.openfeign.FeignClient
+import org.springframework.web.bind.annotation.*
+
+@FeignClient(name = "\${feign.name}", url = "\${feign.url}", configuration = [GafFeignConfiguration::class] )
+interface LogService {
+    @PostMapping(path = ["/gaf/logs"])
+    fun create(@RequestBody data: LogForm): LogForm
+}

+ 37 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/UserService.kt

@@ -0,0 +1,37 @@
+package gaf3.core.services.api
+
+import gaf3.core.cloud.feign.GafFeignConfiguration
+import gaf3.core.data.JsonResult
+import gaf3.core.data.PagedData
+import gaf3.core.services.api.domain.AcctForm
+import gaf3.core.services.api.domain.UserForm
+import gaf3.core.services.api.domain.UserInfo
+import org.springframework.cloud.openfeign.FeignClient
+import org.springframework.web.bind.annotation.*
+
+@FeignClient(name = "\${feign.name}", url = "\${feign.url}", configuration = [GafFeignConfiguration::class] )
+interface UserService {
+    @PostMapping(path = ["/gaf/users"])
+    fun createUser(@RequestBody data: UserForm): UserInfo
+
+    @GetMapping(path = ["/gaf/users"])
+    fun findUsers(filter: UserInfo? = null, skip: Int = 0, limit: Int = 100): PagedData<UserInfo>
+
+    @DeleteMapping(path = ["/gaf/users/{userId}"])
+    fun deleteUser(@PathVariable userId: String): JsonResult<Void>
+
+    @GetMapping(path = ["/gaf/users/{userId}"])
+    fun updateUser(@PathVariable userId: String, @RequestBody data: UserForm): UserInfo
+
+    @GetMapping(path = ["/gaf/users/{userId}"])
+    fun fetchUser(@PathVariable userId: String): UserInfo?
+
+    @PostMapping(path = ["/gaf/users/{userId}/pass"])
+    fun changePass(@PathVariable userId: String, oldpass: String, newpass: String): JsonResult<Void>
+
+    @PostMapping(path = ["/gaf/users/{userId}/acct"])
+    fun updateAcct(@PathVariable userId: String, @RequestBody data: AcctForm): JsonResult<Void>
+
+    @PostMapping(path = ["/gaf/users/login"])
+    fun login(@RequestBody data: AcctForm): JsonResult<UserInfo>
+}

+ 12 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/domain/AcctForm.kt

@@ -0,0 +1,12 @@
+package gaf3.core.services.api.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import javax.validation.constraints.NotEmpty
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class AcctForm @JvmOverloads constructor(var account: String? = null){
+    var secret: String? = null
+    var status: String? = null
+    var expired: Long? = null
+    var remark: String? = null
+}

+ 21 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/domain/CodeForm.kt

@@ -0,0 +1,21 @@
+package gaf3.core.services.api.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import javax.validation.constraints.NotEmpty
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class CodeForm @JvmOverloads constructor(code: String? = null, name: String? = null){
+    @NotEmpty
+    var code: String? = null
+    @NotEmpty
+    var name: String? = null
+
+    var alias: String? = null
+    var status: String? = null
+    var remark: String? = null
+
+    init {
+        this.code = code
+        this.name = name
+    }
+}

+ 16 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/domain/CodeItem.kt

@@ -0,0 +1,16 @@
+package gaf3.core.services.api.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import java.util.*
+import javax.validation.constraints.NotEmpty
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class CodeItem @JvmOverloads constructor(
+    var id: String? = null,
+    var pid: String? = null,
+    var code: String? = null,
+    var name: String? = null,
+    var alias: String? = null,
+    var status: String? = null,
+    var remark: String? = null
+){}

+ 9 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/domain/LogData.kt

@@ -0,0 +1,9 @@
+package gaf3.core.services.api.domain
+
+import java.time.LocalDateTime
+
+class LogData: LogForm() {
+    var id: String? = null
+    var createdAt: LocalDateTime? = null
+    var updatedAt: LocalDateTime? = null
+}

+ 30 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/domain/LogForm.kt

@@ -0,0 +1,30 @@
+package gaf3.core.services.api.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import javax.validation.constraints.NotEmpty
+import javax.validation.constraints.NotNull
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+open class LogForm{
+    @NotEmpty
+    var logType: String? = null
+    @NotEmpty
+    var event: String? = null
+    @NotEmpty
+    var level: String? = null
+    var module: String? = null
+    var action: String? = null
+    var target: String? = null
+    var param: String? = null
+    @NotEmpty
+    var result: String? = null
+    var detail: String? = null
+    var eventTime: String? = null
+    var timestamp: Long? = null
+    var extra: String? = null
+    @NotEmpty
+    var userName: String? = null
+    @NotEmpty
+    var userAcct: String? = null
+    var userIp: String? = null
+}

+ 17 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/domain/LoginForm.kt

@@ -0,0 +1,17 @@
+package gaf3.core.services.api.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import javax.validation.constraints.NotEmpty
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class LoginForm(account: String? = null, password: String? = null){
+    @NotEmpty
+    var account: String? = null
+    @NotEmpty
+    var password: String? = null
+
+    init {
+        this.account = account
+        this.password = password
+    }
+}

+ 18 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/domain/PassForm.kt

@@ -0,0 +1,18 @@
+package gaf3.core.services.api.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import javax.validation.constraints.NotEmpty
+import javax.validation.constraints.NotNull
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class PassForm(oldpass: String? = null, newpass: String? = null){
+    @NotEmpty
+    var oldpass: String? = null
+    @NotEmpty
+    var newpass: String? = null
+
+    init {
+        this.oldpass = oldpass
+        this.newpass = newpass
+    }
+}

+ 14 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/domain/UserForm.kt

@@ -0,0 +1,14 @@
+package gaf3.core.services.api.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import javax.validation.constraints.NotEmpty
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class UserForm @JvmOverloads constructor(var name: String? = null) {
+    var title: String? = null
+    var gender: String? = null
+    var mobile: String? = null
+    var email: String? = null
+    var remark: String? = null
+    var account: AcctForm? = null
+}

+ 19 - 0
services/service-api/src/main/kotlin/gaf3/core/services/api/domain/UserInfo.kt

@@ -0,0 +1,19 @@
+package gaf3.core.services.api.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import java.time.LocalDateTime
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class UserInfo @JvmOverloads constructor(var userId: String? = null) {
+    var name: String? = null
+    var title: String? = null
+    var gender: String? = null
+    var mobile: String? = null
+    var email: String? = null
+    var account: String? = null
+    var status: String? = null
+    var expired: Long? = null
+    var createdAt: LocalDateTime? = null
+    var updatedAt: LocalDateTime? = null
+    var remark: String? = null
+}

+ 21 - 0
services/service-api/src/main/resources/application.yml

@@ -0,0 +1,21 @@
+
+server.port: 8000
+
+feign:
+  name: users
+  url: http://localhost:8081/
+  client:
+    config:
+      users:
+        connectTimeout: 5000
+        readTimeout: 5000
+        loggerLevel: full
+#        errorDecoder: com.example.SimpleErrorDecoder
+#        retryer: com.example.SimpleRetryer
+#        requestInterceptors:
+#          - com.example.FooRequestInterceptor
+#          - com.example.BarRequestInterceptor
+#        decode404: false
+#        encoder: com.example.SimpleEncoder
+#        decoder: com.example.SimpleDecoder
+#        contract: com.example.SimpleContract

+ 30 - 0
services/service-auth/src/main/kotlin/gaf3/core/services/auth/GafAuthConfiguration.kt

@@ -0,0 +1,30 @@
+package gaf3.core.services.auth
+
+import gaf3.core.cloud.GafCloudConfiguration
+import gaf3.core.services.bff.BffServiceConfiguration
+import io.jsonwebtoken.SignatureAlgorithm
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
+import org.springframework.boot.autoconfigure.domain.EntityScan
+import org.springframework.boot.context.properties.EnableConfigurationProperties
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import java.security.Key
+import javax.crypto.spec.SecretKeySpec
+
+@Configuration
+@EnableConfigurationProperties(GafAuthConfigure::class)
+@EntityScan(basePackages = ["gaf3.core.services"])
+@Import(GafCloudConfiguration::class, BffServiceConfiguration::class)
+class GafAuthConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean(name = ["jwtSigningKey"])
+    @Qualifier("jwtSigningKey")
+    fun jwtSigningKey(config: GafAuthConfigure): Key {
+        val secretKeyBytes = config.jwtSecret?.toByteArray()?.copyOf(32)
+        val alg: SignatureAlgorithm = SignatureAlgorithm.HS256
+        return SecretKeySpec(secretKeyBytes, alg.jcaName)
+    }
+}

+ 22 - 0
services/service-auth/src/main/kotlin/gaf3/core/services/auth/GafAuthConfigure.kt

@@ -0,0 +1,22 @@
+package gaf3.core.services.auth
+
+import gaf3.core.util.ValidityUtil
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.validation.annotation.Validated
+import javax.validation.constraints.NotNull
+import javax.validation.constraints.Pattern
+
+@ConfigurationProperties(prefix = "gaf.auth")
+@Validated
+class GafAuthConfigure {
+    @NotNull
+    var jwtSecret: String? = ""
+
+    var jwtIssuer: String? = "gaf"
+
+    @Pattern(regexp = ValidityUtil.VALIDITY_REGEXP)
+    var jwtValidity: String? = "2h"
+
+    val jwtValiditySec: Long get() = ValidityUtil.parseValidity(jwtValidity)
+
+}

+ 19 - 0
services/service-auth/src/main/kotlin/gaf3/core/services/auth/GafAuthServiceController.kt

@@ -0,0 +1,19 @@
+package gaf3.core.services.auth
+
+import gaf3.core.services.auth.domain.AuthToken
+import gaf3.core.services.auth.domain.LoginForm
+import gaf3.core.services.auth.service.GafAuthService
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RestController
+import javax.validation.Valid
+
+@RestController
+class GafAuthServiceController(val service: GafAuthService) {
+
+    @PostMapping(path = ["/gaf/auth/login"])
+    fun login(@RequestBody @Valid form: LoginForm): AuthToken{
+        return service.login(form)
+    }
+
+}

+ 17 - 0
services/service-auth/src/main/kotlin/gaf3/core/services/auth/ServiceAuthApplication.kt

@@ -0,0 +1,17 @@
+package gaf3.core.services.auth
+
+import gaf3.core.cloud.GafCloudConfiguration
+import gaf3.core.services.user.service.UserService
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.context.annotation.Import
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories
+import org.springframework.web.bind.annotation.RestController
+
+@SpringBootApplication
+class ServiceAuthApplication
+
+fun main(args: Array<String>) {
+    runApplication<ServiceAuthApplication>(*args)
+}

+ 5 - 0
services/service-auth/src/main/kotlin/gaf3/core/services/auth/domain/AuthToken.kt

@@ -0,0 +1,5 @@
+package gaf3.core.services.auth.domain
+
+import gaf3.core.services.user.domain.UserInfo
+
+data class AuthToken(val userinfo: UserInfo, val token: String, val roles: Array<String>?) {}

+ 13 - 0
services/service-auth/src/main/kotlin/gaf3/core/services/auth/domain/LoginForm.kt

@@ -0,0 +1,13 @@
+package gaf3.core.services.auth.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import javax.validation.constraints.NotEmpty
+import javax.validation.constraints.NotNull
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+open class LoginForm{
+    @NotEmpty
+    var username: String? = null
+    @NotEmpty
+    var password: String? = null
+}

+ 93 - 0
services/service-auth/src/main/kotlin/gaf3/core/services/auth/service/GafAuthService.kt

@@ -0,0 +1,93 @@
+package gaf3.core.services.auth.service
+
+import com.alibaba.fastjson.JSON
+import gaf3.core.exception.BusinessError
+import gaf3.core.services.auth.GafAuthConfigure
+import gaf3.core.services.auth.domain.AuthToken
+import gaf3.core.services.auth.domain.LoginForm
+import gaf3.core.services.bff.service.BffUserRoleService
+import gaf3.core.services.role.entity.GafRoleItem
+import gaf3.core.services.user.domain.UserInfo
+import gaf3.core.services.user.service.UserService
+import gaf3.core.util.JsonCertToken
+import io.jsonwebtoken.Jwts
+import org.bouncycastle.asn1.x509.X509ObjectIdentifiers
+import org.bouncycastle.cert.X509CertificateHolder
+import org.bouncycastle.crypto.Signer
+import org.bouncycastle.crypto.digests.SHA1Digest
+import org.bouncycastle.crypto.params.AsymmetricKeyParameter
+import org.bouncycastle.crypto.signers.RSADigestSigner
+import org.bouncycastle.crypto.util.PublicKeyFactory
+import org.bouncycastle.util.encoders.Base64
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.stereotype.Service
+import org.springframework.validation.annotation.Validated
+import java.io.IOException
+import java.security.Key
+import java.time.Instant
+import java.util.*
+import java.util.regex.Pattern
+import javax.validation.Valid
+
+
+@Service
+@Validated
+class GafAuthService (@Qualifier("jwtSigningKey")val jwtSigningKey: Key, val config: GafAuthConfigure, val userService: UserService, val roleService: BffUserRoleService) {
+
+    fun login(@Valid form: LoginForm): AuthToken {
+        val user = if(form.username == "@cert")
+            loginByCert(form.password!!)
+        else
+            userService.login(form.username!!, form.password!!)
+
+        // TODO: 查询用户角色
+        var roles: Array<String>? = if(user.account == "admin") {
+            arrayOf("superadmin")
+        } else {
+            roleService.find(GafRoleItem(), user.userId).data?.map {
+                p -> (p.code ?: p.id)!!
+            }?.toTypedArray()
+        }
+
+        // 生成Jwt
+        val token = createJwt(subject = form.username!!,
+                userId = user.userId!!,
+                name = user.name!!,
+                roles = roles)
+        return AuthToken(user, token, roles)
+    }
+
+    fun createJwt(subject: String, userId: String, name: String, roles: Array<String>? = null): String {
+        return Jwts.builder()
+                .setSubject(subject)
+                .setIssuer(config.jwtIssuer ?: "gaf")
+                .setExpiration(Date.from(Instant.now().plusSeconds(config.jwtValiditySec)))
+                .claim("userId", userId)
+                .claim("name", name)
+                .claim("roles", roles)
+                .signWith(jwtSigningKey)
+                .compact()
+    }
+
+    /**
+     * 证书登录
+     */
+    fun loginByCert(token: String): UserInfo {
+        val jct = JsonCertToken.parse(token)
+        // 检查token有效期
+        if (System.currentTimeMillis() / 1000 > jct.exp) {
+            JsonCertToken.log.debug("Token已过期: {}", Date(jct.exp*1000))
+            throw BusinessError(BusinessError.ERR_TOKEN_EXPIRED, "Token验证失败: Token已过期")
+        }
+
+        return userService.fetchByAccount(jct.sub)
+            ?: throw BusinessError(BusinessError.ERR_USER_NOTEXIST, "用户信息不存在")
+    }
+
+
+    companion object {
+        val log: Logger = LoggerFactory.getLogger(GafAuthService::class.java)
+    }
+}

+ 24 - 0
services/service-auth/src/main/resources/application.yml

@@ -0,0 +1,24 @@
+# 全局配置
+gaf:
+  auth:
+    jwt-secret: "GafJwtSecret!@#"
+    jwt-issuer: xms-gaf
+    jwt-validity: 1h
+
+spring:
+  datasource:
+    username: ${dbUser}
+    password: ${dbPwd}
+    url: ${dbUrl}
+    driver-class-name: ${dbDriver}
+  jpa:
+    database-platform: ${dbDialect}
+    show-sql: true
+    hibernate:
+      naming:
+        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
+        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
+      ddl-auto: update
+server:
+  port: 8070
+logging.level.gaf3.core.*: DEBUG

+ 11 - 0
services/service-bff/README.md

@@ -0,0 +1,11 @@
+# BFF服务(Backend for front)
+实现组合查询服务
+## 服务接口类型
+### dept-users
+按部门查询用户
+### user-depts
+查询用户部门
+### user-roles
+查询用户角色
+### user-groups
+查询用户群组

+ 21 - 0
services/service-bff/src/main/kotlin/gaf3/core/services/bff/BffServiceConfiguration.kt

@@ -0,0 +1,21 @@
+package gaf3.core.services.bff
+
+import gaf3.core.services.bff.dao.GafBffUserDeptDAO
+import gaf3.core.services.bind.dao.GafBindItemDAO
+import gaf3.core.services.bind.service.BindService
+import gaf3.core.services.role.dao.GafRoleItemDAO
+import gaf3.core.services.user.dao.GafUserInfoDAO
+import gaf3.core.services.user.service.UserService
+import org.springframework.boot.autoconfigure.domain.EntityScan
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.context.annotation.Configuration
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories
+
+@Configuration("gafBffServiceConfiguration")
+@EntityScan(basePackages = ["gaf3.core.services"])
+@ComponentScan(basePackageClasses = [BffServiceConfiguration::class, BindService::class, UserService::class])
+@EnableJpaRepositories(basePackageClasses = [GafBffUserDeptDAO::class,
+    GafBindItemDAO::class, GafUserInfoDAO::class, GafRoleItemDAO::class])
+class BffServiceConfiguration {
+}
+

+ 31 - 0
services/service-bff/src/main/kotlin/gaf3/core/services/bff/BffServiceController.kt

@@ -0,0 +1,31 @@
+package gaf3.core.services.bff
+
+import gaf3.core.data.PagedData
+import gaf3.core.services.bff.service.BffUserDeptService
+import gaf3.core.services.bff.service.BffUserRoleService
+import gaf3.core.services.role.entity.GafRoleItem
+import gaf3.core.services.user.domain.UserInfo
+import gaf3.core.services.user.entity.GafUserInfo
+import org.springframework.web.bind.annotation.*
+
+@RestController("gafBffServiceController")
+class BffServiceController(val deptUser: BffUserDeptService, val roleUser: BffUserRoleService) {
+//    companion object {
+//        val log: Logger = LoggerFactory.getLogger(BffDeptUserService::class.java)
+//    }
+
+    @GetMapping(path = ["/gaf/bff/dept/users"])
+    fun findDeptUsers(deptId: String?, filter: GafUserInfo, skip: Int? = 0, limit: Int? = 100, exact: Boolean?): PagedData<UserInfo>{
+        return deptUser.find(filter, deptId, skip?:0, limit?: 100, exact ?: true )
+    }
+
+    @GetMapping(path = ["/gaf/bff/role/users"])
+    fun findRoleUsers(roleId: String?, filter: GafUserInfo, skip: Int? = 0, limit: Int? = 100, exact: Boolean?): PagedData<UserInfo>{
+        return roleUser.find(filter, roleId, skip?:0, limit?: 100, exact ?: true )
+    }
+
+    @GetMapping(path = ["/gaf/bff/user/roles"])
+    fun findUserRoles(userId: String?, filter: GafRoleItem, skip: Int? = 0, limit: Int? = 100): PagedData<GafRoleItem>{
+        return roleUser.find(filter, userId, skip?:0, limit?: 100 )
+    }
+}

+ 22 - 0
services/service-bff/src/main/kotlin/gaf3/core/services/bff/ServiceBffApplication.kt

@@ -0,0 +1,22 @@
+package gaf3.core.services.bff
+
+import gaf3.core.cloud.GafCloudConfiguration
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.Import
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@SpringBootApplication
+@RestController
+@Import(GafCloudConfiguration::class)
+class ServiceBffApplication {
+    @RequestMapping(path=["demo"])
+    fun demo(): String {
+        return "hello,world"
+    }
+}
+
+fun main(args: Array<String>) {
+    runApplication<ServiceBffApplication>(*args)
+}

+ 7 - 0
services/service-bff/src/main/kotlin/gaf3/core/services/bff/dao/GafBffUserDeptDAO.kt

@@ -0,0 +1,7 @@
+package gaf3.core.services.bff.dao
+
+import gaf3.core.services.bff.entity.BffUserDept
+import org.springframework.data.jpa.repository.JpaRepository
+
+interface GafBffUserDeptDAO : JpaRepository<BffUserDept, String> {
+}

+ 7 - 0
services/service-bff/src/main/kotlin/gaf3/core/services/bff/dao/GafBffUserRoleDAO.kt

@@ -0,0 +1,7 @@
+package gaf3.core.services.bff.dao
+
+import gaf3.core.services.bff.entity.BffUserRole
+import org.springframework.data.jpa.repository.JpaRepository
+
+interface GafBffUserRoleDAO : JpaRepository<BffUserRole, String> {
+}

+ 46 - 0
services/service-bff/src/main/kotlin/gaf3/core/services/bff/entity/BffBindItem.kt

@@ -0,0 +1,46 @@
+package gaf3.core.services.bff.entity
+
+import org.springframework.data.annotation.Immutable
+import java.io.Serializable
+import java.time.LocalDateTime
+import javax.persistence.*
+
+
+/**
+ * The persistent class for the GAF_GROUP_ROLE database table.
+ *
+ */
+@Entity(name = "GafBffBindItem")
+@Table(name = "GAF_BIND_ITEM")
+@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
+@DiscriminatorColumn(name="BIND_TYPE",discriminatorType=DiscriminatorType.STRING)
+@Immutable
+open class BffBindItem : Serializable {
+
+    @Id
+    @Column(name = "ID", length = 48)
+    open var id: String? = null
+
+    @Column(name = "BIND_TYPE", length = 48, insertable = false, updatable = false)
+    open var type: String? = null
+
+    @Column(name = "SOURCE", length = 48)
+    open var source: String? = null
+
+    @Column(name = "TARGET", length = 48)
+    open var target: String? = null
+
+    @Column(name = "PARAM")
+    open var param: String? = null
+
+    @Column(name = "REMARK")
+    open var remark: String? = null
+
+    @Column(name = "CREATED_AT")
+    open var createdAt: LocalDateTime? = null
+
+    companion object {
+        private const val serialVersionUID = 1L
+    }
+
+}

+ 32 - 0
services/service-bff/src/main/kotlin/gaf3/core/services/bff/entity/BffUserDept.kt

@@ -0,0 +1,32 @@
+package gaf3.core.services.bff.entity
+
+import gaf3.core.services.user.entity.GafUserAccount
+import gaf3.core.services.user.entity.GafUserInfo
+import javax.persistence.*
+
+
+/**
+ * The persistent class for the GAF_BIND_ITEM database table.
+ *
+ */
+@Entity(name = "GafBffUserDept")
+@DiscriminatorValue("user-dept")
+class BffUserDept : BffBindItem() {
+
+//    @OneToOne(fetch = FetchType.LAZY)
+//    @JoinColumn(name = "target", insertable = false, updatable = false)
+//    var dept: GafDeptItem? = null
+
+    @OneToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "source", insertable = false, updatable = false, foreignKey = ForeignKey(value = ConstraintMode.NO_CONSTRAINT, name = "none"))
+    var user: GafUserInfo? = null
+
+    @OneToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "source", insertable = false, updatable = false, foreignKey = ForeignKey(value = ConstraintMode.NO_CONSTRAINT, name = "none"))
+    var acct: GafUserAccount? = null
+
+    companion object {
+        private const val serialVersionUID = 1L
+    }
+
+}

+ 33 - 0
services/service-bff/src/main/kotlin/gaf3/core/services/bff/entity/BffUserRole.kt

@@ -0,0 +1,33 @@
+package gaf3.core.services.bff.entity
+
+import gaf3.core.services.role.entity.GafRoleItem
+import gaf3.core.services.user.entity.GafUserAccount
+import gaf3.core.services.user.entity.GafUserInfo
+import javax.persistence.*
+
+
+/**
+ * The persistent class for the GAF_BIND_ITEM database table.
+ *
+ */
+@Entity(name = "GafBffUserRole")
+@DiscriminatorValue("user-role")
+class BffUserRole : BffBindItem() {
+
+    @OneToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "target", insertable = false, updatable = false, foreignKey = ForeignKey(value = ConstraintMode.NO_CONSTRAINT, name = "none"))
+    var role: GafRoleItem? = null
+
+    @OneToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "source", insertable = false, updatable = false, foreignKey = ForeignKey(value = ConstraintMode.NO_CONSTRAINT, name = "none"))
+    var user: GafUserInfo? = null
+
+    @OneToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "source", insertable = false, updatable = false, foreignKey = ForeignKey(value = ConstraintMode.NO_CONSTRAINT, name = "none"))
+    var acct: GafUserAccount? = null
+
+    companion object {
+        private const val serialVersionUID = 1L
+    }
+
+}

+ 34 - 0
services/service-bff/src/main/kotlin/gaf3/core/services/bff/service/BffUserDeptService.kt

@@ -0,0 +1,34 @@
+package gaf3.core.services.bff.service
+
+import gaf3.core.data.PagedData
+import gaf3.core.jpa.extension.findAll
+import gaf3.core.services.bff.dao.GafBffUserDeptDAO
+import gaf3.core.services.bff.entity.BffUserDept
+import gaf3.core.services.user.domain.UserInfo
+import gaf3.core.services.user.entity.GafUserInfo
+import gaf3.core.services.user.service.UserService
+import org.springframework.data.domain.Example
+import org.springframework.data.domain.PageRequest
+import org.springframework.stereotype.Service
+
+@Service("gafBffUserDeptService")
+class BffUserDeptService(val bffDao: GafBffUserDeptDAO, val userService: UserService) {
+
+    @JvmOverloads
+    fun find(filter: GafUserInfo, deptId: String?, skip: Int = 0, limit: Int = 100, exact: Boolean = true): PagedData<UserInfo> {
+        val example = BffUserDept()
+        example.user = filter
+        example.target = deptId
+        val page:Int = skip/limit
+        val paged = bffDao.findAll(example, PageRequest.of(page, limit), exact)
+        val rs = paged.content.map { p ->
+            if(null == p.user) {
+                UserInfo(p.target)
+            } else {
+                userService.convertDomain(p.user!!, p.acct)
+            }
+        }
+
+        return PagedData(rs, paged.totalElements.toInt())
+    }
+}

+ 46 - 0
services/service-bff/src/main/kotlin/gaf3/core/services/bff/service/BffUserRoleService.kt

@@ -0,0 +1,46 @@
+package gaf3.core.services.bff.service
+
+import gaf3.core.data.PagedData
+import gaf3.core.jpa.extension.findAll
+import gaf3.core.services.bff.dao.GafBffUserRoleDAO
+import gaf3.core.services.bff.entity.BffUserRole
+import gaf3.core.services.role.entity.GafRoleItem
+import gaf3.core.services.user.domain.UserInfo
+import gaf3.core.services.user.entity.GafUserInfo
+import gaf3.core.services.user.service.UserService
+import org.springframework.data.domain.Example
+import org.springframework.data.domain.PageRequest
+import org.springframework.stereotype.Service
+
+@Service("gafBffUserRoleService")
+class BffUserRoleService(val bffDao: GafBffUserRoleDAO, val userService: UserService) {
+
+    fun find(filter: GafUserInfo, roleId: String?, skip: Int = 0, limit: Int = 100, exact: Boolean = true): PagedData<UserInfo> {
+        val example = BffUserRole()
+        example.user = filter
+        example.target = roleId
+        val page:Int = skip/limit
+        val paged = bffDao.findAll(example, PageRequest.of(page, limit), exact)
+        val rs = paged.content.map { p ->
+            if(null == p.user) {
+                UserInfo(p.source)
+            } else {
+                userService.convertDomain(p.user!!, p.acct)
+            }
+        }
+
+        return PagedData(rs, paged.totalElements.toInt())
+    }
+
+    fun find(filter: GafRoleItem, userId: String?, skip: Int = 0, limit: Int = 100): PagedData<GafRoleItem> {
+        val temp = BffUserRole()
+        temp.role = filter
+        temp.source = userId
+        val example = Example.of(temp)
+        val page:Int = skip/limit
+        val paged = bffDao.findAll(example, PageRequest.of(page, limit))
+        val rs = paged.content.map { p -> p.role ?: GafRoleItem(p.target) }
+
+        return PagedData(rs, paged.totalElements.toInt())
+    }
+}

+ 18 - 0
services/service-bff/src/main/resources/application.yml

@@ -0,0 +1,18 @@
+
+spring:
+  datasource:
+    username: ${dbUser}
+    password: ${dbPwd}
+    url: ${dbUrl}
+    driver-class-name: ${dbDriver}
+  jpa:
+    database-platform: ${dbDialect}
+    show-sql: true
+    hibernate:
+      naming:
+        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
+        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
+      ddl-auto: update
+server:
+  port: 8090
+logging.level.gaf3.core.*: DEBUG

+ 14 - 0
services/service-bind/README.md

@@ -0,0 +1,14 @@
+# 关联绑定服务
+处理通用的关联关系,包括角色-用户、角色-群组、用户-群组、用户-部门等
+## 绑定类型定义
+### user-dept
+用户部门,多对多关系
+### user-role
+用户-角色,多对多关系
+### user-group
+*群组用户,多对对关系
+### group-role
+*角色-群组,多对多关系
+### menu-role
+菜单-角色,多对多关系
+

+ 46 - 0
services/service-bind/src/main/kotlin/gaf3/core/services/bind/BindServiceController.kt

@@ -0,0 +1,46 @@
+package gaf3.core.services.bind
+
+import gaf3.core.data.ErrorResult
+import gaf3.core.data.PagedData
+import gaf3.core.services.bind.domain.BatchBindForm
+import gaf3.core.services.bind.domain.BatchBindResult
+import gaf3.core.services.bind.domain.BindForm
+import gaf3.core.services.bind.entity.GafBindItem
+import gaf3.core.services.bind.service.BindService
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.web.bind.annotation.*
+import javax.validation.Valid
+
+@RestController("gafBindServiceController")
+class BindServiceController(@Autowired val service: BindService) {
+//    companion object {
+//        val log: Logger = LoggerFactory.getLogger(UserServiceController::class.java)
+//    }
+
+    @GetMapping(path = ["/gaf/bind/{type}/items"])
+    fun find(@PathVariable type: String, source: String?, target: String?): PagedData<GafBindItem>{
+        return service.find(type, source, target)
+    }
+
+    @PostMapping(path = ["/gaf/bind/{type}/items"])
+    fun create(@PathVariable type: String, @Valid @RequestBody data: BindForm): GafBindItem {
+        return service.create(type, data)
+    }
+
+    @PostMapping(path = ["/gaf/bind/{type}/items/batch"])
+    fun batchCreate(@PathVariable type: String, replace: Boolean?, @Valid @RequestBody data: BatchBindForm): BatchBindResult {
+        return service.batchCreate(type, replace ?: false, data)
+    }
+
+    @DeleteMapping(path = ["/gaf/bind/{type}/items/{id}"])
+    fun delete(@PathVariable type: String, @PathVariable id: String): ErrorResult {
+        service.delete(id)
+        return ErrorResult(0,"deleted")
+    }
+
+    @DeleteMapping(path = ["/gaf/bind/{type}/items"])
+    fun delete(@PathVariable type: String, source: String?, target: String?): ErrorResult{
+        service.delete(type, source, target)
+        return ErrorResult(0,"deleted")
+    }
+}

+ 19 - 0
services/service-bind/src/main/kotlin/gaf3/core/services/bind/ServiceBindApplication.kt

@@ -0,0 +1,19 @@
+package gaf3.core.services.bind
+
+import gaf3.core.cloud.GafCloudConfiguration
+import gaf3.core.services.bind.dao.GafBindItemDAO
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.Import
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories
+import org.springframework.web.bind.annotation.RestController
+
+@SpringBootApplication
+@RestController
+@EnableJpaRepositories(basePackageClasses=[GafBindItemDAO::class])
+@Import(GafCloudConfiguration::class)
+class ServiceBindApplication
+
+fun main(args: Array<String>) {
+    runApplication<ServiceBindApplication>(*args)
+}

+ 18 - 0
services/service-bind/src/main/kotlin/gaf3/core/services/bind/dao/GafBindItemDAO.kt

@@ -0,0 +1,18 @@
+package gaf3.core.services.bind.dao
+
+import gaf3.core.services.bind.entity.GafBindItem
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.data.jpa.repository.Modifying
+import org.springframework.data.jpa.repository.Query
+import org.springframework.data.repository.query.Param
+
+interface GafBindItemDAO : JpaRepository<GafBindItem, String> {
+    @Query(value = "select case when count(p) > 0 then true else false end from GafBindItem p where p.type = ?1 and p.source = ?2 and p.target = ?3")
+    fun exists(type: String, source: String, target: String): Boolean
+
+    @Modifying
+    @Query(value = "delete from GafBindItem p where p.type = :type and (p.source = :source or :source is null) and (p.source = :target or :target is null) ")
+    fun delete(@Param("type") type: String, @Param("source") source: String? = null, @Param("target") target: String? = null): Int
+
+    fun findByTypeAndSource(type: String, source: String): List<GafBindItem>
+}

+ 3 - 0
services/service-bind/src/main/kotlin/gaf3/core/services/bind/domain/BatchBindForm.kt

@@ -0,0 +1,3 @@
+package gaf3.core.services.bind.domain
+
+data class BatchBindForm(val source: String = "", val targets: List<String>? = null, val param: String? = null, val remark: String? = null){}

+ 8 - 0
services/service-bind/src/main/kotlin/gaf3/core/services/bind/domain/BatchBindResult.kt

@@ -0,0 +1,8 @@
+package gaf3.core.services.bind.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import gaf3.core.services.bind.entity.GafBindItem
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class BatchBindResult @JvmOverloads
+constructor(val data: List<GafBindItem>? = null, val skipped: List<GafBindItem>? = null, val deleted: List<GafBindItem>? = null)

+ 3 - 0
services/service-bind/src/main/kotlin/gaf3/core/services/bind/domain/BindForm.kt

@@ -0,0 +1,3 @@
+package gaf3.core.services.bind.domain
+
+data class BindForm(val source: String = "", val target: String = "", val param: String? = null, val remark: String? = null){}

+ 48 - 0
services/service-bind/src/main/kotlin/gaf3/core/services/bind/entity/GafBindItem.kt

@@ -0,0 +1,48 @@
+package gaf3.core.services.bind.entity
+
+import javax.persistence.*
+import java.io.Serializable
+import java.time.LocalDateTime
+import java.util.*
+
+
+/**
+ * The persistent class for the GAF_GROUP_ROLE database table.
+ *
+ */
+@Entity
+@Table(name = "GAF_BIND_ITEM")
+class GafBindItem : Serializable {
+
+    @Id
+    @Column(name = "ID", length = 48)
+    var id: String? = null
+
+    @Column(name = "BIND_TYPE", length = 48)
+    var type: String? = null
+
+    @Column(name = "SOURCE", length = 48)
+    var source: String? = null
+
+    @Column(name = "TARGET", length = 48)
+    var target: String? = null
+
+    @Column(name = "PARAM")
+    var param: String? = null
+
+    @Column(name = "REMARK")
+    var remark: String? = null
+
+    @Column(name = "CREATED_AT")
+    var createdAt: LocalDateTime? = null
+
+    @PrePersist
+    fun insertTime() {
+        createdAt = LocalDateTime.now()
+    }
+
+    companion object {
+        private const val serialVersionUID = 1L
+    }
+
+}

+ 92 - 0
services/service-bind/src/main/kotlin/gaf3/core/services/bind/service/BindService.kt

@@ -0,0 +1,92 @@
+package gaf3.core.services.bind.service
+
+import gaf3.core.data.PagedData
+import gaf3.core.exception.BusinessError
+import gaf3.core.services.bind.dao.GafBindItemDAO
+import gaf3.core.services.bind.domain.BatchBindForm
+import gaf3.core.services.bind.domain.BatchBindResult
+import gaf3.core.services.bind.domain.BindForm
+import gaf3.core.services.bind.entity.GafBindItem
+import gaf3.core.util.DataBeanHelper
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.data.domain.Example
+import org.springframework.data.domain.PageRequest
+import org.springframework.data.domain.Sort
+import org.springframework.stereotype.Service
+import org.springframework.util.Assert.*
+import java.util.*
+import javax.transaction.Transactional
+
+@Service("gafBindService")
+class BindService(@Autowired val itemDao: GafBindItemDAO) {
+    fun create(type: String, form: BindForm): GafBindItem {
+        // TODO: 检查数据约束
+        hasText(type, "type不能为空")
+        hasText(form.source, "source不能为空")
+        hasText(form.target, "target不能为空")
+        if(itemDao.exists(type, form.source, form.target)) throw BusinessError(BusinessError.ERR_DATA_EXISTED, "绑定关系已经存在")
+
+        // TODO: 保存数据
+        var entity = GafBindItem()
+        DataBeanHelper.Bean2Obj(form, entity)
+        entity.id = UUID.randomUUID().toString()
+        entity.type = type
+        entity = itemDao.save(entity)
+        return entity
+    }
+
+    @Transactional
+    fun batchCreate(type: String, replace: Boolean, form: BatchBindForm): BatchBindResult {
+        // 参数检查
+        hasText(type, "type不能为空")
+        hasText(form.source, "source不能为空")
+        notEmpty(form.targets, "targets不能为空")
+        form.targets?.all { p -> !p.isBlank() }?.let { isTrue(it, "targets不能包含空项") }
+        val (source,targets) = form
+        val binds = mutableListOf<GafBindItem>()
+        val skips = mutableListOf<GafBindItem>()
+        val result = mutableListOf<GafBindItem>()
+        binds.addAll(itemDao.findByTypeAndSource(type, source))
+
+        // 循环保存数据
+        // 检查绑定数据,确保业务逻辑正确,绑定数据相关appId和userId存入作冗余字段
+        targets?.forEach { target ->
+            val skipped = binds.removeIf { p -> p.target == target && skips.add(p)}
+            if(!skipped) {
+                var entity = GafBindItem()
+                DataBeanHelper.Bean2Obj(form, entity)
+                entity.id = UUID.randomUUID().toString()
+                entity.type = type
+                entity.target = target
+                entity = itemDao.save(entity)
+                result.add(entity)
+            }
+        }
+        // 删除多余数据
+        if(replace) {
+            binds.forEach { p->itemDao.delete(p) }
+        }
+
+        return BatchBindResult(result, skipped = skips, deleted = if(replace) binds else null)
+    }
+
+    fun delete(id: String) {
+        if(!itemDao.existsById(id)) throw BusinessError(BusinessError.ERR_DATA_NOTEXIST)
+        itemDao.deleteById(id)
+    }
+
+    @Transactional
+    fun delete(type: String, source: String? = null, target: String? = null) {
+        itemDao.delete(type, source, target)
+    }
+
+    fun find(type: String, source: String?, target: String?): PagedData<GafBindItem> {
+        val filter = GafBindItem()
+        filter.type = type
+        filter.source = source
+        filter.target = target
+        val example = Example.of(filter)
+        val rs = itemDao.findAll(example, PageRequest.of(0, 500, Sort.Direction.ASC, "createdAt"))
+        return PagedData(rs.content, rs.totalElements.toInt())
+    }
+}

+ 16 - 0
services/service-bind/src/main/resources/META-INF/orm.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entity-mappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
+        http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd" version="2.2">
+
+    <entity class="gaf3.core.services.bind.entity.GafBindItem" metadata-complete="false">
+        <table name="GAF_BIND_ITEM">
+	        <index column-list="BIND_TYPE" name="IDX_GAF_BIND_ITEM_1" />
+			<index column-list="BIND_TYPE, SOURCE" name="IDX_GAF_BIND_ITEM_2" />
+			<index column-list="BIND_TYPE, TARGET" name="IDX_GAF_BIND_ITEM_3" />
+	        <index column-list="BIND_TYPE, SOURCE, TARGET" name="IDX_GAF_BIND_ITEM_4" unique="true" />
+			<index column-list="CREATED_AT" name="IDX_GAF_BIND_ITEM_5" />
+        </table>
+    </entity>
+
+</entity-mappings>

+ 18 - 0
services/service-bind/src/main/resources/application.yml

@@ -0,0 +1,18 @@
+
+spring:
+  datasource:
+    username: ${dbUser}
+    password: ${dbPwd}
+    url: ${dbUrl}
+    driver-class-name: ${dbDriver}
+  jpa:
+    database-platform: ${dbDialect}
+    show-sql: true
+    hibernate:
+      naming:
+        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
+        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
+      ddl-auto: update
+server:
+  port: 8081
+logging.level.gaf3.core.*: DEBUG

+ 7 - 0
services/service-code/README.md

@@ -0,0 +1,7 @@
+# 字典服务
+### 服务逻辑补充说明
+- type字段存储字典类型的code值
+- type字段为空,代表记录为字典类型
+- alias字段是code字段的补充
+  + 对字典类型数据,alias印版为一个英文缩写或拼音,可以在路径参数中替代code
+  + 对字典项数据,alias可以用来实现拼音检索或者其他检索快捷字符

+ 71 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/CodeServiceController.kt

@@ -0,0 +1,71 @@
+package gaf3.core.services.code
+
+import gaf3.core.data.ErrorResult
+import gaf3.core.data.JsonResult
+import gaf3.core.data.PagedData
+import gaf3.core.services.code.domain.CodeForm
+import gaf3.core.services.code.domain.CodeItem
+import gaf3.core.services.code.service.CodeService
+import org.springframework.web.bind.annotation.*
+import javax.validation.Valid
+
+@RestController
+class CodeServiceController(val service: CodeService) {
+//    companion object {
+//        val log: Logger = LoggerFactory.getLogger(UserServiceController::class.java)
+//    }
+
+    @PostMapping(path = ["/gaf/code/types"])
+    fun createType(@Valid @RequestBody data: CodeForm): JsonResult<CodeItem> {
+        val res = service.create("group", data)
+        return JsonResult(res)
+    }
+
+    @GetMapping(path = ["/gaf/code/types"])
+    fun listTypes(): PagedData<CodeItem> {
+        val rs = service.listTypes()
+        return PagedData(rs, rs.size)
+    }
+
+//    @GetMapping(path = ["/gaf/code/{type}/items"])
+//    fun listCodes(@PathVariable type: String): PagedData<CodeItem>{
+//        return service.listCodes(type)
+//    }
+
+    @GetMapping(path = ["/gaf/code/items"])
+    fun findItems(filter: CodeItem, skip: Int? = 0, limit: Int? = 100): PagedData<CodeItem>{
+        return service.find(filter, skip ?: 0, limit ?: 100)
+    }
+
+    @PostMapping(path = ["/gaf/code/items"])
+    fun create(@RequestParam type: String?, @Valid @RequestBody data: CodeForm): CodeItem {
+        return service.create(type, data)
+    }
+
+    @DeleteMapping(path = ["/gaf/code/items/{id}"])
+    fun delete(@PathVariable id: String): ErrorResult {
+        service.delete(id)
+        return ErrorResult(0,"deleted")
+    }
+
+    @PostMapping(path = ["/gaf/code/items/{id}"])
+    fun update(@PathVariable id: String, @RequestBody data: CodeForm): CodeItem{
+        return service.update(id, data)
+    }
+
+    @GetMapping(path = ["/gaf/code/items/{id}"])
+    fun fetch(@PathVariable id: String): CodeItem? {
+        return service.fetch(id)
+    }
+
+    @GetMapping(path = ["/gaf/code/{type}/{code}"])
+    fun fetchByCode(@PathVariable type: String, @PathVariable code: String): CodeItem?{
+        val filter = CodeItem(type = type, code = code)
+        val rs = service.find(filter, 0, 1)
+        return if(rs.total > 0){
+            rs.data!![0]
+        } else {
+            null
+        }
+    }
+}

+ 18 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/CodeStoreController.kt

@@ -0,0 +1,18 @@
+package gaf3.core.services.code
+
+import gaf3.core.data.PagedData
+import gaf3.core.services.code.domain.CodeItem
+import gaf3.core.services.code.service.impl.AutoCodeStore
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+class CodeStoreController(val service: AutoCodeStore) {
+    @GetMapping(path = ["/gaf/code/{type}/items"])
+    fun load(@PathVariable type: String): PagedData<CodeItem> {
+        return service.load(type).let {
+            PagedData(it, it.size)
+        }
+    }
+}

+ 16 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/GafCodeConfiguration.kt

@@ -0,0 +1,16 @@
+package gaf3.core.services.code
+
+import gaf3.core.services.code.dao.GafCodeItemDAO
+import gaf3.core.services.code.entity.GafCodeItem
+import org.springframework.boot.autoconfigure.domain.EntityScan
+import org.springframework.boot.context.properties.EnableConfigurationProperties
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.context.annotation.Configuration
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories
+
+@Configuration
+@EnableJpaRepositories(basePackageClasses=[GafCodeItemDAO::class])
+@EnableConfigurationProperties(GafCodeConfigure::class)
+@EntityScan(basePackageClasses = [GafCodeItem::class])
+@ComponentScan(basePackageClasses = [GafCodeConfiguration::class])
+class GafCodeConfiguration

+ 31 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/GafCodeConfigure.kt

@@ -0,0 +1,31 @@
+package gaf3.core.services.code
+
+
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.core.io.ClassPathResource
+import org.springframework.core.io.Resource
+
+@ConfigurationProperties(prefix = "gaf.code")
+class GafCodeConfigure {
+
+    /**
+     * 默认存储类型
+     */
+    var storeType: StoreType = StoreType.hybrid
+    /**
+     * 默认资源文件路径
+     */
+    var codePath: Resource = ClassPathResource("/code/")
+    /**
+     * 单独指定存储类型
+     */
+    var storeTypes: Map<String, StoreType> = mutableMapOf()
+
+    fun getStoreType(dict: String): StoreType {
+        return storeTypes.get(dict) ?: storeType
+    }
+
+    enum class StoreType {
+        file, db, hybrid
+    }
+}

+ 17 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/ServiceCodeApplication.kt

@@ -0,0 +1,17 @@
+package gaf3.core.services.code
+
+import gaf3.core.cloud.GafCloudConfiguration
+import gaf3.core.services.code.dao.GafCodeItemDAO
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.Import
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories
+import org.springframework.web.bind.annotation.RestController
+
+@SpringBootApplication
+@Import(GafCloudConfiguration::class)
+class ServiceCodeApplication
+
+fun main(args: Array<String>) {
+    runApplication<ServiceCodeApplication>(*args)
+}

+ 35 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/dao/GafCodeItemDAO.kt

@@ -0,0 +1,35 @@
+package gaf3.core.services.code.dao
+
+import gaf3.core.services.code.entity.GafCodeItem
+import org.hibernate.query.criteria.internal.predicate.ExistsPredicate
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.Pageable
+import org.springframework.data.domain.Sort
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.data.jpa.repository.Modifying
+import org.springframework.data.jpa.repository.Query
+import org.springframework.data.repository.query.Param
+
+interface GafCodeItemDAO : JpaRepository<GafCodeItem, String> {
+
+    @Query(value = "select p from GafCodeItem p where p.type = ?1 order by p.code")
+    fun findByType(type: String, page: Pageable): Page<GafCodeItem>
+
+    fun findAllByType(type: String, sort: Sort): List<GafCodeItem>
+
+    @Query(value = "select p from GafCodeItem p where p.type is null or p.type = '' or p.type = 'group' order by p.code")
+    fun listTypes(): List<GafCodeItem>
+
+    fun findOneByTypeAndAlias(type: String, alias: String): GafCodeItem?
+
+    fun findOneByTypeAndCode(type: String, code: String): GafCodeItem?
+
+    fun existsByTypeAndCode(type: String, code: String): Boolean
+
+    @Modifying
+    @Query(value = "update GafCodeItem p set p.status = :status where p.id = :id")
+    fun status(@Param("id") id: String, @Param("status") status: String)
+
+    @Modifying
+    fun deleteAllByType(type: String);
+}

+ 21 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/domain/CodeForm.kt

@@ -0,0 +1,21 @@
+package gaf3.core.services.code.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import javax.validation.constraints.NotEmpty
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class CodeForm @JvmOverloads constructor(code: String? = null, name: String? = null){
+    @NotEmpty
+    var code: String? = null
+    @NotEmpty
+    var name: String? = null
+
+    var status: String? = null
+    var alias: String? = null
+    var remark: String? = null
+
+    init {
+        this.code = code
+        this.name = name
+    }
+}

+ 14 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/domain/CodeItem.kt

@@ -0,0 +1,14 @@
+package gaf3.core.services.code.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class CodeItem @JvmOverloads constructor(
+    var id: String? = null,
+    var type: String? = null,
+    var code: String? = null,
+    var name: String? = null,
+    var alias: String? = null,
+    var status: String? = null,
+    var remark: String? = null
+){}

+ 45 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/entity/GafCodeItem.kt

@@ -0,0 +1,45 @@
+package gaf3.core.services.code.entity
+
+import javax.persistence.*
+import java.io.Serializable
+import java.util.*
+
+
+/**
+ * The persistent class for the GAF_CODE_ITEM database table.
+ *
+ */
+@Entity
+@Table(name = "GAF_CODE_ITEM")
+class GafCodeItem : Serializable {
+
+    @Id
+    @Column(name = "ID", length = 48)
+    var id: String? = null
+
+    @Column(name = "ITEM_TYPE", length = 48)
+    var type: String? = null // 类型:group - 字典分类; {type} - 字典项;
+
+    @Column(name = "CODE", length = 48)
+    var code: String? = null
+
+    @Column(name = "NAME", length = 48)
+    var name: String? = null
+
+    @Column(name = "ALIAS", length = 48)
+    var alias: String? = null
+
+    @Column(name = "STATUS", length = 1)
+    var status: String? = null
+
+    @Column(name = "PRESET")
+    var preset: Int? = null // 是否预设值: 0-否,1-是;系统预设值不可删除
+
+    @Column(name = "REMARK")
+    var remark: String? = null
+
+    companion object {
+        private const val serialVersionUID = 1L
+    }
+
+}

+ 151 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/service/CodeService.kt

@@ -0,0 +1,151 @@
+package gaf3.core.services.code.service
+
+import gaf3.core.data.PagedData
+import gaf3.core.exception.BusinessError
+import gaf3.core.services.code.dao.GafCodeItemDAO
+import gaf3.core.services.code.domain.CodeForm
+import gaf3.core.services.code.domain.CodeItem
+import gaf3.core.services.code.entity.GafCodeItem
+import gaf3.core.util.DataBeanHelper
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.data.domain.Example
+import org.springframework.data.domain.PageRequest
+import org.springframework.data.domain.Sort
+import org.springframework.data.repository.findByIdOrNull
+import org.springframework.stereotype.Service
+import org.springframework.util.Assert.hasText
+import org.springframework.validation.annotation.Validated
+import java.util.*
+import javax.transaction.Transactional
+import javax.validation.Valid
+
+@Service
+@Validated
+class CodeService (@Autowired private val itemDao: GafCodeItemDAO) {
+    @JvmOverloads
+    fun find(filter: CodeItem? = null, skip: Int = 0, limit: Int = 100): PagedData<CodeItem> {
+        val temp = GafCodeItem()
+        DataBeanHelper.Bean2Obj(filter, temp)
+        val example = Example.of(temp)
+        val page:Int = skip/limit
+        val paged = itemDao.findAll(example, PageRequest.of(page, limit, Sort.Direction.ASC, "code"))
+        val rs = paged.content.map {
+            p->convertDomain(p)
+        }
+
+        return PagedData(rs, paged.totalElements.toInt())
+    }
+
+    fun create(type: String?, @Valid form: CodeForm): CodeItem {
+        // 检查约束条件
+        hasText(form.code, "字典代码不能为空")
+        hasText(type, "字典类型不能为空")
+        if(itemDao.existsByTypeAndCode(type!!, form.code!!)) throw BusinessError(BusinessError.ERR_DATA_EXISTED, "字典代码重复")
+
+        // 检查字典类型
+        if("group" != type) {
+           val item = itemDao.findOneByTypeAndCode("group", type) ?: throw BusinessError(BusinessError.ERR_DATA_EXISTED, "字典类型不存在")
+            if (item.preset == 1) throw BusinessError(BusinessError.ERR_SERVICE_FAULT, "系统预置字典不能添加")
+        }
+
+        // 准备数据
+        var data = GafCodeItem()
+        DataBeanHelper.Bean2Obj(form, data)
+        data.id = UUID.randomUUID().toString()
+        data.type = type
+        data.status = form.status ?: "0"
+        data.preset = 0
+
+        // 保存数据
+        data = itemDao.save(data)
+        return convertDomain(data)
+    }
+
+    @Transactional
+    fun delete(id: String) {
+        // TODO: 验证信息
+        hasText(id, "ID不能为空")
+        // 检查数据存在性
+        val entity = itemDao.findByIdOrNull(id) ?: throw BusinessError(BusinessError.ERR_DATA_NOTEXIST)
+        if(entity.preset == 1) {
+            throw BusinessError(BusinessError.ERR_SERVICE_FAULT, "系统预置数据不能删除")
+        }
+        if(entity.type == null || entity.type == "" || entity.type == "group") {
+            // 删除分类下的字典项
+            itemDao.deleteAllByType(entity.code!!)
+        }
+        // 删除数据
+        itemDao.delete(entity)
+    }
+
+    fun update(id: String, form: CodeForm): CodeItem {
+        // TODO: 验证信息
+        hasText(id, "ID不能为空")
+
+        // TODO: 设置修改的属性
+        var entity = itemDao.findByIdOrNull(id) ?: throw BusinessError(BusinessError.ERR_DATA_NOTEXIST, "字典数据不存在")
+        // 准备数据,禁止修改type、code、preset
+        DataBeanHelper.Bean2Obj(form, entity, "type", "code", "preset")
+
+        entity = itemDao.save(entity)
+        return convertDomain(entity)
+    }
+
+    fun fetch(id: String): CodeItem? {
+        // 验证信息
+        hasText(id, "ID不能为空")
+
+        val entity = itemDao.findByIdOrNull(id) ?: return null
+        return convertDomain(entity)
+    }
+
+    fun fetchByCode(type: String, code: String): CodeItem? {
+        // TODO: 验证信息
+        hasText(type, "type不能为空")
+        hasText(code, "code不能为空")
+        val entity = itemDao.findOneByTypeAndCode(type, code) ?: return null
+        return convertDomain(entity)
+    }
+
+    fun listTypes(): List<CodeItem> {
+        return itemDao.listTypes().map {
+            p->
+            convertDomain(p)
+        }
+    }
+
+    @JvmOverloads
+    fun listCodes(type: String, limit: Int = 2000): PagedData<CodeItem> {
+        val size = if(limit > 5000) 5000 else limit
+        val item = itemDao.findOneByTypeAndCode("", type) ?: itemDao.findOneByTypeAndCode("group", type)
+                ?: itemDao.findOneByTypeAndAlias("", type) ?: itemDao.findOneByTypeAndAlias("group", type)
+                ?: throw BusinessError(BusinessError.ERR_BUSINESS, "字典类型不存在")
+        val paged = itemDao.findByType(item.code ?: item.name ?: item.id!!, PageRequest.of(0, size, Sort.Direction.ASC, "code"))
+        val rs = paged.content.map {
+            p->convertDomain(p)
+        }
+
+        return PagedData(rs, paged.totalElements.toInt())
+    }
+
+//    fun exists(type: String?, code: String): Boolean {
+//        val filter = GafCodeItem()
+//        filter.type = type
+//        filter.code = code
+//        val fields = GafCodeItem::class.memberProperties.map { p-> p.name }
+//        val ignorePaths = fields.subtract(listOf("type", "code")).toTypedArray()
+//        val matcher = ExampleMatcher.matching()
+//                .withIgnorePaths(*ignorePaths)
+//                .withIncludeNullValues()
+//        val example = Example.of(filter, matcher)
+//        return itemDao.exists(example)
+//    }
+
+    companion object {
+        fun convertDomain(entity: GafCodeItem): CodeItem {
+            val data = CodeItem()
+            DataBeanHelper.Bean2Obj(entity, data)
+            return data
+        }
+    }
+}

+ 12 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/service/GafCodeStore.kt

@@ -0,0 +1,12 @@
+package gaf3.core.services.code.service
+
+import gaf3.core.services.code.domain.CodeItem
+
+interface GafCodeStore {
+
+    /**
+     * 加载字典数据
+     */
+    fun load(type: String): List<CodeItem>
+
+}

+ 21 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/service/impl/AutoCodeStore.kt

@@ -0,0 +1,21 @@
+package gaf3.core.services.code.service.impl
+
+import gaf3.core.services.code.GafCodeConfigure
+import gaf3.core.services.code.domain.CodeItem
+import gaf3.core.services.code.service.GafCodeStore
+import org.springframework.stereotype.Service
+
+@Service("autoCodeStore")
+class AutoCodeStore(val config: GafCodeConfigure,
+                    val fileStore: FileCodeStore,
+                    val dbStore: DBCodeStore,
+                    val hybridStore: HybridCodeStore) : GafCodeStore {
+    override fun load(type: String): List<CodeItem> {
+        val service: GafCodeStore = when (config.getStoreType(type)) {
+            GafCodeConfigure.StoreType.db -> dbStore
+            GafCodeConfigure.StoreType.file -> fileStore
+            else -> hybridStore
+        }
+        return service.load(type)
+    }
+}

+ 24 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/service/impl/DBCodeStore.kt

@@ -0,0 +1,24 @@
+package gaf3.core.services.code.service.impl
+
+import gaf3.core.exception.BusinessError
+import gaf3.core.services.code.dao.GafCodeItemDAO
+import gaf3.core.services.code.domain.CodeItem
+import gaf3.core.services.code.service.CodeService
+import gaf3.core.services.code.service.GafCodeStore
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
+import org.springframework.data.domain.Sort
+import org.springframework.stereotype.Service
+
+@Service("dbCodeStore")
+//@ConditionalOnProperty(prefix = "gaf.code", name = ["store-type"], havingValue = "db", matchIfMissing = true)
+class DBCodeStore (@Autowired private val itemDao: GafCodeItemDAO): GafCodeStore {
+    override fun load(type: String): List<CodeItem> {
+        val item = itemDao.findOneByTypeAndAlias("group", type) ?: itemDao.findOneByTypeAndCode("group", type)
+        ?: throw BusinessError(BusinessError.ERR_BUSINESS, "字典类型不存在")
+        val rs = itemDao.findAllByType(item.code ?: item.name ?: item.id!!, Sort.by(Sort.Direction.ASC, "code"))
+        return rs.map {
+            p->CodeService.convertDomain(p)
+        }
+    }
+}

+ 0 - 0
services/service-code/src/main/kotlin/gaf3/core/services/code/service/impl/FileCodeStore.kt


Vissa filer visades inte eftersom för många filer har ändrats