添加权限表

This commit is contained in:
05412 2024-07-12 17:43:11 +08:00
parent e2b56a654c
commit b0616a1098
25 changed files with 198 additions and 81 deletions

View File

@ -47,6 +47,7 @@ dependencies {
implementation "org.noelware.charted.snowflake:snowflake:0.1-beta"
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
testImplementation 'org.springframework.security:spring-security-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

View File

@ -0,0 +1,21 @@
package dev.surl.surl.cfg
import dev.surl.surl.util.numberToKey
import io.jsonwebtoken.security.Keys
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component
import java.util.Date
import javax.crypto.SecretKey
@Component
@ConfigurationProperties(prefix = "base.configs")
class BaseConfig(
/**
* 站点域名
*/
var site: String = "https://surl.org",
var expire: Long = 24 * 60 * 60 * 1000, // token expire time
private var secret: String = numberToKey(Date().time),
var secretKey: SecretKey = Keys.hmacShaKeyFor(secret.toByteArray()),
var tokenHead: String = "token"
)

View File

@ -1,14 +0,0 @@
package dev.surl.surl.cfg
import io.jsonwebtoken.security.Keys
import org.springframework.context.annotation.Configuration
import javax.crypto.SecretKey
@Configuration
open class BaseConfiguration(
val site: String = "https://surl.org",
val expire: Long = 24 * 60 * 60 * 1000,
private val secret: String = "6244a108c0599980e7971845baeb3120",
val secretKey: SecretKey = Keys.hmacShaKeyFor(secret.toByteArray()),
val tokenHead: String = "token"
)

View File

@ -0,0 +1,9 @@
package dev.surl.surl.cfg
import org.springframework.context.annotation.Configuration
@Configuration
open class PatternConfig {
val usernamePattern = Regex("""\w{6,20}""")
val passwordPattern = Regex("""^((?=\S*?[A-Z])(?=\S*?[a-z])(?=\S*?[0-9])(?=\S*?)).{10,}\S$""")
}

View File

@ -0,0 +1,17 @@
package dev.surl.surl.cfg.security
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
@Configuration
open class EncoderConfig {
/**
* 装载BCrypt密码编码器
*/
@Bean
open fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.`$2B`)
}
}

View File

@ -1,6 +1,5 @@
package dev.surl.surl.cfg.security
import dev.surl.surl.cfg.Logging
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
@ -10,8 +9,6 @@ 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
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.filter.OncePerRequestFilter
@Configuration
@ -30,29 +27,20 @@ open class WebSecurityConfig {
headers {
cacheControl { } // 禁用缓存
}
}
return http.build()
}
/**
* 装载BCrypt密码编码器
*/
@Bean
open fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder()
open fun authenticationTokenFilter(): OncePerRequestFilter {
return object: OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
filterChain.doFilter(request, response)
}
}
}
// @Bean
// open fun authenticationTokenFilter(): OncePerRequestFilter {
// return object: OncePerRequestFilter() {
// override fun doFilterInternal(
// request: HttpServletRequest,
// response: HttpServletResponse,
// filterChain: FilterChain
// ) {
// filterChain.doFilter(request, response)
// }
// }
// }
}

View File

@ -0,0 +1,5 @@
package dev.surl.surl.common
enum class Access {
ADMIN, READ, WRITE
}

View File

@ -1,3 +1,3 @@
package dev.surl.surl.common.exception
class UserRegistException(message: String? = null, cause: Throwable? = null): Throwable(message, cause)
class UserRegistException(message: String? = null, cause: Throwable? = null): Exception(message, cause)

View File

