添加spring security、user实体类、数项测试

This commit is contained in:
05412 2024-07-09 17:36:20 +08:00
parent 027774c545
commit 3ee64a5f69
22 changed files with 369 additions and 20 deletions

3
README.MD Normal file
View File

@ -0,0 +1,3 @@
# surl
A simple URL shortener written in kotlin

View File

@ -14,6 +14,12 @@ java {
} }
} }
kotlin {
jvmToolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations { configurations {
compileOnly { compileOnly {
extendsFrom annotationProcessor extendsFrom annotationProcessor
@ -29,14 +35,22 @@ allprojects {
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/jcenter' } maven { url 'https://maven.aliyun.com/repository/jcenter' }
maven { url "https://maven.noelware.org" }
} }
} }
dependencies { dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.52.0")
implementation 'commons-codec:commons-codec'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation "org.noelware.charted.snowflake:snowflake:0.1-beta"
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
runtimeOnly 'org.postgresql:postgresql'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
} }

View File

@ -0,0 +1,6 @@
package dev.surl.surl.cfg
import org.springframework.context.annotation.Configuration
@Configuration
open class BaseConfiguration(val site: String = "https://surl.org")

View File

@ -0,0 +1,27 @@
package dev.surl.surl.cfg.security
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
open class WebSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
println("loading")
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
formLogin { }
httpBasic {
}
}
return http.build()
}
}

View File

@ -0,0 +1,7 @@
package dev.surl.surl.common
data class Msg(
val code: Int = 0,
val msg: String? = null,
val value: Any? = null
)

View File

@ -6,5 +6,5 @@ import org.springframework.web.bind.annotation.RestController
@RestController(value = "/") @RestController(value = "/")
class DefaultFcontroller { class DefaultFcontroller {
@RequestMapping @RequestMapping
fun defaultMessage() = "Hello World!" fun defaultMessage() = 123
} }

View File

@ -0,0 +1,29 @@
package dev.surl.surl.controller
import dev.surl.surl.cfg.BaseConfiguration
import dev.surl.surl.service.SurlService
import jakarta.validation.Valid
import org.hibernate.validator.constraints.Length
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
import java.net.URI
@RestController
class RedirectController {
@GetMapping("/{key}")
fun redirect(
@PathVariable @Valid @Length(min = 11, max = 11, message = "Key must be 11 characters long") key: String,
@Autowired service: SurlService,
@Autowired cfg: BaseConfiguration
): ResponseEntity<Any> {
val redirectUrl = service.getUrlByKey(key)
return if(redirectUrl.isBlank()) {
ResponseEntity.status(302).location(URI.create(cfg.site)).build()
} else {
ResponseEntity.status(302).location(URI.create(redirectUrl)).build()
}
}
}

View File

@ -0,0 +1,31 @@
package dev.surl.surl.controller
import dev.surl.surl.cfg.BaseConfiguration
import dev.surl.surl.common.Msg
import dev.surl.surl.dto.Surl
import dev.surl.surl.service.SurlService
import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
@RestController
@EnableWebSecurity
class SurlAddController {
@PostMapping("/surl/add")
fun addSurl(
@Valid @RequestBody body: Surl, @Autowired service: SurlService, @Autowired cfg: BaseConfiguration
): Any {
return ResponseEntity(Msg(code = 0, value = "${cfg.site}/${service.addSurl(body.url ?: "")}"), HttpStatus.OK)
}
@GetMapping("/login")
fun login():String {
return "loged in"
}
}

View File

@ -0,0 +1,12 @@
package dev.surl.surl.dao
import dev.surl.surl.dsl.Surls
import org.jetbrains.exposed.dao.Entity
import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.id.EntityID
class Surl(id: EntityID<Long>): Entity<Long>(id) {
companion object: EntityClass<Long, Surl>(Surls)
var url by Surls.url
var user by User optionalReferencedOn Surls.user
}

View File

@ -0,0 +1,12 @@
package dev.surl.surl.dao
import dev.surl.surl.dsl.Users
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) {
companion object EntityClass: LongEntityClass<User>(Users)
var username by Users.username
var passwd by Users.passwd
}

View File

@ -0,0 +1,10 @@
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).uniqueIndex()
val user = reference("user", Users).nullable()
override val primaryKey = PrimaryKey(id, name = "PK_surl_id")
}

View File

@ -0,0 +1,10 @@
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()
val passwd = varchar("passwd", 256)
override val primaryKey = PrimaryKey(id, name = "PK_user_id")
}

View File

@ -0,0 +1,12 @@
package dev.surl.surl.dto
import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.validation.constraints.NotNull
import org.hibernate.validator.constraints.Length
data class Surl(
@JsonProperty("url")
@get:NotNull(message = "url cannot be empty")
@get:Length(min = 11, max = 2048, message = "url length invalid")
val url: String?
)

View File

@ -0,0 +1,39 @@
package dev.surl.surl.handler
import dev.surl.surl.common.Msg
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode
import org.springframework.http.ResponseEntity
import org.springframework.validation.method.MethodValidationException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.context.request.WebRequest
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(code = -1, msg = ex.allValidationResults.joinToString(";")), status)
}
override fun handleMethodArgumentNotValid(
ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest
): ResponseEntity<Any> {
return ResponseEntity(
Msg(code = -1, msg = ex.allErrors.joinToString(";") {
it.defaultMessage?.toString() ?: "unknown invalid method argument"
}), status
)
}
@ExceptionHandler(value = [Exception::class])
fun handleException(
ex: Exception, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest
): ResponseEntity<Msg> {
return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown error"), status)
}
}

