feat: Spring Boot JWT应用模板

This commit is contained in:
AiKrai 2025-04-13 20:59:49 +08:00
commit 189e34b7a2
25 changed files with 1117 additions and 0 deletions

43
.gitignore vendored Normal file
View 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
View 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
View File

@ -0,0 +1 @@
rootProject.name = "springboot-template-jwt-api"

View 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)
}

View 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 "退出登录成功"
}
}

View 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!"
}
}

View 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!!)
}
}

View File

@ -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
)
}

View 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)
}
}

View 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
)

View 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?
}

View 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
}
}
}

View 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)
}
}

View 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)
}
}

View 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()
}

View 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>
}

View 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)
}
}

View 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"
}

View 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)
}
}

View 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

View 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

View 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

View 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");

View 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>

View 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() {
}
}