添加jwt支持

This commit is contained in:
05412 2024-07-10 20:00:06 +08:00
parent 3ee64a5f69
commit e2b56a654c
19 changed files with 306 additions and 29 deletions

View File

@ -46,11 +46,14 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-validation'
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'
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'
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
runtimeOnly 'org.postgresql:postgresql' runtimeOnly 'org.postgresql:postgresql'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
} }

View File

@ -2,10 +2,15 @@ package dev.surl.surl
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.context.ConfigurableApplicationContext
@SpringBootApplication @SpringBootApplication
open class SurlApplication open class SurlApplication {
companion object {
lateinit var context: ConfigurableApplicationContext
}
}
fun main(args: Array<String>) { fun main(args: Array<String>) {
runApplication<SurlApplication>(*args) SurlApplication.context = runApplication<SurlApplication>(*args)
} }

View File

@ -1,6 +1,14 @@
package dev.surl.surl.cfg package dev.surl.surl.cfg
import io.jsonwebtoken.security.Keys
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import javax.crypto.SecretKey
@Configuration @Configuration
open class BaseConfiguration(val site: String = "https://surl.org") 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,11 @@
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 <T: Any> logger(clazz: KClass<T>): Logger = LoggerFactory.getLogger(clazz::class.java)
}

View File

@ -1,27 +1,58 @@
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.http.HttpServletRequest
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
import org.springframework.security.config.annotation.web.builders.HttpSecurity 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
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
open class WebSecurityConfig { open class WebSecurityConfig {
/**
* 配置过滤器链
*/
@Bean @Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain { open fun filterChain(http: HttpSecurity): SecurityFilterChain {
println("loading")
http { http {
csrf { disable() } // 关闭csrf
authorizeHttpRequests { authorizeHttpRequests {
authorize(anyRequest, authenticated) authorize(anyRequest, permitAll)
} }
formLogin { } headers {
httpBasic { cacheControl { } // 禁用缓存
} }
} }
return http.build() return http.build()
} }
/**
* 装载BCrypt密码编码器
*/
@Bean
open fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder()
}
// @Bean
// open fun authenticationTokenFilter(): OncePerRequestFilter {
// return object: OncePerRequestFilter() {
// override fun doFilterInternal(
// request: HttpServletRequest,
// response: HttpServletResponse,
// filterChain: FilterChain
// ) {
// filterChain.doFilter(request, response)
// }
// }
// }
} }

View File

@ -1,7 +1,5 @@
package dev.surl.surl.common package dev.surl.surl.common
data class Msg( data class Msg<T>(
val code: Int = 0, val code: Int = 0, val msg: String? = null, val value: T? = null
val msg: String? = null,
val value: Any? = null
) )

View File

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

View File

@ -10,14 +10,14 @@ import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.web.bind.annotation.GetMapping 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.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@EnableWebSecurity @EnableWebSecurity
class SurlAddController { class SurlAddController {
@PostMapping("/surl/add") @RequestMapping("/surl/add")
fun addSurl( fun addSurl(
@Valid @RequestBody body: Surl, @Autowired service: SurlService, @Autowired cfg: BaseConfiguration @Valid @RequestBody body: Surl, @Autowired service: SurlService, @Autowired cfg: BaseConfiguration
): Any { ): Any {

View File

@ -0,0 +1,19 @@
package dev.surl.surl.controller
import dev.surl.surl.common.Msg
import dev.surl.surl.dto.UserDto
import dev.surl.surl.service.UserService
import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController
@RestController
class UserController {
@RequestMapping(method = [RequestMethod.POST], path = ["/reg"])
fun reg(
@Autowired service: UserService, @Valid @RequestBody(required = true) user: UserDto
) = service.addUser(user.username!!, user.password!!)
}

View File

@ -8,5 +8,5 @@ import org.jetbrains.exposed.dao.id.EntityID
class User(id: EntityID<Long>): LongEntity(id) { class User(id: EntityID<Long>): LongEntity(id) {
companion object EntityClass: LongEntityClass<User>(Users) companion object EntityClass: LongEntityClass<User>(Users)
var username by Users.username var username by Users.username
var passwd by Users.passwd var password by Users.password
} }

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 passwd = varchar("passwd", 256) val password = varchar("password", 256)
override val primaryKey = PrimaryKey(id, name = "PK_user_id") override val primaryKey = PrimaryKey(id, name = "PK_user_id")
} }

View File

@ -0,0 +1,17 @@
package dev.surl.surl.dto
import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.validation.constraints.NotNull
import org.hibernate.validator.constraints.Length
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")
val username: String?,
@JsonProperty("password")
@get:Length(max = 16, min = 8, message = "password length must be between 8 and 16")
@get:NotNull(message = "password cannot be null")
val password: String?
)

View File

@ -0,0 +1,36 @@
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)
// }
//}

View File

