diff --git a/build.gradle b/build.gradle index 98dad62..1b7711f 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,6 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } -tasks.named('test') { +test { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/cfg/BaseConfig.kt b/src/main/java/dev/surl/surl/cfg/BaseConfig.kt index 4706425..cd8725e 100644 --- a/src/main/java/dev/surl/surl/cfg/BaseConfig.kt +++ b/src/main/java/dev/surl/surl/cfg/BaseConfig.kt @@ -3,13 +3,19 @@ package dev.surl.surl.cfg import dev.surl.surl.util.numberToKey import io.jsonwebtoken.security.Keys import org.springframework.boot.context.properties.ConfigurationProperties +import java.time.temporal.ChronoUnit import java.util.Date import javax.crypto.SecretKey @ConfigurationProperties(prefix = "base.configs") class BaseConfig( - val site: String = "https://surl.org", + val site: String = "http://127.0.0.1", 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 = listOf("/login"), + secret: String = numberToKey(Date().time).repeat(5), +) { val secretKey: SecretKey = Keys.hmacShaKeyFor(secret.toByteArray()) -) \ No newline at end of file + val whiteList: List = whiteList.map { it.toRegex() } +} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/cfg/Logging.kt b/src/main/java/dev/surl/surl/cfg/Logging.kt index ede4c9d..71fc114 100644 --- a/src/main/java/dev/surl/surl/cfg/Logging.kt +++ b/src/main/java/dev/surl/surl/cfg/Logging.kt @@ -2,13 +2,6 @@ package dev.surl.surl.cfg import org.slf4j.Logger import org.slf4j.LoggerFactory -import org.springframework.stereotype.Component -import kotlin.reflect.KClass - -@Component -open class Logging { - fun logger(clazz: KClass): Logger = LoggerFactory.getLogger(clazz::class.java) -} @Suppress("UNUSED") fun T.logger(): Logger = LoggerFactory.getLogger(this::class.java) diff --git a/src/main/java/dev/surl/surl/cfg/security/EncoderConfig.kt b/src/main/java/dev/surl/surl/cfg/security/EncoderConfig.kt index a24a16d..4dfc9e7 100644 --- a/src/main/java/dev/surl/surl/cfg/security/EncoderConfig.kt +++ b/src/main/java/dev/surl/surl/cfg/security/EncoderConfig.kt @@ -2,6 +2,7 @@ package dev.surl.surl.cfg.security import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.security.crypto.bcrypt.BCrypt import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder @Configuration @@ -14,4 +15,9 @@ open class EncoderConfig { open fun passwordEncoder(): BCryptPasswordEncoder { return BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.`$2B`) } + + @Bean + open fun cryoto(): BCrypt { + return BCrypt() + } } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/cfg/security/WebSecurityConfig.kt b/src/main/java/dev/surl/surl/cfg/security/WebSecurityConfig.kt index 8a823fe..8112854 100644 --- a/src/main/java/dev/surl/surl/cfg/security/WebSecurityConfig.kt +++ b/src/main/java/dev/surl/surl/cfg/security/WebSecurityConfig.kt @@ -1,7 +1,6 @@ package dev.surl.surl.cfg.security -import jakarta.servlet.FilterChain -import jakarta.servlet.http.HttpServletRequest +import dev.surl.surl.util.JwtTokenUtil import jakarta.servlet.http.HttpServletResponse import org.springframework.context.annotation.Bean 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.web.SecurityFilterChain import org.springframework.security.config.annotation.web.invoke -import org.springframework.web.filter.OncePerRequestFilter @Configuration @EnableWebSecurity @@ -18,29 +16,21 @@ open class WebSecurityConfig { * 配置过滤器链 */ @Bean - open fun filterChain(http: HttpSecurity): SecurityFilterChain { + open fun filterChain( + http: HttpSecurity, + jwtTokenUtil: JwtTokenUtil, + response: HttpServletResponse): SecurityFilterChain { http { csrf { disable() } // 关闭csrf + formLogin { disable() } + httpBasic { disable() } authorizeHttpRequests { authorize(anyRequest, permitAll) } headers { - cacheControl { } // 禁用缓存 + cacheControl { } // 禁用缓存 } } return http.build() } - - @Bean - open fun authenticationTokenFilter(): OncePerRequestFilter { - return object: OncePerRequestFilter() { - override fun doFilterInternal( - request: HttpServletRequest, - response: HttpServletResponse, - filterChain: FilterChain - ) { - filterChain.doFilter(request, response) - } - } - } } diff --git a/src/main/java/dev/surl/surl/common/enums/RedisStorage.kt b/src/main/java/dev/surl/surl/common/enums/RedisStorage.kt new file mode 100644 index 0000000..b212ec0 --- /dev/null +++ b/src/main/java/dev/surl/surl/common/enums/RedisStorage.kt @@ -0,0 +1,5 @@ +package dev.surl.surl.common.enums + +enum class RedisStorage { + TOKEN +} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/common/exception/UnauthorizedExcecption.kt b/src/main/java/dev/surl/surl/common/exception/UnauthorizedExcecption.kt new file mode 100644 index 0000000..0fd2f9c --- /dev/null +++ b/src/main/java/dev/surl/surl/common/exception/UnauthorizedExcecption.kt @@ -0,0 +1,3 @@ +package dev.surl.surl.common.exception + +class UnauthorizedExcecption(message: String? = null, cause: Throwable? = null) : Exception(message, cause) \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/controller/SurlAddController.kt b/src/main/java/dev/surl/surl/controller/SurlAddController.kt index 46bed1b..cac79f3 100644 --- a/src/main/java/dev/surl/surl/controller/SurlAddController.kt +++ b/src/main/java/dev/surl/surl/controller/SurlAddController.kt @@ -5,27 +5,13 @@ import dev.surl.surl.common.Msg import dev.surl.surl.dto.SurlDto 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: 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" - } +class SurlAddController(private val service: SurlService, private val cfg: BaseConfig) { + @PostMapping("/api/surl/add") + fun addSurl(@Valid @RequestBody body: SurlDto) = + Msg(code = 0, value = "${cfg.site}/${service.addSurl(body.url ?: "")}") } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/controller/SurlGetController.kt b/src/main/java/dev/surl/surl/controller/SurlGetController.kt new file mode 100644 index 0000000..38b152c --- /dev/null +++ b/src/main/java/dev/surl/surl/controller/SurlGetController.kt @@ -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> { + 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) + } +} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/dsl/Surls.kt b/src/main/java/dev/surl/surl/dsl/Surls.kt index 6b2a682..216d843 100644 --- a/src/main/java/dev/surl/surl/dsl/Surls.kt +++ b/src/main/java/dev/surl/surl/dsl/Surls.kt @@ -4,7 +4,7 @@ import org.jetbrains.exposed.dao.id.IdTable object Surls: IdTable("surl") { override val id = long("id").entityId() - val url = varchar("url", 2048).uniqueIndex() + val url = varchar("url", 2048) val user = reference("user", Users).index().nullable() override val primaryKey = PrimaryKey(id, name = "PK_surl_id") } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/filter/JwtAuthenticationFilter.kt b/src/main/java/dev/surl/surl/filter/JwtAuthenticationFilter.kt deleted file mode 100644 index e282608..0000000 --- a/src/main/java/dev/surl/surl/filter/JwtAuthenticationFilter.kt +++ /dev/null @@ -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) -// } -//} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/filter/JwtAuthenticationTokenFilter.kt b/src/main/java/dev/surl/surl/filter/JwtAuthenticationTokenFilter.kt new file mode 100644 index 0000000..a7a449c --- /dev/null +++ b/src/main/java/dev/surl/surl/filter/JwtAuthenticationTokenFilter.kt @@ -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(code = -1, msg = e.message)) + response.writer.run { + write(responseBody) + flush() + } + return + } + } + filterChain.doFilter(request, response) + } + + private infix fun String.matchedIn(regexes: List): Boolean { + for (regex in regexes) { + if (this.matches(regex)) return true + } + return false + } + + private infix fun String.notMatchedIn(regexes: List): Boolean { + return !(this matchedIn regexes) + } +} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/filter/UsernamePasswordAuthenticationCheckFilter.kt b/src/main/java/dev/surl/surl/filter/UsernamePasswordAuthenticationCheckFilter.kt new file mode 100644 index 0000000..2155de5 --- /dev/null +++ b/src/main/java/dev/surl/surl/filter/UsernamePasswordAuthenticationCheckFilter.kt @@ -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(code = -1, msg = failed?.message))) + writer.flush() + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt b/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt index 82844e0..7f6bcd5 100644 --- a/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt +++ b/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt @@ -2,6 +2,7 @@ package dev.surl.surl.handler import dev.surl.surl.common.Msg import dev.surl.surl.common.exception.UserRegistException +import jakarta.validation.ConstraintViolationException import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.HttpStatusCode @@ -53,7 +54,7 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() { return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown error"), status) } - @ExceptionHandler(value = [IllegalStateException::class ,Exception::class]) + @ExceptionHandler(value = [IllegalStateException::class, Exception::class]) fun handleException( ex: Exception ): ResponseEntity> { @@ -65,4 +66,9 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() { ): ResponseEntity>{ return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown regist error"), HttpStatus.BAD_REQUEST) } + + @ExceptionHandler(value = [ConstraintViolationException::class]) + fun handleConstraintViolationException(ex: Exception): ResponseEntity> { + return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown validation error"), HttpStatus.BAD_REQUEST) + } } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/service/SurlService.kt b/src/main/java/dev/surl/surl/service/SurlService.kt index bcd46d8..566091f 100644 --- a/src/main/java/dev/surl/surl/service/SurlService.kt +++ b/src/main/java/dev/surl/surl/service/SurlService.kt @@ -10,6 +10,7 @@ import org.springframework.stereotype.Service @Service class SurlService { + private val userService: UserService by autowired() fun addSurl(baseurl: String): String = runBlocking { val id = genSnowflakeUID() transaction { @@ -34,4 +35,14 @@ class SurlService { }.firstOrNull()?.get(Surls.url) ?: "" } } + fun getUrlsByUser(username: String): List { + val user = userService.getUserByUsername(username) ?: return emptyList() + return transaction { + Surl.find { + Surls.id eq user.id + }.map { + it.url + } + } + } } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/service/UserService.kt b/src/main/java/dev/surl/surl/service/UserService.kt index 2d643be..68b30dc 100644 --- a/src/main/java/dev/surl/surl/service/UserService.kt +++ b/src/main/java/dev/surl/surl/service/UserService.kt @@ -6,9 +6,9 @@ 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 dev.surl.surl.dto.UserDto +import dev.surl.surl.util.* +import jakarta.validation.Validator import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.transactions.transaction import org.springframework.security.core.authority.SimpleGrantedAuthority @@ -23,7 +23,8 @@ typealias AUser = org.springframework.security.core.userdetails.User @Service class UserService: UserDetailsService { private val passwordEncoder: BCryptPasswordEncoder by autowired() - fun addUser(username: String, password: String): Msg { + private val validator: Validator by autowired() + fun addUser(username: String, password: String): Msg> { val (id, accessId) = runBlocking { Pair(genSnowflakeUID(), genSnowflakeUID()) @@ -45,7 +46,7 @@ class UserService: UserDetailsService { )) } - private fun getUserByUsername(username: String): User? { + fun getUserByUsername(username: String): User? { return transaction { User.find { 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 { val user = getUserByUsername(username) ?: throw UsernameNotFoundException("user '$username' not found") return AUser.builder().apply { diff --git a/src/main/java/dev/surl/surl/util/Autowired.kt b/src/main/java/dev/surl/surl/util/Autowired.kt index 85c7721..2d5a9d0 100644 --- a/src/main/java/dev/surl/surl/util/Autowired.kt +++ b/src/main/java/dev/surl/surl/util/Autowired.kt @@ -12,7 +12,6 @@ class Autowired(private val type: KClass, private val name: String?) SurlApplication.context.getBean(name, type.java) as T } } - operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value } inline fun autowired(name: String? = null) = Autowired(T::class, name) diff --git a/src/main/java/dev/surl/surl/util/CheckSumUtil.kt b/src/main/java/dev/surl/surl/util/CheckSumUtil.kt deleted file mode 100644 index 017a223..0000000 --- a/src/main/java/dev/surl/surl/util/CheckSumUtil.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/util/JwtTokenUtil.kt b/src/main/java/dev/surl/surl/util/JwtTokenUtil.kt index b40d9e4..4f239d7 100644 --- a/src/main/java/dev/surl/surl/util/JwtTokenUtil.kt +++ b/src/main/java/dev/surl/surl/util/JwtTokenUtil.kt @@ -1,75 +1,48 @@ package dev.surl.surl.util import dev.surl.surl.cfg.BaseConfig -import dev.surl.surl.cfg.Logging import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts -import org.hibernate.validator.internal.engine.DefaultClockProvider -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.oxm.ValidationFailureException import org.springframework.stereotype.Component -import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId import java.util.Date @Component -@ConfigurationProperties(prefix = "jwt") -class JwtTokenUtil { - @Autowired - private lateinit var cfg: BaseConfig - - @Autowired - private lateinit var logging: Logging - - private val clock: Clock = DefaultClockProvider.INSTANCE.clock - - fun getToken(identityId: String, authorizes: List): Map { - val now = Date() - val expireAt = now.time + cfg.expire - val expireDate = Date(expireAt) +class JwtTokenUtil(private val cfg: BaseConfig) { + fun getToken(identityId: String, authorizes: List): Pair { + val now = LocalDateTime.now() + val expireAt = Date.from(now.plus(cfg.expire, cfg.unit).atZone(ZoneId.systemDefault()).toInstant()) val token = Jwts.builder().run { subject(identityId) - issuedAt(now) - expiration(expireDate) + issuedAt(Date()) + expiration(expireAt) signWith(cfg.secretKey) claim("roleAuthorizes", authorizes) compact() } - return mapOf( - "expireAt" to expireAt, - "token" to token - ) + return Pair(expireAt, token) } - fun getTokenClaim(token: String?): Claims? { - if(token == null) return null - try { - 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 getTokenClaim(token: String): Claims? { + return Jwts.parser().verifyWith(cfg.secretKey).build().parseSignedClaims(token).payload } - private fun getExpireDateFromToken(token: String): Date? { - 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? { + fun getUsernameFromToken(token: String): String { return getClaimFromToken(token) { it?.subject - } + } ?: throw ValidationFailureException("invalid token, userinfo not found") } private fun getClaimFromToken(token: String, resolver: (Claims?) -> T): T { val claims = getTokenClaim(token) return resolver(claims) } + + fun getTokenFromHeader(header: String): String { + return if (header.startsWith(cfg.tokenHead)) { + header.substring(cfg.tokenHead.length) + } else throw ValidationFailureException("invalid token") + } } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/util/ValidateUtil.kt b/src/main/java/dev/surl/surl/util/ValidateUtil.kt new file mode 100644 index 0000000..a6b45e0 --- /dev/null +++ b/src/main/java/dev/surl/surl/util/ValidateUtil.kt @@ -0,0 +1,12 @@ +package dev.surl.surl.util + +import jakarta.validation.ConstraintViolationException +import jakarta.validation.Validator + +fun 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) + } +} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt b/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt index 79b9c74..b6d7c4b 100644 --- a/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt +++ b/src/main/java/dev/surl/surl/util/redis/RedisUtil.kt @@ -1,23 +1,37 @@ package dev.surl.surl.util.redis import dev.surl.surl.cfg.BaseConfig +import dev.surl.surl.common.enums.RedisStorage import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Component +import java.time.temporal.ChronoUnit import java.util.concurrent.TimeUnit @Suppress("UNUSED") @Component class RedisUtil(private val template: StringRedisTemplate, private val cfg: BaseConfig) { private val ops = template.opsForValue() - fun getString(key: String): String? { - return ops.get(key) - } - fun setString(key: String, value: String, expire: Long = cfg.expire, unit: TimeUnit = TimeUnit.MILLISECONDS) { - ops.set(key, value, expire, unit) + fun getString(key: String, type: RedisStorage? = null): String? { + if (type == null) { + return ops.get(key) + } + return ops.get("${type.name}_$key") } - fun delKey(key: String) { - ops.operations.delete(key) + 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) + return + } + ops.operations.delete("${type.name}_$key") } fun flushdb() { @@ -25,4 +39,15 @@ class RedisUtil(private val template: StringRedisTemplate, private val cfg: Base 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") + } + } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1c35a36..c1fe6d1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -36,7 +36,7 @@ spring: min-idle: 1 max-wait: 100ms exposed: - show-sql: false + show-sql: true generate-ddl: true logging: level: @@ -45,4 +45,9 @@ logging: base: configs: site: http://127.0.0.1:18888 - expire: 3600000 \ No newline at end of file + expire: 3600000 + secret: Is#45Ddw29apkbHawwaHb4d^&w29apkbHawwaHb4d^& + white-list: + - ^/login$ + - ^/reg$ + - ^(?!\/api).*$ \ No newline at end of file diff --git a/src/test/java/dev/surl/surl/SecurityTests.kt b/src/test/java/dev/surl/surl/SecurityTests.kt deleted file mode 100644 index 4e02bd2..0000000 --- a/src/test/java/dev/surl/surl/SecurityTests.kt +++ /dev/null @@ -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()) - } -} \ No newline at end of file diff --git a/src/test/java/dev/surl/surl/SurlApplicationTests.java b/src/test/java/dev/surl/surl/SurlApplicationTests.java deleted file mode 100644 index 7f7cb7e..0000000 --- a/src/test/java/dev/surl/surl/SurlApplicationTests.java +++ /dev/null @@ -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() { - } - -} diff --git a/src/test/java/dev/surl/surl/UtilTests.kt b/src/test/java/dev/surl/surl/UtilTests.kt deleted file mode 100644 index cdc9d78..0000000 --- a/src/test/java/dev/surl/surl/UtilTests.kt +++ /dev/null @@ -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")) - } -} \ No newline at end of file