添加注释
This commit is contained in:
parent
26e229f05f
commit
dd9ebbaa65
@ -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<String>) {
|
||||
// 启动并获取上下文
|
||||
SurlApplication.context = runApplication<SurlApplication>(*args)
|
||||
}
|
@ -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<String> = listOf("/login"),
|
||||
|
||||
/**
|
||||
* JWT密钥
|
||||
*/
|
||||
secret: String = numberToKey(Date().time).repeat(5),
|
||||
) {
|
||||
val secretKey: SecretKey = Keys.hmacShaKeyFor(secret.toByteArray())
|
||||
|
@ -3,8 +3,8 @@ package dev.surl.surl.cfg
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* 获取日志对象的扩展函数
|
||||
*/
|
||||
@Suppress("UNUSED")
|
||||
fun <T: Any> T.logger(): Logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Suppress("UNUSED")
|
||||
fun logger(name: String): Logger = LoggerFactory.getLogger(name)
|
@ -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$""")
|
||||
}
|
@ -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<String, String> {
|
||||
return StringRedisTemplate(factory)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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 { } // 禁用缓存
|
||||
|
@ -1,5 +1,8 @@
|
||||
package dev.surl.surl.common
|
||||
|
||||
/**
|
||||
* 用户权限枚举
|
||||
*/
|
||||
enum class Access {
|
||||
ADMIN, READ, WRITE
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
package dev.surl.surl.common
|
||||
|
||||
/**
|
||||
* 通用接口返回格式
|
||||
*/
|
||||
data class Msg<T>(
|
||||
val code: Int = 0, val msg: String? = null, val value: T? = null
|
||||
)
|
@ -1,5 +1,8 @@
|
||||
package dev.surl.surl.common.enums
|
||||
|
||||
/**
|
||||
* Redis存储的前缀
|
||||
*/
|
||||
enum class RedisStorage {
|
||||
TOKEN
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
package dev.surl.surl.common.exception
|
||||
|
||||
/**
|
||||
* 自定义权限异常
|
||||
*/
|
||||
class UnauthorizedExcecption(message: String? = null, cause: Throwable? = null) : Exception(message, cause)
|
@ -1,3 +1,6 @@
|
||||
package dev.surl.surl.common.exception
|
||||
|
||||
/**
|
||||
* 自定义注册异常
|
||||
*/
|
||||
class UserRegistException(message: String? = null, cause: Throwable? = null): Exception(message, cause)
|
@ -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<Any> {
|
||||
val redirectUrl = service.getUrlByKey(key)
|
||||
return if(redirectUrl.isBlank()) {
|
||||
// 根据key获取原始链接
|
||||
val redirectUrl = service.getUrlByKey(key)
|
||||
return if (redirectUrl.isBlank()) {
|
||||
// 未找到,返回异常信息
|
||||
ResponseEntity(Msg<String>(code = -1, msg = "key `$key` not found"), HttpStatus.NOT_FOUND)
|
||||
} else {
|
||||
// 找到,发送302跳转
|
||||
ResponseEntity.status(302).location(URI.create(redirectUrl)).build()
|
||||
}
|
||||
}
|
||||
|
@ -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<String> {
|
||||
|
||||
// 从认证头获取用户名
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
@ -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<List<String>> {
|
||||
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)
|
||||
}
|
||||
}
|
@ -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!!) // 新增用户
|
||||
}
|
@ -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<Long>): Entity<Long>(id) {
|
||||
|
||||
/**
|
||||
* 短链接实体类伴生对象,用于crud操作
|
||||
*/
|
||||
companion object: EntityClass<Long, Surl>(Surls)
|
||||
|
||||
/**
|
||||
* 短链接url
|
||||
*/
|
||||
var url by Surls.url
|
||||
|
||||
/**
|
||||
* 短链接所属用户
|
||||
*/
|
||||
var user by User optionalReferencedOn Surls.user
|
||||
}
|
@ -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<Long>): LongEntity(id) {
|
||||
|
||||
/**
|
||||
* 用户实体伴生对象,用于CRUD操作
|
||||
*/
|
||||
companion object EntityClass: LongEntityClass<User>(Users)
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
var username by Users.username
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
var password by Users.password
|
||||
}
|
@ -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<Long>): LongEntity(id) {
|
||||
|
||||
/**
|
||||
* 伴生对象,用于CRUD操作
|
||||
*/
|
||||
companion object EntityClass: LongEntityClass<UserAccess>(UserAccesses)
|
||||
|
||||
/**
|
||||
* 权限,枚举类型,自动转换为数据库存储的整数
|
||||
*/
|
||||
var access by UserAccesses.access.transform(toColumn = {
|
||||
it.ordinal.toShort()
|
||||
}, toReal = {
|
||||
Access.entries[it.toInt()]
|
||||
})
|
||||
|
||||
/**
|
||||
* 用户
|
||||
*/
|
||||
var user by User referencedOn UserAccesses.user
|
||||
}
|
@ -2,6 +2,9 @@ package dev.surl.surl.dsl
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IdTable
|
||||
|
||||
/**
|
||||
* 短链接表
|
||||
*/
|
||||
object Surls: IdTable<Long>("surl") {
|
||||
override val id = long("id").entityId()
|
||||
val url = varchar("url", 2048)
|
||||
|
@ -2,6 +2,9 @@ package dev.surl.surl.dsl
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IdTable
|
||||
|
||||
/**
|
||||
* 用户权限表
|
||||
*/
|
||||
object UserAccesses: IdTable<Long>("user_access") {
|
||||
override val id = long("id").entityId()
|
||||
val user = reference("user", Users).index()
|
||||
|
@ -2,6 +2,9 @@ package dev.surl.surl.dsl
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IdTable
|
||||
|
||||
/**
|
||||
* 用户权限表
|
||||
*/
|
||||
object Users: IdTable<Long>("users") {
|
||||
override val id = long("id").entityId()
|
||||
val username = varchar("username", 256).uniqueIndex()
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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<String>(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<Regex>): 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<Regex>): Boolean {
|
||||
return !(this matchedIn regexes)
|
||||
}
|
||||
|
@ -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?
|
||||
) {
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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<Any> {
|
||||
return ResponseEntity(Msg<String>(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<Any> {
|
||||
@ -48,12 +61,18 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求体解析异常
|
||||
*/
|
||||
override fun handleHttpMessageNotReadable(
|
||||
ex: HttpMessageNotReadableException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest
|
||||
): ResponseEntity<Any> {
|
||||
return ResponseEntity(Msg<String>(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<Msg<String>>{
|
||||
return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown regist error"), HttpStatus.BAD_REQUEST)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理校验异常
|
||||
*/
|
||||
@ExceptionHandler(value = [ConstraintViolationException::class])
|
||||
fun handleConstraintViolationException(ex: Exception): ResponseEntity<Msg<String>> {
|
||||
return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown validation error"), HttpStatus.BAD_REQUEST)
|
||||
|
@ -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<String>) = 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<String> {
|
||||
val user = userService.getUserByUsername(username) ?: return emptyList()
|
||||
return transaction {
|
||||
Surl.find {
|
||||
Surls.id eq user.id
|
||||
Surls.user eq user.id
|
||||
}.map {
|
||||
it.url
|
||||
}
|
||||
|
@ -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<Map<String, String>> {
|
||||
|
||||
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 {
|
||||
|
@ -4,6 +4,9 @@ import dev.surl.surl.SurlApplication
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* 注入代理类
|
||||
*/
|
||||
class Autowired<T : Any>(private val type: KClass<T>, private val name: String?) {
|
||||
private val value: T by lazy {
|
||||
if (name == null) {
|
||||
@ -14,4 +17,8 @@ class Autowired<T : Any>(private val type: KClass<T>, private val name: String?)
|
||||
}
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入代理器
|
||||
*/
|
||||
inline fun <reified T : Any> autowired(name: String? = null) = Autowired(T::class, name)
|
||||
|
@ -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<String>): Pair<Date, String> {
|
||||
/**
|
||||
* 生成token
|
||||
* @param username 用户名
|
||||
* @param authorizes 用户角色
|
||||
* @return Pair<Date, String> token过期时间,token
|
||||
*/
|
||||
fun getToken(username: String, authorizes: List<String>): Pair<Date, String> {
|
||||
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 <T> 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -3,6 +3,11 @@ package dev.surl.surl.util
|
||||
import jakarta.validation.ConstraintViolationException
|
||||
import jakarta.validation.Validator
|
||||
|
||||
/**
|
||||
* 请求体验证
|
||||
* @param dto 待验证的dto
|
||||
* @param validator 验证器
|
||||
*/
|
||||
fun <T: Any?> validate(dto: T,validator: Validator) {
|
||||
if(dto == null) throw IllegalArgumentException("dto for validation is null")
|
||||
val violations = validator.validate(dto)
|
||||
|
@ -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
|
||||
|
@ -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$
|
||||
|
Loading…
Reference in New Issue
Block a user