@ -0,0 +1,18 @@
package dev.surl.surl.handler
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.web.access.AccessDeniedHandler
import org.springframework.web.bind.annotation.ControllerAdvice
@ControllerAdvice
class AccessHandler: AccessDeniedHandler {
override fun handle(
request: HttpServletRequest?,
response: HttpServletResponse?,
accessDeniedException: AccessDeniedException?
) {
response?.sendRedirect("/login")
}
}

View File

@ -1,10 +1,12 @@
package dev.surl.surl.handler 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 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
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.validation.method.MethodValidationException import org.springframework.validation.method.MethodValidationException
import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ControllerAdvice
@ -17,23 +19,35 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() {
override fun handleMethodValidationException( override fun handleMethodValidationException(
ex: MethodValidationException, headers: HttpHeaders, status: HttpStatus, request: WebRequest ex: MethodValidationException, headers: HttpHeaders, status: HttpStatus, request: WebRequest
): ResponseEntity<Any> { ): ResponseEntity<Any> {
return ResponseEntity(Msg(code = -1, msg = ex.allValidationResults.joinToString(";")), status) return ResponseEntity(Msg<String>(code = -1, msg = ex.allValidationResults.joinToString(";")), status)
} }
override fun handleMethodArgumentNotValid( override fun handleMethodArgumentNotValid(
ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest
): ResponseEntity<Any> { ): ResponseEntity<Any> {
return ResponseEntity( return ResponseEntity(
Msg(code = -1, msg = ex.allErrors.joinToString(";") { Msg<String>(code = -1, msg = ex.allErrors.joinToString(";") {
it.defaultMessage?.toString() ?: "unknown invalid method argument" it.defaultMessage?.toString() ?: "unknown invalid method argument"
}), status }), status
) )
} }
override fun handleHttpMessageNotReadable(
ex: HttpMessageNotReadableException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest
): ResponseEntity<Any> {
return ResponseEntity(Msg<String>(code = -1, msg = ex.message ?: "unknown error"), status)
}
@ExceptionHandler(value = [Exception::class]) @ExceptionHandler(value = [Exception::class])
fun handleException( fun handleException(
ex: Exception, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest ex: Exception
): ResponseEntity<Msg> { ): ResponseEntity<Msg<String>> {
return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown error"), status) return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown error"), HttpStatus.INTERNAL_SERVER_ERROR)
}
@ExceptionHandler(value = [UserRegistException::class])
fun handleUserRegistException(ex: Exception, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest
): ResponseEntity<Msg<String>>{
return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown regist error"), status)
} }
} }

View File

@ -11,10 +11,11 @@ import org.springframework.stereotype.Service
class SurlService { class SurlService {
fun addSurl(baseurl: String): String = runBlocking { fun addSurl(baseurl: String): String = runBlocking {
val id = genSnowflakeUID() val id = genSnowflakeUID()
Surl.new { transaction {
this.id._value = id Surl.new(id) {
url = baseurl url = baseurl
} }
}
return@runBlocking numberToKey(id) return@runBlocking numberToKey(id)
} }

View File

@ -1,3 +1,43 @@
package dev.surl.surl.service package dev.surl.surl.service
class UserService import dev.surl.surl.SurlApplication
import dev.surl.surl.common.Msg
import dev.surl.surl.common.exception.UserRegistException
import dev.surl.surl.dao.User
import dev.surl.surl.dsl.Users
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.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service
@Service
class UserService(@Autowired private val passwordEncoder: BCryptPasswordEncoder) {
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 encryptedPassword = passwordEncoder.encode(password)
transaction {
if (isUserExist(username)) {
throw UserRegistException("user is existed")
}
User.new(id) {
this.username = username
this.password = encryptedPassword
}
}
@Suppress("unused") return Msg(value = object {
val id = numberToKey(id)
val username = username
})
}
private fun isUserExist(username: String) = User.find {
Users.username eq username
}.toList().isNotEmpty()
}

View File

@ -0,0 +1,75 @@
package dev.surl.surl.util
import dev.surl.surl.cfg.BaseConfiguration
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.stereotype.Component
import java.time.Clock
import java.util.Date
@Component
@ConfigurationProperties(prefix = "jwt")
class JwtTokenUtil {
@Autowired
private lateinit var cfg: BaseConfiguration
@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 {
subject(identityId)
issuedAt(now)
expiration(expireDate)
signWith(cfg.secretKey)
claim("roleAuthorizes", authorizes)
compact()
}
return mapOf(
"expireAt" to expireAt,
"token" to 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 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? {
return getClaimFromToken(token) {
it?.subject
}
}
private fun <T> getClaimFromToken(token: String, resolver: (Claims?) -> T): T {
val claims = getTokenClaim(token)
return resolver(claims)
}
}

View File

@ -1,5 +1,7 @@
server: server:
port: 18888 port: 18888
servlet:
context-path: /api/v1
spring: spring:
profiles: profiles:
active: default active: default
@ -27,10 +29,6 @@ spring:
exposed: exposed:
show-sql: false show-sql: false
generate-ddl: true generate-ddl: true
security:
user:
name: admin
password: 123
logging: logging:
level: level:
root: info root: info