View File

@ -0,0 +1,33 @@
package dev.surl.surl.service
import dev.surl.surl.dao.Surl
import dev.surl.surl.dsl.Surls
import dev.surl.surl.util.*
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.transactions.transaction
import org.springframework.stereotype.Service
@Service
class SurlService {
fun addSurl(baseurl: String): String = runBlocking {
val id = genSnowflakeUID()
Surl.new {
this.id._value = id
url = baseurl
}
return@runBlocking numberToKey(id)
}
fun getUrlByKey(key: String): String {
return transaction {
val results = Surls.select(Surls.url).where {
Surls.id eq keyToNumber(key)
}.toList()
if (results.isNotEmpty()) {
return@transaction results.first()[Surls.url]
} else {
return@transaction ""
}
}
}
}

View File

@ -0,0 +1,3 @@
package dev.surl.surl.service
class UserService

View File

@ -1,8 +1,11 @@
package dev.surl.surl.util package dev.surl.surl.util
import org.springframework.util.DigestUtils import org.apache.commons.codec.digest.DigestUtils
fun md5(str: String): String { fun md5(str: String): String {
val cksum = DigestUtils.md5Digest(str.toByteArray()) return DigestUtils.md5Hex(str)
return cksum.joinToString("") { "%02x".format(it) } }
fun sha1(str: String): String {
return DigestUtils.sha1Hex(str)
} }

View File

@ -1,15 +1,36 @@
package dev.surl.surl.util package dev.surl.surl.util
import java.util.concurrent.ThreadLocalRandom import org.noelware.charted.snowflake.Snowflake
import kotlin.math.pow
private val CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray() private val CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!*().-_~".toCharArray()
private const val LENGTH = 16 private const val LENGTH = 11
private val snowflake = Snowflake()
fun genSurlUID(): String { fun numberToKey(number: Long): String {
return genSurlUID(LENGTH) var num = number
val sb = StringBuilder()
for(i in LENGTH - 1 downTo 0) {
val remainder = num % CHARS.size
sb.append(CHARS[remainder.toInt()])
num /= CHARS.size
if(num == 0L) {
break
}
}
return sb.reverse().toString()
} }
fun genSurlUID(length: Int): String { fun keyToNumber(key: String): Long {
val random = ThreadLocalRandom.current() var sum = 0L
return CharArray(length) { CHARS[random.nextInt(CHARS.size)] }.concatToString() for(i in key.indices) {
val char = key[i]
val index = CHARS.indexOf(char)
sum += index * (CHARS.size.toDouble().pow(key.length - i - 1).toLong())
}
return sum
}
suspend fun genSnowflakeUID(): Long {
return snowflake.generate().value
} }

View File

@ -1,7 +1,37 @@
server: server:
port: 18888 #服务端口 port: 18888
spring: spring:
profiles:
active: default
application: application:
name: surl # 应用名称 name: surl
main: main:
banner-mode: off # 关闭banner banner-mode: off
datasource:
name: postgres
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/postgres?reWriteBatchedInserts=true
username: postgres
password: f6059e64-889b-4f31-97a7-deaeb1a36d58
hikari:
connection-test-query: SELECT 1
connection-timeout: 30000
maximum-pool-size: 10
jackson:
deserialization:
use-big-decimal-for-floats: true
use-big-integer-for-ints: true
time-zone: Asia/Shanghai
serialization:
indent-output: true
exposed:
show-sql: false
generate-ddl: true
security:
user:
name: admin
password: 123
logging:
level:
root: info
Exposed: warn

View File

@ -0,0 +1,28 @@
package dev.surl.surl
import dev.surl.surl.service.SurlService
import dev.surl.surl.util.genSnowflakeUID
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class Benchmark {
@Test
fun tpsTest(@Autowired service: SurlService) {
val now = System.currentTimeMillis()
runBlocking {
for(i in 1..128) {
launch {
for (j in 1..100) {
service.addSurl("https://surl.org/${genSnowflakeUID()}")
}
}
}
}
val timeEllapsed = System.currentTimeMillis() - now
println("TPS: ${128 * 100.0 * 1000 / timeEllapsed}")
}
}

View File

@ -0,0 +1,18 @@
package dev.surl.surl
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
@SpringBootTest
@AutoConfigureMockMvc
class SecurityTests(@Autowired val mvc: MockMvc) {
@Test
fun `surl add page`() {
mvc.perform(formLogin("/login").user("user").password("1233")).andExpect(authenticated())
}
}

View File

@ -1,7 +1,8 @@
package dev.surl.surl package dev.surl.surl
import dev.surl.surl.util.genSurlUID import dev.surl.surl.util.keyToNumber
import dev.surl.surl.util.md5 import dev.surl.surl.util.md5
import dev.surl.surl.util.numberToKey
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class UtilTests { class UtilTests {
@ -11,8 +12,8 @@ class UtilTests {
} }
@Test @Test
fun randomUIDTest() { fun numberToUIDTest() {
println(genSurlUID()) println(numberToKey(1810519439309799440))
println(genSurlUID(8)) println(keyToNumber("0IYJbl*DiYE"))
} }
} }