添加权限表

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.noelware.charted.snowflake:snowflake:0.1-beta"
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6' implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.springframework.security:spring-security-test'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
annotationProcessor '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 package dev.surl.surl.cfg.security
import dev.surl.surl.cfg.Logging
import jakarta.servlet.FilterChain import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse 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.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.config.annotation.web.invoke 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 import org.springframework.web.filter.OncePerRequestFilter
@Configuration @Configuration
@ -30,29 +27,20 @@ open class WebSecurityConfig {
headers { headers {
cacheControl { } // 禁用缓存 cacheControl { } // 禁用缓存
} }
} }
return http.build() return http.build()
} }
/**
* 装载BCrypt密码编码器
*/
@Bean @Bean
open fun passwordEncoder(): BCryptPasswordEncoder { open fun authenticationTokenFilter(): OncePerRequestFilter {
return BCryptPasswordEncoder() 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 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 package dev.surl.surl.controller
import dev.surl.surl.cfg.BaseConfiguration import dev.surl.surl.cfg.BaseConfig
import dev.surl.surl.service.SurlService import dev.surl.surl.service.SurlService
import jakarta.validation.Valid import jakarta.validation.Valid
import org.hibernate.validator.constraints.Length import org.hibernate.validator.constraints.Length
@ -15,9 +15,9 @@ import java.net.URI
class RedirectController { class RedirectController {
@GetMapping("/{key}") @GetMapping("/{key}")
fun redirect( 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 service: SurlService,
@Autowired cfg: BaseConfiguration @Autowired cfg: BaseConfig
): ResponseEntity<Any> { ): ResponseEntity<Any> {
val redirectUrl = service.getUrlByKey(key) val redirectUrl = service.getUrlByKey(key)
return if(redirectUrl.isBlank()) { return if(redirectUrl.isBlank()) {

View File

@ -1,8 +1,8 @@
package dev.surl.surl.controller 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.common.Msg
import dev.surl.surl.dto.Surl import dev.surl.surl.dto.SurlDto
import dev.surl.surl.service.SurlService import dev.surl.surl.service.SurlService
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.RestController
class SurlAddController { class SurlAddController {
@RequestMapping("/surl/add") @RequestMapping("/surl/add")
fun addSurl( fun addSurl(
@Valid @RequestBody body: Surl, @Autowired service: SurlService, @Autowired cfg: BaseConfiguration @Valid @RequestBody body: SurlDto, @Autowired service: SurlService, @Autowired cfg: BaseConfig
): Any { ): Any {
return ResponseEntity(Msg(code = 0, value = "${cfg.site}/${service.addSurl(body.url ?: "")}"), HttpStatus.OK) 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") { object Surls: IdTable<Long>("surl") {
override val id = long("id").entityId() override val id = long("id").entityId()
val url = varchar("url", 2048).uniqueIndex() 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") 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") { object Users: IdTable<Long>("users") {
override val id = long("id").entityId() override val id = long("id").entityId()
val username = varchar("username", 256).uniqueIndex() 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") 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 jakarta.validation.constraints.NotNull
import org.hibernate.validator.constraints.Length import org.hibernate.validator.constraints.Length
data class Surl( data class SurlDto(
@JsonProperty("url") @JsonProperty("url")
@get:NotNull(message = "url cannot be empty") @get:NotNull(message = "url cannot be empty")
@get:Length(min = 11, max = 2048, message = "url length invalid") @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 jakarta.validation.constraints.NotNull
import org.hibernate.validator.constraints.Length import org.hibernate.validator.constraints.Length
class UserDto ( data class UserDto (
@JsonProperty("username") @JsonProperty("username")
@get:Length(max = 16, min = 4, message = "username length must be between 4 and 16") @get:Length(max = 16, min = 4, message = "username length must be between 4 and 16")
@get:NotNull(message = "username cannot be null") @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) return ResponseEntity(Msg<String>(code = -1, msg = ex.message ?: "unknown error"), status)
} }
@ExceptionHandler(value = [Exception::class]) @ExceptionHandler(value = [IllegalStateException::class ,Exception::class])
fun handleException( fun handleException(
ex: Exception ex: Exception
): ResponseEntity<Msg<String>> { ): ResponseEntity<Msg<String>> {
@ -46,8 +46,8 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() {
} }
@ExceptionHandler(value = [UserRegistException::class]) @ExceptionHandler(value = [UserRegistException::class])
fun handleUserRegistException(ex: Exception, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest fun handleUserRegistException(ex: Exception
): ResponseEntity<Msg<String>>{ ): 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 url = baseurl
} }
} }
return@runBlocking numberToKey(id) numberToKey(id)
} }
fun getUrlByKey(key: String): String { fun getUrlByKey(key: String): String {
return transaction { return transaction {
val results = Surls.select(Surls.url).where { Surls.select(Surls.url).where {
Surls.id eq keyToNumber(key) Surls.id eq keyToNumber(key)
}.toList() }.firstOrNull()?.get(Surls.url) ?: ""
if (results.isNotEmpty()) {
return@transaction results.first()[Surls.url]
} else {
return@transaction ""
}
} }
} }
} }

View File

@ -1,43 +1,77 @@
package dev.surl.surl.service 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.Msg
import dev.surl.surl.common.exception.UserRegistException import dev.surl.surl.common.exception.UserRegistException
import dev.surl.surl.dao.User import dev.surl.surl.dao.User
import dev.surl.surl.dao.UserAccess
import dev.surl.surl.dsl.Users import dev.surl.surl.dsl.Users
import dev.surl.surl.util.autowired
import dev.surl.surl.util.genSnowflakeUID import dev.surl.surl.util.genSnowflakeUID
import dev.surl.surl.util.numberToKey import dev.surl.surl.util.numberToKey
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.transactions.transaction 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.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
typealias AUser = org.springframework.security.core.userdetails.User
@Service @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> { 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, accessId) = runBlocking {
val id = runBlocking { Pair(genSnowflakeUID(), genSnowflakeUID())
genSnowflakeUID()
} }
val encryptedPassword = passwordEncoder.encode(password) val encryptedPassword = passwordEncoder.encode(password)
transaction { transaction {
if (isUserExist(username)) { if (isUserExist(username)) {
throw UserRegistException("user is existed") throw UserRegistException("user is existed")
} }
User.new(id) { val user = User.new(id) {
this.username = username this.username = username
this.password = encryptedPassword this.password = encryptedPassword
} }
addDefaultAccess(accessId, user)
} }
@Suppress("unused") return Msg(value = object { return Msg(value = mapOf(
val id = numberToKey(id) "id" to numberToKey(id),
val username = username "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 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 package dev.surl.surl.util
import dev.surl.surl.cfg.BaseConfiguration import dev.surl.surl.cfg.BaseConfig
import dev.surl.surl.cfg.Logging import dev.surl.surl.cfg.Logging
import io.jsonwebtoken.Claims import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
@ -15,7 +15,7 @@ import java.util.Date
@ConfigurationProperties(prefix = "jwt") @ConfigurationProperties(prefix = "jwt")
class JwtTokenUtil { class JwtTokenUtil {
@Autowired @Autowired
private lateinit var cfg: BaseConfiguration private lateinit var cfg: BaseConfig
@Autowired @Autowired
private lateinit var logging: Logging private lateinit var logging: Logging

View File

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