增加认证
This commit is contained in:
parent
c01841e742
commit
53b38a3307
@ -58,6 +58,6 @@ dependencies {
|
|||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
@ -3,13 +3,19 @@ package dev.surl.surl.cfg
|
|||||||
import dev.surl.surl.util.numberToKey
|
import dev.surl.surl.util.numberToKey
|
||||||
import io.jsonwebtoken.security.Keys
|
import io.jsonwebtoken.security.Keys
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "base.configs")
|
@ConfigurationProperties(prefix = "base.configs")
|
||||||
class BaseConfig(
|
class BaseConfig(
|
||||||
val site: String = "https://surl.org",
|
val site: String = "http://127.0.0.1",
|
||||||
val expire: Long = 3600000, // token expire time
|
val expire: Long = 3600000, // token expire time
|
||||||
private val secret: String = numberToKey(Date().time).repeat(5),
|
val unit: ChronoUnit = ChronoUnit.MILLIS,
|
||||||
|
val tokenHead: String = "Bearer ",
|
||||||
|
whiteList: List<String> = listOf("/login"),
|
||||||
|
secret: String = numberToKey(Date().time).repeat(5),
|
||||||
|
) {
|
||||||
val secretKey: SecretKey = Keys.hmacShaKeyFor(secret.toByteArray())
|
val secretKey: SecretKey = Keys.hmacShaKeyFor(secret.toByteArray())
|
||||||
)
|
val whiteList: List<Regex> = whiteList.map { it.toRegex() }
|
||||||
|
}
|
@ -2,13 +2,6 @@ package dev.surl.surl.cfg
|
|||||||
|
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
@Component
|
|
||||||
open class Logging {
|
|
||||||
fun <T: Any> logger(clazz: KClass<T>): Logger = LoggerFactory.getLogger(clazz::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
fun <T: Any> T.logger(): Logger = LoggerFactory.getLogger(this::class.java)
|
fun <T: Any> T.logger(): Logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
@ -2,6 +2,7 @@ package dev.surl.surl.cfg.security
|
|||||||
|
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCrypt
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ -14,4 +15,9 @@ open class EncoderConfig {
|
|||||||
open fun passwordEncoder(): BCryptPasswordEncoder {
|
open fun passwordEncoder(): BCryptPasswordEncoder {
|
||||||
return BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.`$2B`)
|
return BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.`$2B`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun cryoto(): BCrypt {
|
||||||
|
return BCrypt()
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
package dev.surl.surl.cfg.security
|
package dev.surl.surl.cfg.security
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain
|
import dev.surl.surl.util.JwtTokenUtil
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
|
||||||
import jakarta.servlet.http.HttpServletResponse
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
@ -9,7 +8,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.web.filter.OncePerRequestFilter
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@ -18,9 +16,14 @@ open class WebSecurityConfig {
|
|||||||
* 配置过滤器链
|
* 配置过滤器链
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
open fun filterChain(
|
||||||
|
http: HttpSecurity,
|
||||||
|
jwtTokenUtil: JwtTokenUtil,
|
||||||
|
response: HttpServletResponse): SecurityFilterChain {
|
||||||
http {
|
http {
|
||||||
csrf { disable() } // 关闭csrf
|
csrf { disable() } // 关闭csrf
|
||||||
|
formLogin { disable() }
|
||||||
|
httpBasic { disable() }
|
||||||
authorizeHttpRequests {
|
authorizeHttpRequests {
|
||||||
authorize(anyRequest, permitAll)
|
authorize(anyRequest, permitAll)
|
||||||
}
|
}
|
||||||
@ -30,17 +33,4 @@ open class WebSecurityConfig {
|
|||||||
}
|
}
|
||||||
return http.build()
|
return http.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@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/enums/RedisStorage.kt
Normal file
5
src/main/java/dev/surl/surl/common/enums/RedisStorage.kt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package dev.surl.surl.common.enums
|
||||||
|
|
||||||
|
enum class RedisStorage {
|
||||||
|
TOKEN
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package dev.surl.surl.common.exception
|
||||||
|
|
||||||
|
class UnauthorizedExcecption(message: String? = null, cause: Throwable? = null) : Exception(message, cause)
|
@ -5,27 +5,13 @@ import dev.surl.surl.common.Msg
|
|||||||
import dev.surl.surl.dto.SurlDto
|
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.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.PostMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@EnableWebSecurity
|
class SurlAddController(private val service: SurlService, private val cfg: BaseConfig) {
|
||||||
class SurlAddController {
|
@PostMapping("/api/surl/add")
|
||||||
@PostMapping("/surl/add")
|
fun addSurl(@Valid @RequestBody body: SurlDto) =
|
||||||
fun addSurl(
|
Msg(code = 0, value = "${cfg.site}/${service.addSurl(body.url ?: "")}")
|
||||||
@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)
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/login")
|
|
||||||
fun login():String {
|
|
||||||
return "loged in"
|
|
||||||
}
|
|
||||||
}
|
}
|
26
src/main/java/dev/surl/surl/controller/SurlGetController.kt
Normal file
26
src/main/java/dev/surl/surl/controller/SurlGetController.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package dev.surl.surl.controller
|
||||||
|
|
||||||
|
import dev.surl.surl.cfg.logger
|
||||||
|
import dev.surl.surl.common.Msg
|
||||||
|
import dev.surl.surl.service.SurlService
|
||||||
|
import dev.surl.surl.util.JwtTokenUtil
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
@GetMapping(path = ["/api/surl/get"])
|
||||||
|
fun getUrlsByUser(@RequestHeader headers: HttpHeaders): Msg<List<String>> {
|
||||||
|
logger().logger().info("headers: $headers")
|
||||||
|
val token = jwtTokenUtil.getTokenFromHeader(headers[HttpHeaders.AUTHORIZATION]?.last() ?: "")
|
||||||
|
logger().logger().info("token: $token")
|
||||||
|
val username = jwtTokenUtil.getUsernameFromToken(token)
|
||||||
|
val urls = surlService.getUrlsByUser(username)
|
||||||
|
return Msg(value = urls)
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ 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)
|
||||||
val user = reference("user", Users).index().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")
|
||||||
}
|
}
|
@ -1,36 +0,0 @@
|
|||||||
package dev.surl.surl.filter
|
|
||||||
//
|
|
||||||
//import dev.surl.surl.cfg.BaseConfiguration
|
|
||||||
//import dev.surl.surl.util.JwtTokenUtil
|
|
||||||
//import jakarta.servlet.FilterChain
|
|
||||||
//import jakarta.servlet.http.HttpServletRequest
|
|
||||||
//import jakarta.servlet.http.HttpServletResponse
|
|
||||||
//import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
//import org.springframework.security.core.context.SecurityContextHolder
|
|
||||||
//import org.springframework.stereotype.Component
|
|
||||||
//import org.springframework.web.filter.OncePerRequestFilter
|
|
||||||
//
|
|
||||||
//@Component
|
|
||||||
//class JwtAuthenticationFilter: OncePerRequestFilter() {
|
|
||||||
// @Autowired
|
|
||||||
// lateinit var jwtTokenUtil: JwtTokenUtil
|
|
||||||
//
|
|
||||||
// @Autowired
|
|
||||||
// lateinit var cfg: BaseConfiguration
|
|
||||||
// override fun doFilterInternal(
|
|
||||||
// request: HttpServletRequest,
|
|
||||||
// response: HttpServletResponse,
|
|
||||||
// filterChain: FilterChain
|
|
||||||
// ) {
|
|
||||||
// val token: String? = request.getHeader(cfg.tokenHead)
|
|
||||||
// if(token.isNullOrEmpty()) {
|
|
||||||
// val claims = jwtTokenUtil.getTokenClaim(token)
|
|
||||||
// if(claims != null) {
|
|
||||||
// if(!jwtTokenUtil.isTokenExpired(token) && SecurityContextHolder.getContext().authentication == null) {
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// filterChain.doFilter(request, response)
|
|
||||||
// }
|
|
||||||
//}
|
|
@ -0,0 +1,65 @@
|
|||||||
|
package dev.surl.surl.filter
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import dev.surl.surl.cfg.BaseConfig
|
||||||
|
import dev.surl.surl.common.Msg
|
||||||
|
import dev.surl.surl.common.enums.RedisStorage
|
||||||
|
import dev.surl.surl.common.exception.UnauthorizedExcecption
|
||||||
|
import dev.surl.surl.util.JwtTokenUtil
|
||||||
|
import dev.surl.surl.util.redis.RedisUtil
|
||||||
|
import jakarta.servlet.FilterChain
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class JwtAuthenticationTokenFilter(
|
||||||
|
private val jwtTokenUtil: JwtTokenUtil,
|
||||||
|
private val om: ObjectMapper,
|
||||||
|
private val cfg: BaseConfig,
|
||||||
|
private val redisUtil: RedisUtil
|
||||||
|
) : OncePerRequestFilter() {
|
||||||
|
override fun doFilterInternal(
|
||||||
|
request: HttpServletRequest,
|
||||||
|
response: HttpServletResponse,
|
||||||
|
filterChain: FilterChain
|
||||||
|
) {
|
||||||
|
if (request.servletPath notMatchedIn cfg.whiteList) {
|
||||||
|
try {
|
||||||
|
val exp = UnauthorizedExcecption("unauthorized")
|
||||||
|
val authHeader = request.getHeader(HttpHeaders.AUTHORIZATION) ?: throw exp
|
||||||
|
val token = jwtTokenUtil.getTokenFromHeader(authHeader)
|
||||||
|
val cachedToken = run {
|
||||||
|
try {
|
||||||
|
return@run redisUtil.getString(jwtTokenUtil.getUsernameFromToken(token), RedisStorage.TOKEN)
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
throw exp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
write(responseBody)
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
private infix fun String.matchedIn(regexes: List<Regex>): Boolean {
|
||||||
|
for (regex in regexes) {
|
||||||
|
if (this.matches(regex)) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private infix fun String.notMatchedIn(regexes: List<Regex>): Boolean {
|
||||||
|
return !(this matchedIn regexes)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package dev.surl.surl.filter
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import dev.surl.surl.common.Msg
|
||||||
|
import dev.surl.surl.common.enums.RedisStorage
|
||||||
|
import dev.surl.surl.dto.UserDto
|
||||||
|
import dev.surl.surl.service.UserService
|
||||||
|
import dev.surl.surl.util.JwtTokenUtil
|
||||||
|
import dev.surl.surl.util.redis.RedisUtil
|
||||||
|
import dev.surl.surl.util.validate
|
||||||
|
import jakarta.servlet.FilterChain
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import jakarta.validation.ConstraintViolationException
|
||||||
|
import jakarta.validation.Validator
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||||
|
import org.springframework.security.core.Authentication
|
||||||
|
import org.springframework.security.core.AuthenticationException
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class UsernamePasswordAuthenticationCheckFilter(
|
||||||
|
private val om: ObjectMapper,
|
||||||
|
private val validator: Validator,
|
||||||
|
private val jwtTokenUtil: JwtTokenUtil,
|
||||||
|
private val userService: UserService,
|
||||||
|
private val redisUtil: RedisUtil
|
||||||
|
) : 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) {
|
||||||
|
throw object : AuthenticationException(e.constraintViolations.joinToString(";") { it.message }) {}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw object : AuthenticationException(e.message) {}
|
||||||
|
}
|
||||||
|
if (!userService.authUser(userDto)) throw object : AuthenticationException("auth faild") {}
|
||||||
|
return authenticationManager.authenticate(
|
||||||
|
UsernamePasswordAuthenticationToken(
|
||||||
|
userDto.username, userDto.password, mutableListOf()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun successfulAuthentication(
|
||||||
|
request: HttpServletRequest?, response: HttpServletResponse?, chain: FilterChain?, authResult: Authentication?
|
||||||
|
) {
|
||||||
|
val (expireAt, token) = jwtTokenUtil.getToken(authResult!!.name, authResult.authorities.map { it.authority })
|
||||||
|
redisUtil.setString(authResult.name, token, RedisStorage.TOKEN)
|
||||||
|
val responseBody = om.writeValueAsString(
|
||||||
|
Msg(
|
||||||
|
code = 0, value = mapOf("expireAt" to expireAt, "token" to token)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
response?.run {
|
||||||
|
writer.write(responseBody)
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unsuccessfulAuthentication(
|
||||||
|
request: HttpServletRequest?, response: HttpServletResponse?, failed: AuthenticationException?
|
||||||
|
) {
|
||||||
|
response?.run {
|
||||||
|
status = HttpServletResponse.SC_UNAUTHORIZED
|
||||||
|
writer.write(om.writeValueAsString(Msg<String>(code = -1, msg = failed?.message)))
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package dev.surl.surl.handler
|
|||||||
|
|
||||||
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 jakarta.validation.ConstraintViolationException
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.HttpStatusCode
|
import org.springframework.http.HttpStatusCode
|
||||||
@ -65,4 +66,9 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() {
|
|||||||
): ResponseEntity<Msg<String>>{
|
): ResponseEntity<Msg<String>>{
|
||||||
return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown regist error"), HttpStatus.BAD_REQUEST)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
@ -10,6 +10,7 @@ import org.springframework.stereotype.Service
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class SurlService {
|
class SurlService {
|
||||||
|
private val userService: UserService by autowired()
|
||||||
fun addSurl(baseurl: String): String = runBlocking {
|
fun addSurl(baseurl: String): String = runBlocking {
|
||||||
val id = genSnowflakeUID()
|
val id = genSnowflakeUID()
|
||||||
transaction {
|
transaction {
|
||||||
@ -34,4 +35,14 @@ class SurlService {
|
|||||||
}.firstOrNull()?.get(Surls.url) ?: ""
|
}.firstOrNull()?.get(Surls.url) ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun getUrlsByUser(username: String): List<String> {
|
||||||
|
val user = userService.getUserByUsername(username) ?: return emptyList()
|
||||||
|
return transaction {
|
||||||
|
Surl.find {
|
||||||
|
Surls.id eq user.id
|
||||||
|
}.map {
|
||||||
|
it.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -6,9 +6,9 @@ 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.dao.UserAccess
|
||||||
import dev.surl.surl.dsl.Users
|
import dev.surl.surl.dsl.Users
|
||||||
import dev.surl.surl.util.autowired
|
import dev.surl.surl.dto.UserDto
|
||||||
import dev.surl.surl.util.genSnowflakeUID
|
import dev.surl.surl.util.*
|
||||||
import dev.surl.surl.util.numberToKey
|
import jakarta.validation.Validator
|
||||||
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.security.core.authority.SimpleGrantedAuthority
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
@ -23,7 +23,8 @@ typealias AUser = org.springframework.security.core.userdetails.User
|
|||||||
@Service
|
@Service
|
||||||
class UserService: UserDetailsService {
|
class UserService: UserDetailsService {
|
||||||
private val passwordEncoder: BCryptPasswordEncoder by autowired()
|
private val passwordEncoder: BCryptPasswordEncoder by autowired()
|
||||||
fun addUser(username: String, password: String): Msg<Any> {
|
private val validator: Validator by autowired()
|
||||||
|
fun addUser(username: String, password: String): Msg<Map<String, String>> {
|
||||||
|
|
||||||
val (id, accessId) = runBlocking {
|
val (id, accessId) = runBlocking {
|
||||||
Pair(genSnowflakeUID(), genSnowflakeUID())
|
Pair(genSnowflakeUID(), genSnowflakeUID())
|
||||||
@ -45,7 +46,7 @@ class UserService: UserDetailsService {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUserByUsername(username: String): User? {
|
fun getUserByUsername(username: String): User? {
|
||||||
return transaction {
|
return transaction {
|
||||||
User.find {
|
User.find {
|
||||||
Users.username eq username
|
Users.username eq username
|
||||||
@ -64,6 +65,12 @@ class UserService: UserDetailsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
override fun loadUserByUsername(username: String): UserDetails {
|
override fun loadUserByUsername(username: String): UserDetails {
|
||||||
val user = getUserByUsername(username) ?: throw UsernameNotFoundException("user '$username' not found")
|
val user = getUserByUsername(username) ?: throw UsernameNotFoundException("user '$username' not found")
|
||||||
return AUser.builder().apply {
|
return AUser.builder().apply {
|
||||||
|
@ -12,7 +12,6 @@ class Autowired<T : Any>(private val type: KClass<T>, private val name: String?)
|
|||||||
SurlApplication.context.getBean(name, type.java) as T
|
SurlApplication.context.getBean(name, type.java) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
|
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
|
||||||
}
|
}
|
||||||
inline fun <reified T : Any> autowired(name: String? = null) = Autowired(T::class, name)
|
inline fun <reified T : Any> autowired(name: String? = null) = Autowired(T::class, name)
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
package dev.surl.surl.util
|
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
|
||||||
|
|
||||||
fun md5(str: String): String {
|
|
||||||
return DigestUtils.md5Hex(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sha1(str: String): String {
|
|
||||||
return DigestUtils.sha1Hex(str)
|
|
||||||
}
|
|
@ -1,75 +1,48 @@
|
|||||||
package dev.surl.surl.util
|
package dev.surl.surl.util
|
||||||
|
|
||||||
import dev.surl.surl.cfg.BaseConfig
|
import dev.surl.surl.cfg.BaseConfig
|
||||||
import dev.surl.surl.cfg.Logging
|
|
||||||
import io.jsonwebtoken.Claims
|
import io.jsonwebtoken.Claims
|
||||||
import io.jsonwebtoken.Jwts
|
import io.jsonwebtoken.Jwts
|
||||||
import org.hibernate.validator.internal.engine.DefaultClockProvider
|
import org.springframework.oxm.ValidationFailureException
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.time.Clock
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@ConfigurationProperties(prefix = "jwt")
|
class JwtTokenUtil(private val cfg: BaseConfig) {
|
||||||
class JwtTokenUtil {
|
fun getToken(identityId: String, authorizes: List<String>): Pair<Date, String> {
|
||||||
@Autowired
|
val now = LocalDateTime.now()
|
||||||
private lateinit var cfg: BaseConfig
|
val expireAt = Date.from(now.plus(cfg.expire, cfg.unit).atZone(ZoneId.systemDefault()).toInstant())
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private lateinit var logging: Logging
|
|
||||||
|
|
||||||
private val clock: Clock = DefaultClockProvider.INSTANCE.clock
|
|
||||||
|
|
||||||
fun getToken(identityId: String, authorizes: List<String>): Map<String, Any> {
|
|
||||||
val now = Date()
|
|
||||||
val expireAt = now.time + cfg.expire
|
|
||||||
val expireDate = Date(expireAt)
|
|
||||||
val token = Jwts.builder().run {
|
val token = Jwts.builder().run {
|
||||||
subject(identityId)
|
subject(identityId)
|
||||||
issuedAt(now)
|
issuedAt(Date())
|
||||||
expiration(expireDate)
|
expiration(expireAt)
|
||||||
signWith(cfg.secretKey)
|
signWith(cfg.secretKey)
|
||||||
claim("roleAuthorizes", authorizes)
|
claim("roleAuthorizes", authorizes)
|
||||||
compact()
|
compact()
|
||||||
}
|
}
|
||||||
return mapOf(
|
return Pair(expireAt, token)
|
||||||
"expireAt" to expireAt,
|
|
||||||
"token" to token
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTokenClaim(token: String?): Claims? {
|
private fun getTokenClaim(token: String): Claims? {
|
||||||
if(token == null) return null
|
|
||||||
try {
|
|
||||||
return Jwts.parser().verifyWith(cfg.secretKey).build().parseSignedClaims(token).payload
|
return Jwts.parser().verifyWith(cfg.secretKey).build().parseSignedClaims(token).payload
|
||||||
} catch (e: Exception) {
|
|
||||||
logging.logger(JwtTokenUtil::class).error("get token claim error", e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getExpireDateFromToken(token: String): Date? {
|
fun getUsernameFromToken(token: String): String {
|
||||||
return getClaimFromToken(token) {
|
|
||||||
it?.expiration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTokenExpired(token: String?): Boolean {
|
|
||||||
if(token == null) return true
|
|
||||||
val expireDate = getExpireDateFromToken(token)
|
|
||||||
return if(expireDate == null) true else expireDate.time < clock.millis()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUsernameFromToken(token: String): String? {
|
|
||||||
return getClaimFromToken(token) {
|
return getClaimFromToken(token) {
|
||||||
it?.subject
|
it?.subject
|
||||||
}
|
} ?: throw ValidationFailureException("invalid token, userinfo not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> getClaimFromToken(token: String, resolver: (Claims?) -> T): T {
|
private fun <T> getClaimFromToken(token: String, resolver: (Claims?) -> T): T {
|
||||||
val claims = getTokenClaim(token)
|
val claims = getTokenClaim(token)
|
||||||
return resolver(claims)
|
return resolver(claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getTokenFromHeader(header: String): String {
|
||||||
|
return if (header.startsWith(cfg.tokenHead)) {
|
||||||
|
header.substring(cfg.tokenHead.length)
|
||||||
|
} else throw ValidationFailureException("invalid token")
|
||||||
|
}
|
||||||
}
|
}
|
12
src/main/java/dev/surl/surl/util/ValidateUtil.kt
Normal file
12
src/main/java/dev/surl/surl/util/ValidateUtil.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package dev.surl.surl.util
|
||||||
|
|
||||||
|
import jakarta.validation.ConstraintViolationException
|
||||||
|
import jakarta.validation.Validator
|
||||||
|
|
||||||
|
fun <T: Any?> validate(dto: T,validator: Validator) {
|
||||||
|
if(dto == null) throw IllegalArgumentException("dto for validation is null")
|
||||||
|
val violations = validator.validate(dto)
|
||||||
|
if(violations.isNotEmpty()) {
|
||||||
|
throw ConstraintViolationException(violations)
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,37 @@
|
|||||||
package dev.surl.surl.util.redis
|
package dev.surl.surl.util.redis
|
||||||
|
|
||||||
import dev.surl.surl.cfg.BaseConfig
|
import dev.surl.surl.cfg.BaseConfig
|
||||||
|
import dev.surl.surl.common.enums.RedisStorage
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate
|
import org.springframework.data.redis.core.StringRedisTemplate
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
@Component
|
@Component
|
||||||
class RedisUtil(private val template: StringRedisTemplate, private val cfg: BaseConfig) {
|
class RedisUtil(private val template: StringRedisTemplate, private val cfg: BaseConfig) {
|
||||||
private val ops = template.opsForValue()
|
private val ops = template.opsForValue()
|
||||||
fun getString(key: String): String? {
|
fun getString(key: String, type: RedisStorage? = null): String? {
|
||||||
|
if (type == null) {
|
||||||
return ops.get(key)
|
return ops.get(key)
|
||||||
}
|
}
|
||||||
fun setString(key: String, value: String, expire: Long = cfg.expire, unit: TimeUnit = TimeUnit.MILLISECONDS) {
|
return ops.get("${type.name}_$key")
|
||||||
ops.set(key, value, expire, unit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delKey(key: String) {
|
fun setString(key: String, value: String, type: RedisStorage? = null) {
|
||||||
|
if (type == null) {
|
||||||
|
ops.set(key, value, cfg.expire, chronoUnitToTimeUnit(cfg.unit))
|
||||||
|
} else {
|
||||||
|
ops.set("${type.name}_$key", value, cfg.expire, chronoUnitToTimeUnit(cfg.unit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delKey(key: String, type: RedisStorage? = null) {
|
||||||
|
if (type == null) {
|
||||||
ops.operations.delete(key)
|
ops.operations.delete(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ops.operations.delete("${type.name}_$key")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun flushdb() {
|
fun flushdb() {
|
||||||
@ -25,4 +39,15 @@ class RedisUtil(private val template: StringRedisTemplate, private val cfg: Base
|
|||||||
it.serverCommands().flushDb()
|
it.serverCommands().flushDb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun chronoUnitToTimeUnit(unit: ChronoUnit): TimeUnit {
|
||||||
|
return when (unit) {
|
||||||
|
ChronoUnit.MILLIS -> TimeUnit.MILLISECONDS
|
||||||
|
ChronoUnit.SECONDS -> TimeUnit.SECONDS
|
||||||
|
ChronoUnit.MINUTES -> TimeUnit.MINUTES
|
||||||
|
ChronoUnit.HOURS -> TimeUnit.HOURS
|
||||||
|
ChronoUnit.DAYS -> TimeUnit.DAYS
|
||||||
|
else -> throw IllegalArgumentException("unsupported unit: $unit")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -36,7 +36,7 @@ spring:
|
|||||||
min-idle: 1
|
min-idle: 1
|
||||||
max-wait: 100ms
|
max-wait: 100ms
|
||||||
exposed:
|
exposed:
|
||||||
show-sql: false
|
show-sql: true
|
||||||
generate-ddl: true
|
generate-ddl: true
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
@ -46,3 +46,8 @@ base:
|
|||||||
configs:
|
configs:
|
||||||
site: http://127.0.0.1:18888
|
site: http://127.0.0.1:18888
|
||||||
expire: 3600000
|
expire: 3600000
|
||||||
|
secret: Is#45Ddw29apkbHawwaHb4d^&w29apkbHawwaHb4d^&
|
||||||
|
white-list:
|
||||||
|
- ^/login$
|
||||||
|
- ^/reg$
|
||||||
|
- ^(?!\/api).*$
|
@ -1,18 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package dev.surl.surl;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
class SurlApplicationTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void contextLoads() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package dev.surl.surl
|
|
||||||
|
|
||||||
import dev.surl.surl.util.keyToNumber
|
|
||||||
import dev.surl.surl.util.md5
|
|
||||||
import dev.surl.surl.util.numberToKey
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
class UtilTests {
|
|
||||||
@Test
|
|
||||||
fun md5Test() {
|
|
||||||
assert(md5("sd") == "6226f7cbe59e99a90b5cef6f94f966fd")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun numberToUIDTest() {
|
|
||||||
println(numberToKey(1810519439309799440))
|
|
||||||
println(keyToNumber("0IYJbl*DiYE"))
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user