From dd9ebbaa6505b9a9eaff29e3195c86292b9ad3ff Mon Sep 17 00:00:00 2001 From: 05412 Date: Thu, 18 Jul 2024 09:49:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/dev/surl/surl/SurlApplication.kt | 2 + src/main/java/dev/surl/surl/cfg/BaseConfig.kt | 28 +++++++++++- src/main/java/dev/surl/surl/cfg/Logging.kt | 6 +-- .../java/dev/surl/surl/cfg/PatternConfig.kt | 10 ----- .../java/dev/surl/surl/cfg/RedisConfig.kt | 9 +++- .../surl/surl/cfg/security/EncoderConfig.kt | 6 --- .../surl/cfg/security/WebSecurityConfig.kt | 9 ++-- src/main/java/dev/surl/surl/common/Access.kt | 3 ++ src/main/java/dev/surl/surl/common/Msg.kt | 3 ++ .../surl/surl/common/enums/RedisStorage.kt | 3 ++ .../exception/UnauthorizedExcecption.kt | 3 ++ .../common/exception/UserRegistException.kt | 3 ++ .../surl/controller/RedirectController.kt | 24 +++++++--- .../surl/surl/controller/SurlAddController.kt | 32 +++++++++++-- .../surl/surl/controller/SurlGetController.kt | 17 +++++-- .../surl/surl/controller/UserController.kt | 8 +++- src/main/java/dev/surl/surl/dao/Surl.kt | 15 +++++++ src/main/java/dev/surl/surl/dao/User.kt | 15 +++++++ src/main/java/dev/surl/surl/dao/UserAccess.kt | 15 +++++++ src/main/java/dev/surl/surl/dsl/Surls.kt | 3 ++ .../java/dev/surl/surl/dsl/UserAccesses.kt | 3 ++ src/main/java/dev/surl/surl/dsl/Users.kt | 3 ++ src/main/java/dev/surl/surl/dto/SurlDto.kt | 3 ++ src/main/java/dev/surl/surl/dto/UserDto.kt | 3 ++ .../filter/JwtAuthenticationTokenFilter.kt | 14 ++++++ ...ernamePasswordAuthenticationCheckFilter.kt | 15 +++++++ .../dev/surl/surl/handler/AccessHandler.kt | 4 ++ .../surl/handler/DefaultExceptionHandler.kt | 25 +++++++++++ .../java/dev/surl/surl/service/SurlService.kt | 29 +++++++++++- .../java/dev/surl/surl/service/UserService.kt | 45 +++++++++++++++++-- src/main/java/dev/surl/surl/util/Autowired.kt | 7 +++ .../java/dev/surl/surl/util/JwtTokenUtil.kt | 45 ++++++++++++++++++- src/main/java/dev/surl/surl/util/UIDUtil.kt | 14 ++++++ .../java/dev/surl/surl/util/ValidateUtil.kt | 5 +++ .../dev/surl/surl/util/redis/RedisUtil.kt | 29 ++++++++++++ src/main/resources/application.yml | 4 +- 36 files changed, 414 insertions(+), 48 deletions(-) delete mode 100644 src/main/java/dev/surl/surl/cfg/PatternConfig.kt diff --git a/src/main/java/dev/surl/surl/SurlApplication.kt b/src/main/java/dev/surl/surl/SurlApplication.kt index bc255fe..c20a789 100644 --- a/src/main/java/dev/surl/surl/SurlApplication.kt +++ b/src/main/java/dev/surl/surl/SurlApplication.kt @@ -9,11 +9,13 @@ import org.springframework.context.ConfigurableApplicationContext @SpringBootApplication @EnableConfigurationProperties(BaseConfig::class) open class SurlApplication { + // 伴生对象,用于获取上下文 companion object { lateinit var context: ConfigurableApplicationContext } } fun main(args: Array) { + // 启动并获取上下文 SurlApplication.context = runApplication(*args) } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/cfg/BaseConfig.kt b/src/main/java/dev/surl/surl/cfg/BaseConfig.kt index cd8725e..c4dd393 100644 --- a/src/main/java/dev/surl/surl/cfg/BaseConfig.kt +++ b/src/main/java/dev/surl/surl/cfg/BaseConfig.kt @@ -7,13 +7,39 @@ import java.time.temporal.ChronoUnit import java.util.Date import javax.crypto.SecretKey +/** + * 基础配置 + */ @ConfigurationProperties(prefix = "base.configs") class BaseConfig( + /** + * 主站域名/IP + */ val site: String = "http://127.0.0.1", - val expire: Long = 3600000, // token expire time + + /** + * token过期数值 + */ + val expire: Long = 3600000, + + /** + * token过期单位 + */ val unit: ChronoUnit = ChronoUnit.MILLIS, + + /** + * token头 + */ val tokenHead: String = "Bearer ", + + /** + * 免认证白名单 + */ whiteList: List = listOf("/login"), + + /** + * JWT密钥 + */ secret: String = numberToKey(Date().time).repeat(5), ) { val secretKey: SecretKey = Keys.hmacShaKeyFor(secret.toByteArray()) diff --git a/src/main/java/dev/surl/surl/cfg/Logging.kt b/src/main/java/dev/surl/surl/cfg/Logging.kt index 71fc114..403cf40 100644 --- a/src/main/java/dev/surl/surl/cfg/Logging.kt +++ b/src/main/java/dev/surl/surl/cfg/Logging.kt @@ -3,8 +3,8 @@ package dev.surl.surl.cfg import org.slf4j.Logger import org.slf4j.LoggerFactory +/** + * 获取日志对象的扩展函数 + */ @Suppress("UNUSED") fun T.logger(): Logger = LoggerFactory.getLogger(this::class.java) - -@Suppress("UNUSED") -fun logger(name: String): Logger = LoggerFactory.getLogger(name) \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/cfg/PatternConfig.kt b/src/main/java/dev/surl/surl/cfg/PatternConfig.kt deleted file mode 100644 index 4a10bf1..0000000 --- a/src/main/java/dev/surl/surl/cfg/PatternConfig.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.surl.surl.cfg - -import org.springframework.context.annotation.Configuration - -@Configuration -@Suppress("UNUSED") -open class PatternConfig { - val usernamePattern = Regex("""\w{6,20}""") - val passwordPattern = Regex("""^((?=\S*?[A-Z])(?=\S*?[a-z])(?=\S*?[0-9])(?=\S*?)).{10,}\S$""") -} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/cfg/RedisConfig.kt b/src/main/java/dev/surl/surl/cfg/RedisConfig.kt index b9c496b..66792da 100644 --- a/src/main/java/dev/surl/surl/cfg/RedisConfig.kt +++ b/src/main/java/dev/surl/surl/cfg/RedisConfig.kt @@ -2,14 +2,21 @@ package dev.surl.surl.cfg import org.springframework.context.annotation.Bean import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Component +/** + * Redis配置类 + */ @Component class RedisConfig { + /** + * 默认RedisTemplate + */ @Bean - fun baseRedis(factory: RedisConnectionFactory): StringRedisTemplate { + fun baseRedis(factory: RedisConnectionFactory): RedisTemplate { return StringRedisTemplate(factory) } } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/cfg/security/EncoderConfig.kt b/src/main/java/dev/surl/surl/cfg/security/EncoderConfig.kt index 4dfc9e7..a24a16d 100644 --- a/src/main/java/dev/surl/surl/cfg/security/EncoderConfig.kt +++ b/src/main/java/dev/surl/surl/cfg/security/EncoderConfig.kt @@ -2,7 +2,6 @@ package dev.surl.surl.cfg.security import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.security.crypto.bcrypt.BCrypt import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder @Configuration @@ -15,9 +14,4 @@ open class EncoderConfig { open fun passwordEncoder(): BCryptPasswordEncoder { return BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.`$2B`) } - - @Bean - open fun cryoto(): BCrypt { - return BCrypt() - } } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/cfg/security/WebSecurityConfig.kt b/src/main/java/dev/surl/surl/cfg/security/WebSecurityConfig.kt index 8112854..98c6105 100644 --- a/src/main/java/dev/surl/surl/cfg/security/WebSecurityConfig.kt +++ b/src/main/java/dev/surl/surl/cfg/security/WebSecurityConfig.kt @@ -9,6 +9,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.web.SecurityFilterChain import org.springframework.security.config.annotation.web.invoke +/** + * 网安配置 + */ @Configuration @EnableWebSecurity open class WebSecurityConfig { @@ -22,10 +25,10 @@ open class WebSecurityConfig { response: HttpServletResponse): SecurityFilterChain { http { csrf { disable() } // 关闭csrf - formLogin { disable() } - httpBasic { disable() } + formLogin { disable() } // 关闭表单登录 + httpBasic { disable() } // 关闭basic认证 authorizeHttpRequests { - authorize(anyRequest, permitAll) + authorize(anyRequest, permitAll) // 放行所有请求 } headers { cacheControl { } // 禁用缓存 diff --git a/src/main/java/dev/surl/surl/common/Access.kt b/src/main/java/dev/surl/surl/common/Access.kt index a4bdbb4..feff53f 100644 --- a/src/main/java/dev/surl/surl/common/Access.kt +++ b/src/main/java/dev/surl/surl/common/Access.kt @@ -1,5 +1,8 @@ package dev.surl.surl.common +/** + * 用户权限枚举 + */ enum class Access { ADMIN, READ, WRITE } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/common/Msg.kt b/src/main/java/dev/surl/surl/common/Msg.kt index 6652f06..4004b7f 100644 --- a/src/main/java/dev/surl/surl/common/Msg.kt +++ b/src/main/java/dev/surl/surl/common/Msg.kt @@ -1,5 +1,8 @@ package dev.surl.surl.common +/** + * 通用接口返回格式 + */ data class Msg( val code: Int = 0, val msg: String? = null, val value: T? = null ) \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/common/enums/RedisStorage.kt b/src/main/java/dev/surl/surl/common/enums/RedisStorage.kt index b212ec0..81c4d9e 100644 --- a/src/main/java/dev/surl/surl/common/enums/RedisStorage.kt +++ b/src/main/java/dev/surl/surl/common/enums/RedisStorage.kt @@ -1,5 +1,8 @@ package dev.surl.surl.common.enums +/** + * Redis存储的前缀 + */ enum class RedisStorage { TOKEN } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/common/exception/UnauthorizedExcecption.kt b/src/main/java/dev/surl/surl/common/exception/UnauthorizedExcecption.kt index 0fd2f9c..1022854 100644 --- a/src/main/java/dev/surl/surl/common/exception/UnauthorizedExcecption.kt +++ b/src/main/java/dev/surl/surl/common/exception/UnauthorizedExcecption.kt @@ -1,3 +1,6 @@ package dev.surl.surl.common.exception +/** + * 自定义权限异常 + */ class UnauthorizedExcecption(message: String? = null, cause: Throwable? = null) : Exception(message, cause) \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/common/exception/UserRegistException.kt b/src/main/java/dev/surl/surl/common/exception/UserRegistException.kt index 1ba5479..579b199 100644 --- a/src/main/java/dev/surl/surl/common/exception/UserRegistException.kt +++ b/src/main/java/dev/surl/surl/common/exception/UserRegistException.kt @@ -1,3 +1,6 @@ package dev.surl.surl.common.exception +/** + * 自定义注册异常 + */ class UserRegistException(message: String? = null, cause: Throwable? = null): Exception(message, cause) \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/controller/RedirectController.kt b/src/main/java/dev/surl/surl/controller/RedirectController.kt index eff00a9..be95535 100644 --- a/src/main/java/dev/surl/surl/controller/RedirectController.kt +++ b/src/main/java/dev/surl/surl/controller/RedirectController.kt @@ -12,20 +12,30 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RestController import java.net.URI +/** + * 短链接跳转控制器 + */ @RestController class RedirectController(private val service: SurlService) { + + /** + * 短链接跳转 + */ @GetMapping("/{key}") fun redirect( - @PathVariable - @Valid - @Length(min = 1, max = 11, message = "Key length is not valid") - @Pattern(regexp = "[\\w!*().\\-_~]+", message = "Key format is not valid") - key: String + @PathVariable @Valid @Length( + min = 1, + max = 11, + message = "Key length is not valid" + ) @Pattern(regexp = "[\\w!*().\\-_~]+", message = "Key format is not valid") key: String ): ResponseEntity { - val redirectUrl = service.getUrlByKey(key) - return if(redirectUrl.isBlank()) { + // 根据key获取原始链接 + val redirectUrl = service.getUrlByKey(key) + return if (redirectUrl.isBlank()) { + // 未找到,返回异常信息 ResponseEntity(Msg(code = -1, msg = "key `$key` not found"), HttpStatus.NOT_FOUND) } else { + // 找到,发送302跳转 ResponseEntity.status(302).location(URI.create(redirectUrl)).build() } } diff --git a/src/main/java/dev/surl/surl/controller/SurlAddController.kt b/src/main/java/dev/surl/surl/controller/SurlAddController.kt index cac79f3..f4d8e06 100644 --- a/src/main/java/dev/surl/surl/controller/SurlAddController.kt +++ b/src/main/java/dev/surl/surl/controller/SurlAddController.kt @@ -4,14 +4,40 @@ import dev.surl.surl.cfg.BaseConfig import dev.surl.surl.common.Msg import dev.surl.surl.dto.SurlDto import dev.surl.surl.service.SurlService +import dev.surl.surl.util.JwtTokenUtil import jakarta.validation.Valid +import org.springframework.http.HttpHeaders import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RestController +/** + * 短链接新增控制器 + */ @RestController -class SurlAddController(private val service: SurlService, private val cfg: BaseConfig) { +class SurlAddController( + private val service: SurlService, private val cfg: BaseConfig, private val jwtTokenUtil: JwtTokenUtil +) { + /** + * 短链接新增 + */ @PostMapping("/api/surl/add") - fun addSurl(@Valid @RequestBody body: SurlDto) = - Msg(code = 0, value = "${cfg.site}/${service.addSurl(body.url ?: "")}") + fun addSurl(@RequestHeader headers: HttpHeaders, @Valid @RequestBody body: SurlDto): Msg { + + // 从认证头获取用户名 + val username = jwtTokenUtil.getUsernameFromHeader(headers) + + // 获取主站域名/IP + val site = cfg.site + + // 添加短链接 + val key = service.addSurl(body.url ?: "", username) + + // 拼接短链接 + val url = "$site/$key" + + return Msg(code = 0, value = url) + } + } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/controller/SurlGetController.kt b/src/main/java/dev/surl/surl/controller/SurlGetController.kt index 7a349d6..9addb75 100644 --- a/src/main/java/dev/surl/surl/controller/SurlGetController.kt +++ b/src/main/java/dev/surl/surl/controller/SurlGetController.kt @@ -8,16 +8,25 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RestController +/** + * 获取用户名下短链接列表控制器 + */ @RestController class SurlGetController( - private val surlService: SurlService, - private val jwtTokenUtil: JwtTokenUtil + private val surlService: SurlService, private val jwtTokenUtil: JwtTokenUtil ) { + /** + * 获取用户名下短链接列表 + */ @GetMapping(path = ["/api/surl/get"]) fun getUrlsByUser(@RequestHeader headers: HttpHeaders): Msg> { - val token = jwtTokenUtil.getTokenFromHeader(headers[HttpHeaders.AUTHORIZATION]?.last() ?: "") - val username = jwtTokenUtil.getUsernameFromToken(token) + + // 从认证头获取用户名 + val username = jwtTokenUtil.getUsernameFromHeader(headers) + + // 获取用户名下短链接列表 val urls = surlService.getUrlsByUser(username) + return Msg(value = urls) } } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/controller/UserController.kt b/src/main/java/dev/surl/surl/controller/UserController.kt index c65926a..54e0f6d 100644 --- a/src/main/java/dev/surl/surl/controller/UserController.kt +++ b/src/main/java/dev/surl/surl/controller/UserController.kt @@ -9,10 +9,16 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMethod import org.springframework.web.bind.annotation.RestController +/** + * 用户操作控制器 + */ @RestController class UserController { + /** + * 用户注册 + */ @RequestMapping(method = [RequestMethod.POST], path = ["/reg"]) fun reg( @Autowired service: UserService, @Valid @RequestBody(required = true) user: UserDto - ) = service.addUser(user.username!!, user.password!!) + ) = service.addUser(user.username!!, user.password!!) // 新增用户 } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/dao/Surl.kt b/src/main/java/dev/surl/surl/dao/Surl.kt index 3d52dc9..3f7fb81 100644 --- a/src/main/java/dev/surl/surl/dao/Surl.kt +++ b/src/main/java/dev/surl/surl/dao/Surl.kt @@ -5,9 +5,24 @@ import org.jetbrains.exposed.dao.Entity import org.jetbrains.exposed.dao.EntityClass import org.jetbrains.exposed.dao.id.EntityID +/** + * 短链接实体类 + */ @Suppress("UNUSED") class Surl(id: EntityID): Entity(id) { + + /** + * 短链接实体类伴生对象,用于crud操作 + */ companion object: EntityClass(Surls) + + /** + * 短链接url + */ var url by Surls.url + + /** + * 短链接所属用户 + */ var user by User optionalReferencedOn Surls.user } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/dao/User.kt b/src/main/java/dev/surl/surl/dao/User.kt index 6b5dd8e..7c10b98 100644 --- a/src/main/java/dev/surl/surl/dao/User.kt +++ b/src/main/java/dev/surl/surl/dao/User.kt @@ -5,8 +5,23 @@ import org.jetbrains.exposed.dao.LongEntity import org.jetbrains.exposed.dao.LongEntityClass import org.jetbrains.exposed.dao.id.EntityID +/** + * 用户实体 + */ class User(id: EntityID): LongEntity(id) { + + /** + * 用户实体伴生对象,用于CRUD操作 + */ companion object EntityClass: LongEntityClass(Users) + + /** + * 用户名 + */ var username by Users.username + + /** + * 密码 + */ var password by Users.password } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/dao/UserAccess.kt b/src/main/java/dev/surl/surl/dao/UserAccess.kt index 0476650..07e7efb 100644 --- a/src/main/java/dev/surl/surl/dao/UserAccess.kt +++ b/src/main/java/dev/surl/surl/dao/UserAccess.kt @@ -6,12 +6,27 @@ import org.jetbrains.exposed.dao.LongEntity import org.jetbrains.exposed.dao.LongEntityClass import org.jetbrains.exposed.dao.id.EntityID +/** + * 用户权限实体类 + */ class UserAccess(id: EntityID): LongEntity(id) { + + /** + * 伴生对象,用于CRUD操作 + */ companion object EntityClass: LongEntityClass(UserAccesses) + + /** + * 权限,枚举类型,自动转换为数据库存储的整数 + */ var access by UserAccesses.access.transform(toColumn = { it.ordinal.toShort() }, toReal = { Access.entries[it.toInt()] }) + + /** + * 用户 + */ var user by User referencedOn UserAccesses.user } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/dsl/Surls.kt b/src/main/java/dev/surl/surl/dsl/Surls.kt index 216d843..db76b14 100644 --- a/src/main/java/dev/surl/surl/dsl/Surls.kt +++ b/src/main/java/dev/surl/surl/dsl/Surls.kt @@ -2,6 +2,9 @@ package dev.surl.surl.dsl import org.jetbrains.exposed.dao.id.IdTable +/** + * 短链接表 + */ object Surls: IdTable("surl") { override val id = long("id").entityId() val url = varchar("url", 2048) diff --git a/src/main/java/dev/surl/surl/dsl/UserAccesses.kt b/src/main/java/dev/surl/surl/dsl/UserAccesses.kt index 711688a..1313846 100644 --- a/src/main/java/dev/surl/surl/dsl/UserAccesses.kt +++ b/src/main/java/dev/surl/surl/dsl/UserAccesses.kt @@ -2,6 +2,9 @@ package dev.surl.surl.dsl import org.jetbrains.exposed.dao.id.IdTable +/** + * 用户权限表 + */ object UserAccesses: IdTable("user_access") { override val id = long("id").entityId() val user = reference("user", Users).index() diff --git a/src/main/java/dev/surl/surl/dsl/Users.kt b/src/main/java/dev/surl/surl/dsl/Users.kt index 645835c..d09b2ae 100644 --- a/src/main/java/dev/surl/surl/dsl/Users.kt +++ b/src/main/java/dev/surl/surl/dsl/Users.kt @@ -2,6 +2,9 @@ package dev.surl.surl.dsl import org.jetbrains.exposed.dao.id.IdTable +/** + * 用户权限表 + */ object Users: IdTable("users") { override val id = long("id").entityId() val username = varchar("username", 256).uniqueIndex() diff --git a/src/main/java/dev/surl/surl/dto/SurlDto.kt b/src/main/java/dev/surl/surl/dto/SurlDto.kt index f09c814..de402a7 100644 --- a/src/main/java/dev/surl/surl/dto/SurlDto.kt +++ b/src/main/java/dev/surl/surl/dto/SurlDto.kt @@ -4,6 +4,9 @@ import com.fasterxml.jackson.annotation.JsonProperty import jakarta.validation.constraints.NotNull import org.hibernate.validator.constraints.Length +/** + * 短链接新增请求体 + */ data class SurlDto( @JsonProperty("url") @get:NotNull(message = "url cannot be empty") diff --git a/src/main/java/dev/surl/surl/dto/UserDto.kt b/src/main/java/dev/surl/surl/dto/UserDto.kt index 18539c2..32ed805 100644 --- a/src/main/java/dev/surl/surl/dto/UserDto.kt +++ b/src/main/java/dev/surl/surl/dto/UserDto.kt @@ -4,6 +4,9 @@ import com.fasterxml.jackson.annotation.JsonProperty import jakarta.validation.constraints.NotNull import org.hibernate.validator.constraints.Length +/** + * 用户信息请求体 + */ data class UserDto ( @JsonProperty("username") @get:Length(max = 16, min = 4, message = "username length must be between 4 and 16") diff --git a/src/main/java/dev/surl/surl/filter/JwtAuthenticationTokenFilter.kt b/src/main/java/dev/surl/surl/filter/JwtAuthenticationTokenFilter.kt index a7a449c..7ec7ddd 100644 --- a/src/main/java/dev/surl/surl/filter/JwtAuthenticationTokenFilter.kt +++ b/src/main/java/dev/surl/surl/filter/JwtAuthenticationTokenFilter.kt @@ -14,6 +14,9 @@ import org.springframework.http.HttpHeaders import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter +/** + * JWT认证过滤器 + */ @Component class JwtAuthenticationTokenFilter( private val jwtTokenUtil: JwtTokenUtil, @@ -26,8 +29,10 @@ class JwtAuthenticationTokenFilter( response: HttpServletResponse, filterChain: FilterChain ) { + // 检查请求路径是否在白名单内 if (request.servletPath notMatchedIn cfg.whiteList) { try { + // 验证token val exp = UnauthorizedExcecption("unauthorized") val authHeader = request.getHeader(HttpHeaders.AUTHORIZATION) ?: throw exp val token = jwtTokenUtil.getTokenFromHeader(authHeader) @@ -38,8 +43,10 @@ class JwtAuthenticationTokenFilter( throw exp } } + // redis缓存内检查不到已存在token拒绝认证,抛出异常 if (cachedToken != token) throw exp } catch (e: UnauthorizedExcecption) { + // 认证失败 response.status = HttpServletResponse.SC_UNAUTHORIZED val responseBody = om.writeValueAsString(Msg(code = -1, msg = e.message)) response.writer.run { @@ -49,9 +56,13 @@ class JwtAuthenticationTokenFilter( return } } + // 认证成功 filterChain.doFilter(request, response) } + /** + * 判断字符串是否匹配正则列表 + */ private infix fun String.matchedIn(regexes: List): Boolean { for (regex in regexes) { if (this.matches(regex)) return true @@ -59,6 +70,9 @@ class JwtAuthenticationTokenFilter( return false } + /** + * 判断字符串是否不匹配正则列表 + */ private infix fun String.notMatchedIn(regexes: List): Boolean { return !(this matchedIn regexes) } diff --git a/src/main/java/dev/surl/surl/filter/UsernamePasswordAuthenticationCheckFilter.kt b/src/main/java/dev/surl/surl/filter/UsernamePasswordAuthenticationCheckFilter.kt index 2155de5..f95e420 100644 --- a/src/main/java/dev/surl/surl/filter/UsernamePasswordAuthenticationCheckFilter.kt +++ b/src/main/java/dev/surl/surl/filter/UsernamePasswordAuthenticationCheckFilter.kt @@ -21,6 +21,9 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic import org.springframework.stereotype.Component import java.nio.charset.StandardCharsets +/** + * 登录过滤器 + */ @Component class UsernamePasswordAuthenticationCheckFilter( private val om: ObjectMapper, @@ -31,15 +34,21 @@ class UsernamePasswordAuthenticationCheckFilter( ) : UsernamePasswordAuthenticationFilter() { init { + // 设置登录地址 setFilterProcessesUrl("/login") authenticationManager = AuthenticationManager { it } } + /** + * 尝试登录 + */ override fun attemptAuthentication(request: HttpServletRequest?, response: HttpServletResponse?): Authentication { request ?: throw IllegalArgumentException("request is null") val userDto = request.run { om.readValue(String(inputStream.readAllBytes(), StandardCharsets.UTF_8), UserDto::class.java) } + + // 尝试验证登录信息 try { validate(userDto, validator) } catch (e: ConstraintViolationException) { @@ -55,6 +64,9 @@ class UsernamePasswordAuthenticationCheckFilter( ) } + /** + * 登录成功,生成并返回token + */ override fun successfulAuthentication( request: HttpServletRequest?, response: HttpServletResponse?, chain: FilterChain?, authResult: Authentication? ) { @@ -71,6 +83,9 @@ class UsernamePasswordAuthenticationCheckFilter( } } + /** + * 登录失败, 返回错误信息 + */ override fun unsuccessfulAuthentication( request: HttpServletRequest?, response: HttpServletResponse?, failed: AuthenticationException? ) { diff --git a/src/main/java/dev/surl/surl/handler/AccessHandler.kt b/src/main/java/dev/surl/surl/handler/AccessHandler.kt index f25a843..5c07c5c 100644 --- a/src/main/java/dev/surl/surl/handler/AccessHandler.kt +++ b/src/main/java/dev/surl/surl/handler/AccessHandler.kt @@ -6,6 +6,9 @@ import org.springframework.security.access.AccessDeniedException import org.springframework.security.web.access.AccessDeniedHandler import org.springframework.web.bind.annotation.ControllerAdvice +/** + * 访问权限异常处理器 + */ @ControllerAdvice class AccessHandler: AccessDeniedHandler { override fun handle( @@ -13,6 +16,7 @@ class AccessHandler: AccessDeniedHandler { response: HttpServletResponse?, accessDeniedException: AccessDeniedException? ) { + // 跳转登录页 response?.sendRedirect("/login") } } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt b/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt index 7f6bcd5..7fbdcd8 100644 --- a/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt +++ b/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt @@ -16,14 +16,24 @@ import org.springframework.web.context.request.WebRequest import org.springframework.web.method.annotation.HandlerMethodValidationException import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler +/** + * 自定义异常处理 + */ @ControllerAdvice class DefaultExceptionHandler : ResponseEntityExceptionHandler() { + + /** + * 处理方法参数校验异常 + */ override fun handleMethodValidationException( ex: MethodValidationException, headers: HttpHeaders, status: HttpStatus, request: WebRequest ): ResponseEntity { return ResponseEntity(Msg(code = -1, msg = ex.allValidationResults.joinToString(";")), status) } + /** + * 处理方法参数校验异常 + */ override fun handleHandlerMethodValidationException( ex: HandlerMethodValidationException, headers: HttpHeaders, @@ -38,6 +48,9 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() { }), status) } + /** + * 处理方法参数校验异常 + */ override fun handleMethodArgumentNotValid( ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest ): ResponseEntity { @@ -48,12 +61,18 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() { ) } + /** + * 处理请求体解析异常 + */ override fun handleHttpMessageNotReadable( ex: HttpMessageNotReadableException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest ): ResponseEntity { return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown error"), status) } + /** + * 处理其他异常 + */ @ExceptionHandler(value = [IllegalStateException::class, Exception::class]) fun handleException( ex: Exception @@ -61,12 +80,18 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() { return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown error"), HttpStatus.INTERNAL_SERVER_ERROR) } + /** + * 处理用户注册异常 + */ @ExceptionHandler(value = [UserRegistException::class]) fun handleUserRegistException(ex: Exception ): ResponseEntity>{ return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown regist error"), HttpStatus.BAD_REQUEST) } + /** + * 处理校验异常 + */ @ExceptionHandler(value = [ConstraintViolationException::class]) fun handleConstraintViolationException(ex: Exception): ResponseEntity> { return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown validation error"), HttpStatus.BAD_REQUEST) diff --git a/src/main/java/dev/surl/surl/service/SurlService.kt b/src/main/java/dev/surl/surl/service/SurlService.kt index 566091f..4d8a9b1 100644 --- a/src/main/java/dev/surl/surl/service/SurlService.kt +++ b/src/main/java/dev/surl/surl/service/SurlService.kt @@ -8,19 +8,35 @@ import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.transactions.transaction import org.springframework.stereotype.Service +/** + * 短链接服务 + */ @Service class SurlService { private val userService: UserService by autowired() - fun addSurl(baseurl: String): String = runBlocking { + + /** + * 添加短链接 + * @param baseurl 原始链接 + * @param username 用户名 + */ + fun addSurl(baseurl: String, username: String): String = runBlocking { + // 使用雪花算法生成id val id = genSnowflakeUID() transaction { Surl.new(id) { url = baseurl + user = userService.getUserByUsername(username) } } + // 返回id转换后的生成的key numberToKey(id) } + /** + * 批量添加短链接 + * @param baseurls 原始链接列表 + */ fun batchAddSurl(baseurls: List) = transaction { Surls.batchInsert(baseurls, shouldReturnGeneratedValues = false) { this[Surls.url] = it @@ -28,6 +44,10 @@ class SurlService { } } + /** + * 根据key获取原始链接 + * @param key 短链接key + */ fun getUrlByKey(key: String): String { return transaction { Surls.select(Surls.url).where { @@ -35,11 +55,16 @@ class SurlService { }.firstOrNull()?.get(Surls.url) ?: "" } } + + /** + * 根据用户名获取短链接列表 + * @param username 用户名 + */ fun getUrlsByUser(username: String): List { val user = userService.getUserByUsername(username) ?: return emptyList() return transaction { Surl.find { - Surls.id eq user.id + Surls.user eq user.id }.map { it.url } diff --git a/src/main/java/dev/surl/surl/service/UserService.kt b/src/main/java/dev/surl/surl/service/UserService.kt index 68b30dc..0661507 100644 --- a/src/main/java/dev/surl/surl/service/UserService.kt +++ b/src/main/java/dev/surl/surl/service/UserService.kt @@ -20,15 +20,26 @@ import org.springframework.stereotype.Service typealias AUser = org.springframework.security.core.userdetails.User +/** + * 用户服务 + */ @Service class UserService: UserDetailsService { private val passwordEncoder: BCryptPasswordEncoder by autowired() private val validator: Validator by autowired() + + /** + * 注册用户 + * @param username 用户名 + * @param password 密码 + * @return 注册成功返回用户id和用户名 + */ fun addUser(username: String, password: String): Msg> { - val (id, accessId) = runBlocking { - Pair(genSnowflakeUID(), genSnowflakeUID()) + val id = runBlocking { + genSnowflakeUID() } + // 密码加密 val encryptedPassword = passwordEncoder.encode(password) transaction { if (isUserExist(username)) { @@ -38,7 +49,8 @@ class UserService: UserDetailsService { this.username = username this.password = encryptedPassword } - addDefaultAccess(accessId, user) + // 添加默认权限 + addDefaultAccess(user) } return Msg(value = mapOf( "id" to numberToKey(id), @@ -46,6 +58,11 @@ class UserService: UserDetailsService { )) } + /** + * 根据用户名获取用户信息 + * @param username 用户名 + * @return 用户信息 + */ fun getUserByUsername(username: String): User? { return transaction { User.find { @@ -54,23 +71,43 @@ class UserService: UserDetailsService { } } + /** + * 判断用户是否存在 + * @param username 用户名 + * @return 用户是否存在 + */ private fun isUserExist(username: String) = !User.find { Users.username eq username }.empty() - private fun addDefaultAccess(id: Long, user: User) { + /** + * 添加默认权限 + * @param user 用户 + */ + private fun addDefaultAccess(user: User) { + val id = runBlocking { genSnowflakeUID() } UserAccess.new(id) { this.access = Access.READ this.user = user } } + /** + * 验证用户密码 + * @param userDto 用户信息 + * @return 验证结果 + */ fun authUser(userDto: UserDto):Boolean { validate(userDto, validator) val user = getUserByUsername(userDto.username!!) ?: throw UsernameNotFoundException("user `${userDto.username}` not found") return passwordEncoder.matches(userDto.password!!, user.password) } + /** + * 根据用户名获取用户信息 + * @param username 用户名 + * @return 用户信息 + */ override fun loadUserByUsername(username: String): UserDetails { val user = getUserByUsername(username) ?: throw UsernameNotFoundException("user '$username' not found") return AUser.builder().apply { diff --git a/src/main/java/dev/surl/surl/util/Autowired.kt b/src/main/java/dev/surl/surl/util/Autowired.kt index 2d5a9d0..1fc3229 100644 --- a/src/main/java/dev/surl/surl/util/Autowired.kt +++ b/src/main/java/dev/surl/surl/util/Autowired.kt @@ -4,6 +4,9 @@ import dev.surl.surl.SurlApplication import kotlin.reflect.KClass import kotlin.reflect.KProperty +/** + * 注入代理类 + */ class Autowired(private val type: KClass, private val name: String?) { private val value: T by lazy { if (name == null) { @@ -14,4 +17,8 @@ class Autowired(private val type: KClass, private val name: String?) } operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value } + +/** + * 注入代理器 + */ inline fun autowired(name: String? = null) = Autowired(T::class, name) diff --git a/src/main/java/dev/surl/surl/util/JwtTokenUtil.kt b/src/main/java/dev/surl/surl/util/JwtTokenUtil.kt index 4f239d7..c815e9b 100644 --- a/src/main/java/dev/surl/surl/util/JwtTokenUtil.kt +++ b/src/main/java/dev/surl/surl/util/JwtTokenUtil.kt @@ -3,19 +3,29 @@ package dev.surl.surl.util import dev.surl.surl.cfg.BaseConfig import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts +import org.springframework.http.HttpHeaders import org.springframework.oxm.ValidationFailureException import org.springframework.stereotype.Component import java.time.LocalDateTime import java.time.ZoneId import java.util.Date +/** + * JWT token 工具类 + */ @Component class JwtTokenUtil(private val cfg: BaseConfig) { - fun getToken(identityId: String, authorizes: List): Pair { + /** + * 生成token + * @param username 用户名 + * @param authorizes 用户角色 + * @return Pair token过期时间,token + */ + fun getToken(username: String, authorizes: List): Pair { val now = LocalDateTime.now() val expireAt = Date.from(now.plus(cfg.expire, cfg.unit).atZone(ZoneId.systemDefault()).toInstant()) val token = Jwts.builder().run { - subject(identityId) + subject(username) issuedAt(Date()) expiration(expireAt) signWith(cfg.secretKey) @@ -25,24 +35,55 @@ class JwtTokenUtil(private val cfg: BaseConfig) { return Pair(expireAt, token) } + /** + * 获取token信息 + * @param token token + * @return Claims + */ private fun getTokenClaim(token: String): Claims? { return Jwts.parser().verifyWith(cfg.secretKey).build().parseSignedClaims(token).payload } + /** + * 从token中获取用户名 + * @param token token + * @return 用户名 + */ fun getUsernameFromToken(token: String): String { return getClaimFromToken(token) { it?.subject } ?: throw ValidationFailureException("invalid token, userinfo not found") } + /** + * 从token中获取claims + * @param token token + * @param resolver 解析器 + * @return T 解析结果 + */ private fun getClaimFromToken(token: String, resolver: (Claims?) -> T): T { val claims = getTokenClaim(token) return resolver(claims) } + /** + * 从header中获取token + * @param header header + * @return token + */ fun getTokenFromHeader(header: String): String { return if (header.startsWith(cfg.tokenHead)) { header.substring(cfg.tokenHead.length) } else throw ValidationFailureException("invalid token") } + + /** + * 从header中获取用户名 + * @param headers headers + * @return 用户名 + */ + fun getUsernameFromHeader(headers: HttpHeaders): String { + val token = getTokenFromHeader(headers[HttpHeaders.AUTHORIZATION]?.last() ?: "") + return getUsernameFromToken(token) + } } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/util/UIDUtil.kt b/src/main/java/dev/surl/surl/util/UIDUtil.kt index 5409fe4..afb7834 100644 --- a/src/main/java/dev/surl/surl/util/UIDUtil.kt +++ b/src/main/java/dev/surl/surl/util/UIDUtil.kt @@ -3,9 +3,16 @@ package dev.surl.surl.util import org.noelware.charted.snowflake.Snowflake import kotlin.math.pow +// 70进制映射 private val CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!*().-_~".toCharArray() + +// 雪花ID生成器 private val snowflake = Snowflake() +/** + * 将数字转换为key,70进制 + * @param number number + */ fun numberToKey(number: Long): String { if(number == 0L) throw Exception("serial number cannot be zero") var num = number @@ -18,6 +25,10 @@ fun numberToKey(number: Long): String { return sb.reverse().toString() } +/** + * 将key转换为10进制数字 + * @param key key + */ fun keyToNumber(key: String): Long { var sum = 0L for(i in key.indices) { @@ -28,6 +39,9 @@ fun keyToNumber(key: String): Long { return sum } +/** + * 生成雪花ID + */ suspend fun genSnowflakeUID(): Long { return snowflake.generate().value } diff --git a/src/main/java/dev/surl/surl/util/ValidateUtil.kt b/src/main/java/dev/surl/surl/util/ValidateUtil.kt index a6b45e0..87e74ca 100644 --- a/src/main/java/dev/surl/surl/util/ValidateUtil.kt +++ b/src/main/java/dev/surl/surl/util/ValidateUtil.kt @@ -3,6 +3,11 @@ package dev.surl.surl.util import jakarta.validation.ConstraintViolationException import jakarta.validation.Validator +/** + * 请求体验证 + * @param dto 待验证的dto + * @param validator 验证器 + */ fun validate(dto: T,validator: Validator) { if(dto == null) throw IllegalArgumentException("dto for validation is null") val violations = validator.validate(dto) diff --git a/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt b/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt index b6d7c4b..cb66dc5 100644 --- a/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt +++ b/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt @@ -7,10 +7,20 @@ import org.springframework.stereotype.Component import java.time.temporal.ChronoUnit import java.util.concurrent.TimeUnit +/** + * Redis工具类 + */ @Suppress("UNUSED") @Component class RedisUtil(private val template: StringRedisTemplate, private val cfg: BaseConfig) { private val ops = template.opsForValue() + + /** + * 获取字符串 + * @param key 键 + * @param type 存储类型 + * @return 字符串 + */ fun getString(key: String, type: RedisStorage? = null): String? { if (type == null) { return ops.get(key) @@ -18,6 +28,12 @@ class RedisUtil(private val template: StringRedisTemplate, private val cfg: Base return ops.get("${type.name}_$key") } + /** + * 设置字符串 + * @param key 键 + * @param value 值 + * @param type 存储类型 + */ fun setString(key: String, value: String, type: RedisStorage? = null) { if (type == null) { ops.set(key, value, cfg.expire, chronoUnitToTimeUnit(cfg.unit)) @@ -26,6 +42,11 @@ class RedisUtil(private val template: StringRedisTemplate, private val cfg: Base } } + /** + * 删除键 + * @param key 键 + * @param type 存储类型 + */ fun delKey(key: String, type: RedisStorage? = null) { if (type == null) { ops.operations.delete(key) @@ -34,12 +55,20 @@ class RedisUtil(private val template: StringRedisTemplate, private val cfg: Base ops.operations.delete("${type.name}_$key") } + /** + * 清空数据库 + */ fun flushdb() { template.execute { it.serverCommands().flushDb() } } + /** + * 将ChronoUnit转换为TimeUnit + * @param unit ChronoUnit + * @return TimeUnit + */ private fun chronoUnitToTimeUnit(unit: ChronoUnit): TimeUnit { return when (unit) { ChronoUnit.MILLIS -> TimeUnit.MILLISECONDS diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 669c421..126080d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -24,6 +24,7 @@ spring: time-zone: Asia/Shanghai serialization: indent-output: true + date-format: yyyy-MM-dd HH:mm:ss.SSS data: redis: host: localhost @@ -45,7 +46,8 @@ logging: base: configs: site: http://127.0.0.1:18888 - expire: 3600000 + expire: 6 + unit: hours secret: Is#45Ddw29apkbHawwaHb4d^&w29apkbHawwaHb4d^& white-list: - ^/login$