添加jwt支持
This commit is contained in:
parent
3ee64a5f69
commit
e2b56a654c
@ -46,11 +46,14 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
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'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
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'
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,15 @@ package dev.surl.surl
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.ConfigurableApplicationContext
|
||||
|
||||
@SpringBootApplication
|
||||
open class SurlApplication
|
||||
open class SurlApplication {
|
||||
companion object {
|
||||
lateinit var context: ConfigurableApplicationContext
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<SurlApplication>(*args)
|
||||
SurlApplication.context = runApplication<SurlApplication>(*args)
|
||||
}
|
||||
|
@ -1,6 +1,14 @@
|
||||
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")
|
||||
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"
|
||||
)
|
||||
|
11
src/main/java/dev/surl/surl/cfg/Logging.kt
Normal file
11
src/main/java/dev/surl/surl/cfg/Logging.kt
Normal 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)
|
||||
}
|
@ -1,27 +1,58 @@
|
||||
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.Configuration
|
||||
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
|
||||
@EnableWebSecurity
|
||||
open class WebSecurityConfig {
|
||||
/**
|
||||
* 配置过滤器链
|
||||
*/
|
||||
@Bean
|
||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
|
||||
println("loading")
|
||||
http {
|
||||
csrf { disable() } // 关闭csrf
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
authorize(anyRequest, permitAll)
|
||||
}
|
||||
formLogin { }
|
||||
httpBasic {
|
||||
headers {
|
||||
cacheControl { } // 禁用缓存
|
||||
}
|
||||
|
||||
}
|
||||
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)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
package dev.surl.surl.common
|
||||
|
||||
data class Msg(
|
||||
val code: Int = 0,
|
||||
val msg: String? = null,
|
||||
val value: Any? = null
|
||||
data class Msg<T>(
|
||||
val code: Int = 0, val msg: String? = null, val value: T? = null
|
||||
)
|
@ -0,0 +1,3 @@
|
||||
package dev.surl.surl.common.exception
|
||||
|
||||
class UserRegistException(message: String? = null, cause: Throwable? = null): Throwable(message, cause)
|
@ -10,14 +10,14 @@ 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.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@EnableWebSecurity
|
||||
class SurlAddController {
|
||||
@PostMapping("/surl/add")
|
||||
@RequestMapping("/surl/add")
|
||||
fun addSurl(
|
||||
@Valid @RequestBody body: Surl, @Autowired service: SurlService, @Autowired cfg: BaseConfiguration
|
||||
): Any {
|
||||
|
19
src/main/java/dev/surl/surl/controller/UserController.kt
Normal file
19
src/main/java/dev/surl/surl/controller/UserController.kt
Normal 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!!)
|
||||
}
|
@ -8,5 +8,5 @@ import org.jetbrains.exposed.dao.id.EntityID
|
||||
class User(id: EntityID<Long>): LongEntity(id) {
|
||||
companion object EntityClass: LongEntityClass<User>(Users)
|
||||
var username by Users.username
|
||||
var passwd by Users.passwd
|
||||
var password by Users.password
|
||||
}
|
@ -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 passwd = varchar("passwd", 256)
|
||||
val password = varchar("password", 256)
|
||||
override val primaryKey = PrimaryKey(id, name = "PK_user_id")
|
||||
}
|
17
src/main/java/dev/surl/surl/dto/UserDto.kt
Normal file
17
src/main/java/dev/surl/surl/dto/UserDto.kt
Normal 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?
|
||||
)
|
@ -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)
|
||||
// }
|
||||
//}
|
18
src/main/java/dev/surl/surl/handler/AccessHandler.kt
Normal file
18
src/main/java/dev/surl/surl/handler/AccessHandler.kt
Normal 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")
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
package dev.surl.surl.handler
|
||||
|
||||
import dev.surl.surl.common.Msg
|
||||
import dev.surl.surl.common.exception.UserRegistException
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.HttpStatusCode
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||
import org.springframework.validation.method.MethodValidationException
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
@ -17,23 +19,35 @@ class DefaultExceptionHandler : ResponseEntityExceptionHandler() {
|
||||
override fun handleMethodValidationException(
|
||||
ex: MethodValidationException, headers: HttpHeaders, status: HttpStatus, request: WebRequest
|
||||
): 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(
|
||||
ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest
|
||||
): ResponseEntity<Any> {
|
||||
return ResponseEntity(
|
||||
Msg(code = -1, msg = ex.allErrors.joinToString(";") {
|
||||
Msg<String>(code = -1, msg = ex.allErrors.joinToString(";") {
|
||||
it.defaultMessage?.toString() ?: "unknown invalid method argument"
|
||||
}), 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])
|
||||
fun handleException(
|
||||
ex: Exception, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest
|
||||
): ResponseEntity<Msg> {
|
||||
return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown error"), status)
|
||||
ex: Exception
|
||||
): ResponseEntity<Msg<String>> {
|
||||
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)
|
||||
}
|
||||
}
|
@ -11,9 +11,10 @@ import org.springframework.stereotype.Service
|
||||
class SurlService {
|
||||
fun addSurl(baseurl: String): String = runBlocking {
|
||||
val id = genSnowflakeUID()
|
||||
Surl.new {
|
||||
this.id._value = id
|
||||
url = baseurl
|
||||
transaction {
|
||||
Surl.new(id) {
|
||||
url = baseurl
|
||||
}
|
||||
}
|
||||
return@runBlocking numberToKey(id)
|
||||
}
|
||||
|
@ -1,3 +1,43 @@
|
||||
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()
|
||||
}
|
75
src/main/java/dev/surl/surl/util/JwtTokenUtil.kt
Normal file
75
src/main/java/dev/surl/surl/util/JwtTokenUtil.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
server:
|
||||
port: 18888
|
||||
servlet:
|
||||
context-path: /api/v1
|
||||
spring:
|
||||
profiles:
|
||||
active: default
|
||||
@ -27,10 +29,6 @@ spring:
|
||||
exposed:
|
||||
show-sql: false
|
||||
generate-ddl: true
|
||||
security:
|
||||
user:
|
||||
name: admin
|
||||
password: 123
|
||||
logging:
|
||||
level:
|
||||
root: info
|
||||
|
Loading…
Reference in New Issue
Block a user