@ -1,6 +1,6 @@
package dev.surl.surl.controller
import dev.surl.surl.cfg.BaseConfiguration
import dev.surl.surl.cfg.BaseConfig
import dev.surl.surl.service.SurlService
import jakarta.validation.Valid
import org.hibernate.validator.constraints.Length
@ -15,9 +15,9 @@ import java.net.URI
class RedirectController {
@GetMapping("/{key}")
fun redirect(
@PathVariable @Valid @Length(min = 11, max = 11, message = "Key must be 11 characters long") key: String,
@PathVariable @Valid @Length(min = 1, max = 11, message = "Key length is not valid") key: String,
@Autowired service: SurlService,
@Autowired cfg: BaseConfiguration
@Autowired cfg: BaseConfig
): ResponseEntity<Any> {
val redirectUrl = service.getUrlByKey(key)
return if(redirectUrl.isBlank()) {

View File

@ -1,8 +1,8 @@
package dev.surl.surl.controller
import dev.surl.surl.cfg.BaseConfiguration
import dev.surl.surl.cfg.BaseConfig
import dev.surl.surl.common.Msg
import dev.surl.surl.dto.Surl
import dev.surl.surl.dto.SurlDto
import dev.surl.surl.service.SurlService
import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired
@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.RestController
class SurlAddController {
@RequestMapping("/surl/add")
fun addSurl(
@Valid @RequestBody body: Surl, @Autowired service: SurlService, @Autowired cfg: BaseConfiguration
@Valid @RequestBody body: SurlDto, @Autowired service: SurlService, @Autowired cfg: BaseConfig
): Any {
return ResponseEntity(Msg(code = 0, value = "${cfg.site}/${service.addSurl(body.url ?: "")}"), HttpStatus.OK)
}

View File

@ -0,0 +1,18 @@
package dev.surl.surl.dao
import dev.surl.surl.common.Access
import dev.surl.surl.dao.Surl.Companion.referrersOn
import dev.surl.surl.dsl.UserAccesses
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) {
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
}

View File

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

View File

@ -0,0 +1,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()
val access = short("access")
}

View File

@ -5,6 +5,6 @@ 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 password = varchar("password", 256)
val password = char("password", 60)
override val primaryKey = PrimaryKey(id, name = "PK_user_id")
}

View File

@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.validation.constraints.NotNull
import org.hibernate.validator.constraints.Length
data class Surl(
data class SurlDto(
@JsonProperty("url")
@get:NotNull(message = "url cannot be empty")
@get:Length(min = 11, max = 2048, message = "url length invalid")

View File

@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.validation.constraints.NotNull
import org.hibernate.validator.constraints.Length
class UserDto (
data class UserDto (
@JsonProperty("username")
@get:Length(max = 16, min = 4, message = "username length must be between 4 and 16")
@get:NotNull(message = "username cannot be null")

View File

@ -38,7 +38,7 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() {
return ResponseEntity(Msg<String>(code = -1, msg = ex.message ?: "unknown error"), status)
}
@ExceptionHandler(value = [Exception::class])
@ExceptionHandler(value = [IllegalStateException::class ,Exception::class])
fun handleException(
ex: Exception
): ResponseEntity<Msg<String>> {
@ -46,8 +46,8 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() {
}
@ExceptionHandler(value = [UserRegistException::class])
fun handleUserRegistException(ex: Exception, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest
fun handleUserRegistException(ex: Exception
): ResponseEntity<Msg<String>>{
return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown regist error"), status)
return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown regist error"), HttpStatus.BAD_REQUEST)
}
}

View File

@ -16,19 +16,14 @@ class SurlService {
url = baseurl
}
}
return@runBlocking numberToKey(id)
numberToKey(id)
}
fun getUrlByKey(key: String): String {
return transaction {
val results = Surls.select(Surls.url).where {
Surls.select(Surls.url).where {
Surls.id eq keyToNumber(key)
}.toList()
if (results.isNotEmpty()) {
return@transaction results.first()[Surls.url]
} else {
return@transaction ""
}
}.firstOrNull()?.get(Surls.url) ?: ""
}
}
}

View File

@ -1,43 +1,77 @@
package dev.surl.surl.service
import dev.surl.surl.SurlApplication
import dev.surl.surl.common.Access
import dev.surl.surl.common.Msg
import dev.surl.surl.common.exception.UserRegistException
import dev.surl.surl.dao.User
import dev.surl.surl.dao.UserAccess
import dev.surl.surl.dsl.Users
import dev.surl.surl.util.autowired
import dev.surl.surl.util.genSnowflakeUID
import dev.surl.surl.util.numberToKey
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.transactions.transaction
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service
typealias AUser = org.springframework.security.core.userdetails.User
@Service
class UserService(@Autowired private val passwordEncoder: BCryptPasswordEncoder) {
class UserService: UserDetailsService {
private val passwordEncoder: BCryptPasswordEncoder by autowired()
fun addUser(username: String, password: String): Msg<Any> {
// if (passwordEncoder == null) throw Exception("password encoder is null")
// val passwordEncoder = SurlApplication.context.getBean("bcryptor") as PasswordEncoder
val id = runBlocking {
genSnowflakeUID()
val (id, accessId) = runBlocking {
Pair(genSnowflakeUID(), genSnowflakeUID())
}
val encryptedPassword = passwordEncoder.encode(password)
transaction {
if (isUserExist(username)) {
throw UserRegistException("user is existed")
}
User.new(id) {
val user = User.new(id) {
this.username = username
this.password = encryptedPassword
}
addDefaultAccess(accessId, user)
}
@Suppress("unused") return Msg(value = object {
val id = numberToKey(id)
val username = username
})
return Msg(value = mapOf(
"id" to numberToKey(id),
"username" to username
))
}
private fun isUserExist(username: String) = User.find {
private fun getUserByUsername(username: String): User? {
return transaction {
User.find {
Users.username eq username
}.firstOrNull()
}
}
private fun isUserExist(username: String) = !User.find {
Users.username eq username
}.toList().isNotEmpty()
}.empty()
private fun addDefaultAccess(id: Long, user: User) {
UserAccess.new(id) {
this.access = Access.READ
this.user = user
}
}
override fun loadUserByUsername(username: String): UserDetails {
val user = getUserByUsername(username) ?: throw UsernameNotFoundException("user '$username' not found")
return AUser.builder().apply {
username(user.username)
password(passwordEncoder.encode(user.password))
authorities(listOf(
SimpleGrantedAuthority("ROLE_USER")
))
}.build()
}
}

View File

@ -0,0 +1,18 @@
package dev.surl.surl.util
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) {
SurlApplication.context.getBean(type.java) as T
} else {
SurlApplication.context.getBean(name, type.java) as T
}
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
}
inline fun <reified T : Any> autowired(name: String? = null) = Autowired(T::class, name)

View File

@ -1,6 +1,6 @@
package dev.surl.surl.util
import dev.surl.surl.cfg.BaseConfiguration
import dev.surl.surl.cfg.BaseConfig
import dev.surl.surl.cfg.Logging
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jwts
@ -15,7 +15,7 @@ import java.util.Date
@ConfigurationProperties(prefix = "jwt")
class JwtTokenUtil {
@Autowired
private lateinit var cfg: BaseConfiguration
private lateinit var cfg: BaseConfig
@Autowired
private lateinit var logging: Logging

View File

@ -4,19 +4,16 @@ import org.noelware.charted.snowflake.Snowflake
import kotlin.math.pow
private val CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!*().-_~".toCharArray()
private const val LENGTH = 11
private val snowflake = Snowflake()
fun numberToKey(number: Long): String {
if(number == 0L) throw Exception("serial number cannot be zero")
var num = number
val sb = StringBuilder()
for(i in LENGTH - 1 downTo 0) {
while(num != 0L) {
val remainder = num % CHARS.size
sb.append(CHARS[remainder.toInt()])
num /= CHARS.size
if(num == 0L) {
break
}
}
return sb.reverse().toString()
}

View File

@ -0,0 +1,16 @@
package dev.surl.surl.util.redis
import org.springframework.data.redis.core.StringRedisTemplate
class RedisUtil {
private val template = StringRedisTemplate()
fun get(key: String): String? {
return template.opsForValue().get(key)
}
fun batchSet(map: Map<String, String>) {
template.executePipelined {
}
}
}

View File

@ -32,4 +32,7 @@ spring:
logging:
level:
root: info
Exposed: warn
Exposed: debug
base:
configs:
expire: 1000