feat: Spring Boot JWT应用模板
This commit is contained in:
commit
189e34b7a2
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
HELP.md
|
||||||
|
.gradle
|
||||||
|
gradle/
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
logs/
|
||||||
|
config/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Kotlin ###
|
||||||
|
.kotlin
|
||||||
78
build.gradle.kts
Normal file
78
build.gradle.kts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "1.9.25"
|
||||||
|
kotlin("plugin.spring") version "1.9.25"
|
||||||
|
id("org.springframework.boot") version "3.4.4"
|
||||||
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
|
id("com.google.devtools.ksp") version "1.9.25-1.0.20"
|
||||||
|
id("tech.argonariod.gradle-plugin-jimmer") version "1.2.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.app"
|
||||||
|
version = "0.0.1-SNAPSHOT"
|
||||||
|
val jimmerVersion = "0.9.73"
|
||||||
|
val knife4jVersion = "4.4.0"
|
||||||
|
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll("-Xjsr305=strict")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test> {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
jimmer {
|
||||||
|
version = jimmerVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("reflect"))
|
||||||
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.1")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
|
|
||||||
|
// redis
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-data-redis")
|
||||||
|
// implementation("com.fasterxml.jackson.core:jackson-databind")
|
||||||
|
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
// springdoc
|
||||||
|
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6")
|
||||||
|
|
||||||
|
implementation("org.babyfish.jimmer:jimmer-spring-boot-starter:${jimmerVersion}")
|
||||||
|
|
||||||
|
// db
|
||||||
|
implementation("org.postgresql:postgresql")
|
||||||
|
// implementation("com.ongres.scram:client:2.1")
|
||||||
|
|
||||||
|
// lombok
|
||||||
|
compileOnly("org.projectlombok:lombok")
|
||||||
|
|
||||||
|
// hutool
|
||||||
|
implementation("cn.hutool:hutool-all:5.8.24")
|
||||||
|
// swagger
|
||||||
|
implementation("com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jVersion}")
|
||||||
|
|
||||||
|
// sa-token
|
||||||
|
implementation("cn.dev33:sa-token-spring-boot3-starter:1.42.0")
|
||||||
|
implementation("cn.dev33:sa-token-jwt:1.42.0")
|
||||||
|
|
||||||
|
|
||||||
|
// implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.3")
|
||||||
|
}
|
||||||
1
settings.gradle.kts
Normal file
1
settings.gradle.kts
Normal file
@ -0,0 +1 @@
|
|||||||
|
rootProject.name = "springboot-template-jwt-api"
|
||||||
11
src/main/kotlin/com/app/Application.kt
Normal file
11
src/main/kotlin/com/app/Application.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package com.app
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.boot.runApplication
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
class SpringbootTemplateJwtApiApplication
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
runApplication<SpringbootTemplateJwtApiApplication>(*args)
|
||||||
|
}
|
||||||
43
src/main/kotlin/com/app/controller/AuthorizeController.kt
Normal file
43
src/main/kotlin/com/app/controller/AuthorizeController.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package com.app.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil
|
||||||
|
import com.app.data.dto.SignInRequest
|
||||||
|
import com.app.service.UserService
|
||||||
|
import io.swagger.v3.oas.annotations.Operation
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag
|
||||||
|
import org.springframework.validation.annotation.Validated
|
||||||
|
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
|
||||||
|
|
||||||
|
@Validated
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
@Tag(name = "登录校验相关", description = "包括用户登录、注册、验证码请求等操作。")
|
||||||
|
class AuthorizeController(
|
||||||
|
private val userService: UserService
|
||||||
|
) {
|
||||||
|
@Operation(summary = "注册")
|
||||||
|
@PostMapping("/register")
|
||||||
|
fun register(
|
||||||
|
@RequestBody signInRequest: SignInRequest
|
||||||
|
): String {
|
||||||
|
return userService.signIn(signInRequest.username, signInRequest.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "登录")
|
||||||
|
@PostMapping("/login")
|
||||||
|
fun login(
|
||||||
|
@RequestBody signInRequest: SignInRequest
|
||||||
|
): String {
|
||||||
|
return userService.signIn(signInRequest.username, signInRequest.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "退出登录")
|
||||||
|
@PostMapping("/logout")
|
||||||
|
fun logout(): String {
|
||||||
|
StpUtil.logout()
|
||||||
|
return "退出登录成功"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/kotlin/com/app/controller/HelloController.kt
Normal file
18
src/main/kotlin/com/app/controller/HelloController.kt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package com.app.controller
|
||||||
|
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaIgnore
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/")
|
||||||
|
class HelloController {
|
||||||
|
|
||||||
|
@SaIgnore
|
||||||
|
@GetMapping("/")
|
||||||
|
fun hello(): String {
|
||||||
|
return "Hello, World!"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/main/kotlin/com/app/controller/UserController.kt
Normal file
29
src/main/kotlin/com/app/controller/UserController.kt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package com.app.controller
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckRole
|
||||||
|
import com.app.data.model.User
|
||||||
|
import com.app.service.UserService
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag
|
||||||
|
import org.babyfish.jimmer.Page
|
||||||
|
import org.springframework.validation.annotation.Validated
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@Validated
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/user")
|
||||||
|
@Tag(name = "用户", description = "")
|
||||||
|
class UserController(
|
||||||
|
private val userService: UserService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@SaCheckRole("admin")
|
||||||
|
@PostMapping("/list")
|
||||||
|
fun list(
|
||||||
|
pageNum: Int? = 1,
|
||||||
|
pageSize: Int? = 10
|
||||||
|
): Page<User> {
|
||||||
|
return userService.list(pageNum!!, pageSize!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.example.controller.exception
|
||||||
|
|
||||||
|
import com.app.data.RespBean
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController
|
||||||
|
import org.springframework.boot.web.error.ErrorAttributeOptions
|
||||||
|
import org.springframework.boot.web.servlet.error.ErrorAttributes
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专用用于处理错误页面的Controller
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("\${server.error.path:\${error.path:/error}}")
|
||||||
|
class ErrorPageController(errorAttributes: ErrorAttributes) : AbstractErrorController(errorAttributes) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有错误在这里统一处理,自动解析状态码和原因
|
||||||
|
* @param request 请求
|
||||||
|
* @return 失败响应
|
||||||
|
*/
|
||||||
|
@RequestMapping
|
||||||
|
fun error(request: HttpServletRequest): RespBean<Void> {
|
||||||
|
val status = getStatus(request)
|
||||||
|
val errorAttributes = getErrorAttributes(request, attributeOptions)
|
||||||
|
val message = convertErrorMessage(status)
|
||||||
|
.orElse(errorAttributes["message"].toString())
|
||||||
|
return RespBean.failure(status.value(), message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对于一些特殊的状态码,错误信息转换
|
||||||
|
* @param status 状态码
|
||||||
|
* @return 错误信息
|
||||||
|
*/
|
||||||
|
private fun convertErrorMessage(status: HttpStatus): Optional<String> {
|
||||||
|
val value = when (status.value()) {
|
||||||
|
400 -> "请求参数有误"
|
||||||
|
404 -> "请求的接口不存在"
|
||||||
|
405 -> "请求方法错误"
|
||||||
|
500 -> "内部错误,请联系管理员"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误属性获取选项,这里额外添加了错误消息和异常类型
|
||||||
|
* @return 选项
|
||||||
|
*/
|
||||||
|
private val attributeOptions: ErrorAttributeOptions
|
||||||
|
get() = ErrorAttributeOptions.defaults()
|
||||||
|
.including(
|
||||||
|
ErrorAttributeOptions.Include.MESSAGE,
|
||||||
|
ErrorAttributeOptions.Include.EXCEPTION
|
||||||
|
)
|
||||||
|
}
|
||||||
60
src/main/kotlin/com/app/data/RespBean.kt
Normal file
60
src/main/kotlin/com/app/data/RespBean.kt
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package com.app.data
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONUtil
|
||||||
|
import org.slf4j.MDC
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应实体类封装,Rest风格
|
||||||
|
* @param id 请求ID
|
||||||
|
* @param code 状态码
|
||||||
|
* @param data 响应数据
|
||||||
|
* @param message 其他消息
|
||||||
|
* @param T 响应数据类型
|
||||||
|
*/
|
||||||
|
data class RespBean<T>(
|
||||||
|
val id: Long,
|
||||||
|
val code: Int,
|
||||||
|
val data: T?,
|
||||||
|
val message: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun <T> success(data: T?): RespBean<T> {
|
||||||
|
return RespBean(requestId(), 200, data, "请求成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> success(): RespBean<T> {
|
||||||
|
return success(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> forbidden(message: String): RespBean<T> {
|
||||||
|
return failure(403, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> unauthorized(message: String): RespBean<T> {
|
||||||
|
return failure(401, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> failure(code: Int, message: String): RespBean<T> {
|
||||||
|
return RespBean(requestId(), code, null, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前请求ID方便快速定位错误
|
||||||
|
* @return ID
|
||||||
|
*/
|
||||||
|
private fun requestId(): Long {
|
||||||
|
val requestId = Optional.ofNullable(MDC.get("reqId")).orElse("0")
|
||||||
|
return requestId.toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速将当前实体转换为JSON字符串格式
|
||||||
|
* @return JSON字符串
|
||||||
|
*/
|
||||||
|
fun asJsonString(): String {
|
||||||
|
return JSONUtil.toJsonStr(this)
|
||||||
|
// return JSONObject.toJSONString(this, JSONWriter.Feature.WriteNulls)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main/kotlin/com/app/data/dto/SignInRequest.kt
Normal file
10
src/main/kotlin/com/app/data/dto/SignInRequest.kt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package com.app.data.dto
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
|
data class SignInRequest (
|
||||||
|
@Schema(title = "username", description = "账号名称", defaultValue = "")
|
||||||
|
var username: String,
|
||||||
|
@Schema(title = "password", description = "账号密码", defaultValue = "")
|
||||||
|
var password: String
|
||||||
|
)
|
||||||
36
src/main/kotlin/com/app/data/model/User.kt
Normal file
36
src/main/kotlin/com/app/data/model/User.kt
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package com.app.data.model
|
||||||
|
|
||||||
|
import org.babyfish.jimmer.sql.*
|
||||||
|
import java.sql.Timestamp
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "sys_user")
|
||||||
|
interface User {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
val userId: Long
|
||||||
|
|
||||||
|
val deptId: Long?
|
||||||
|
|
||||||
|
@Column(name = "user_name")
|
||||||
|
val userName: String
|
||||||
|
|
||||||
|
val nickName: String?
|
||||||
|
|
||||||
|
val userType: String?
|
||||||
|
|
||||||
|
val email: String?
|
||||||
|
val phone: String?
|
||||||
|
val sex: String?
|
||||||
|
val avatar: String?
|
||||||
|
val password: String?
|
||||||
|
val status: String?
|
||||||
|
val delFlag: String?
|
||||||
|
val loginIp: String?
|
||||||
|
val loginDate: Timestamp?
|
||||||
|
val createBy: String?
|
||||||
|
val createTime: Timestamp?
|
||||||
|
val updateBy: String?
|
||||||
|
val updateTime: Timestamp?
|
||||||
|
val remark: String?
|
||||||
|
}
|
||||||
70
src/main/kotlin/com/app/filter/CorsFilter.kt
Normal file
70
src/main/kotlin/com/app/filter/CorsFilter.kt
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package com.app.filter
|
||||||
|
|
||||||
|
import com.app.utlis.Const
|
||||||
|
import jakarta.servlet.FilterChain
|
||||||
|
import jakarta.servlet.ServletException
|
||||||
|
import jakarta.servlet.http.HttpFilter
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.core.annotation.Order
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨域配置过滤器,仅处理跨域,添加跨域响应头
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Order(Const.ORDER_CORS)
|
||||||
|
class CorsFilter : HttpFilter() {
|
||||||
|
|
||||||
|
@Value("\${spring.web.cors.origin}")
|
||||||
|
private lateinit var origin: String
|
||||||
|
|
||||||
|
@Value("\${spring.web.cors.credentials}")
|
||||||
|
private var credentials: Boolean = false
|
||||||
|
|
||||||
|
@Value("\${spring.web.cors.methods}")
|
||||||
|
private lateinit var methods: String
|
||||||
|
|
||||||
|
@Throws(IOException::class, ServletException::class)
|
||||||
|
override fun doFilter(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
||||||
|
addCorsHeader(request, response)
|
||||||
|
chain.doFilter(request, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加所有跨域相关响应头
|
||||||
|
* @param request 请求
|
||||||
|
* @param response 响应
|
||||||
|
*/
|
||||||
|
private fun addCorsHeader(request: HttpServletRequest, response: HttpServletResponse) {
|
||||||
|
response.addHeader("Access-Control-Allow-Origin", resolveOrigin(request))
|
||||||
|
response.addHeader("Access-Control-Allow-Methods", resolveMethod())
|
||||||
|
response.addHeader("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
||||||
|
if (credentials) {
|
||||||
|
response.addHeader("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析配置文件中的请求方法
|
||||||
|
* @return 解析得到的请求头值
|
||||||
|
*/
|
||||||
|
private fun resolveMethod(): String {
|
||||||
|
return if (methods == "*") "GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PATCH" else methods
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析配置文件中的请求原始站点
|
||||||
|
* @param request 请求
|
||||||
|
* @return 解析得到的请求头值
|
||||||
|
*/
|
||||||
|
private fun resolveOrigin(request: HttpServletRequest): String {
|
||||||
|
return if (origin == "*") {
|
||||||
|
request.getHeader("Origin") ?: "*"
|
||||||
|
} else {
|
||||||
|
origin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/main/kotlin/com/app/filter/FlowLimitingFilter.kt
Normal file
89
src/main/kotlin/com/app/filter/FlowLimitingFilter.kt
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package com.app.filter
|
||||||
|
|
||||||
|
import com.app.data.RespBean
|
||||||
|
import com.app.utlis.Const
|
||||||
|
import com.app.utlis.FlowUtils
|
||||||
|
import jakarta.annotation.Resource
|
||||||
|
import jakarta.servlet.FilterChain
|
||||||
|
import jakarta.servlet.ServletException
|
||||||
|
import jakarta.servlet.http.HttpFilter
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import lombok.extern.slf4j.Slf4j
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.core.annotation.Order
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.PrintWriter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流控制过滤器
|
||||||
|
* 防止用户高频请求接口,借助Redis进行限流
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@Order(Const.ORDER_FLOW_LIMIT)
|
||||||
|
class FlowLimitingFilter : HttpFilter() {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private lateinit var template: StringRedisTemplate
|
||||||
|
|
||||||
|
// 指定时间内最大请求次数限制
|
||||||
|
@Value("\${spring.web.flow.limit}")
|
||||||
|
private var limit: Int = 0
|
||||||
|
|
||||||
|
// 计数时间周期
|
||||||
|
@Value("\${spring.web.flow.period}")
|
||||||
|
private var period: Int = 0
|
||||||
|
|
||||||
|
// 超出请求限制封禁时间
|
||||||
|
@Value("\${spring.web.flow.block}")
|
||||||
|
private var block: Int = 0
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private lateinit var utils: FlowUtils
|
||||||
|
|
||||||
|
@Throws(IOException::class, ServletException::class)
|
||||||
|
override fun doFilter(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
||||||
|
val address = request.remoteAddr
|
||||||
|
if ("OPTIONS" != request.method && !tryCount(address)) {
|
||||||
|
writeBlockMessage(response)
|
||||||
|
} else {
|
||||||
|
chain.doFilter(request, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试对指定IP地址请求计数,如果被限制则无法继续访问
|
||||||
|
* @param address 请求IP地址
|
||||||
|
* @return 是否操作成功
|
||||||
|
*/
|
||||||
|
private fun tryCount(address: String): Boolean {
|
||||||
|
synchronized(address.intern()) {
|
||||||
|
if (template.hasKey(Const.FLOW_LIMIT_BLOCK + address) == true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val counterKey = Const.FLOW_LIMIT_COUNTER + address
|
||||||
|
val blockKey = Const.FLOW_LIMIT_BLOCK + address
|
||||||
|
return utils.limitPeriodCheck(counterKey, blockKey, block, limit, period)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为响应编写拦截内容,提示用户操作频繁
|
||||||
|
* @param response 响应
|
||||||
|
* @throws IOException 可能的异常
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun writeBlockMessage(response: HttpServletResponse) {
|
||||||
|
response.status = 429
|
||||||
|
response.contentType = "application/json;charset=utf-8"
|
||||||
|
val writer: PrintWriter = response.writer
|
||||||
|
writer.write(RespBean.failure<Any>(429, "请求频率过快,请稍后再试").asJsonString())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = org.slf4j.LoggerFactory.getLogger(FlowLimitingFilter::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/main/kotlin/com/app/filter/RequestLogFilter.kt
Normal file
107
src/main/kotlin/com/app/filter/RequestLogFilter.kt
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package com.app.filter
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil
|
||||||
|
import cn.hutool.core.lang.Snowflake
|
||||||
|
import cn.hutool.json.JSONObject
|
||||||
|
import jakarta.servlet.FilterChain
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import lombok.extern.slf4j.Slf4j
|
||||||
|
import org.slf4j.MDC
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter
|
||||||
|
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
class RequestLogFilter(
|
||||||
|
private val snowflake: Snowflake
|
||||||
|
) : OncePerRequestFilter() {
|
||||||
|
|
||||||
|
private val ignores = setOf("/chatjava", "/ai", "/doc.html", "/swagger-ui", "/v3/api-docs")
|
||||||
|
|
||||||
|
override fun doFilterInternal(
|
||||||
|
request: HttpServletRequest,
|
||||||
|
response: HttpServletResponse,
|
||||||
|
filterChain: FilterChain
|
||||||
|
) {
|
||||||
|
if (isIgnoreUrl(request.servletPath)) {
|
||||||
|
filterChain.doFilter(request, response)
|
||||||
|
} else {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
logRequestStart(request)
|
||||||
|
val wrapper = ContentCachingResponseWrapper(response)
|
||||||
|
filterChain.doFilter(request, wrapper)
|
||||||
|
logRequestEnd(wrapper, startTime)
|
||||||
|
wrapper.copyBodyToResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判定当前请求url是否不需要日志打印
|
||||||
|
*
|
||||||
|
* @param url 路径
|
||||||
|
* @return 是否忽略
|
||||||
|
*/
|
||||||
|
private fun isIgnoreUrl(url: String): Boolean {
|
||||||
|
return ignores.any { url.startsWith(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求结束时的日志打印,包含处理耗时以及响应结果
|
||||||
|
*
|
||||||
|
* @param wrapper 用于读取响应结果的包装类
|
||||||
|
* @param startTime 起始时间
|
||||||
|
*/
|
||||||
|
fun logRequestEnd(wrapper: ContentCachingResponseWrapper, startTime: Long) {
|
||||||
|
val time = System.currentTimeMillis() - startTime
|
||||||
|
val status = wrapper.status
|
||||||
|
val content = if (status != 200) "$status 错误"
|
||||||
|
else String(wrapper.contentAsByteArray)
|
||||||
|
|
||||||
|
log.info("\n>>>>>请求处理耗时:[{}ms] 响应结果:{}", time, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求开始时的日志打印,包含请求全部信息,以及对应用户角色
|
||||||
|
*
|
||||||
|
* @param request 请求
|
||||||
|
*/
|
||||||
|
fun logRequestStart(request: HttpServletRequest) {
|
||||||
|
val reqId = snowflake.nextId()
|
||||||
|
MDC.put("reqId", reqId.toString())
|
||||||
|
|
||||||
|
val params = JSONObject().apply {
|
||||||
|
request.parameterMap.forEach { (k, v) ->
|
||||||
|
put(k, if (v.isNotEmpty()) v[0] else null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
val id = StpUtil.getLoginId()
|
||||||
|
log.info("""
|
||||||
|
|
||||||
|
>>>>>请求ID:[${reqId}]
|
||||||
|
>>>>>请求URL:["${request.servletPath}"](${request.method})
|
||||||
|
>>>>>远程IP:[${request.remoteAddr}]
|
||||||
|
>>>>>用户名:[username]
|
||||||
|
>>>>>用户ID:$id
|
||||||
|
>>>>>角色:${StpUtil.getRoleList()}
|
||||||
|
>>>>>请求参数列表: [$params]
|
||||||
|
""".trimIndent())
|
||||||
|
} else {
|
||||||
|
log.info("""
|
||||||
|
|
||||||
|
>>>>>请求ID:[${reqId}]
|
||||||
|
>>>>>请求URL:["${request.servletPath}"](${request.method})
|
||||||
|
>>>>>远程IP地址:[${request.remoteAddr}]
|
||||||
|
>>>>>身份:未验证
|
||||||
|
>>>>>请求参数列表: [$params]
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = org.slf4j.LoggerFactory.getLogger(RequestLogFilter::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main/kotlin/com/app/repository/UserRepository.kt
Normal file
30
src/main/kotlin/com/app/repository/UserRepository.kt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package com.app.repository
|
||||||
|
|
||||||
|
import com.app.data.model.User
|
||||||
|
import com.app.data.model.userId
|
||||||
|
import com.app.data.model.userName
|
||||||
|
import org.babyfish.jimmer.Page
|
||||||
|
import org.babyfish.jimmer.spring.repository.KRepository
|
||||||
|
import org.babyfish.jimmer.sql.kt.ast.expression.eq
|
||||||
|
import org.babyfish.jimmer.sql.kt.ast.expression.`eq?`
|
||||||
|
|
||||||
|
|
||||||
|
interface UserRepository : KRepository<User, Long> {
|
||||||
|
fun findUser(
|
||||||
|
pageIndex: Int = 0,
|
||||||
|
pageSize: Int = 10,
|
||||||
|
id: Long? = null,
|
||||||
|
name: String? = null,
|
||||||
|
email: String? = null,
|
||||||
|
phone: String? = null,
|
||||||
|
): Page<User> =
|
||||||
|
sql.createQuery(User::class) {
|
||||||
|
where(table.userId `eq?` id)
|
||||||
|
select(table)
|
||||||
|
}.fetchPage(pageIndex, pageSize)
|
||||||
|
|
||||||
|
fun findByUserName(username: String): User? = sql.createQuery(User::class) {
|
||||||
|
where(table.userName eq username)
|
||||||
|
select(table)
|
||||||
|
}.fetchOne()
|
||||||
|
}
|
||||||
13
src/main/kotlin/com/app/service/UserService.kt
Normal file
13
src/main/kotlin/com/app/service/UserService.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package com.app.service
|
||||||
|
|
||||||
|
import com.app.data.model.User
|
||||||
|
import org.babyfish.jimmer.Page
|
||||||
|
|
||||||
|
interface UserService {
|
||||||
|
fun signIn(username: String, password: String): String
|
||||||
|
|
||||||
|
fun list(
|
||||||
|
pageNum: Int = 0,
|
||||||
|
pageSize: Int = 10
|
||||||
|
): Page<User>
|
||||||
|
}
|
||||||
37
src/main/kotlin/com/app/service/impl/UserServiceImpl.kt
Normal file
37
src/main/kotlin/com/app/service/impl/UserServiceImpl.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package com.app.service.impl
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil
|
||||||
|
import cn.dev33.satoken.stp.parameter.SaLoginParameter
|
||||||
|
import cn.hutool.crypto.SecureUtil
|
||||||
|
import com.app.data.model.User
|
||||||
|
import com.app.repository.UserRepository
|
||||||
|
import com.app.service.UserService
|
||||||
|
import org.babyfish.jimmer.Page
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class UserServiceImpl(
|
||||||
|
private val userRepository: UserRepository
|
||||||
|
): UserService {
|
||||||
|
override fun signIn(username: String, password: String): String {
|
||||||
|
// withContext(Dispatchers.IO) {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
val user = userRepository.findByUserName(username) ?: throw Exception("User not found")
|
||||||
|
if (user.password != SecureUtil.sha1(password)) {
|
||||||
|
throw Exception("Invalid password")
|
||||||
|
}
|
||||||
|
val parameter = SaLoginParameter.create()
|
||||||
|
.setExtraData(mapOf("deptId" to user.deptId))
|
||||||
|
StpUtil.login(user.userId.toString(), parameter)
|
||||||
|
val token = StpUtil.getTokenInfo()
|
||||||
|
return token.tokenValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun list(
|
||||||
|
pageNum: Int,
|
||||||
|
pageSize: Int
|
||||||
|
): Page<User> {
|
||||||
|
return userRepository.findUser(pageNum, pageSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/main/kotlin/com/app/utlis/Const.kt
Normal file
31
src/main/kotlin/com/app/utlis/Const.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package com.app.utlis
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一些常量字符串整合
|
||||||
|
*/
|
||||||
|
object Const {
|
||||||
|
// JWT令牌
|
||||||
|
const val JWT_BLACK_LIST = "jwt:blacklist:"
|
||||||
|
const val JWT_FREQUENCY = "jwt:frequency:"
|
||||||
|
|
||||||
|
// 请求频率限制
|
||||||
|
const val FLOW_LIMIT_COUNTER = "flow:counter:"
|
||||||
|
const val FLOW_LIMIT_BLOCK = "flow:block:"
|
||||||
|
|
||||||
|
// 邮件验证码
|
||||||
|
const val VERIFY_EMAIL_LIMIT = "verify:email:limit:"
|
||||||
|
const val VERIFY_EMAIL_DATA = "verify:email:data:"
|
||||||
|
|
||||||
|
// 过滤器优先级
|
||||||
|
const val ORDER_FLOW_LIMIT = -101
|
||||||
|
const val ORDER_CORS = -102
|
||||||
|
|
||||||
|
// 请求自定义属性
|
||||||
|
const val ATTR_USER_ID = "userId"
|
||||||
|
|
||||||
|
// 消息队列
|
||||||
|
const val MQ_MAIL = "mail"
|
||||||
|
|
||||||
|
// 用户角色
|
||||||
|
const val ROLE_DEFAULT = "user"
|
||||||
|
}
|
||||||
92
src/main/kotlin/com/app/utlis/FlowUtils.kt
Normal file
92
src/main/kotlin/com/app/utlis/FlowUtils.kt
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package com.app.utlis
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource
|
||||||
|
import lombok.extern.slf4j.Slf4j
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流通用工具
|
||||||
|
* 针对于不同的情况进行限流操作,支持限流升级
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
class FlowUtils {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private lateinit var template: StringRedisTemplate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对于单次频率限制,请求成功后,在冷却时间内不得再次进行请求,如3秒内不能再次发起请求
|
||||||
|
* @param key 键
|
||||||
|
* @param blockTime 限制时间
|
||||||
|
* @return 是否通过限流检查
|
||||||
|
*/
|
||||||
|
fun limitOnceCheck(key: String, blockTime: Int): Boolean {
|
||||||
|
return internalCheck(key, 1, blockTime) { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对于单次频率限制,请求成功后,在冷却时间内不得再次进行请求
|
||||||
|
* 如3秒内不能再次发起请求,如果不听劝阻继续发起请求,将限制更长时间
|
||||||
|
* @param key 键
|
||||||
|
* @param frequency 请求频率
|
||||||
|
* @param baseTime 基础限制时间
|
||||||
|
* @param upgradeTime 升级限制时间
|
||||||
|
* @return 是否通过限流检查
|
||||||
|
*/
|
||||||
|
fun limitOnceUpgradeCheck(key: String, frequency: Int, baseTime: Int, upgradeTime: Int): Boolean {
|
||||||
|
return internalCheck(key, frequency, baseTime) { overclock ->
|
||||||
|
if (overclock) {
|
||||||
|
template.opsForValue().set(key, "1", upgradeTime.toLong(), TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对于在时间段内多次请求限制,如3秒内限制请求20次,超出频率则封禁一段时间
|
||||||
|
* @param counterKey 计数键
|
||||||
|
* @param blockKey 封禁键
|
||||||
|
* @param blockTime 封禁时间
|
||||||
|
* @param frequency 请求频率
|
||||||
|
* @param period 计数周期
|
||||||
|
* @return 是否通过限流检查
|
||||||
|
*/
|
||||||
|
fun limitPeriodCheck(counterKey: String, blockKey: String, blockTime: Int, frequency: Int, period: Int): Boolean {
|
||||||
|
return internalCheck(counterKey, frequency, period) { overclock ->
|
||||||
|
if (overclock) {
|
||||||
|
template.opsForValue().set(blockKey, "", blockTime.toLong(), TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
!overclock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部使用请求限制主要逻辑
|
||||||
|
* @param key 计数键
|
||||||
|
* @param frequency 请求频率
|
||||||
|
* @param period 计数周期
|
||||||
|
* @param action 限制行为与策略
|
||||||
|
* @return 是否通过限流检查
|
||||||
|
*/
|
||||||
|
private fun internalCheck(key: String, frequency: Int, period: Int, action: (Boolean) -> Boolean): Boolean {
|
||||||
|
val count = template.opsForValue().get(key)
|
||||||
|
return if (count != null) {
|
||||||
|
val value = template.opsForValue().increment(key) ?: 0L
|
||||||
|
val c = count.toInt()
|
||||||
|
if (value != c + 1L) {
|
||||||
|
template.expire(key, period.toLong(), TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
action(value > frequency)
|
||||||
|
} else {
|
||||||
|
template.opsForValue().set(key, "1", period.toLong(), TimeUnit.SECONDS)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = org.slf4j.LoggerFactory.getLogger(FlowUtils::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/resources/application-knife4j.yml
Normal file
17
src/main/resources/application-knife4j.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# springdoc-openapi项目配置
|
||||||
|
springdoc:
|
||||||
|
swagger-ui:
|
||||||
|
path: /swagger-ui.html
|
||||||
|
tags-sorter: alpha
|
||||||
|
operations-sorter: alpha
|
||||||
|
api-docs:
|
||||||
|
path: /v3/api-docs
|
||||||
|
group-configs:
|
||||||
|
- group: 'default'
|
||||||
|
paths-to-match: '/**'
|
||||||
|
packages-to-scan: com.app
|
||||||
|
# knife4j的增强配置,不需要增强可以不配
|
||||||
|
knife4j:
|
||||||
|
enable: true
|
||||||
|
setting:
|
||||||
|
language: zh_cn
|
||||||
26
src/main/resources/application-satoken.yml
Normal file
26
src/main/resources/application-satoken.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# sa-token 配置
|
||||||
|
sa-token:
|
||||||
|
# 是否在初始化配置时打印版本字符画
|
||||||
|
is-print: false
|
||||||
|
# token 名称 (同时也是 cookie 名称)
|
||||||
|
token-name: Authorization
|
||||||
|
# token前缀,例如填写 Bearer 实际传参 satoken: Bearer xxxx-xxxx-xxxx-xxxx
|
||||||
|
token-prefix: Bearer
|
||||||
|
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
|
||||||
|
timeout: 2592000000000
|
||||||
|
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
|
||||||
|
active-timeout: -1
|
||||||
|
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||||
|
is-concurrent: true
|
||||||
|
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
|
||||||
|
is-share: true
|
||||||
|
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||||
|
token-style: random-128
|
||||||
|
# 是否输出操作日志
|
||||||
|
is-log: false
|
||||||
|
# jwt秘钥
|
||||||
|
jwt-secret-key: sadfjldjfaklfjkekslkrjke
|
||||||
|
# 是否尝试从 header 里读取 Token
|
||||||
|
is-read-header: true
|
||||||
|
# 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
|
||||||
|
auto-renew: true
|
||||||
42
src/main/resources/application.yml
Normal file
42
src/main/resources/application.yml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
server:
|
||||||
|
port: 18080
|
||||||
|
servlet:
|
||||||
|
context-path: /
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: springboot-template-jwt-api
|
||||||
|
datasource:
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
url: jdbc:postgresql://xxx:5432/template
|
||||||
|
username: xxx
|
||||||
|
password: xxx
|
||||||
|
mvc:
|
||||||
|
pathmatch:
|
||||||
|
matching-strategy: ANT_PATH_MATCHER
|
||||||
|
profiles:
|
||||||
|
active: knife4j,satoken
|
||||||
|
web:
|
||||||
|
verify:
|
||||||
|
mail-limit: 60
|
||||||
|
flow:
|
||||||
|
limit: 50
|
||||||
|
period: 3
|
||||||
|
block: 30
|
||||||
|
cors:
|
||||||
|
origin: '*'
|
||||||
|
credentials: false
|
||||||
|
methods: '*'
|
||||||
|
|
||||||
|
|
||||||
|
# jimmer配置参考https://babyfish-ct.github.io/jimmer-doc/zh/docs/spring/appendix
|
||||||
|
jimmer:
|
||||||
|
# Kotlin项目必须配置 默认java
|
||||||
|
language: kotlin
|
||||||
|
# Jimmer方言类名
|
||||||
|
dialect: org.babyfish.jimmer.sql.dialect.PostgresDialect
|
||||||
|
# 是否打印SQL日志 默认false
|
||||||
|
show-sql: true
|
||||||
|
# 是否格式化SQL日志 默认false
|
||||||
|
pretty-sql: true
|
||||||
|
# 如果非NONE,验证数据库结构和代码实体类型结构的一致性,如果不一致,WARNING导致日志告警,ERROR导致报错
|
||||||
|
database-validation-mode: ERROR
|
||||||
68
src/main/resources/db/sys_user.sql
Normal file
68
src/main/resources/db/sys_user.sql
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Navicat Premium Dump SQL
|
||||||
|
|
||||||
|
Source Server : aliyuncs
|
||||||
|
Source Server Type : PostgreSQL
|
||||||
|
Source Server Version : 170004 (170004)
|
||||||
|
Source Catalog : template
|
||||||
|
Source Schema : public
|
||||||
|
|
||||||
|
Target Server Type : PostgreSQL
|
||||||
|
Target Server Version : 170004 (170004)
|
||||||
|
File Encoding : 65001
|
||||||
|
|
||||||
|
Date: 13/04/2025 20:57:33
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for sys_user
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS "public"."sys_user";
|
||||||
|
CREATE TABLE "public"."sys_user" (
|
||||||
|
"user_id" int8 NOT NULL,
|
||||||
|
"dept_id" int8,
|
||||||
|
"user_name" varchar(30) COLLATE "pg_catalog"."default" NOT NULL,
|
||||||
|
"nick_name" varchar(30) COLLATE "pg_catalog"."default",
|
||||||
|
"user_type" varchar(2) COLLATE "pg_catalog"."default",
|
||||||
|
"email" varchar(50) COLLATE "pg_catalog"."default",
|
||||||
|
"phone" varchar(11) COLLATE "pg_catalog"."default",
|
||||||
|
"sex" char(1) COLLATE "pg_catalog"."default",
|
||||||
|
"avatar" varchar(100) COLLATE "pg_catalog"."default",
|
||||||
|
"password" varchar(100) COLLATE "pg_catalog"."default",
|
||||||
|
"status" char(1) COLLATE "pg_catalog"."default",
|
||||||
|
"del_flag" char(1) COLLATE "pg_catalog"."default",
|
||||||
|
"login_ip" varchar(128) COLLATE "pg_catalog"."default",
|
||||||
|
"login_date" timestamp(6),
|
||||||
|
"create_by" varchar(64) COLLATE "pg_catalog"."default",
|
||||||
|
"create_time" timestamp(6),
|
||||||
|
"update_by" varchar(64) COLLATE "pg_catalog"."default",
|
||||||
|
"update_time" timestamp(6),
|
||||||
|
"remark" varchar(500) COLLATE "pg_catalog"."default"
|
||||||
|
)
|
||||||
|
;
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."user_id" IS '用户ID';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."dept_id" IS '部门ID';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."user_name" IS '用户账号';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."nick_name" IS '用户昵称';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."user_type" IS '用户类型(00系统用户)';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."email" IS '用户邮箱';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."phone" IS '手机号码';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."sex" IS '用户性别(0男 1女 2未知)';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."avatar" IS '头像地址';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."password" IS '密码';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."status" IS '帐号状态(0正常 1停用)';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."del_flag" IS '删除标志(0代表存在 2代表删除)';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."login_ip" IS '最后登录IP';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."login_date" IS '最后登录时间';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."create_by" IS '创建者';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."create_time" IS '创建时间';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."update_by" IS '更新者';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."update_time" IS '更新时间';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."remark" IS '备注';
|
||||||
|
COMMENT ON TABLE "public"."sys_user" IS '用户信息表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Primary Key structure for table sys_user
|
||||||
|
-- ----------------------------
|
||||||
|
ALTER TABLE "public"."sys_user" ADD CONSTRAINT "sys_user_pkey" PRIMARY KEY ("user_id");
|
||||||
93
src/main/resources/logback.xml
Normal file
93
src/main/resources/logback.xml
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<!-- 日志存放路径 -->
|
||||||
|
<property name="log.path" value="./logs" />
|
||||||
|
<!-- 日志输出格式 -->
|
||||||
|
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
|
||||||
|
|
||||||
|
<!-- 控制台输出 -->
|
||||||
|
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 系统日志输出 -->
|
||||||
|
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${log.path}/sys-info.log</file>
|
||||||
|
<!-- 循环政策:基于时间创建日志文件 -->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<!-- 日志文件名格式 -->
|
||||||
|
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<!-- 日志最大的历史 60天 -->
|
||||||
|
<maxHistory>60</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<!-- 过滤的级别 -->
|
||||||
|
<level>INFO</level>
|
||||||
|
<!-- 匹配时的操作:接收(记录) -->
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${log.path}/sys-error.log</file>
|
||||||
|
<!-- 循环政策:基于时间创建日志文件 -->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<!-- 日志文件名格式 -->
|
||||||
|
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<!-- 日志最大的历史 60天 -->
|
||||||
|
<maxHistory>60</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<!-- 过滤的级别 -->
|
||||||
|
<level>ERROR</level>
|
||||||
|
<!-- 匹配时的操作:接收(记录) -->
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 用户访问日志输出 -->
|
||||||
|
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${log.path}/sys-user.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<!-- 按天回滚 daily -->
|
||||||
|
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<!-- 日志最大的历史 60天 -->
|
||||||
|
<maxHistory>60</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 系统模块日志级别控制 -->
|
||||||
|
<logger name="com.app" level="info" />
|
||||||
|
<!-- Spring日志级别控制 -->
|
||||||
|
<logger name="org.springframework" level="warn" />
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="console" />
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!--系统操作日志-->
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="file_info" />
|
||||||
|
<appender-ref ref="file_error" />
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!--系统用户操作日志-->
|
||||||
|
<logger name="sys-user" level="info">
|
||||||
|
<appender-ref ref="sys-user"/>
|
||||||
|
</logger>
|
||||||
|
</configuration>
|
||||||
13
src/test/kotlin/com/app/ApplicationTests.kt
Normal file
13
src/test/kotlin/com/app/ApplicationTests.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package com.app
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class ApplicationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun contextLoads() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user