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