添加权限表
This commit is contained in:
parent
e2b56a654c
commit
b0616a1098
@ -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'
|
||||
|
21
src/main/java/dev/surl/surl/cfg/BaseConfig.kt
Normal file
21
src/main/java/dev/surl/surl/cfg/BaseConfig.kt
Normal 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"
|
||||
)
|
@ -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"
|
||||
)
|
9
src/main/java/dev/surl/surl/cfg/PatternConfig.kt
Normal file
9
src/main/java/dev/surl/surl/cfg/PatternConfig.kt
Normal 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$""")
|
||||
}
|
17
src/main/java/dev/surl/surl/cfg/security/EncoderConfig.kt
Normal file
17
src/main/java/dev/surl/surl/cfg/security/EncoderConfig.kt
Normal 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`)
|
||||
}
|
||||
}
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
5
src/main/java/dev/surl/surl/common/Access.kt
Normal file
5
src/main/java/dev/surl/surl/common/Access.kt
Normal file
@ -0,0 +1,5 @@
|
||||
package dev.surl.surl.common
|
||||
|
||||
enum class Access {
|
||||
ADMIN, READ, WRITE
|
||||
}
|
@ -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)
|
@ -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()) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
18
src/main/java/dev/surl/surl/dao/UserAccess.kt
Normal file
18
src/main/java/dev/surl/surl/dao/UserAccess.kt
Normal 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
|
||||
}
|
@ -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")
|
||||
}
|
9
src/main/java/dev/surl/surl/dsl/UserAccesses.kt
Normal file
9
src/main/java/dev/surl/surl/dsl/UserAccesses.kt
Normal 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")
|
||||
}
|
@ -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")
|
||||
}
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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) ?: ""
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}.toList().isNotEmpty()
|
||||
}.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isUserExist(username: String) = !User.find {
|
||||
Users.username eq username
|
||||
}.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()
|
||||
}
|
||||
}
|
18
src/main/java/dev/surl/surl/util/Autowired.kt
Normal file
18
src/main/java/dev/surl/surl/util/Autowired.kt
Normal 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)
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
16
src/main/java/dev/surl/surl/util/redis/RedisUtil.kt
Normal file
16
src/main/java/dev/surl/surl/util/redis/RedisUtil.kt
Normal 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 {
|
||||
}
|
||||
}
|
||||
}
|
@ -32,4 +32,7 @@ spring:
|
||||
logging:
|
||||
level:
|
||||
root: info
|
||||
Exposed: warn
|
||||
Exposed: debug
|
||||
base:
|
||||
configs:
|
||||
expire: 1000
|
Loading…
Reference in New Issue
Block a user