diff --git a/build.gradle b/build.gradle index b9c4c25..728c46c 100644 --- a/build.gradle +++ b/build.gradle @@ -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' } diff --git a/src/main/java/dev/surl/surl/SurlApplication.kt b/src/main/java/dev/surl/surl/SurlApplication.kt index 431d6fb..48414d6 100644 --- a/src/main/java/dev/surl/surl/SurlApplication.kt +++ b/src/main/java/dev/surl/surl/SurlApplication.kt @@ -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) { - runApplication(*args) + SurlApplication.context = runApplication(*args) } diff --git a/src/main/java/dev/surl/surl/cfg/BaseConfiguration.kt b/src/main/java/dev/surl/surl/cfg/BaseConfiguration.kt index ff99b4c..6622b85 100644 --- a/src/main/java/dev/surl/surl/cfg/BaseConfiguration.kt +++ b/src/main/java/dev/surl/surl/cfg/BaseConfiguration.kt @@ -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" +) diff --git a/src/main/java/dev/surl/surl/cfg/Logging.kt b/src/main/java/dev/surl/surl/cfg/Logging.kt new file mode 100644 index 0000000..acc319b --- /dev/null +++ b/src/main/java/dev/surl/surl/cfg/Logging.kt @@ -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 logger(clazz: KClass): Logger = LoggerFactory.getLogger(clazz::class.java) +} \ 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 2d26029..16ada58 100644 --- a/src/main/java/dev/surl/surl/cfg/security/WebSecurityConfig.kt +++ b/src/main/java/dev/surl/surl/cfg/security/WebSecurityConfig.kt @@ -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) +// } +// } +// } } diff --git a/src/main/java/dev/surl/surl/common/Msg.kt b/src/main/java/dev/surl/surl/common/Msg.kt index 753c107..6652f06 100644 --- a/src/main/java/dev/surl/surl/common/Msg.kt +++ b/src/main/java/dev/surl/surl/common/Msg.kt @@ -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( + val code: Int = 0, val msg: String? = null, val value: T? = null ) \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/common/exception/UserRegistException.kt b/src/main/java/dev/surl/surl/common/exception/UserRegistException.kt new file mode 100644 index 0000000..4c0efee --- /dev/null +++ b/src/main/java/dev/surl/surl/common/exception/UserRegistException.kt @@ -0,0 +1,3 @@ +package dev.surl.surl.common.exception + +class UserRegistException(message: String? = null, cause: Throwable? = null): Throwable(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 5c9adb2..77ff86e 100644 --- a/src/main/java/dev/surl/surl/controller/SurlAddController.kt +++ b/src/main/java/dev/surl/surl/controller/SurlAddController.kt @@ -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 { diff --git a/src/main/java/dev/surl/surl/controller/UserController.kt b/src/main/java/dev/surl/surl/controller/UserController.kt new file mode 100644 index 0000000..6c060c9 --- /dev/null +++ b/src/main/java/dev/surl/surl/controller/UserController.kt @@ -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!!) +} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/dao/User.kt b/src/main/java/dev/surl/surl/dao/User.kt index 879b9a1..6b5dd8e 100644 --- a/src/main/java/dev/surl/surl/dao/User.kt +++ b/src/main/java/dev/surl/surl/dao/User.kt @@ -8,5 +8,5 @@ import org.jetbrains.exposed.dao.id.EntityID class User(id: EntityID): LongEntity(id) { companion object EntityClass: LongEntityClass(Users) var username by Users.username - var passwd by Users.passwd + var password by Users.password } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/dsl/Users.kt b/src/main/java/dev/surl/surl/dsl/Users.kt index c0e431a..5dd2cfe 100644 --- a/src/main/java/dev/surl/surl/dsl/Users.kt +++ b/src/main/java/dev/surl/surl/dsl/Users.kt @@ -5,6 +5,6 @@ import org.jetbrains.exposed.dao.id.IdTable object Users: IdTable("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") } \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/dto/UserDto.kt b/src/main/java/dev/surl/surl/dto/UserDto.kt new file mode 100644 index 0000000..aa1c538 --- /dev/null +++ b/src/main/java/dev/surl/surl/dto/UserDto.kt @@ -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? +) \ 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 new file mode 100644 index 0000000..e282608 --- /dev/null +++ b/src/main/java/dev/surl/surl/filter/JwtAuthenticationFilter.kt @@ -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) +// } +//} \ No newline at end of file diff --git a/src/main/java/dev/surl/surl/handler/AccessHandler.kt b/src/main/java/dev/surl/surl/handler/AccessHandler.kt new file mode 100644 index 0000000..f25a843 --- /dev/null +++ b/src/main/java/dev/surl/surl/handler/AccessHandler.kt @@ -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") + } +} \ 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 7c32277..2057108 100644 --- a/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt +++ b/src/main/java/dev/surl/surl/handler/DefaultExceptionHandler.kt @@ -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 { - return ResponseEntity(Msg(code = -1, msg = ex.allValidationResults.joinToString(";")), status) + return ResponseEntity(Msg(code = -1, msg = ex.allValidationResults.joinToString(";")), status) } override fun handleMethodArgumentNotValid( ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest ): ResponseEntity { return ResponseEntity( - Msg(code = -1, msg = ex.allErrors.joinToString(";") { + Msg(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 { + return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown error"), status) + } + @ExceptionHandler(value = [Exception::class]) fun handleException( - ex: Exception, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest - ): ResponseEntity { - return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown error"), status) + ex: Exception + ): ResponseEntity> { + 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>{ + return ResponseEntity(Msg(code = -1, msg = ex.message ?: "unknown regist error"), status) } } \ 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 3809681..370eb1c 100644 --- a/src/main/java/dev/surl/surl/service/SurlService.kt +++ b/src/main/java/dev/surl/surl/service/SurlService.kt @@ -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) } diff --git a/src/main/java/dev/surl/surl/service/UserService.kt b/src/main/java/dev/surl/surl/service/UserService.kt index fc32f37..9e0cc99 100644 --- a/src/main/java/dev/surl/surl/service/UserService.kt +++ b/src/main/java/dev/surl/surl/service/UserService.kt @@ -1,3 +1,43 @@ package dev.surl.surl.service -class UserService \ No newline at end of file +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 { +// 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() +} \ 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 new file mode 100644 index 0000000..36fa18d --- /dev/null +++ b/src/main/java/dev/surl/surl/util/JwtTokenUtil.kt @@ -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): Map { + 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 getClaimFromToken(token: String, resolver: (Claims?) -> T): T { + val claims = getTokenClaim(token) + return resolver(claims) + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 295b28e..a6a3c1d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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