diff --git a/.gitignore b/.gitignore index cda3d0a..e88b371 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ -config/ -gradle/ +/config +/gradle ### IntelliJ IDEA ### .idea diff --git a/vertx-demo/src/main/kotlin/app/config/Constant.kt b/vertx-demo/src/main/kotlin/app/config/Constant.kt new file mode 100644 index 0000000..fd90e50 --- /dev/null +++ b/vertx-demo/src/main/kotlin/app/config/Constant.kt @@ -0,0 +1,7 @@ +package app.config + +class Constant { + companion object { + const val USER = "user:" + } +} diff --git a/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt b/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt new file mode 100644 index 0000000..2f9ac5a --- /dev/null +++ b/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt @@ -0,0 +1,93 @@ +package app.config + +import app.config.auth.JWTAuthProvider +import cn.hutool.core.lang.Snowflake +import cn.hutool.core.util.IdUtil +import com.google.inject.* +import com.google.inject.name.Names +import io.vertx.core.Vertx +import io.vertx.core.http.HttpServer +import io.vertx.core.http.HttpServerOptions +import io.vertx.ext.auth.jwt.JWTAuth +import io.vertx.mysqlclient.MySQLBuilder +import io.vertx.mysqlclient.MySQLConnectOptions +import io.vertx.pgclient.PgBuilder +import io.vertx.pgclient.PgConnectOptions +import io.vertx.sqlclient.Pool +import io.vertx.sqlclient.PoolOptions +import io.vertx.sqlclient.SqlClient +import kotlinx.coroutines.CoroutineScope +import org.aikrai.vertx.config.Config +import org.aikrai.vertx.config.DefaultScope +import org.aikrai.vertx.db.tx.TxMgrHolder.initTxMgr + +object InjectConfig { + suspend fun configure(vertx: Vertx): Injector { + Config.init(vertx) + return Guice.createInjector(InjectorModule(vertx)) + } +} + +class InjectorModule( + private val vertx: Vertx, +) : AbstractModule() { + override fun configure() { + val pool = getDbPool().also { initTxMgr(it) } + val coroutineScope = DefaultScope(vertx) + + for ((key, value) in Config.getConfigMap()) { + bind(String::class.java).annotatedWith(Names.named(key)).toInstance(value.toString()) + } + bind(Vertx::class.java).toInstance(vertx) + bind(CoroutineScope::class.java).toInstance(coroutineScope) + bind(HttpServer::class.java).toInstance(vertx.createHttpServer(HttpServerOptions())) + bind(Snowflake::class.java).toInstance(IdUtil.getSnowflake()) + bind(JWTAuth::class.java).toProvider(JWTAuthProvider::class.java).`in`(Singleton::class.java) + + // 绑定 DbPool 为单例 + bind(Pool::class.java).toInstance(pool) + bind(SqlClient::class.java).toInstance(pool) + } + + private fun getDbPool(): Pool { +// val type = configMap["databases.type"].toString() +// val name = configMap["databases.name"].toString() +// val host = configMap["databases.host"].toString() +// val port = configMap["databases.port"].toString() +// val user = configMap["databases.username"].toString() +// val password = configMap["databases.password"].toString() +// val dbMap = Config.getKey("databases") as Map + val type = Config.getKey("databases.type").toString() + val name = Config.getKey("databases.name").toString() + val host = Config.getKey("databases.host").toString() + val port = Config.getKey("databases.port").toString() + val user = Config.getKey("databases.username").toString() + val password = Config.getKey("databases.password").toString() + + val poolOptions = PoolOptions().setMaxSize(10) + val pool = when (type.lowercase()) { + "mysql" -> { + val clientOptions = MySQLConnectOptions() + .setHost(host) + .setPort(port.toInt()) + .setDatabase(name) + .setUser(user) + .setPassword(password) + .setTcpKeepAlive(true) + MySQLBuilder.pool().connectingTo(clientOptions).with(poolOptions).using(vertx).build() + } + "postgre", "postgresql" -> { + val clientOptions = PgConnectOptions() + .setHost(host) + .setPort(port.toInt()) + .setDatabase(name) + .setUser(user) + .setPassword(password) + .setTcpKeepAlive(true) + PgBuilder.pool().connectingTo(clientOptions).with(poolOptions).using(vertx).build() + } + else -> throw IllegalArgumentException("Unsupported database type: $type") + } + return pool + } +} diff --git a/vertx-demo/src/main/kotlin/app/config/auth/AuthHandler.kt b/vertx-demo/src/main/kotlin/app/config/auth/AuthHandler.kt new file mode 100644 index 0000000..e9d9307 --- /dev/null +++ b/vertx-demo/src/main/kotlin/app/config/auth/AuthHandler.kt @@ -0,0 +1,31 @@ +package app.config.auth + +import app.config.Constant +import app.domain.user.UserRepository +import app.util.CacheUtil +import com.google.inject.Inject +import io.vertx.ext.auth.jwt.JWTAuth +import org.aikrai.vertx.auth.Attributes +import org.aikrai.vertx.auth.AuthUser +import org.aikrai.vertx.auth.Principal +import org.aikrai.vertx.auth.TokenUtil + +class AuthHandler @Inject constructor( + private val jwtAuth: JWTAuth, + private val userRepository: UserRepository, + private val cacheUtil: CacheUtil +) { + + suspend fun handle(token: String): AuthUser? { + val userInfo = TokenUtil.authenticate(jwtAuth, token) ?: return null + val userId = userInfo.principal().getString("id").toLong() + val user = cacheUtil.get(Constant.USER + userId) ?: userRepository.get(userId)?.let { + cacheUtil.put(Constant.USER + userId, it) + } ?: return null + return AuthUser( + Principal(userId, user), + // get roles and permissions from database + Attributes(setOf("admin"), setOf("user:list")), + ) + } +} diff --git a/vertx-demo/src/main/kotlin/app/config/auth/JWTAuthProvider.kt b/vertx-demo/src/main/kotlin/app/config/auth/JWTAuthProvider.kt new file mode 100644 index 0000000..ab096b3 --- /dev/null +++ b/vertx-demo/src/main/kotlin/app/config/auth/JWTAuthProvider.kt @@ -0,0 +1,24 @@ +package app.config.auth + +import com.google.inject.Inject +import com.google.inject.Provider +import com.google.inject.name.Named +import io.vertx.core.Vertx +import io.vertx.ext.auth.PubSecKeyOptions +import io.vertx.ext.auth.jwt.JWTAuth +import io.vertx.ext.auth.jwt.JWTAuthOptions + +class JWTAuthProvider @Inject constructor( + private val vertx: Vertx, + @Named("jwt.key") private val key: String +) : Provider { + override fun get(): JWTAuth { + val options = JWTAuthOptions() + .addPubSecKey( + PubSecKeyOptions() + .setAlgorithm("HS256") + .setBuffer(key) + ) + return JWTAuth.create(vertx, options) + } +} diff --git a/vertx-demo/src/main/kotlin/app/config/auth/JwtAuthenticationHandler.kt b/vertx-demo/src/main/kotlin/app/config/auth/JwtAuthenticationHandler.kt new file mode 100644 index 0000000..c0bbd6c --- /dev/null +++ b/vertx-demo/src/main/kotlin/app/config/auth/JwtAuthenticationHandler.kt @@ -0,0 +1,56 @@ +package app.config.auth + +import io.vertx.ext.web.RoutingContext +import io.vertx.ext.web.handler.AuthenticationHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.aikrai.vertx.utlis.Meta + +class JwtAuthenticationHandler( + private val coroutineScope: CoroutineScope, + private val authHandler: AuthHandler, + private val context: String, +) : AuthenticationHandler { + + var exclude = mutableListOf( + "/auth/**", + ) + + override fun handle(event: RoutingContext) { + val path = event.request().path().replace("$context/", "/").replace("//", "/") + if (isPathExcluded(path, exclude)) { + event.next() + return + } + + val authorization = event.request().getHeader("Authorization") ?: null + if (authorization == null || !authorization.startsWith("token ")) { + event.fail(401, Meta.unauthorized("无效Token")) + return + } + + val token = authorization.substring(6) + + coroutineScope.launch { + val authUser = authHandler.handle(token) + if (authUser != null) { + event.setUser(authUser) + event.next() + } else { + event.fail(401, Meta.unauthorized("token")) + } + } + } + + private fun isPathExcluded(path: String, excludePatterns: List): Boolean { + for (pattern in excludePatterns) { + val regexPattern = pattern + .replace("**", ".+") + .replace("*", "[^/]+") + .replace("?", ".") + val isExclude = path.matches(regexPattern.toRegex()) + if (isExclude) return true + } + return false + } +} diff --git a/vertx-demo/src/main/resources/config/dev/application-database.yml b/vertx-demo/src/main/resources/config/dev/application-database.yml new file mode 100644 index 0000000..a9dc41d --- /dev/null +++ b/vertx-demo/src/main/resources/config/dev/application-database.yml @@ -0,0 +1,9 @@ +databases: + has-open: true + type: mysql + driver-class-name: com.mysql.jdbc.Driver + name: vertx-demo + host: 127.0.0.1 + port: 3306 + username: root + password: 123456 diff --git a/vertx-demo/src/main/resources/config/prod/application-database.yml b/vertx-demo/src/main/resources/config/prod/application-database.yml new file mode 100644 index 0000000..a9dc41d --- /dev/null +++ b/vertx-demo/src/main/resources/config/prod/application-database.yml @@ -0,0 +1,9 @@ +databases: + has-open: true + type: mysql + driver-class-name: com.mysql.jdbc.Driver + name: vertx-demo + host: 127.0.0.1 + port: 3306 + username: root + password: 123456 diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt new file mode 100644 index 0000000..1723e9c --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt @@ -0,0 +1,92 @@ +package org.aikrai.vertx.config + +import io.vertx.config.ConfigRetriever +import io.vertx.config.ConfigRetrieverOptions +import io.vertx.config.ConfigStoreOptions +import io.vertx.core.Vertx +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import io.vertx.kotlin.coroutines.coAwait +import org.aikrai.vertx.utlis.FlattenUtil +import java.util.concurrent.atomic.AtomicReference + +object Config { + private val retriever = AtomicReference(null) + private var configMap = emptyMap() + + suspend fun init(vertx: Vertx) { + if (retriever.get() != null) return + val configRetriever = load(vertx) + val cas = retriever.compareAndSet(null, configRetriever) + if (cas) { + val configObj = configRetriever.config.coAwait() + configMap = FlattenUtil.flattenJsonObject(configObj) + } + } + + fun getKey(key: String): Any? { + if (retriever.get() == null) throw IllegalStateException("Config not initialized") + // 检查 configMap 中是否存在指定的 key + return if (configMap.containsKey(key)) { + configMap[key] + } else { + // 找到所有以 key 开头的条目 + configMap.filterKeys { it.startsWith(key) } + } + } + + fun getConfigMap(): Map { + return configMap + } + + private suspend fun load(vertx: Vertx): ConfigRetriever { + val sysStore = ConfigStoreOptions().setType("sys") + val envStore = ConfigStoreOptions().setType("env").setConfig(JsonObject().put("raw-data", true)) + val bootstrapStore = ConfigStoreOptions().setType("file").setFormat("yaml") + .setConfig(JsonObject().put("path", "bootstrap.yml")) + + val bootstrapOptions = ConfigRetrieverOptions() + .addStore(bootstrapStore) + .addStore(sysStore) + .addStore(envStore) + + val bootstrapRetriever = ConfigRetriever.create(vertx, bootstrapOptions) + val bootstrapConfig = bootstrapRetriever.config.coAwait() + val useDir = bootstrapConfig.getString("user.dir") + val environment = bootstrapConfig.getJsonObject("server").getString("active") + + // 创建资源目录配置存储 + val rDirectoryStore = createDirectoryStore("$useDir/src/main/resources/config") + // 创建项目根目录配置存储 + val pDirectoryStore = createDirectoryStore(useDir) + // 创建环境相关配置存储 + val directoryStore = createDirectoryStore("config${if (!environment.isNullOrBlank()) "/$environment" else ""}") + + // 后加载的配置会覆盖前面加载的相同的配置 + val options = ConfigRetrieverOptions() + // 项目的resources目录下 + .addStore(rDirectoryStore) + // 项目根目录下 + .addStore(pDirectoryStore) + // 项目根目录下的config目录 + .addStore(directoryStore) + // bootstrap.yml 文件 + .addStore(bootstrapStore) + return ConfigRetriever.create(vertx, options) + } + + private fun createDirectoryStore(path: String): ConfigStoreOptions { + return ConfigStoreOptions() + .setType("directory") + .setConfig( + JsonObject().put("path", path).put( + "filesets", + JsonArray() + .add(JsonObject().put("pattern", "*.yml").put("format", "yaml")) + .add(JsonObject().put("pattern", "*.yaml").put("format", "yaml")) + .add(JsonObject().put("pattern", "*.properties").put("format", "properties")) + .add(JsonObject().put("pattern", "*.json").put("format", "json")) + ) + ) + } +} diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/DefaultScope.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/DefaultScope.kt new file mode 100644 index 0000000..10a947e --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/DefaultScope.kt @@ -0,0 +1,15 @@ +package org.aikrai.vertx.config + +import io.vertx.core.Vertx +import io.vertx.kotlin.coroutines.dispatcher +import kotlinx.coroutines.CoroutineScope +import kotlin.coroutines.CoroutineContext +import kotlin.properties.Delegates + +class DefaultScope(private val vertx: Vertx) : CoroutineScope { + override var coroutineContext: CoroutineContext by Delegates.notNull() + + init { + coroutineContext = vertx.orCreateContext.dispatcher() + } +} diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/FailureParser.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/FailureParser.kt new file mode 100644 index 0000000..82faec2 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/FailureParser.kt @@ -0,0 +1,47 @@ +package org.aikrai.vertx.config + +import io.vertx.mysqlclient.MySQLException +import io.vertx.pgclient.PgException +import org.aikrai.vertx.jackson.JsonUtil +import org.aikrai.vertx.utlis.Meta +import java.sql.SQLException + +object FailureParser { + private fun Throwable.toMeta(): Meta { + val error = this as? Meta + if (error != null) { + return error + } + + val name = javaClass.simpleName + + val pgException = this as? PgException + if (pgException != null) { + return Meta(name, pgException.errorMessage ?: "", null) + } + + val mysqlException = this as? MySQLException + if (mysqlException != null) { + return Meta(name, mysqlException.message ?: "", null) + } + + val message = if (message != null) message.orEmpty() else toString() + return Meta(name, message, null) + } + + private fun Throwable.info(): String { + if (this is Meta) { + return JsonUtil.toJsonStr(this) + } + return stackTraceToString() + } + + data class Failure(val statusCode: Int, val response: Meta) + + fun parse(statusCode: Int, error: Throwable): Failure { + return when (error) { + is SQLException -> Failure(statusCode, Meta.failure(error.javaClass.name, "执行错误")) + else -> Failure(statusCode, error.toMeta()) + } + } +}