feat: Spring Boot JWT应用模板
This commit is contained in:
parent
189e34b7a2
commit
29f5e1b75f
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,8 +5,8 @@ build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
logs/
|
||||
config/
|
||||
/logs
|
||||
/config
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
|
||||
15
src/main/kotlin/com/app/config/HuToolConfig.kt
Normal file
15
src/main/kotlin/com/app/config/HuToolConfig.kt
Normal file
@ -0,0 +1,15 @@
|
||||
package com.app.config
|
||||
|
||||
import cn.hutool.core.lang.Snowflake
|
||||
import cn.hutool.core.util.IdUtil
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
class HuToolConfig {
|
||||
|
||||
@Bean
|
||||
fun snowflake(): Snowflake {
|
||||
return IdUtil.getSnowflake()
|
||||
}
|
||||
}
|
||||
54
src/main/kotlin/com/app/config/ResponseAdvisor.kt
Normal file
54
src/main/kotlin/com/app/config/ResponseAdvisor.kt
Normal file
@ -0,0 +1,54 @@
|
||||
package com.app.config
|
||||
|
||||
import com.app.data.RespBean
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.core.MethodParameter
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.converter.HttpMessageConverter
|
||||
import org.springframework.http.server.ServerHttpRequest
|
||||
import org.springframework.http.server.ServerHttpResponse
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
|
||||
|
||||
@ControllerAdvice
|
||||
class ResponseAdvisor(
|
||||
private val objectMapper: ObjectMapper
|
||||
) : ResponseBodyAdvice<Any> {
|
||||
|
||||
@Value("#{'\${ignore.response.ignoreUris:/swagger,/actuator,/api-docs,/v3/api-docs,/doc.html}'.split(',')}")
|
||||
private lateinit var ignoreUris: Array<String>
|
||||
|
||||
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun beforeBodyWrite(
|
||||
body: Any?,
|
||||
returnType: MethodParameter,
|
||||
selectedContentType: MediaType,
|
||||
selectedConverterType: Class<out HttpMessageConverter<*>>,
|
||||
request: ServerHttpRequest,
|
||||
response: ServerHttpResponse
|
||||
): Any? {
|
||||
if (body is RespBean<*>) {
|
||||
return body
|
||||
}
|
||||
|
||||
val requestUri = request.uri.toString()
|
||||
if (ignoreUris.any { requestUri.contains(it) }) {
|
||||
return body
|
||||
}
|
||||
|
||||
if (body is String) {
|
||||
response.headers.contentType = MediaType.APPLICATION_JSON
|
||||
return objectMapper.writeValueAsString(RespBean.success(body))
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
return RespBean.success<Any>(null)
|
||||
}
|
||||
|
||||
return RespBean.success(body)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package com.app.config.exception
|
||||
|
||||
|
||||
import com.app.data.RespBean
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.validation.BindException
|
||||
import org.springframework.validation.FieldError
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.PrintStream
|
||||
import java.security.SignatureException
|
||||
|
||||
@Component
|
||||
class ExceptionAdviceHandel {
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(ExceptionAdviceHandel::class.java)
|
||||
}
|
||||
|
||||
fun getDefaultExceptionResponse(req: HttpServletRequest, error: Throwable): RespBean<*> {
|
||||
return when (error) {
|
||||
is SignatureException -> signatureException(error)
|
||||
is MethodArgumentNotValidException -> methodArgumentExceptionAdvice(error)
|
||||
is BindException -> bindException(error)
|
||||
is HttpMessageNotReadableException -> httpMessageNotReadableExceptionAdvice(error)
|
||||
else -> throwableAdvice(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理通用异常
|
||||
*/
|
||||
private fun throwableAdvice(error: Throwable): RespBean<Void> {
|
||||
logger.error("系统发生未处理异常", error)
|
||||
|
||||
// 获取详细错误信息
|
||||
val printStackTrace = ByteArrayOutputStream()
|
||||
error.printStackTrace(PrintStream(printStackTrace))
|
||||
|
||||
// 查找第一个应用包下的堆栈信息,用于定位错误
|
||||
val stackElement = error.stackTrace.firstOrNull {
|
||||
it.className.startsWith("com.app")
|
||||
}
|
||||
|
||||
// 格式化错误信息
|
||||
val errorMsg = if (stackElement != null) {
|
||||
"系统异常: ${error.message ?: "Unknown error"} (位置: ${stackElement.className}:${stackElement.lineNumber})"
|
||||
} else {
|
||||
"系统异常: ${error.message ?: "Unknown error"}"
|
||||
}
|
||||
|
||||
return RespBean.failure(HttpStatus.INTERNAL_SERVER_ERROR.value(), errorMsg)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理方法参数验证异常
|
||||
*/
|
||||
private fun methodArgumentExceptionAdvice(error: MethodArgumentNotValidException): RespBean<Void> {
|
||||
logger.error("数据格式不正确:{}", error.message)
|
||||
val sb = StringBuilder()
|
||||
error.bindingResult.fieldErrors.forEach { errorInfo ->
|
||||
appendExceptionInfo(sb, errorInfo)
|
||||
}
|
||||
return RespBean.failure(HttpStatus.BAD_REQUEST.value(), sb.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理绑定异常
|
||||
*/
|
||||
private fun bindException(error: BindException): RespBean<Void> {
|
||||
logger.error("数据格式不正确:{}", error.message)
|
||||
val sb = StringBuilder()
|
||||
error.bindingResult.fieldErrors.forEach { fieldError ->
|
||||
appendExceptionInfo(sb, fieldError)
|
||||
}
|
||||
return RespBean.failure(HttpStatus.BAD_REQUEST.value(), sb.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加异常信息到StringBuilder
|
||||
*/
|
||||
private fun appendExceptionInfo(sb: StringBuilder, errorInfo: FieldError) {
|
||||
if (sb.isNotEmpty()) {
|
||||
sb.append("; ")
|
||||
}
|
||||
sb.append(errorInfo.field).append(": ").append(errorInfo.defaultMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理HTTP消息不可读异常
|
||||
*/
|
||||
private fun httpMessageNotReadableExceptionAdvice(error: HttpMessageNotReadableException): RespBean<Void> {
|
||||
logger.error("参数数据类型不匹配:{}", error.message)
|
||||
return RespBean.failure(HttpStatus.BAD_REQUEST.value(), "参数数据类型不匹配: ${error.message}")
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理签名异常
|
||||
*/
|
||||
private fun signatureException(error: SignatureException): RespBean<Void> {
|
||||
logger.info("签名异常", error)
|
||||
return RespBean.failure(420, error.message ?: "签名异常")
|
||||
}
|
||||
}
|
||||
55
src/main/kotlin/com/app/config/exception/GlobalException.kt
Normal file
55
src/main/kotlin/com/app/config/exception/GlobalException.kt
Normal file
@ -0,0 +1,55 @@
|
||||
package com.app.config.exception
|
||||
|
||||
import com.app.data.RespBean
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.validation.ValidationException
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||
import org.springframework.validation.BindException
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.ResponseBody
|
||||
|
||||
/**
|
||||
* 全局异常统一处理
|
||||
*/
|
||||
@ControllerAdvice
|
||||
@ResponseBody
|
||||
class GlobalException(
|
||||
private val exceptionAdviceHandel: ExceptionAdviceHandel
|
||||
) {
|
||||
|
||||
// 所有未明确处理的异常将通过exceptionAdviceHandel统一处理
|
||||
@ExceptionHandler(Throwable::class)
|
||||
fun handleAllExceptions(req: HttpServletRequest, error: Throwable): RespBean<*> {
|
||||
log.error("全局异常处理", error)
|
||||
return exceptionAdviceHandel.getDefaultExceptionResponse(req, error)
|
||||
}
|
||||
|
||||
// 验证相关异常处理
|
||||
@ExceptionHandler(ValidationException::class)
|
||||
fun validateError(exception: ValidationException): RespBean<Void> {
|
||||
log.warn("验证不通过: [{}: {}]", exception.javaClass.name, exception.message)
|
||||
return RespBean.failure(HttpStatus.BAD_REQUEST.value(), "请求参数有误: ${exception.message}")
|
||||
}
|
||||
|
||||
// 处理参数校验异常
|
||||
@ExceptionHandler(MethodArgumentNotValidException::class, BindException::class)
|
||||
fun handleBindingErrors(req: HttpServletRequest, error: Exception): RespBean<*> {
|
||||
log.warn("参数校验异常", error)
|
||||
return exceptionAdviceHandel.getDefaultExceptionResponse(req, error)
|
||||
}
|
||||
|
||||
// 处理HTTP消息转换异常
|
||||
@ExceptionHandler(HttpMessageNotReadableException::class)
|
||||
fun handleHttpMessageNotReadableException(req: HttpServletRequest, error: HttpMessageNotReadableException): RespBean<*> {
|
||||
log.warn("HTTP消息转换异常", error)
|
||||
return exceptionAdviceHandel.getDefaultExceptionResponse(req, error)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(GlobalException::class.java)
|
||||
}
|
||||
}
|
||||
88
src/main/kotlin/com/app/config/satoken/SaTokenConfigure.kt
Normal file
88
src/main/kotlin/com/app/config/satoken/SaTokenConfigure.kt
Normal file
@ -0,0 +1,88 @@
|
||||
package com.app.config.satoken
|
||||
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder
|
||||
import cn.dev33.satoken.filter.SaServletFilter
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor
|
||||
import cn.dev33.satoken.jwt.StpLogicJwtForSimple
|
||||
import cn.dev33.satoken.stp.StpLogic
|
||||
import cn.dev33.satoken.util.SaResult
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
|
||||
/**
|
||||
* [Sa-Token 权限认证] 配置类
|
||||
* @author click33
|
||||
*/
|
||||
@Configuration
|
||||
class SaTokenConfigure : WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 注册 Sa-Token 拦截器打开注解鉴权功能
|
||||
*/
|
||||
override fun addInterceptors(registry: InterceptorRegistry) {
|
||||
// 注册 Sa-Token 拦截器打开注解鉴权功能
|
||||
registry.addInterceptor(SaInterceptor())
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/api/auth/login",
|
||||
"/api/auth/register",
|
||||
"/api/sms",
|
||||
"/api/mobLogin",
|
||||
"/swagger/**",
|
||||
"/swagger-ui.html",
|
||||
"/swagger-resources/**",
|
||||
"/doc.html",
|
||||
"/v2/**",
|
||||
"/v3/**",
|
||||
"/v2/api-docs/**",
|
||||
"/v2/api-docs-ext/**",
|
||||
"/v3/api-docs/**",
|
||||
"/favicon.ico",
|
||||
"/error"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sa-Token 整合 jwt
|
||||
*/
|
||||
@Bean
|
||||
fun getStpLogicJwt(): StpLogic {
|
||||
return StpLogicJwtForSimple()
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 [Sa-Token 全局过滤器]
|
||||
*/
|
||||
@Bean
|
||||
fun getSaServletFilter(): SaServletFilter {
|
||||
return SaServletFilter()
|
||||
// 指定 [拦截路由] 与 [放行路由]
|
||||
.addInclude("/**") // .addExclude("/favicon.ico")
|
||||
// 认证函数: 每次请求执行
|
||||
.setAuth {
|
||||
// println("---------- sa全局认证 ${SaHolder.getRequest().requestPath}")
|
||||
}
|
||||
// 异常处理函数:每次认证函数发生异常时执行此函数
|
||||
.setError { e ->
|
||||
println("---------- sa全局异常 ")
|
||||
e.printStackTrace()
|
||||
SaResult.error(e.message)
|
||||
}
|
||||
// 前置函数:在每次认证函数之前执行
|
||||
.setBeforeAuth {
|
||||
// ---------- 设置一些安全响应头 ----------
|
||||
SaHolder.getResponse()
|
||||
// 服务器名称
|
||||
.setServer("sa-server")
|
||||
// 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
|
||||
.setHeader("X-Frame-Options", "SAMEORIGIN")
|
||||
// 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面
|
||||
.setHeader("X-XSS-Protection", "1; mode=block")
|
||||
// 禁用浏览器内容嗅探
|
||||
.setHeader("X-Content-Type-Options", "nosniff")
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/main/kotlin/com/app/config/satoken/StpInterfaceImpl.kt
Normal file
43
src/main/kotlin/com/app/config/satoken/StpInterfaceImpl.kt
Normal file
@ -0,0 +1,43 @@
|
||||
package com.app.config.satoken
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface
|
||||
import com.app.service.UserService
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
/**
|
||||
* 自定义权限验证接口扩展
|
||||
*/
|
||||
@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展
|
||||
class StpInterfaceImpl(
|
||||
private val userService: UserService
|
||||
) : StpInterface {
|
||||
/**
|
||||
* 返回一个账号所拥有的权限码集合
|
||||
*/
|
||||
override fun getPermissionList(loginId: Any, loginType: String): List<String> {
|
||||
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
|
||||
// userService.selectById(loginId as Long)
|
||||
|
||||
return listOf(
|
||||
"101",
|
||||
"user-add",
|
||||
"user-delete",
|
||||
"user-update",
|
||||
"user-get",
|
||||
"article-get"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的角色标识集合
|
||||
*/
|
||||
override fun getRoleList(loginId: Any, loginType: String): List<String> {
|
||||
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
|
||||
return listOf("admin", "super-admin")
|
||||
|
||||
// 实际业务代码示例:
|
||||
// val id = loginId as Long
|
||||
// val userEntity = userMapper.selectById(id)
|
||||
// return listOf(userEntity.role)
|
||||
}
|
||||
}
|
||||
61
src/main/kotlin/com/app/config/swagger/SwaggerConfig.kt
Normal file
61
src/main/kotlin/com/app/config/swagger/SwaggerConfig.kt
Normal file
@ -0,0 +1,61 @@
|
||||
package com.app.config.swagger
|
||||
|
||||
import cn.hutool.core.util.RandomUtil
|
||||
import io.swagger.v3.oas.models.OpenAPI
|
||||
import io.swagger.v3.oas.models.info.Info
|
||||
import io.swagger.v3.oas.models.info.License
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
fun orderGlobalOpenApiCustomizer(): GlobalOpenApiCustomizer {
|
||||
return GlobalOpenApiCustomizer { openApi ->
|
||||
openApi.tags?.forEach { tag ->
|
||||
val map = HashMap<String, Any>()
|
||||
map["x-order"] = RandomUtil.randomInt(0, 100)
|
||||
tag.extensions = map
|
||||
}
|
||||
|
||||
if (openApi.paths != null) {
|
||||
openApi.addExtension("x-test123", "333")
|
||||
openApi.paths.addExtension("x-abb", RandomUtil.randomInt(1, 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
@Bean
|
||||
fun customOpenAPI(): OpenAPI {
|
||||
return OpenAPI()
|
||||
.info(
|
||||
Info()
|
||||
.title("XXX用户系统API")
|
||||
.version("1.0")
|
||||
.description("Knife4j集成springdoc-openapi示例")
|
||||
.termsOfService("http://doc.xiaominfo.com")
|
||||
.license(
|
||||
License().name("Apache 2.0")
|
||||
.url("http://doc.xiaominfo.com")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// @Bean
|
||||
// fun csrsApi(): GroupedOpenApi {
|
||||
// return GroupedOpenApi.builder()
|
||||
// .group("csrs系统模块")
|
||||
// .packagesToScan("com.csrs.web.controller.csrs")
|
||||
// .build()
|
||||
// }
|
||||
//
|
||||
// @Bean
|
||||
// fun aiApi(): GroupedOpenApi {
|
||||
// return GroupedOpenApi.builder()
|
||||
// .group("ai模块")
|
||||
// .packagesToScan("com.csrs.web.controller.ai")
|
||||
// .build()
|
||||
// }
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user