From af3746fe80cc04da362666dff0ab28fdf028a30a Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 28 Sep 2023 01:35:41 +0800 Subject: [PATCH 01/24] Add dependencies --- pom.xml | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/pom.xml b/pom.xml index ee5348b..63b530a 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,11 @@ runtime true + + com.mysql + mysql-connector-j + runtime + org.projectlombok lombok @@ -60,6 +65,50 @@ spring-security-test test + + + com.baomidou + mybatis-plus-boot-starter + 3.5.3.1 + + + com.baomidou + mybatis-plus-boot-starter-test + 3.5.3.1 + test + + + com.alibaba + druid-spring-boot-starter + 1.2.16 + + + com.baomidou + mybatis-plus-generator + 3.5.3.1 + + + org.apache.velocity + velocity-engine-core + 2.3 + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.1.0 + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-actuator + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + @@ -85,6 +134,21 @@ org.jetbrains.kotlin kotlin-maven-plugin + + + compile + compile + + compile + + + + src/main/kotlin + target/generated-sources/annotations + + + + -Xjsr305=strict -- 2.49.1 From ec38de33f5c898432b6abd08936af88ac39fd9d1 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 28 Sep 2023 01:36:16 +0800 Subject: [PATCH 02/24] Add @EnableTransactionManagement --- src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt index f77dbd7..d3741f1 100644 --- a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt +++ b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt @@ -2,8 +2,10 @@ package top.fatweb.api import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.transaction.annotation.EnableTransactionManagement @SpringBootApplication +@EnableTransactionManagement class FatWebApiApplication fun main(args: Array) { -- 2.49.1 From 6be65c4496e98bad174ef5ac7b6e73c756f33492 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 28 Sep 2023 01:36:24 +0800 Subject: [PATCH 03/24] Add RedisUtil --- .../kotlin/top/fatweb/api/utils/RedisUtil.kt | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/main/kotlin/top/fatweb/api/utils/RedisUtil.kt diff --git a/src/main/kotlin/top/fatweb/api/utils/RedisUtil.kt b/src/main/kotlin/top/fatweb/api/utils/RedisUtil.kt new file mode 100644 index 0000000..426b772 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/utils/RedisUtil.kt @@ -0,0 +1,182 @@ +package top.fatweb.api.utils + +import org.springframework.data.redis.core.BoundSetOperations +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit + +@Suppress("UNCHECKED_CAST") +@Component +class RedisUtil(private val redisTemplate: RedisTemplate) { + /** + * 设置有效时间 + * + * @param key 缓存的键 + * @param timeout 超时时间 + * @param timeUnit 时间颗粒度 + * @return true=设置成功;false=设置失败 + */ + fun setExpire(key: String, timeout: Long, timeUnit: TimeUnit = TimeUnit.SECONDS) = + redisTemplate.expire(key, timeout, timeUnit) + + /** + * 获取有效时间 + * + * @param key 缓存的键 + * @return 有效时间 + */ + fun getExpire(key: String, timeUnit: TimeUnit = TimeUnit.SECONDS) = redisTemplate.getExpire(key, timeUnit) + + /** + * 判断 key 是否存在 + * + * @param key 缓存的键 + * @return true=存在; false=不存在 + */ + fun hasKey(key: String) = redisTemplate.hasKey(key) + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + fun keys(pattern: String): Set = redisTemplate.keys(pattern) + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键 + * @param value 缓存的值 + */ + fun setObject(key: String, value: Any) = redisTemplate.opsForValue().set(key, value) + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键 + * @param value 缓存的值 + * @param timeout 超时时间 + * @param timeUnit 时间颗粒度 + */ + fun setObject(key: String, value: Any, timeout: Long, timeUnit: TimeUnit = TimeUnit.SECONDS) = + redisTemplate.opsForValue().set(key, value, timeout, timeUnit) + + + /** + * 获得缓存的基本对象 + * + * @param key 缓存的键 + * @return 缓存的值 + */ + fun getObject(key: String) = redisTemplate.opsForValue().get(key) as? T + + /** + * 删除单个对象 + * + * @param key 缓存的键 + * @return true=删除成功;false=删除失败 + */ + fun delObject(key: String) = redisTemplate.delete(key) + + /** + * 删除对象集合 + * + * @param collection 键集合 + * @return 删除个数 + */ + fun delObject(collection: Collection) = redisTemplate.delete(collection) + + /** + * 缓存 List 数据 + * + * @param key 缓存的键 + * @param dataList 缓存的 List 数据 + * @return 缓存的个数 + */ + fun setList(key: String, dataList: List) = redisTemplate.opsForList().rightPushAll(key, dataList) + + /** + * 获得缓存的 List 数据 + * + * @param key 缓存的键 + * @return 缓存的键对应的 List 数据 + */ + fun getList(key: String): List? = redisTemplate.opsForList().range(key, 0, -1) as? List + + /** + * 缓存 Set 数据 + * + * @param key 缓存的键 + * @param dataSet 缓存的 Set 数据 + * @return 缓存数据的对象 + */ + fun setSet(key: String, dataSet: Set): BoundSetOperations { + val boundSetOps: BoundSetOperations = redisTemplate.boundSetOps(key) + for (data in dataSet) { + boundSetOps.add(data) + } + return boundSetOps + } + + /** + * 获得缓存的 Set 数据 + * + * @param key 缓存的键 + * @return 缓存的键对应的 Set 数据 + */ + fun getSet(key: String): Set? = redisTemplate.opsForSet().members(key) as? Set + + /** + * 缓存 Map 数据 + * + * @param key 缓存的键 + * @param dataMap 缓存的 Map 数据 + */ + fun setMap(key: String, dataMap: Map) = redisTemplate.opsForHash().putAll(key, dataMap) + + /** + * 获得缓存的 Map 数据 + * + * @param key 缓存的键 + * @return 缓存的键对应的 Map 数据 + */ + fun getMap(key: String): Map? = redisTemplate.opsForHash().entries(key) as? Map + + /** + * 往 Hash 中存入数据 + * + * @param key Redis 键 + * @param hKey Hash 键 + * @param value 值 + */ + fun setMapValue(key: String, hKey: String, value: Any) = + redisTemplate.opsForHash().put(key, hKey, value) + + /** + * 获取 Hash 中的数据 + * + * @param key Redis 键 + * @param hKey Hash 键 + * @return Hash 中的对象 + */ + fun getMapValue(key: String, hKey: String) = redisTemplate.opsForHash().get(key, hKey) + + /** + * 删除 Hash 中的数据 + * + * @param key Redis 键 + * @param hKey Hash 键 + */ + fun delMapValue(key: String, hKey: String) = redisTemplate.opsForHash().delete(key, hKey) + + /** + * 获取多个 Hash 中的数据 + * + * @param key Redis 键 + * @param hKeys Hash 键集合 + * @return Hash 对象集合 + */ + fun getMultiMapValue(key: String, hKeys: Collection): List = + redisTemplate.opsForHash().multiGet(key, hKeys) +} \ No newline at end of file -- 2.49.1 From ef3810c55f03673585565f14a763eb5ca9458677 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 28 Sep 2023 10:59:25 +0800 Subject: [PATCH 04/24] Rename FatwebApiApplicationTests to FatWebApiApplicationTests --- .../top/fatweb/api/FatwebApiApplicationTests.kt | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/test/kotlin/top/fatweb/api/FatwebApiApplicationTests.kt diff --git a/src/test/kotlin/top/fatweb/api/FatwebApiApplicationTests.kt b/src/test/kotlin/top/fatweb/api/FatwebApiApplicationTests.kt deleted file mode 100644 index 3c32b03..0000000 --- a/src/test/kotlin/top/fatweb/api/FatwebApiApplicationTests.kt +++ /dev/null @@ -1,13 +0,0 @@ -package top.fatweb.api - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class FatwebApiApplicationTests { - - @Test - fun contextLoads() { - } - -} -- 2.49.1 From c57064d0f80f2fe497f6f746f9eb871049dc5d6d Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 28 Sep 2023 10:59:48 +0800 Subject: [PATCH 05/24] Rename FatwebApiApplicationTests to FatWebApiApplicationTests --- .../top/fatweb/api/FatWebApiApplicationTests.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt diff --git a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt new file mode 100644 index 0000000..a4324ff --- /dev/null +++ b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt @@ -0,0 +1,13 @@ +package top.fatweb.api + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class FatWebApiApplicationTests { + + @Test + fun contextLoads() { + } + +} -- 2.49.1 From 91555b8160e14175c3424a351b79df1062776a41 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 28 Sep 2023 18:02:06 +0800 Subject: [PATCH 06/24] Add configs and properties --- .gitignore | 1 + pom.xml | 4 ++- .../top/fatweb/api/FatWebApiApplication.kt | 12 ++++++- .../fatweb/api/config/MybatisPlusConfig.kt | 20 +++++++++++ .../top/fatweb/api/config/RedisConfig.kt | 34 +++++++++++++++++++ .../top/fatweb/api/config/SwaggerConfig.kt | 19 +++++++++++ .../fatweb/api/constants/SecurityConstants.kt | 29 ++++++++++++++++ .../fatweb/api/constants/ServerConstants.kt | 17 ++++++++++ .../top/fatweb/api/filter/ExceptionFilter.kt | 15 ++++++++ .../filter/JwtAuthenticationTokenFilter.kt | 26 ++++++++++++++ .../kotlin/top/fatweb/api/utils/JwtUtil.kt | 5 +++ .../resources/application-config-template.yml | 32 +++++++++++++++++ src/main/resources/application.yml | 14 ++++++++ .../fatweb/api/FatWebApiApplicationTests.kt | 4 +++ 14 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/top/fatweb/api/config/MybatisPlusConfig.kt create mode 100644 src/main/kotlin/top/fatweb/api/config/RedisConfig.kt create mode 100644 src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt create mode 100644 src/main/kotlin/top/fatweb/api/constants/SecurityConstants.kt create mode 100644 src/main/kotlin/top/fatweb/api/constants/ServerConstants.kt create mode 100644 src/main/kotlin/top/fatweb/api/filter/ExceptionFilter.kt create mode 100644 src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt create mode 100644 src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt create mode 100644 src/main/resources/application-config-template.yml diff --git a/.gitignore b/.gitignore index 549e00a..ab4ff27 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ build/ ### VS Code ### .vscode/ +/application-config.yml diff --git a/pom.xml b/pom.xml index 63b530a..bff9773 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,8 @@ 17 1.8.22 + ${maven.build.timestamp} + yyyy-MM-dd'T'HH:mm:ss @@ -80,7 +82,7 @@ com.alibaba druid-spring-boot-starter - 1.2.16 + 1.2.19 com.baomidou diff --git a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt index d3741f1..5dbcf42 100644 --- a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt +++ b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt @@ -1,13 +1,23 @@ package top.fatweb.api +import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.transaction.annotation.EnableTransactionManagement +import java.io.File @SpringBootApplication @EnableTransactionManagement class FatWebApiApplication fun main(args: Array) { - runApplication(*args) + val logger = LoggerFactory.getLogger("main") + + if (File("application-config.yml").exists()) { + runApplication(*args) + } else { + logger.warn("File ‘application.yml’ cannot be found in the running path. The configuration file template 'application.example.yml' has been created. Please change the configuration file content and rename it to 'application.yml', and then restart the server.") + FatWebApiApplication::class.java.getResource("/application-config-template.yml")?.readText() + ?.let { File("application-config.example.yml").writeText(it) } + } } diff --git a/src/main/kotlin/top/fatweb/api/config/MybatisPlusConfig.kt b/src/main/kotlin/top/fatweb/api/config/MybatisPlusConfig.kt new file mode 100644 index 0000000..2ce739c --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/config/MybatisPlusConfig.kt @@ -0,0 +1,20 @@ +package top.fatweb.api.config + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class MybatisPlusConfig { + @Bean + fun mybatisPlusInterceptor(): MybatisPlusInterceptor { + val mybatisPlusInterceptor = MybatisPlusInterceptor() + mybatisPlusInterceptor.addInnerInterceptor(OptimisticLockerInnerInterceptor()) + mybatisPlusInterceptor.addInnerInterceptor(PaginationInnerInterceptor()) + + return mybatisPlusInterceptor + } + +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt b/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt new file mode 100644 index 0000000..fd13a65 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt @@ -0,0 +1,34 @@ +package top.fatweb.api.config + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer +import org.springframework.data.redis.serializer.StringRedisSerializer + +@Configuration +class RedisConfig { + @Bean + fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<*, *> { + val redisTemplate = RedisTemplate() + redisTemplate.connectionFactory = redisConnectionFactory + val stringRedisSerializer = StringRedisSerializer() + val objectMapper = ObjectMapper().registerModules(JavaTimeModule()) + val anyJackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(objectMapper, Any::class.java) + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + redisTemplate.keySerializer = stringRedisSerializer + redisTemplate.valueSerializer = anyJackson2JsonRedisSerializer + + // Hash的key也采用StringRedisSerializer的序列化方式 + redisTemplate.hashKeySerializer = stringRedisSerializer + redisTemplate.hashValueSerializer = anyJackson2JsonRedisSerializer + + redisTemplate.afterPropertiesSet() + + return redisTemplate + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt b/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt new file mode 100644 index 0000000..6ce83a6 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt @@ -0,0 +1,19 @@ +package top.fatweb.api.config + +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Contact +import io.swagger.v3.oas.models.info.Info +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import top.fatweb.api.constants.ServerConstants + +@Configuration +class SwaggerConfig { + + @Bean + fun customOpenAPI(): OpenAPI? { + val contact = Contact().name("FatttSnake").url("https://fatweb.top").email("fatttsnake@fatweb.top") + return OpenAPI().info(Info().title("FatWeb API 文档").description("FatWeb 后端 API 文档,包含各个 Controller 调用信息").contact(contact).version( + ServerConstants.version)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/constants/SecurityConstants.kt b/src/main/kotlin/top/fatweb/api/constants/SecurityConstants.kt new file mode 100644 index 0000000..e92897a --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/constants/SecurityConstants.kt @@ -0,0 +1,29 @@ +package top.fatweb.api.constants + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.stereotype.Component +import java.security.MessageDigest +import java.util.concurrent.TimeUnit + +@Component +@ConfigurationProperties("app.security") +object SecurityConstants { + var headerString = "Authorization" + + var tokenPrefix = "Bearer " + + var jwtTtl = 2L + + var jwtTtlUnit = TimeUnit.HOURS + + lateinit var jwtKey: String + + private fun ByteArray.hex(): String { + return joinToString("") { "%02X".format(it) } + } + + private fun String.md5(): String { + val bytes = MessageDigest.getInstance("MD5").digest(this.toByteArray()) + return bytes.hex() + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/constants/ServerConstants.kt b/src/main/kotlin/top/fatweb/api/constants/ServerConstants.kt new file mode 100644 index 0000000..adabb4e --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/constants/ServerConstants.kt @@ -0,0 +1,17 @@ +package top.fatweb.api.constants + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.stereotype.Component +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime + +@Component +@ConfigurationProperties("app") +object ServerConstants { + lateinit var version: String + + lateinit var buildTime: String + + fun buildZoneDateTime(zoneId: ZoneId = ZoneId.systemDefault()): ZonedDateTime = LocalDateTime.parse(buildTime).atZone(ZoneId.of("UTC")).withZoneSameInstant(zoneId) +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/filter/ExceptionFilter.kt b/src/main/kotlin/top/fatweb/api/filter/ExceptionFilter.kt new file mode 100644 index 0000000..4fcfbc2 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/filter/ExceptionFilter.kt @@ -0,0 +1,15 @@ +package top.fatweb.api.filter + +import jakarta.servlet.* +import java.lang.Exception + +class ExceptionFilter : Filter { + override fun doFilter(servletRequest: ServletRequest?, servletResponse: ServletResponse?, filterChain: FilterChain?) { + try { + filterChain!!.doFilter(servletRequest, servletResponse) + } catch (e: Exception) { + servletRequest?.setAttribute("filter.error", e) + servletRequest?.getRequestDispatcher("/error/thrown")?.forward(servletRequest, servletResponse) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt b/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt new file mode 100644 index 0000000..9319248 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt @@ -0,0 +1,26 @@ +package top.fatweb.api.filter + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.util.StringUtils +import org.springframework.web.filter.OncePerRequestFilter +import top.fatweb.api.constants.SecurityConstants +import top.fatweb.api.utils.RedisUtil + +class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val token = request.getHeader(SecurityConstants.headerString) + + if (!StringUtils.hasText(token) || "/error/thrown" == request.servletPath) { + filterChain.doFilter(request, response) + return + } + + + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt b/src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt new file mode 100644 index 0000000..9d18414 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt @@ -0,0 +1,5 @@ +package top.fatweb.api.utils + +class JwtUtil { + +} \ No newline at end of file diff --git a/src/main/resources/application-config-template.yml b/src/main/resources/application-config-template.yml new file mode 100644 index 0000000..88a4403 --- /dev/null +++ b/src/main/resources/application-config-template.yml @@ -0,0 +1,32 @@ +server: +# port: 8080 # Server port + +spring: + datasource: + url: jdbc:mysql://localhost # MySQL url + username: root # MySQL username + password: root # MySQL password + + druid: +# initial-size: 20 # Initial number of connections +# min-idle: 20 # Minimum number of connection pools +# max-active: 20 # Maximum number of connection pools +# max-wait: 3000 # Maximum connection timeout +# min-evictable-idle-time-millis: 300000 # Minimum time for a connection to live in the pool +# max-evictable-idle-time-millis: 900000 # Maximum time for a connection to live in the pool +# break-after-acquire-failure: true # Terminate the retry when the server fails to reconnect for the specified number of times +# connection-error-retry-attempts: 5 # Number of failed reconnections + + data: + redis: +# database: 0 # Redis database (default: 0) +# host: localhost # Redis host (default: localhost) +# port: 6379 # Redis port (default: 6379) +# password: # Password of redis +# connect-timeout: 3000 # Redis connect timeout +# lettuce: +# pool: +# min-idle: 0 +# max-idle: 8 +# max-active: 8 +# max-wait: -1ms \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8b13789..27104c4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1 +1,15 @@ +app: + version: @project.version@ + build-time: @build.timestamp@ + security: + header-string: "Authorization" + token-prefix: "Bearer " + jwt-ttl: 2 + jwt-ttl-unit: hours +spring: + profiles: + active: config + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver \ No newline at end of file diff --git a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt index a4324ff..34fc696 100644 --- a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt +++ b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt @@ -2,12 +2,16 @@ package top.fatweb.api import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest +import top.fatweb.api.constants.SecurityConstants +import java.security.MessageDigest +import java.util.* @SpringBootTest class FatWebApiApplicationTests { @Test fun contextLoads() { + SecurityConstants.jwtKey } } -- 2.49.1 From 04f552275988bad7a7bb20adb948c7f1627efea5 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 5 Oct 2023 20:59:16 +0800 Subject: [PATCH 07/24] Add pom multiple profiles --- pom.xml | 61 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index bff9773..ddec85f 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,52 @@ 0.0.1-SNAPSHOT fatweb-api fatweb-api + + + + dev + + true + + env + dev + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.3.0 + + + + + prod + + + env + prod + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + com.github.xiaoymin + knife4j-openapi3-ui + + + org.webjars + swagger-ui + + + + + + + 17 1.8.22 @@ -52,11 +98,6 @@ mysql-connector-j runtime - - org.projectlombok - lombok - true - org.springframework.boot spring-boot-starter-test @@ -94,15 +135,15 @@ velocity-engine-core 2.3 - - com.github.xiaoymin - knife4j-openapi3-jakarta-spring-boot-starter - 4.1.0 - org.springframework.boot spring-boot-starter-data-redis + + com.auth0 + java-jwt + 4.3.0 + org.springframework.boot spring-boot-starter-actuator -- 2.49.1 From 86eca4918d9517f3ed04fe711b2295d961d4e588 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 5 Oct 2023 20:59:38 +0800 Subject: [PATCH 08/24] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ab4ff27..318632d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ build/ ### VS Code ### .vscode/ /application-config.yml +/application-config.example.yml -- 2.49.1 From 5e555569c25e668ff4820128b1508ae85523b99b Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 5 Oct 2023 21:00:01 +0800 Subject: [PATCH 09/24] Add t_user table schema --- db/schema.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 db/schema.sql diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..a1140fd --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,12 @@ +drop table if exists t_user; + +create table if not exists t_user +( + id bigint not null primary key, + username varchar(20) not null comment '用户名', + password char(70) not null comment '密码', + enable int not null comment '启用', + deleted bigint not null default 0, + version int not null default 0, + constraint t_user_unique unique (username, deleted) +) comment '用户'; -- 2.49.1 From ca4fa261879d4db7c14dac20363995ad604ab8ac Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 5 Oct 2023 21:04:08 +0800 Subject: [PATCH 10/24] Update config file --- .../kotlin/top/fatweb/api/FatWebApiApplication.kt | 12 ++++++++++-- .../{constants => constant}/SecurityConstants.kt | 14 +++----------- .../{constants => constant}/ServerConstants.kt | 5 +++-- .../resources/application-config-template.yml | 9 +++++++++ src/main/resources/application.yml | 15 +++++++++------ 5 files changed, 34 insertions(+), 21 deletions(-) rename src/main/kotlin/top/fatweb/api/{constants => constant}/SecurityConstants.kt (51%) rename src/main/kotlin/top/fatweb/api/{constants => constant}/ServerConstants.kt (74%) diff --git a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt index 5dbcf42..9d83e83 100644 --- a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt +++ b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt @@ -5,6 +5,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.transaction.annotation.EnableTransactionManagement import java.io.File +import java.util.* @SpringBootApplication @EnableTransactionManagement @@ -17,7 +18,14 @@ fun main(args: Array) { runApplication(*args) } else { logger.warn("File ‘application.yml’ cannot be found in the running path. The configuration file template 'application.example.yml' has been created. Please change the configuration file content and rename it to 'application.yml', and then restart the server.") - FatWebApiApplication::class.java.getResource("/application-config-template.yml")?.readText() - ?.let { File("application-config.example.yml").writeText(it) } + FatWebApiApplication::class.java.getResource("/application-config-template.yml")?.readText() + ?.let { + File("application-config.example.yml").writeText( + it.replace( + "\$uuid\$", + UUID.randomUUID().toString() + ) + ) + } } } diff --git a/src/main/kotlin/top/fatweb/api/constants/SecurityConstants.kt b/src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt similarity index 51% rename from src/main/kotlin/top/fatweb/api/constants/SecurityConstants.kt rename to src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt index e92897a..e868e1f 100644 --- a/src/main/kotlin/top/fatweb/api/constants/SecurityConstants.kt +++ b/src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt @@ -1,8 +1,7 @@ -package top.fatweb.api.constants +package top.fatweb.api.constant import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.stereotype.Component -import java.security.MessageDigest import java.util.concurrent.TimeUnit @Component @@ -16,14 +15,7 @@ object SecurityConstants { var jwtTtlUnit = TimeUnit.HOURS - lateinit var jwtKey: String + var jwtKey = "FatWeb" - private fun ByteArray.hex(): String { - return joinToString("") { "%02X".format(it) } - } - - private fun String.md5(): String { - val bytes = MessageDigest.getInstance("MD5").digest(this.toByteArray()) - return bytes.hex() - } + var jwtIssuer = "FatWeb" } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/constants/ServerConstants.kt b/src/main/kotlin/top/fatweb/api/constant/ServerConstants.kt similarity index 74% rename from src/main/kotlin/top/fatweb/api/constants/ServerConstants.kt rename to src/main/kotlin/top/fatweb/api/constant/ServerConstants.kt index adabb4e..5f55621 100644 --- a/src/main/kotlin/top/fatweb/api/constants/ServerConstants.kt +++ b/src/main/kotlin/top/fatweb/api/constant/ServerConstants.kt @@ -1,4 +1,4 @@ -package top.fatweb.api.constants +package top.fatweb.api.constant import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.stereotype.Component @@ -13,5 +13,6 @@ object ServerConstants { lateinit var buildTime: String - fun buildZoneDateTime(zoneId: ZoneId = ZoneId.systemDefault()): ZonedDateTime = LocalDateTime.parse(buildTime).atZone(ZoneId.of("UTC")).withZoneSameInstant(zoneId) + fun buildZoneDateTime(zoneId: ZoneId = ZoneId.systemDefault()): ZonedDateTime = + LocalDateTime.parse(buildTime).atZone(ZoneId.of("UTC")).withZoneSameInstant(zoneId) } \ No newline at end of file diff --git a/src/main/resources/application-config-template.yml b/src/main/resources/application-config-template.yml index 88a4403..1e91da0 100644 --- a/src/main/resources/application-config-template.yml +++ b/src/main/resources/application-config-template.yml @@ -1,3 +1,12 @@ +app: + security: +# header-string: "Authorization" # The key of head to get token +# token-prefix: "Bearer " # Token prefix +# jwt-ttl: 2 # The life of token +# jwt-ttl-unit: hours # Unit of life of token + jwt-key: $uuid$ # Key to generate token +# jwt-issuer: FatWeb # Token issuer + server: # port: 8080 # Server port diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 27104c4..cec8de8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,15 +1,18 @@ app: version: @project.version@ build-time: @build.timestamp@ - security: - header-string: "Authorization" - token-prefix: "Bearer " - jwt-ttl: 2 - jwt-ttl-unit: hours spring: profiles: active: config datasource: type: com.alibaba.druid.pool.DruidDataSource - driver-class-name: com.mysql.cj.jdbc.Driver \ No newline at end of file + driver-class-name: com.mysql.cj.jdbc.Driver +mybatis-plus: + global-config: + db-config: + logic-delete-field: deleted + logic-not-delete-value: 0 + logic-delete-value: id + id-type: assign_id + type-aliases-package: top.fatweb.api.entity -- 2.49.1 From 6a9ad8d490821b3d40e25af3f8137fd2d3c6558d Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 5 Oct 2023 21:05:44 +0800 Subject: [PATCH 11/24] Reformat SwaggerConfig --- src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt b/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt index 6ce83a6..9b23066 100644 --- a/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt +++ b/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt @@ -5,7 +5,7 @@ import io.swagger.v3.oas.models.info.Contact import io.swagger.v3.oas.models.info.Info import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import top.fatweb.api.constants.ServerConstants +import top.fatweb.api.constant.ServerConstants @Configuration class SwaggerConfig { @@ -13,7 +13,11 @@ class SwaggerConfig { @Bean fun customOpenAPI(): OpenAPI? { val contact = Contact().name("FatttSnake").url("https://fatweb.top").email("fatttsnake@fatweb.top") - return OpenAPI().info(Info().title("FatWeb API 文档").description("FatWeb 后端 API 文档,包含各个 Controller 调用信息").contact(contact).version( - ServerConstants.version)) + return OpenAPI().info( + Info().title("FatWeb API 文档").description("FatWeb 后端 API 文档,包含各个 Controller 调用信息") + .contact(contact).version( + ServerConstants.version + ) + ) } } \ No newline at end of file -- 2.49.1 From 78de04713f3bf490036a8eaf1096ce9cdd447430 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 5 Oct 2023 21:10:05 +0800 Subject: [PATCH 12/24] Add exception handler --- .../top/fatweb/api/config/FilterConfig.kt | 18 ++++++++++ .../api/controller/ExceptionController.kt | 14 ++++++++ .../fatweb/api/entity/common/BusinessCode.kt | 6 ++++ .../fatweb/api/entity/common/ResponseCode.kt | 22 ++++++++++++ .../api/entity/common/ResponseResult.kt | 29 +++++++++++++++ .../top/fatweb/api/filter/ExceptionFilter.kt | 14 ++++++-- .../fatweb/api/handler/ExceptionHandler.kt | 35 +++++++++++++++++++ 7 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/top/fatweb/api/config/FilterConfig.kt create mode 100644 src/main/kotlin/top/fatweb/api/controller/ExceptionController.kt create mode 100644 src/main/kotlin/top/fatweb/api/entity/common/BusinessCode.kt create mode 100644 src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt create mode 100644 src/main/kotlin/top/fatweb/api/entity/common/ResponseResult.kt create mode 100644 src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt diff --git a/src/main/kotlin/top/fatweb/api/config/FilterConfig.kt b/src/main/kotlin/top/fatweb/api/config/FilterConfig.kt new file mode 100644 index 0000000..71374f9 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/config/FilterConfig.kt @@ -0,0 +1,18 @@ +package top.fatweb.api.config + +import org.springframework.boot.web.servlet.FilterRegistrationBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import top.fatweb.api.filter.ExceptionFilter + +@Configuration +class FilterConfig { + @Bean + fun exceptionFilterRegistrationBean(exceptionFilter: ExceptionFilter): FilterRegistrationBean { + val registrationBean = FilterRegistrationBean(exceptionFilter) + registrationBean.setBeanName("exceptionFilter") + registrationBean.order = -100 + + return registrationBean + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/controller/ExceptionController.kt b/src/main/kotlin/top/fatweb/api/controller/ExceptionController.kt new file mode 100644 index 0000000..dcb7833 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/controller/ExceptionController.kt @@ -0,0 +1,14 @@ +package top.fatweb.api.controller + +import jakarta.servlet.http.HttpServletRequest +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/error") +class ExceptionController { + @RequestMapping("/thrown") + fun thrown(request: HttpServletRequest) { + throw request.getAttribute("filter.error") as RuntimeException + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/common/BusinessCode.kt b/src/main/kotlin/top/fatweb/api/entity/common/BusinessCode.kt new file mode 100644 index 0000000..02178f5 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/entity/common/BusinessCode.kt @@ -0,0 +1,6 @@ +package top.fatweb.api.entity.common + +enum class BusinessCode(val code: Int) { + SYSTEM(100), + DATABASE(200) +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt b/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt new file mode 100644 index 0000000..8749e68 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt @@ -0,0 +1,22 @@ +package top.fatweb.api.entity.common + +enum class ResponseCode(val code: Int) { + SYSTEM_OK(BusinessCode.SYSTEM, 0), + SYSTEM_LOGIN_SUCCESS(BusinessCode.SYSTEM, 20), + SYSTEM_PASSWORD_CHANGE_SUCCESS(BusinessCode.SYSTEM, 21), + SYSTEM_LOGOUT_SUCCESS(BusinessCode.SYSTEM, 22), + SYSTEM_TOKEN_RENEW_SUCCESS(BusinessCode.SYSTEM, 23), + SYSTEM_UNAUTHORIZED(BusinessCode.SYSTEM, 30), + SYSTEM_ACCESS_DENIED(BusinessCode.SYSTEM, 31), + SYSTEM_USER_DISABLE(BusinessCode.SYSTEM, 32), + SYSTEM_LOGIN_USERNAME_PASSWORD_ERROR(BusinessCode.SYSTEM, 33), + SYSTEM_OLD_PASSWORD_NOT_MATCH(BusinessCode.SYSTEM, 34), + SYSTEM_LOGOUT_FAILED(BusinessCode.SYSTEM, 35), + SYSTEM_TOKEN_ILLEGAL(BusinessCode.SYSTEM, 36), + SYSTEM_TOKEN_HAS_EXPIRED(BusinessCode.SYSTEM, 37), + SYSTEM_REQUEST_ILLEGAL(BusinessCode.SYSTEM, 40), + SYSTEM_ERROR(BusinessCode.SYSTEM, 50), + SYSTEM_TIMEOUT(BusinessCode.SYSTEM, 51); + + constructor(businessCode: BusinessCode, code: Int) : this(businessCode.code * 100 + code) +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/common/ResponseResult.kt b/src/main/kotlin/top/fatweb/api/entity/common/ResponseResult.kt new file mode 100644 index 0000000..9559c1d --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/entity/common/ResponseResult.kt @@ -0,0 +1,29 @@ +package top.fatweb.api.entity.common + +import io.swagger.v3.oas.annotations.media.Schema +import java.io.Serializable + +class ResponseResult private constructor( + @Schema(description = "响应码", defaultValue = "200") + val code: Int, + + @Schema(description = "是否调用成功") + val success: Boolean, + + @Schema(description = "信息") + val msg: String, + + @Schema(description = "数据") + val data: T? +) : Serializable { + companion object { + fun build(code: ResponseCode, success: Boolean, msg: String, data: T?) = + ResponseResult(code.code, success, msg, data) + + fun success(code: ResponseCode = ResponseCode.SYSTEM_OK, msg: String = "success", data: T? = null) = + build(code, true, msg, data) + + fun fail(code: ResponseCode = ResponseCode.SYSTEM_ERROR, msg: String = "fail", data: T? = null) = + build(code, false, msg, data) + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/filter/ExceptionFilter.kt b/src/main/kotlin/top/fatweb/api/filter/ExceptionFilter.kt index 4fcfbc2..3097036 100644 --- a/src/main/kotlin/top/fatweb/api/filter/ExceptionFilter.kt +++ b/src/main/kotlin/top/fatweb/api/filter/ExceptionFilter.kt @@ -1,10 +1,18 @@ package top.fatweb.api.filter -import jakarta.servlet.* -import java.lang.Exception +import jakarta.servlet.Filter +import jakarta.servlet.FilterChain +import jakarta.servlet.ServletRequest +import jakarta.servlet.ServletResponse +import org.springframework.stereotype.Component +@Component class ExceptionFilter : Filter { - override fun doFilter(servletRequest: ServletRequest?, servletResponse: ServletResponse?, filterChain: FilterChain?) { + override fun doFilter( + servletRequest: ServletRequest?, + servletResponse: ServletResponse?, + filterChain: FilterChain? + ) { try { filterChain!!.doFilter(servletRequest, servletResponse) } catch (e: Exception) { diff --git a/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt b/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt new file mode 100644 index 0000000..4d982d0 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt @@ -0,0 +1,35 @@ +package top.fatweb.api.handler + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.security.authentication.InsufficientAuthenticationException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import top.fatweb.api.entity.common.ResponseCode +import top.fatweb.api.entity.common.ResponseResult + +@RestControllerAdvice +class ExceptionHandler { + private val log: Logger = LoggerFactory.getLogger(this::class.java) + + @ExceptionHandler(value = [Exception::class]) + fun exceptionHandler(e: Exception): ResponseResult<*> { + return when (e) { + is InsufficientAuthenticationException -> { + log.debug(e.localizedMessage, e) + ResponseResult.fail(ResponseCode.SYSTEM_UNAUTHORIZED, e.localizedMessage, null) + } + + is HttpMessageNotReadableException -> { + log.debug(e.localizedMessage, e) + ResponseResult.fail(ResponseCode.SYSTEM_REQUEST_ILLEGAL, e.localizedMessage.split(":")[0], null) + } + + else -> { + log.error(e.localizedMessage, e) + ResponseResult.fail(ResponseCode.SYSTEM_ERROR, data = null) + } + } + } +} \ No newline at end of file -- 2.49.1 From 8e5375ab30fa0c7f6c356b1a92b79ca337866eb9 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Thu, 5 Oct 2023 21:11:22 +0800 Subject: [PATCH 13/24] Add authentication --- .../top/fatweb/api/annotation/ApiVersion.kt | 14 +++ .../top/fatweb/api/config/SecurityConfig.kt | 92 +++++++++++++++++++ .../api/config/WebMvcRegistrationsConfig.kt | 11 +++ .../fatweb/api/controller/UserController.kt | 17 ++++ .../permission/AuthenticationController.kt | 21 +++++ .../fatweb/api/entity/permission/LoginUser.kt | 36 ++++++++ .../top/fatweb/api/entity/permission/User.kt | 56 +++++++++++ .../api/exception/TokenHasExpiredException.kt | 3 + .../filter/JwtAuthenticationTokenFilter.kt | 28 +++++- .../api/handler/JwtAccessDeniedHandler.kt | 19 ++++ .../JwtAuthenticationEntryPointHandler.kt | 19 ++++ .../top/fatweb/api/mapper/UserMapper.kt | 16 ++++ .../top/fatweb/api/service/IUserService.kt | 14 +++ .../api/service/impl/UserServiceImpl.kt | 18 ++++ .../permission/IAuthenticationService.kt | 11 +++ .../impl/AuthenticationServiceImpl.kt | 61 ++++++++++++ .../util/ApiResponseMappingHandlerMapping.kt | 31 +++++++ .../fatweb/api/util/ApiVersionCondition.kt | 26 ++++++ .../kotlin/top/fatweb/api/util/JwtUtil.kt | 67 ++++++++++++++ .../fatweb/api/{utils => util}/RedisUtil.kt | 5 +- .../kotlin/top/fatweb/api/util/WebUtil.kt | 10 ++ .../kotlin/top/fatweb/api/utils/JwtUtil.kt | 5 - src/main/resources/mapper/UserMapper.xml | 5 + .../fatweb/api/FatWebApiApplicationTests.kt | 10 +- 24 files changed, 580 insertions(+), 15 deletions(-) create mode 100644 src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt create mode 100644 src/main/kotlin/top/fatweb/api/config/SecurityConfig.kt create mode 100644 src/main/kotlin/top/fatweb/api/config/WebMvcRegistrationsConfig.kt create mode 100644 src/main/kotlin/top/fatweb/api/controller/UserController.kt create mode 100644 src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt create mode 100644 src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt create mode 100644 src/main/kotlin/top/fatweb/api/entity/permission/User.kt create mode 100644 src/main/kotlin/top/fatweb/api/exception/TokenHasExpiredException.kt create mode 100644 src/main/kotlin/top/fatweb/api/handler/JwtAccessDeniedHandler.kt create mode 100644 src/main/kotlin/top/fatweb/api/handler/JwtAuthenticationEntryPointHandler.kt create mode 100644 src/main/kotlin/top/fatweb/api/mapper/UserMapper.kt create mode 100644 src/main/kotlin/top/fatweb/api/service/IUserService.kt create mode 100644 src/main/kotlin/top/fatweb/api/service/impl/UserServiceImpl.kt create mode 100644 src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt create mode 100644 src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt create mode 100644 src/main/kotlin/top/fatweb/api/util/ApiResponseMappingHandlerMapping.kt create mode 100644 src/main/kotlin/top/fatweb/api/util/ApiVersionCondition.kt create mode 100644 src/main/kotlin/top/fatweb/api/util/JwtUtil.kt rename src/main/kotlin/top/fatweb/api/{utils => util}/RedisUtil.kt (97%) create mode 100644 src/main/kotlin/top/fatweb/api/util/WebUtil.kt delete mode 100644 src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt create mode 100644 src/main/resources/mapper/UserMapper.xml diff --git a/src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt b/src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt new file mode 100644 index 0000000..f087e97 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt @@ -0,0 +1,14 @@ +package top.fatweb.api.annotation + +import org.springframework.core.annotation.AliasFor + + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class ApiVersion( + @get:AliasFor("version") + val value: Int = 1, + + @get:AliasFor("value") + val version: Int = 1 +) diff --git a/src/main/kotlin/top/fatweb/api/config/SecurityConfig.kt b/src/main/kotlin/top/fatweb/api/config/SecurityConfig.kt new file mode 100644 index 0000000..5836f3d --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/config/SecurityConfig.kt @@ -0,0 +1,92 @@ +package top.fatweb.api.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.* +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import top.fatweb.api.filter.JwtAuthenticationTokenFilter +import top.fatweb.api.handler.JwtAccessDeniedHandler +import top.fatweb.api.handler.JwtAuthenticationEntryPointHandler + +@Configuration +@EnableMethodSecurity +class SecurityConfig( + val jwtAuthenticationTokenFilter: JwtAuthenticationTokenFilter, + val authenticationEntryPointHandler: JwtAuthenticationEntryPointHandler, + val accessDeniedHandler: JwtAccessDeniedHandler +) { + @Bean + fun passwordEncoder() = BCryptPasswordEncoder() + + @Bean + fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager = + authenticationConfiguration.authenticationManager + + @Bean + fun corsConfigurationSource(): UrlBasedCorsConfigurationSource { + val corsConfiguration = CorsConfiguration() + corsConfiguration.allowedMethods = listOf("*") + corsConfiguration.allowedHeaders = listOf("*") + corsConfiguration.maxAge = 3600L + corsConfiguration.allowedOrigins = listOf("*") + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", corsConfiguration) + + return source + } + + @Bean + fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain = httpSecurity + // Disable CSRF + .csrf { csrfConfigurer: CsrfConfigurer -> csrfConfigurer.disable() } + // Do not get SecurityContent by Session + .sessionManagement { sessionManagementConfigurer: SessionManagementConfigurer -> + sessionManagementConfigurer.sessionCreationPolicy( + SessionCreationPolicy.STATELESS + ) + } + .authorizeHttpRequests { authorizeHttpRequests: AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry -> + authorizeHttpRequests + // Allow anonymous access + .requestMatchers( + "/api/v*/login", + "/error/thrown", + "/doc.html", + "/swagger-ui/**", + "/webjars/**", + "/v3/**", + "/swagger-ui.html", + "/favicon.ico" + ).anonymous() + // Authentication required + .anyRequest().authenticated() + } + + .logout { logoutConfigurer: LogoutConfigurer -> logoutConfigurer.disable() } + + .exceptionHandling { exceptionHandlingConfigurer: ExceptionHandlingConfigurer -> + exceptionHandlingConfigurer.authenticationEntryPoint( + authenticationEntryPointHandler + ) + exceptionHandlingConfigurer.accessDeniedHandler( + accessDeniedHandler + ) + } + + .cors { cors: CorsConfigurer -> + cors.configurationSource( + corsConfigurationSource() + ) + } + + .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter::class.java).build() +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/config/WebMvcRegistrationsConfig.kt b/src/main/kotlin/top/fatweb/api/config/WebMvcRegistrationsConfig.kt new file mode 100644 index 0000000..1db6a0f --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/config/WebMvcRegistrationsConfig.kt @@ -0,0 +1,11 @@ +package top.fatweb.api.config + +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping +import top.fatweb.api.util.ApiResponseMappingHandlerMapping + +@Configuration +class WebMvcRegistrationsConfig : WebMvcRegistrations { + override fun getRequestMappingHandlerMapping(): RequestMappingHandlerMapping = ApiResponseMappingHandlerMapping() +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/controller/UserController.kt b/src/main/kotlin/top/fatweb/api/controller/UserController.kt new file mode 100644 index 0000000..188caf5 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/controller/UserController.kt @@ -0,0 +1,17 @@ +package top.fatweb.api.controller + +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +/** + *

+ * 用户 前端控制器 + *

+ * + * @author FatttSnake + * @since 2023-10-04 + */ +@RestController +@RequestMapping("/api/user") +class UserController + diff --git a/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt b/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt new file mode 100644 index 0000000..c36bdac --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt @@ -0,0 +1,21 @@ +package top.fatweb.api.controller.permission + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.* +import top.fatweb.api.annotation.ApiVersion +import top.fatweb.api.entity.common.ResponseCode +import top.fatweb.api.entity.common.ResponseResult +import top.fatweb.api.entity.permission.User +import top.fatweb.api.service.permission.IAuthenticationService + +@Tag(name = "身份认证", description = "身份认证相关接口") +@RestController +@RequestMapping("/api/{apiVersion}") +@ApiVersion(2) +class AuthenticationController(val loginService: IAuthenticationService) { + @Operation(summary = "登录") + @PostMapping("/login") + fun login(@PathVariable apiVersion: String, @RequestBody user: User) = + ResponseResult.success(ResponseCode.SYSTEM_LOGIN_SUCCESS, "Login success", loginService.login(user)) +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt b/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt new file mode 100644 index 0000000..f160c2c --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt @@ -0,0 +1,36 @@ +package top.fatweb.api.entity.permission + +import com.fasterxml.jackson.annotation.JsonIgnore +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.UserDetails + +class LoginUser() : UserDetails { + lateinit var user: User + + @JsonIgnore + private var authorities: List? = null + + constructor(user: User) : this() { + this.user = user + } + + @JsonIgnore + override fun getAuthorities(): List { + authorities?.let { return it } + authorities = emptyList() + + return authorities as List + } + + override fun getPassword(): String? = user.password + + override fun getUsername(): String? = user.username + + override fun isAccountNonExpired(): Boolean = true + + override fun isAccountNonLocked(): Boolean = true + + override fun isCredentialsNonExpired(): Boolean = true + + override fun isEnabled(): Boolean = user.enable == 1 +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/permission/User.kt b/src/main/kotlin/top/fatweb/api/entity/permission/User.kt new file mode 100644 index 0000000..3af3d08 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/entity/permission/User.kt @@ -0,0 +1,56 @@ +package top.fatweb.api.entity.permission + +import com.baomidou.mybatisplus.annotation.* +import java.io.Serializable + +/** + *

+ * 用户 + *

+ * + * @author FatttSnake + * @since 2023-10-04 + */ +@TableName("t_user") +class User : Serializable { + + @TableId("id") + var id: Long? = null + + /** + * 用户名 + */ + @TableField("username") + var username: String? = null + + /** + * 密码 + */ + @TableField("password") + var password: String? = null + + /** + * 启用 + */ + @TableField("enable") + var enable: Int? = null + + @TableField("deleted") + @TableLogic + var deleted: Long? = null + + @TableField("version") + @Version + var version: Int? = null + + override fun toString(): String { + return "User{" + + "id=" + id + + ", username=" + username + + ", password=" + password + + ", enable=" + enable + + ", deleted=" + deleted + + ", version=" + version + + "}" + } +} diff --git a/src/main/kotlin/top/fatweb/api/exception/TokenHasExpiredException.kt b/src/main/kotlin/top/fatweb/api/exception/TokenHasExpiredException.kt new file mode 100644 index 0000000..2b0521f --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/exception/TokenHasExpiredException.kt @@ -0,0 +1,3 @@ +package top.fatweb.api.exception + +class TokenHasExpiredException : RuntimeException("Token has expired") \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt b/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt index 9319248..74f6ec0 100644 --- a/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt +++ b/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt @@ -3,24 +3,44 @@ package top.fatweb.api.filter import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component import org.springframework.util.StringUtils import org.springframework.web.filter.OncePerRequestFilter -import top.fatweb.api.constants.SecurityConstants -import top.fatweb.api.utils.RedisUtil +import top.fatweb.api.constant.SecurityConstants +import top.fatweb.api.entity.permission.LoginUser +import top.fatweb.api.exception.TokenHasExpiredException +import top.fatweb.api.util.JwtUtil +import top.fatweb.api.util.RedisUtil +import java.util.concurrent.TimeUnit +@Component class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRequestFilter() { override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { - val token = request.getHeader(SecurityConstants.headerString) + val tokenWithPrefix = request.getHeader(SecurityConstants.headerString) - if (!StringUtils.hasText(token) || "/error/thrown" == request.servletPath) { + if (!StringUtils.hasText(tokenWithPrefix) || "/error/thrown" == request.servletPath) { filterChain.doFilter(request, response) return } + val token = tokenWithPrefix.removePrefix(SecurityConstants.tokenPrefix) + JwtUtil.parseJwt(token) + val redisKey = "${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32) + val loginUser = redisUtil.getObject(redisKey) + loginUser ?: let { throw TokenHasExpiredException() } + + redisUtil.setExpire(redisKey, 20, TimeUnit.MINUTES) + + val authenticationToken = UsernamePasswordAuthenticationToken(loginUser, null, loginUser.authorities) + SecurityContextHolder.getContext().authentication = authenticationToken + + filterChain.doFilter(request, response) } } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/handler/JwtAccessDeniedHandler.kt b/src/main/kotlin/top/fatweb/api/handler/JwtAccessDeniedHandler.kt new file mode 100644 index 0000000..22e03c6 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/handler/JwtAccessDeniedHandler.kt @@ -0,0 +1,19 @@ +package top.fatweb.api.handler + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.stereotype.Component + +@Component +class JwtAccessDeniedHandler : AccessDeniedHandler { + override fun handle( + request: HttpServletRequest?, + response: HttpServletResponse?, + accessDeniedException: AccessDeniedException? + ) { + request?.setAttribute("filter.error", accessDeniedException) + request?.getRequestDispatcher("/error/thrown")?.forward(request, response) + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/handler/JwtAuthenticationEntryPointHandler.kt b/src/main/kotlin/top/fatweb/api/handler/JwtAuthenticationEntryPointHandler.kt new file mode 100644 index 0000000..cf5b808 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/handler/JwtAuthenticationEntryPointHandler.kt @@ -0,0 +1,19 @@ +package top.fatweb.api.handler + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.stereotype.Component + +@Component +class JwtAuthenticationEntryPointHandler : AuthenticationEntryPoint { + override fun commence( + request: HttpServletRequest?, + response: HttpServletResponse?, + authException: AuthenticationException? + ) { + request?.setAttribute("filter.error", authException) + request?.getRequestDispatcher("/error/thrown")?.forward(request, response) + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/mapper/UserMapper.kt b/src/main/kotlin/top/fatweb/api/mapper/UserMapper.kt new file mode 100644 index 0000000..8b37df3 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/mapper/UserMapper.kt @@ -0,0 +1,16 @@ +package top.fatweb.api.mapper + +import com.baomidou.mybatisplus.core.mapper.BaseMapper +import org.apache.ibatis.annotations.Mapper +import top.fatweb.api.entity.permission.User + +/** + *

+ * 用户 Mapper 接口 + *

+ * + * @author FatttSnake + * @since 2023-10-04 + */ +@Mapper +interface UserMapper : BaseMapper diff --git a/src/main/kotlin/top/fatweb/api/service/IUserService.kt b/src/main/kotlin/top/fatweb/api/service/IUserService.kt new file mode 100644 index 0000000..7bc58e9 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/service/IUserService.kt @@ -0,0 +1,14 @@ +package top.fatweb.api.service + +import com.baomidou.mybatisplus.extension.service.IService +import top.fatweb.api.entity.permission.User + +/** + *

+ * 用户 服务类 + *

+ * + * @author FatttSnake + * @since 2023-10-04 + */ +interface IUserService : IService diff --git a/src/main/kotlin/top/fatweb/api/service/impl/UserServiceImpl.kt b/src/main/kotlin/top/fatweb/api/service/impl/UserServiceImpl.kt new file mode 100644 index 0000000..e522f6b --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/service/impl/UserServiceImpl.kt @@ -0,0 +1,18 @@ +package top.fatweb.api.service.impl + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl +import org.springframework.stereotype.Service +import top.fatweb.api.entity.permission.User +import top.fatweb.api.mapper.UserMapper +import top.fatweb.api.service.IUserService + +/** + *

+ * 用户 服务实现类 + *

+ * + * @author FatttSnake + * @since 2023-10-04 + */ +@Service +class UserServiceImpl : ServiceImpl(), IUserService diff --git a/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt b/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt new file mode 100644 index 0000000..b58a4b6 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt @@ -0,0 +1,11 @@ +package top.fatweb.api.service.permission + +import top.fatweb.api.entity.permission.User + +interface IAuthenticationService { + fun login(user: User): HashMap + + fun logout(token: String): Boolean + + fun renewToken(token: String): HashMap +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt b/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt new file mode 100644 index 0000000..b742f3d --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt @@ -0,0 +1,61 @@ +package top.fatweb.api.service.permission.impl + +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.stereotype.Service +import top.fatweb.api.constant.SecurityConstants +import top.fatweb.api.entity.permission.LoginUser +import top.fatweb.api.entity.permission.User +import top.fatweb.api.service.permission.IAuthenticationService +import top.fatweb.api.util.JwtUtil +import top.fatweb.api.util.RedisUtil +import top.fatweb.api.util.WebUtil +import java.util.concurrent.TimeUnit + +@Service +class AuthenticationServiceImpl( + private val authenticationManager: AuthenticationManager, + private val redisUtil: RedisUtil +) : IAuthenticationService { + override fun login(user: User): HashMap { + val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(user.username, user.password) + val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken) + authentication ?: let { + throw RuntimeException("Login failed") + } + + val loginUser = authentication.principal as LoginUser + loginUser.user.password = "" + val userId = loginUser.user.id.toString() + val jwt = JwtUtil.createJwt(userId) + + jwt ?: let { + throw RuntimeException("Login failed") + } + + val hashMap = hashMapOf("token" to jwt) + val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt.substring(0, 32) + redisUtil.setObject(redisKey, loginUser, 20, TimeUnit.MINUTES) + + return hashMap + } + + override fun logout(token: String): Boolean = + redisUtil.delObject("${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32)) + + override fun renewToken(token: String): HashMap { + val oldRedisKey = "${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32) + redisUtil.delObject(oldRedisKey) + val jwt = JwtUtil.createJwt(WebUtil.getLoginUserId().toString()) + + jwt ?: let { + throw RuntimeException("Login failed") + } + + val hashMap = hashMapOf("token" to jwt) + val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt.substring(0, 32) + redisUtil.setObject(redisKey, WebUtil.getLoginUser(), 20, TimeUnit.MINUTES) + + return hashMap + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/util/ApiResponseMappingHandlerMapping.kt b/src/main/kotlin/top/fatweb/api/util/ApiResponseMappingHandlerMapping.kt new file mode 100644 index 0000000..8dafc0e --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/util/ApiResponseMappingHandlerMapping.kt @@ -0,0 +1,31 @@ +package top.fatweb.api.util + +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.servlet.mvc.condition.RequestCondition +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping +import top.fatweb.api.annotation.ApiVersion +import java.lang.reflect.Method + +class ApiResponseMappingHandlerMapping : RequestMappingHandlerMapping() { + private val versionFlag = "{apiVersion}" + + private fun createCondition(clazz: Class<*>): RequestCondition? { + val classRequestMapping = clazz.getAnnotation(RequestMapping::class.java) + classRequestMapping ?: let { return null } + val mappingUrlBuilder = StringBuilder() + if (classRequestMapping.value.isNotEmpty()) { + mappingUrlBuilder.append(classRequestMapping.value[0]) + } + val mappingUrl = mappingUrlBuilder.toString() + if (!mappingUrl.contains(versionFlag)) { + return null + } + val apiVersion = clazz.getAnnotation(ApiVersion::class.java) + + return if (apiVersion == null) ApiVersionCondition(1) else ApiVersionCondition(apiVersion.version) + } + + override fun getCustomMethodCondition(method: Method): RequestCondition<*>? = createCondition(method.javaClass) + + override fun getCustomTypeCondition(handlerType: Class<*>): RequestCondition<*>? = createCondition(handlerType) +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/util/ApiVersionCondition.kt b/src/main/kotlin/top/fatweb/api/util/ApiVersionCondition.kt new file mode 100644 index 0000000..381edaa --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/util/ApiVersionCondition.kt @@ -0,0 +1,26 @@ +package top.fatweb.api.util + +import jakarta.servlet.http.HttpServletRequest +import org.springframework.web.servlet.mvc.condition.RequestCondition +import java.util.regex.Pattern + +class ApiVersionCondition(private val apiVersion: Int) : RequestCondition { + private val versionPrefixPattern: Pattern = Pattern.compile(".*v(\\d+).*") + + override fun combine(other: ApiVersionCondition): ApiVersionCondition = ApiVersionCondition(other.apiVersion) + + override fun getMatchingCondition(request: HttpServletRequest): ApiVersionCondition? { + val matcher = versionPrefixPattern.matcher(request.requestURI) + if (matcher.find()) { + val version = matcher.group(1).toInt() + if (version >= this.apiVersion) { + return this + } + } + + return null + } + + override fun compareTo(other: ApiVersionCondition, request: HttpServletRequest): Int = + other.apiVersion - this.apiVersion +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/util/JwtUtil.kt b/src/main/kotlin/top/fatweb/api/util/JwtUtil.kt new file mode 100644 index 0000000..ea7c5aa --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/util/JwtUtil.kt @@ -0,0 +1,67 @@ +package top.fatweb.api.util + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.interfaces.DecodedJWT +import top.fatweb.api.constant.SecurityConstants +import java.util.* +import java.util.concurrent.TimeUnit +import javax.crypto.spec.SecretKeySpec + +object JwtUtil { + private fun getUUID() = UUID.randomUUID().toString().replace("-", "") + + /** + * 生成加密后的秘钥 secretKey + * + * @return 密钥 + */ + private fun generalKey(): SecretKeySpec { + val encodeKey = Base64.getDecoder().decode(SecurityConstants.jwtKey) + return SecretKeySpec(encodeKey, 0, encodeKey.size, "AES") + } + + private fun algorithm(): Algorithm = Algorithm.HMAC256(generalKey().toString()) + + /** + * 创建 token + * + * @param subject token 中存放的数据(json格式) + * @param ttl token 生存时间 + * @param timeUnit ttl 时间单位 + * @param uuid 唯一 ID + * @return jwt 串 + */ + fun createJwt( + subject: String, + ttl: Long = SecurityConstants.jwtTtl, + timeUnit: TimeUnit = SecurityConstants.jwtTtlUnit, + uuid: String = getUUID() + ): String? { + val nowMillis = System.currentTimeMillis() + val nowDate = Date(nowMillis) + val unitTtl = (ttl * when (timeUnit) { + TimeUnit.DAYS -> 24 * 60 * 60 * 1000 + TimeUnit.HOURS -> 60 * 60 * 1000 + TimeUnit.MINUTES -> 60 * 1000 + TimeUnit.SECONDS -> 1000 + TimeUnit.MILLISECONDS -> 1 + TimeUnit.NANOSECONDS -> 1 / 1000 + TimeUnit.MICROSECONDS -> 1 / 1000 / 1000 + }) + val expMillis = nowMillis + unitTtl + val expDate = Date(expMillis) + + return JWT.create().withJWTId(uuid).withSubject(subject).withIssuer(SecurityConstants.jwtIssuer) + .withIssuedAt(nowDate).withExpiresAt(expDate).sign(algorithm()) + } + + /** + * 解析 jwt + * + * @param jwt jwt 串 + * @return 解析内容 + */ + fun parseJwt(jwt: String): DecodedJWT = + JWT.require(algorithm()).build().verify(jwt) +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/utils/RedisUtil.kt b/src/main/kotlin/top/fatweb/api/util/RedisUtil.kt similarity index 97% rename from src/main/kotlin/top/fatweb/api/utils/RedisUtil.kt rename to src/main/kotlin/top/fatweb/api/util/RedisUtil.kt index 426b772..35991a8 100644 --- a/src/main/kotlin/top/fatweb/api/utils/RedisUtil.kt +++ b/src/main/kotlin/top/fatweb/api/util/RedisUtil.kt @@ -1,4 +1,4 @@ -package top.fatweb.api.utils +package top.fatweb.api.util import org.springframework.data.redis.core.BoundSetOperations import org.springframework.data.redis.core.RedisTemplate @@ -141,7 +141,8 @@ class RedisUtil(private val redisTemplate: RedisTemplate) { * @param key 缓存的键 * @return 缓存的键对应的 Map 数据 */ - fun getMap(key: String): Map? = redisTemplate.opsForHash().entries(key) as? Map + fun getMap(key: String): Map? = + redisTemplate.opsForHash().entries(key) as? Map /** * 往 Hash 中存入数据 diff --git a/src/main/kotlin/top/fatweb/api/util/WebUtil.kt b/src/main/kotlin/top/fatweb/api/util/WebUtil.kt new file mode 100644 index 0000000..892c0c6 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/util/WebUtil.kt @@ -0,0 +1,10 @@ +package top.fatweb.api.util + +import org.springframework.security.core.context.SecurityContextHolder +import top.fatweb.api.entity.permission.LoginUser + +object WebUtil { + fun getLoginUser() = SecurityContextHolder.getContext().authentication.principal as LoginUser + + fun getLoginUserId() = getLoginUser().user.id +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt b/src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt deleted file mode 100644 index 9d18414..0000000 --- a/src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt +++ /dev/null @@ -1,5 +0,0 @@ -package top.fatweb.api.utils - -class JwtUtil { - -} \ No newline at end of file diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..cecc32c --- /dev/null +++ b/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt index 34fc696..9eb700d 100644 --- a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt +++ b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt @@ -1,17 +1,19 @@ package top.fatweb.api +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest -import top.fatweb.api.constants.SecurityConstants -import java.security.MessageDigest -import java.util.* +import top.fatweb.api.constant.SecurityConstants @SpringBootTest class FatWebApiApplicationTests { @Test fun contextLoads() { - SecurityConstants.jwtKey } + @Test + fun removePrefixTest() { + assertEquals("12312", "Bearer 12312".removePrefix(SecurityConstants.tokenPrefix)) + } } -- 2.49.1 From 79e65f0785b6513085a2dac9b40cac39ffb5fbb1 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Fri, 6 Oct 2023 01:53:25 +0800 Subject: [PATCH 14/24] Add login --- pom.xml | 26 +++++++++++++++---- .../top/fatweb/api/FatWebApiApplication.kt | 14 +++++----- .../top/fatweb/api/annotation/ApiVersion.kt | 7 ++--- .../permission/AuthenticationController.kt | 22 +++++++++++----- .../fatweb/api/entity/common/ResponseCode.kt | 16 +++++++----- .../api/entity/converter/UserConverter.kt | 14 ++++++++++ .../top/fatweb/api/entity/param/LoginParam.kt | 16 ++++++++++++ .../top/fatweb/api/entity/permission/User.kt | 16 +++++------- .../fatweb/api/handler/ExceptionHandler.kt | 19 ++++++++++++++ .../permission/impl/UserDetailsServiceImpl.kt | 19 ++++++++++++++ .../resources/application-config-template.yml | 2 +- .../fatweb/api/FatWebApiApplicationTests.kt | 11 ++++++++ 12 files changed, 141 insertions(+), 41 deletions(-) create mode 100644 src/main/kotlin/top/fatweb/api/entity/converter/UserConverter.kt create mode 100644 src/main/kotlin/top/fatweb/api/entity/param/LoginParam.kt create mode 100644 src/main/kotlin/top/fatweb/api/service/permission/impl/UserDetailsServiceImpl.kt diff --git a/pom.xml b/pom.xml index ddec85f..81e5aad 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,10 @@ org.springframework.boot spring-boot-starter-web
+ + org.springframework.boot + spring-boot-starter-validation + com.fasterxml.jackson.module jackson-module-kotlin @@ -112,12 +116,12 @@ com.baomidou mybatis-plus-boot-starter - 3.5.3.1 + 3.5.3.2 com.baomidou mybatis-plus-boot-starter-test - 3.5.3.1 + 3.5.3.2 test @@ -125,11 +129,13 @@ druid-spring-boot-starter 1.2.19 + org.apache.velocity velocity-engine-core @@ -142,7 +148,7 @@ com.auth0 java-jwt - 4.3.0 + 4.4.0 org.springframework.boot @@ -152,6 +158,16 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + + org.mapstruct + mapstruct + 1.5.5.Final + + + org.mapstruct + mapstruct-processor + 1.5.5.Final +
@@ -180,7 +196,7 @@ compile - compile + process-sources compile diff --git a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt index 9d83e83..1920cd3 100644 --- a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt +++ b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt @@ -18,14 +18,12 @@ fun main(args: Array) { runApplication(*args) } else { logger.warn("File ‘application.yml’ cannot be found in the running path. The configuration file template 'application.example.yml' has been created. Please change the configuration file content and rename it to 'application.yml', and then restart the server.") - FatWebApiApplication::class.java.getResource("/application-config-template.yml")?.readText() - ?.let { - File("application-config.example.yml").writeText( - it.replace( - "\$uuid\$", - UUID.randomUUID().toString() - ) + FatWebApiApplication::class.java.getResource("/application-config-template.yml")?.readText()?.let { + File("application-config.example.yml").writeText( + it.replace( + "\$uuid\$", UUID.randomUUID().toString().replace("-", "") ) - } + ) + } } } diff --git a/src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt b/src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt index f087e97..a0b064c 100644 --- a/src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt +++ b/src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt @@ -2,13 +2,10 @@ package top.fatweb.api.annotation import org.springframework.core.annotation.AliasFor - @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class ApiVersion( - @get:AliasFor("version") - val value: Int = 1, + @get:AliasFor("version") val value: Int = 1, - @get:AliasFor("value") - val version: Int = 1 + @get:AliasFor("value") val version: Int = 1 ) diff --git a/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt b/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt index c36bdac..2d26a12 100644 --- a/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt +++ b/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt @@ -2,20 +2,30 @@ package top.fatweb.api.controller.permission import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag -import org.springframework.web.bind.annotation.* +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController import top.fatweb.api.annotation.ApiVersion import top.fatweb.api.entity.common.ResponseCode import top.fatweb.api.entity.common.ResponseResult -import top.fatweb.api.entity.permission.User +import top.fatweb.api.entity.converter.UserConverter +import top.fatweb.api.entity.param.LoginParam import top.fatweb.api.service.permission.IAuthenticationService @Tag(name = "身份认证", description = "身份认证相关接口") -@RestController +@Suppress("MVCPathVariableInspection") @RequestMapping("/api/{apiVersion}") @ApiVersion(2) -class AuthenticationController(val loginService: IAuthenticationService) { +@RestController +class AuthenticationController(val loginService: IAuthenticationService, val userConverter: UserConverter) { @Operation(summary = "登录") @PostMapping("/login") - fun login(@PathVariable apiVersion: String, @RequestBody user: User) = - ResponseResult.success(ResponseCode.SYSTEM_LOGIN_SUCCESS, "Login success", loginService.login(user)) + fun login(@Valid @RequestBody loginParam: LoginParam) = + ResponseResult.success( + ResponseCode.SYSTEM_LOGIN_SUCCESS, + "Login success", + loginService.login(userConverter.loginParamToUser(loginParam)) + ) } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt b/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt index 8749e68..f6095bb 100644 --- a/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt +++ b/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt @@ -7,14 +7,16 @@ enum class ResponseCode(val code: Int) { SYSTEM_LOGOUT_SUCCESS(BusinessCode.SYSTEM, 22), SYSTEM_TOKEN_RENEW_SUCCESS(BusinessCode.SYSTEM, 23), SYSTEM_UNAUTHORIZED(BusinessCode.SYSTEM, 30), - SYSTEM_ACCESS_DENIED(BusinessCode.SYSTEM, 31), - SYSTEM_USER_DISABLE(BusinessCode.SYSTEM, 32), - SYSTEM_LOGIN_USERNAME_PASSWORD_ERROR(BusinessCode.SYSTEM, 33), - SYSTEM_OLD_PASSWORD_NOT_MATCH(BusinessCode.SYSTEM, 34), - SYSTEM_LOGOUT_FAILED(BusinessCode.SYSTEM, 35), - SYSTEM_TOKEN_ILLEGAL(BusinessCode.SYSTEM, 36), - SYSTEM_TOKEN_HAS_EXPIRED(BusinessCode.SYSTEM, 37), + SYSTEM_USERNAME_NOT_FOUND(BusinessCode.SYSTEM, 31), + SYSTEM_ACCESS_DENIED(BusinessCode.SYSTEM, 32), + SYSTEM_USER_DISABLE(BusinessCode.SYSTEM, 33), + SYSTEM_LOGIN_USERNAME_PASSWORD_ERROR(BusinessCode.SYSTEM, 34), + SYSTEM_OLD_PASSWORD_NOT_MATCH(BusinessCode.SYSTEM, 35), + SYSTEM_LOGOUT_FAILED(BusinessCode.SYSTEM, 36), + SYSTEM_TOKEN_ILLEGAL(BusinessCode.SYSTEM, 37), + SYSTEM_TOKEN_HAS_EXPIRED(BusinessCode.SYSTEM, 38), SYSTEM_REQUEST_ILLEGAL(BusinessCode.SYSTEM, 40), + SYSTEM_ARGUMENT_NOT_VALID(BusinessCode.SYSTEM, 41), SYSTEM_ERROR(BusinessCode.SYSTEM, 50), SYSTEM_TIMEOUT(BusinessCode.SYSTEM, 51); diff --git a/src/main/kotlin/top/fatweb/api/entity/converter/UserConverter.kt b/src/main/kotlin/top/fatweb/api/entity/converter/UserConverter.kt new file mode 100644 index 0000000..8d24463 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/entity/converter/UserConverter.kt @@ -0,0 +1,14 @@ +package top.fatweb.api.entity.converter + +import org.mapstruct.Mapper +import org.mapstruct.Mapping +import org.mapstruct.Mappings +import top.fatweb.api.entity.param.LoginParam +import top.fatweb.api.entity.permission.User + +@Mapper(componentModel = "spring") +interface UserConverter { + @Mappings(Mapping(source = "username", target = "username"), Mapping(source = "password", target = "password")) + fun loginParamToUser(loginParam: LoginParam): User + +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/param/LoginParam.kt b/src/main/kotlin/top/fatweb/api/entity/param/LoginParam.kt new file mode 100644 index 0000000..4ed8d0c --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/entity/param/LoginParam.kt @@ -0,0 +1,16 @@ +package top.fatweb.api.entity.param + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import java.io.Serializable + +class LoginParam : Serializable { + + @Schema(description = "用户名", example = "test", required = true) + @NotBlank(message = "Username can not be blank") + val username: String? = null + + @Schema(description = "密码", example = "test123456", required = true) + @NotBlank(message = "Password can not be blank") + val password: String? = null +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/permission/User.kt b/src/main/kotlin/top/fatweb/api/entity/permission/User.kt index 3af3d08..bff63ab 100644 --- a/src/main/kotlin/top/fatweb/api/entity/permission/User.kt +++ b/src/main/kotlin/top/fatweb/api/entity/permission/User.kt @@ -12,7 +12,12 @@ import java.io.Serializable * @since 2023-10-04 */ @TableName("t_user") -class User : Serializable { +class User() : Serializable { + constructor(username: String, password: String, enable: Boolean = true) : this() { + this.username = username + this.password = password + this.enable = if (enable) 1 else 0 + } @TableId("id") var id: Long? = null @@ -44,13 +49,6 @@ class User : Serializable { var version: Int? = null override fun toString(): String { - return "User{" + - "id=" + id + - ", username=" + username + - ", password=" + password + - ", enable=" + enable + - ", deleted=" + deleted + - ", version=" + version + - "}" + return "User{id=$id, username=$username, password=$password, enable=$enable, deleted=$deleted, version=$version}" } } diff --git a/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt b/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt index 4d982d0..412c401 100644 --- a/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt +++ b/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt @@ -3,7 +3,10 @@ package top.fatweb.api.handler import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.security.authentication.BadCredentialsException import org.springframework.security.authentication.InsufficientAuthenticationException +import org.springframework.security.authentication.InternalAuthenticationServiceException +import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice import top.fatweb.api.entity.common.ResponseCode @@ -26,6 +29,22 @@ class ExceptionHandler { ResponseResult.fail(ResponseCode.SYSTEM_REQUEST_ILLEGAL, e.localizedMessage.split(":")[0], null) } + is MethodArgumentNotValidException -> { + log.debug(e.localizedMessage, e) + val errorMessage = e.allErrors.map { error -> error.defaultMessage }.joinToString(". ") + ResponseResult.fail(ResponseCode.SYSTEM_ARGUMENT_NOT_VALID, errorMessage, null) + } + + is InternalAuthenticationServiceException -> { + log.debug(e.localizedMessage, e) + ResponseResult.fail(ResponseCode.SYSTEM_USERNAME_NOT_FOUND, e.localizedMessage, null) + } + + is BadCredentialsException -> { + log.debug(e.localizedMessage, e) + ResponseResult.fail(ResponseCode.SYSTEM_LOGIN_USERNAME_PASSWORD_ERROR, e.localizedMessage, null) + } + else -> { log.error(e.localizedMessage, e) ResponseResult.fail(ResponseCode.SYSTEM_ERROR, data = null) diff --git a/src/main/kotlin/top/fatweb/api/service/permission/impl/UserDetailsServiceImpl.kt b/src/main/kotlin/top/fatweb/api/service/permission/impl/UserDetailsServiceImpl.kt new file mode 100644 index 0000000..6b78e76 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/service/permission/impl/UserDetailsServiceImpl.kt @@ -0,0 +1,19 @@ +package top.fatweb.api.service.permission.impl + +import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.stereotype.Service +import top.fatweb.api.entity.permission.LoginUser +import top.fatweb.api.entity.permission.User +import top.fatweb.api.service.IUserService + +@Service +class UserDetailsServiceImpl(val userService: IUserService) : UserDetailsService { + override fun loadUserByUsername(username: String?): UserDetails { + val user = userService.getOne(KtQueryWrapper(User()).eq(User::username, username)) + user ?: let { throw Exception("Username not found") } + + return LoginUser(user) + } +} \ No newline at end of file diff --git a/src/main/resources/application-config-template.yml b/src/main/resources/application-config-template.yml index 1e91da0..5280697 100644 --- a/src/main/resources/application-config-template.yml +++ b/src/main/resources/application-config-template.yml @@ -4,7 +4,7 @@ app: # token-prefix: "Bearer " # Token prefix # jwt-ttl: 2 # The life of token # jwt-ttl-unit: hours # Unit of life of token - jwt-key: $uuid$ # Key to generate token + jwt-key: $uuid$ # Key to generate token (Only numbers and letters allow) # jwt-issuer: FatWeb # Token issuer server: diff --git a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt index 9eb700d..9dc0d31 100644 --- a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt +++ b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt @@ -16,4 +16,15 @@ class FatWebApiApplicationTests { fun removePrefixTest() { assertEquals("12312", "Bearer 12312".removePrefix(SecurityConstants.tokenPrefix)) } + + /* + @Test + fun addUser(@Autowired userService: IUserService, @Autowired passwordEncoder: PasswordEncoder) { + val username = "admin" + val rawPassword = "admin" + val encodedPassword = passwordEncoder.encode(rawPassword) + val user = User(username, encodedPassword) + userService.save(user) + } + */ } -- 2.49.1 From 03534e4fa9d558410c62b9471fc540fb8995b927 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Fri, 6 Oct 2023 14:52:03 +0800 Subject: [PATCH 15/24] Finish authentication --- .../top/fatweb/api/config/RedisConfig.kt | 14 ++++++-- .../fatweb/api/constant/SecurityConstants.kt | 4 +++ .../permission/AuthenticationController.kt | 32 ++++++++++++++----- .../{entity => }/converter/UserConverter.kt | 4 +-- .../fatweb/api/entity/permission/LoginUser.kt | 8 +++++ .../filter/JwtAuthenticationTokenFilter.kt | 8 ++--- .../fatweb/api/handler/ExceptionHandler.kt | 13 ++++++++ .../api/{entity => }/param/LoginParam.kt | 3 +- .../permission/IAuthenticationService.kt | 6 ++-- .../impl/AuthenticationServiceImpl.kt | 30 +++++++++-------- .../kotlin/top/fatweb/api/util/WebUtil.kt | 6 ++++ src/main/kotlin/top/fatweb/api/vo/LoginVo.kt | 11 +++++++ src/main/kotlin/top/fatweb/api/vo/TokenVo.kt | 11 +++++++ .../resources/application-config-template.yml | 4 ++- 14 files changed, 121 insertions(+), 33 deletions(-) rename src/main/kotlin/top/fatweb/api/{entity => }/converter/UserConverter.kt (81%) rename src/main/kotlin/top/fatweb/api/{entity => }/param/LoginParam.kt (86%) create mode 100644 src/main/kotlin/top/fatweb/api/vo/LoginVo.kt create mode 100644 src/main/kotlin/top/fatweb/api/vo/TokenVo.kt diff --git a/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt b/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt index fd13a65..75eee51 100644 --- a/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt +++ b/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt @@ -1,5 +1,8 @@ package top.fatweb.api.config +import com.fasterxml.jackson.annotation.JsonAutoDetect +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.PropertyAccessor import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import org.springframework.context.annotation.Bean @@ -16,8 +19,15 @@ class RedisConfig { val redisTemplate = RedisTemplate() redisTemplate.connectionFactory = redisConnectionFactory val stringRedisSerializer = StringRedisSerializer() - val objectMapper = ObjectMapper().registerModules(JavaTimeModule()) - val anyJackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(objectMapper, Any::class.java) + val objectMapper = ObjectMapper().registerModules(JavaTimeModule()).apply { + setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY) + activateDefaultTyping( + this.polymorphicTypeValidator, + ObjectMapper.DefaultTyping.NON_FINAL, + JsonTypeInfo.As.PROPERTY + ) + } + val anyJackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(objectMapper, Any::class.java) // 使用StringRedisSerializer来序列化和反序列化redis的key值 redisTemplate.keySerializer = stringRedisSerializer diff --git a/src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt b/src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt index e868e1f..c2b1738 100644 --- a/src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt +++ b/src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt @@ -18,4 +18,8 @@ object SecurityConstants { var jwtKey = "FatWeb" var jwtIssuer = "FatWeb" + + var redisTtl = 20L + + var redisTtlUnit = TimeUnit.MINUTES } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt b/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt index 2d26a12..63f68b4 100644 --- a/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt +++ b/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt @@ -2,30 +2,46 @@ package top.fatweb.api.controller.permission import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import top.fatweb.api.annotation.ApiVersion +import top.fatweb.api.converter.UserConverter import top.fatweb.api.entity.common.ResponseCode import top.fatweb.api.entity.common.ResponseResult -import top.fatweb.api.entity.converter.UserConverter -import top.fatweb.api.entity.param.LoginParam +import top.fatweb.api.param.LoginParam import top.fatweb.api.service.permission.IAuthenticationService +import top.fatweb.api.util.WebUtil @Tag(name = "身份认证", description = "身份认证相关接口") @Suppress("MVCPathVariableInspection") @RequestMapping("/api/{apiVersion}") @ApiVersion(2) @RestController -class AuthenticationController(val loginService: IAuthenticationService, val userConverter: UserConverter) { +class AuthenticationController(val authenticationService: IAuthenticationService, val userConverter: UserConverter) { @Operation(summary = "登录") @PostMapping("/login") fun login(@Valid @RequestBody loginParam: LoginParam) = ResponseResult.success( ResponseCode.SYSTEM_LOGIN_SUCCESS, "Login success", - loginService.login(userConverter.loginParamToUser(loginParam)) + authenticationService.login(userConverter.loginParamToUser(loginParam)) + ) + + @Operation(summary = "登出") + @PostMapping("/logout") + fun logout(request: HttpServletRequest) = + when (authenticationService.logout(WebUtil.getToken(request))) { + true -> ResponseResult.success(ResponseCode.SYSTEM_LOGOUT_SUCCESS, "Logout success", null) + false -> ResponseResult.fail(ResponseCode.SYSTEM_LOGOUT_FAILED, "Logout failed", null) + } + + @Operation(summary = "更新 Token") + @GetMapping("/token") + fun renewToken(request: HttpServletRequest) = + ResponseResult.success( + ResponseCode.SYSTEM_TOKEN_RENEW_SUCCESS, + "Token renew success", + authenticationService.renewToken(WebUtil.getToken(request)) ) } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/converter/UserConverter.kt b/src/main/kotlin/top/fatweb/api/converter/UserConverter.kt similarity index 81% rename from src/main/kotlin/top/fatweb/api/entity/converter/UserConverter.kt rename to src/main/kotlin/top/fatweb/api/converter/UserConverter.kt index 8d24463..71b980a 100644 --- a/src/main/kotlin/top/fatweb/api/entity/converter/UserConverter.kt +++ b/src/main/kotlin/top/fatweb/api/converter/UserConverter.kt @@ -1,10 +1,10 @@ -package top.fatweb.api.entity.converter +package top.fatweb.api.converter import org.mapstruct.Mapper import org.mapstruct.Mapping import org.mapstruct.Mappings -import top.fatweb.api.entity.param.LoginParam import top.fatweb.api.entity.permission.User +import top.fatweb.api.param.LoginParam @Mapper(componentModel = "spring") interface UserConverter { diff --git a/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt b/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt index f160c2c..951d1e2 100644 --- a/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt +++ b/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt @@ -1,9 +1,11 @@ package top.fatweb.api.entity.permission import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonTypeInfo import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.UserDetails +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) class LoginUser() : UserDetails { lateinit var user: User @@ -22,15 +24,21 @@ class LoginUser() : UserDetails { return authorities as List } + @JsonIgnore override fun getPassword(): String? = user.password + @JsonIgnore override fun getUsername(): String? = user.username + @JsonIgnore override fun isAccountNonExpired(): Boolean = true + @JsonIgnore override fun isAccountNonLocked(): Boolean = true + @JsonIgnore override fun isCredentialsNonExpired(): Boolean = true + @JsonIgnore override fun isEnabled(): Boolean = user.enable == 1 } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt b/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt index 74f6ec0..048aa39 100644 --- a/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt +++ b/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt @@ -13,7 +13,7 @@ import top.fatweb.api.entity.permission.LoginUser import top.fatweb.api.exception.TokenHasExpiredException import top.fatweb.api.util.JwtUtil import top.fatweb.api.util.RedisUtil -import java.util.concurrent.TimeUnit +import top.fatweb.api.util.WebUtil @Component class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRequestFilter() { @@ -29,14 +29,14 @@ class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRe return } - val token = tokenWithPrefix.removePrefix(SecurityConstants.tokenPrefix) + val token = WebUtil.getToken(tokenWithPrefix) JwtUtil.parseJwt(token) - val redisKey = "${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32) + val redisKey = "${SecurityConstants.jwtIssuer}_login:" + token val loginUser = redisUtil.getObject(redisKey) loginUser ?: let { throw TokenHasExpiredException() } - redisUtil.setExpire(redisKey, 20, TimeUnit.MINUTES) + redisUtil.setExpire(redisKey, SecurityConstants.redisTtl, SecurityConstants.redisTtlUnit) val authenticationToken = UsernamePasswordAuthenticationToken(loginUser, null, loginUser.authorities) SecurityContextHolder.getContext().authentication = authenticationToken diff --git a/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt b/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt index 412c401..5e742e1 100644 --- a/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt +++ b/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt @@ -1,5 +1,7 @@ package top.fatweb.api.handler +import com.auth0.jwt.exceptions.JWTDecodeException +import com.auth0.jwt.exceptions.SignatureVerificationException import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.http.converter.HttpMessageNotReadableException @@ -11,6 +13,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice import top.fatweb.api.entity.common.ResponseCode import top.fatweb.api.entity.common.ResponseResult +import top.fatweb.api.exception.TokenHasExpiredException @RestControllerAdvice class ExceptionHandler { @@ -45,6 +48,16 @@ class ExceptionHandler { ResponseResult.fail(ResponseCode.SYSTEM_LOGIN_USERNAME_PASSWORD_ERROR, e.localizedMessage, null) } + is SignatureVerificationException, is JWTDecodeException -> { + log.debug(e.localizedMessage, e) + ResponseResult.fail(ResponseCode.SYSTEM_TOKEN_ILLEGAL, "Token illegal", null) + } + + is TokenHasExpiredException -> { + log.debug(e.localizedMessage, e) + ResponseResult.fail(ResponseCode.SYSTEM_TOKEN_HAS_EXPIRED, e.localizedMessage, null) + } + else -> { log.error(e.localizedMessage, e) ResponseResult.fail(ResponseCode.SYSTEM_ERROR, data = null) diff --git a/src/main/kotlin/top/fatweb/api/entity/param/LoginParam.kt b/src/main/kotlin/top/fatweb/api/param/LoginParam.kt similarity index 86% rename from src/main/kotlin/top/fatweb/api/entity/param/LoginParam.kt rename to src/main/kotlin/top/fatweb/api/param/LoginParam.kt index 4ed8d0c..701ab12 100644 --- a/src/main/kotlin/top/fatweb/api/entity/param/LoginParam.kt +++ b/src/main/kotlin/top/fatweb/api/param/LoginParam.kt @@ -1,9 +1,10 @@ -package top.fatweb.api.entity.param +package top.fatweb.api.param import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotBlank import java.io.Serializable +@Schema(description = "登录请求参数") class LoginParam : Serializable { @Schema(description = "用户名", example = "test", required = true) diff --git a/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt b/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt index b58a4b6..28becfc 100644 --- a/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt +++ b/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt @@ -1,11 +1,13 @@ package top.fatweb.api.service.permission import top.fatweb.api.entity.permission.User +import top.fatweb.api.vo.LoginVo +import top.fatweb.api.vo.TokenVo interface IAuthenticationService { - fun login(user: User): HashMap + fun login(user: User): LoginVo fun logout(token: String): Boolean - fun renewToken(token: String): HashMap + fun renewToken(token: String): TokenVo } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt b/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt index b742f3d..d08bc10 100644 --- a/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt +++ b/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt @@ -10,14 +10,15 @@ import top.fatweb.api.service.permission.IAuthenticationService import top.fatweb.api.util.JwtUtil import top.fatweb.api.util.RedisUtil import top.fatweb.api.util.WebUtil -import java.util.concurrent.TimeUnit +import top.fatweb.api.vo.LoginVo +import top.fatweb.api.vo.TokenVo @Service class AuthenticationServiceImpl( private val authenticationManager: AuthenticationManager, private val redisUtil: RedisUtil ) : IAuthenticationService { - override fun login(user: User): HashMap { + override fun login(user: User): LoginVo { val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(user.username, user.password) val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken) authentication ?: let { @@ -33,18 +34,17 @@ class AuthenticationServiceImpl( throw RuntimeException("Login failed") } - val hashMap = hashMapOf("token" to jwt) - val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt.substring(0, 32) - redisUtil.setObject(redisKey, loginUser, 20, TimeUnit.MINUTES) + val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt + redisUtil.setObject(redisKey, loginUser, SecurityConstants.redisTtl, SecurityConstants.redisTtlUnit) - return hashMap + return LoginVo(jwt) } override fun logout(token: String): Boolean = - redisUtil.delObject("${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32)) + redisUtil.delObject("${SecurityConstants.jwtIssuer}_login:" + token) - override fun renewToken(token: String): HashMap { - val oldRedisKey = "${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32) + override fun renewToken(token: String): TokenVo { + val oldRedisKey = "${SecurityConstants.jwtIssuer}_login:" + token redisUtil.delObject(oldRedisKey) val jwt = JwtUtil.createJwt(WebUtil.getLoginUserId().toString()) @@ -52,10 +52,14 @@ class AuthenticationServiceImpl( throw RuntimeException("Login failed") } - val hashMap = hashMapOf("token" to jwt) - val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt.substring(0, 32) - redisUtil.setObject(redisKey, WebUtil.getLoginUser(), 20, TimeUnit.MINUTES) + val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt + redisUtil.setObject( + redisKey, + WebUtil.getLoginUser(), + SecurityConstants.redisTtl, + SecurityConstants.redisTtlUnit + ) - return hashMap + return TokenVo(jwt) } } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/util/WebUtil.kt b/src/main/kotlin/top/fatweb/api/util/WebUtil.kt index 892c0c6..da37681 100644 --- a/src/main/kotlin/top/fatweb/api/util/WebUtil.kt +++ b/src/main/kotlin/top/fatweb/api/util/WebUtil.kt @@ -1,10 +1,16 @@ package top.fatweb.api.util +import jakarta.servlet.http.HttpServletRequest import org.springframework.security.core.context.SecurityContextHolder +import top.fatweb.api.constant.SecurityConstants import top.fatweb.api.entity.permission.LoginUser object WebUtil { fun getLoginUser() = SecurityContextHolder.getContext().authentication.principal as LoginUser fun getLoginUserId() = getLoginUser().user.id + + fun getToken(tokenWithPrefix: String) = tokenWithPrefix.removePrefix(SecurityConstants.tokenPrefix) + + fun getToken(request: HttpServletRequest) = getToken(request.getHeader(SecurityConstants.headerString)) } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/vo/LoginVo.kt b/src/main/kotlin/top/fatweb/api/vo/LoginVo.kt new file mode 100644 index 0000000..8727d54 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/vo/LoginVo.kt @@ -0,0 +1,11 @@ +package top.fatweb.api.vo + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "登录返回参数") +data class LoginVo( + @Schema( + description = "Token", + example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkYTllYjFkYmVmZDQ0OWRkOThlOGNjNzZlNzZkMDgyNSIsInN1YiI6IjE3MDk5ODYwNTg2Nzk5NzU5MzgiLCJpc3MiOiJGYXRXZWIiLCJpYXQiOjE2OTY1MjgxMTcsImV4cCI6MTY5NjUzNTMxN30.U2ZsyrGk7NbsP-DJfdz9xgWSfect5r2iKQnlEsscAA8" + ) val token: String +) \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/vo/TokenVo.kt b/src/main/kotlin/top/fatweb/api/vo/TokenVo.kt new file mode 100644 index 0000000..c17ea01 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/vo/TokenVo.kt @@ -0,0 +1,11 @@ +package top.fatweb.api.vo + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Token") +data class TokenVo( + @Schema( + description = "Token", + example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkYTllYjFkYmVmZDQ0OWRkOThlOGNjNzZlNzZkMDgyNSIsInN1YiI6IjE3MDk5ODYwNTg2Nzk5NzU5MzgiLCJpc3MiOiJGYXRXZWIiLCJpYXQiOjE2OTY1MjgxMTcsImV4cCI6MTY5NjUzNTMxN30.U2ZsyrGk7NbsP-DJfdz9xgWSfect5r2iKQnlEsscAA8" + ) val token: String +) \ No newline at end of file diff --git a/src/main/resources/application-config-template.yml b/src/main/resources/application-config-template.yml index 5280697..487ce6e 100644 --- a/src/main/resources/application-config-template.yml +++ b/src/main/resources/application-config-template.yml @@ -3,9 +3,11 @@ app: # header-string: "Authorization" # The key of head to get token # token-prefix: "Bearer " # Token prefix # jwt-ttl: 2 # The life of token -# jwt-ttl-unit: hours # Unit of life of token +# jwt-ttl-unit: hours # Unit of life of token [nanoseconds, microseconds, milliseconds, seconds, minutes, hours, days] jwt-key: $uuid$ # Key to generate token (Only numbers and letters allow) # jwt-issuer: FatWeb # Token issuer +# redis-ttl: 20 # The life of token in redis +# redis-ttl-unit: minutes # Unit of life of token in redis [nanoseconds, microseconds, milliseconds, seconds, minutes, hours, days] server: # port: 8080 # Server port -- 2.49.1 From 243d7654e7c5082927ceb25094f5c3947154ba0c Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Fri, 6 Oct 2023 15:00:08 +0800 Subject: [PATCH 16/24] Hide ExceptionController in swagger --- .../kotlin/top/fatweb/api/controller/ExceptionController.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/top/fatweb/api/controller/ExceptionController.kt b/src/main/kotlin/top/fatweb/api/controller/ExceptionController.kt index dcb7833..e5dfad2 100644 --- a/src/main/kotlin/top/fatweb/api/controller/ExceptionController.kt +++ b/src/main/kotlin/top/fatweb/api/controller/ExceptionController.kt @@ -1,9 +1,11 @@ package top.fatweb.api.controller +import io.swagger.v3.oas.annotations.Hidden import jakarta.servlet.http.HttpServletRequest import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +@Hidden @RestController @RequestMapping("/error") class ExceptionController { -- 2.49.1 From e085005f73213e922371de757a14681a417061fc Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Sat, 7 Oct 2023 02:53:28 +0800 Subject: [PATCH 17/24] Remove mapstruct --- pom.xml | 33 ++++--------------- .../top/fatweb/api/converter/UserConverter.kt | 17 ++++++---- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/pom.xml b/pom.xml index 81e5aad..f65cf62 100644 --- a/pom.xml +++ b/pom.xml @@ -129,13 +129,13 @@ druid-spring-boot-starter 1.2.19 - + org.apache.velocity velocity-engine-core @@ -158,16 +158,6 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 - - org.mapstruct - mapstruct - 1.5.5.Final - - - org.mapstruct - mapstruct-processor - 1.5.5.Final - @@ -181,14 +171,6 @@ org.springframework.boot spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - org.jetbrains.kotlin @@ -226,5 +208,4 @@ - diff --git a/src/main/kotlin/top/fatweb/api/converter/UserConverter.kt b/src/main/kotlin/top/fatweb/api/converter/UserConverter.kt index 71b980a..1bfd381 100644 --- a/src/main/kotlin/top/fatweb/api/converter/UserConverter.kt +++ b/src/main/kotlin/top/fatweb/api/converter/UserConverter.kt @@ -1,14 +1,17 @@ package top.fatweb.api.converter -import org.mapstruct.Mapper -import org.mapstruct.Mapping -import org.mapstruct.Mappings +import org.springframework.stereotype.Component import top.fatweb.api.entity.permission.User import top.fatweb.api.param.LoginParam -@Mapper(componentModel = "spring") -interface UserConverter { - @Mappings(Mapping(source = "username", target = "username"), Mapping(source = "password", target = "password")) - fun loginParamToUser(loginParam: LoginParam): User +@Component +object UserConverter { + fun loginParamToUser(loginParam: LoginParam): User { + val user = User().apply { + username = loginParam.username + password = loginParam.password + } + return user + } } \ No newline at end of file -- 2.49.1 From 3b8069322a518a665ba27d9f952a530da10bd34d Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Sat, 7 Oct 2023 02:54:04 +0800 Subject: [PATCH 18/24] Reformat code --- src/main/kotlin/top/fatweb/api/config/RedisConfig.kt | 4 +--- src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt b/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt index 75eee51..14d3ee0 100644 --- a/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt +++ b/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt @@ -22,9 +22,7 @@ class RedisConfig { val objectMapper = ObjectMapper().registerModules(JavaTimeModule()).apply { setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY) activateDefaultTyping( - this.polymorphicTypeValidator, - ObjectMapper.DefaultTyping.NON_FINAL, - JsonTypeInfo.As.PROPERTY + this.polymorphicTypeValidator, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ) } val anyJackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(objectMapper, Any::class.java) diff --git a/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt b/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt index 9b23066..fde4588 100644 --- a/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt +++ b/src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt @@ -16,8 +16,8 @@ class SwaggerConfig { return OpenAPI().info( Info().title("FatWeb API 文档").description("FatWeb 后端 API 文档,包含各个 Controller 调用信息") .contact(contact).version( - ServerConstants.version - ) + ServerConstants.version + ) ) } } \ No newline at end of file -- 2.49.1 From 723bdd4c29fb8d6b8d1e75902ce8475b5629dfca Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Sun, 8 Oct 2023 17:17:37 +0800 Subject: [PATCH 19/24] Recode unit test --- .../fatweb/api/FatWebApiApplicationTests.kt | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt index 9dc0d31..6d0c220 100644 --- a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt +++ b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt @@ -2,29 +2,33 @@ package top.fatweb.api import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.test.context.junit.jupiter.SpringExtension import top.fatweb.api.constant.SecurityConstants +import top.fatweb.api.util.JwtUtil -@SpringBootTest +@ExtendWith(SpringExtension::class) class FatWebApiApplicationTests { - @Test - fun contextLoads() { - } - @Test fun removePrefixTest() { assertEquals("12312", "Bearer 12312".removePrefix(SecurityConstants.tokenPrefix)) } + @Test + fun jwtTest() { + val jwt = JwtUtil.createJwt("User") + assertEquals("User", jwt?.let { JwtUtil.parseJwt(it).subject }) + } + /* - @Test - fun addUser(@Autowired userService: IUserService, @Autowired passwordEncoder: PasswordEncoder) { - val username = "admin" - val rawPassword = "admin" - val encodedPassword = passwordEncoder.encode(rawPassword) - val user = User(username, encodedPassword) - userService.save(user) - } - */ + @Test + fun addUser(@Autowired userService: IUserService, @Autowired passwordEncoder: PasswordEncoder) { + val username = "admin" + val rawPassword = "admin" + val encodedPassword = passwordEncoder.encode(rawPassword) + val user = User(username, encodedPassword) + userService.save(user) + } + */ } -- 2.49.1 From 82c9fcb10fb9b6cb394ccc231459e09ba287bce8 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Sun, 8 Oct 2023 23:33:59 +0800 Subject: [PATCH 20/24] Change the example config path --- .gitignore | 4 +++- .../kotlin/top/fatweb/api/FatWebApiApplication.kt | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 318632d..d65ea6e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,5 +31,7 @@ build/ ### VS Code ### .vscode/ + +### Custom ### /application-config.yml -/application-config.example.yml +data diff --git a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt index 1920cd3..3c635e2 100644 --- a/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt +++ b/src/main/kotlin/top/fatweb/api/FatWebApiApplication.kt @@ -14,12 +14,19 @@ class FatWebApiApplication fun main(args: Array) { val logger = LoggerFactory.getLogger("main") + if (!File("data").isDirectory) { + if (!File("data").mkdir()) { + logger.error("Can not create directory 'data', please try again later.") + return + } + } + if (File("application-config.yml").exists()) { runApplication(*args) } else { - logger.warn("File ‘application.yml’ cannot be found in the running path. The configuration file template 'application.example.yml' has been created. Please change the configuration file content and rename it to 'application.yml', and then restart the server.") + logger.warn("File ‘application.yml’ cannot be found in the running path. The configuration file template 'application.example.yml' has been created in directory 'data'. Please change the configuration file content, rename it to 'application.yml' and put it in the running path, and then restart the server.") FatWebApiApplication::class.java.getResource("/application-config-template.yml")?.readText()?.let { - File("application-config.example.yml").writeText( + File("data/application-config.example.yml").writeText( it.replace( "\$uuid\$", UUID.randomUUID().toString().replace("-", "") ) -- 2.49.1 From cfcbe8a40d051516bbea9531cfc9a50ad55cdebf Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Sun, 8 Oct 2023 23:34:19 +0800 Subject: [PATCH 21/24] Remove native-maven-plugin --- pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pom.xml b/pom.xml index f65cf62..abe9e27 100644 --- a/pom.xml +++ b/pom.xml @@ -164,10 +164,6 @@ ${project.basedir}/src/main/kotlin ${project.basedir}/src/test/kotlin - - org.graalvm.buildtools - native-maven-plugin - org.springframework.boot spring-boot-maven-plugin -- 2.49.1 From 90cee680462079f6b278a41e094e981c994fd1cf Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Sun, 8 Oct 2023 23:34:34 +0800 Subject: [PATCH 22/24] Add docker builder --- Dockerfile | 12 ++++++++++++ build-docker.sh | 9 +++++++++ 2 files changed, 21 insertions(+) create mode 100644 Dockerfile create mode 100644 build-docker.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..332d586 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM eclipse-temurin:17-jdk-alpine +LABEL authors="FatttSnake" + +VOLUME /data + +ARG DEPENDENCY=target/extracted +COPY ${EXTRACTED}/dependencies/ / +COPY ${EXTRACTED}/spring-boot-loader/ / +COPY ${EXTRACTED}/snapshot-dependencies/ / +COPY ${EXTRACTED}/application/ / + +ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher", "${JAVA_OPTS}"] \ No newline at end of file diff --git a/build-docker.sh b/build-docker.sh new file mode 100644 index 0000000..6b42feb --- /dev/null +++ b/build-docker.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +JAR_NAME=`ls target | grep api-|grep -v original` +JAR_VERSION=${JAR_NAME%.*} +JAR_VERSION=${JAR_VERSION#*-} + +mkdir target/extracted +java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted +docker build -t hub.fatweb.top/fatweb-api:latest -t hub.fatweb.top/fatweb-api:$JAR_VERSION -t hub.fatweb.top/fatweb-api:$JAR_VERSION-$(date "+%Y%m%d%H%M%S") . \ No newline at end of file -- 2.49.1 From 477e11fb47a9a18b6d2b6c543f587204c51a77b4 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Mon, 9 Oct 2023 00:16:18 +0800 Subject: [PATCH 23/24] Fix Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 332d586..f50582a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ LABEL authors="FatttSnake" VOLUME /data -ARG DEPENDENCY=target/extracted +ARG EXTRACTED=target/extracted COPY ${EXTRACTED}/dependencies/ / COPY ${EXTRACTED}/spring-boot-loader/ / COPY ${EXTRACTED}/snapshot-dependencies/ / -- 2.49.1 From 6a474492598a6a63633019eb0924e1048aa62fab Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Mon, 9 Oct 2023 00:25:18 +0800 Subject: [PATCH 24/24] Fix Dockerfile --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index f50582a..957b824 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ ARG EXTRACTED=target/extracted COPY ${EXTRACTED}/dependencies/ / COPY ${EXTRACTED}/spring-boot-loader/ / COPY ${EXTRACTED}/snapshot-dependencies/ / +RUN true COPY ${EXTRACTED}/application/ / ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher", "${JAVA_OPTS}"] \ No newline at end of file -- 2.49.1