diff --git a/src/main/kotlin/plus/maa/backend/common/annotation/SensitiveWordDetection.kt b/src/main/kotlin/plus/maa/backend/common/annotation/SensitiveWordDetection.kt deleted file mode 100644 index d976ec10..00000000 --- a/src/main/kotlin/plus/maa/backend/common/annotation/SensitiveWordDetection.kt +++ /dev/null @@ -1,19 +0,0 @@ -package plus.maa.backend.common.annotation - -/** - * 敏感词检测注解

- * 用于方法上,标注该方法需要进行敏感词检测

- * 通过 SpEL 表达式获取方法参数 - * - * @author lixuhuilll - * Date: 2023-08-25 18:50 - */ -@MustBeDocumented -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) -@Retention(AnnotationRetention.RUNTIME) -annotation class SensitiveWordDetection( - /** - * SpEL 表达式 - */ - vararg val value: String = [], -) diff --git a/src/main/kotlin/plus/maa/backend/common/aop/SensitiveWordAop.kt b/src/main/kotlin/plus/maa/backend/common/aop/SensitiveWordAop.kt deleted file mode 100644 index f19be503..00000000 --- a/src/main/kotlin/plus/maa/backend/common/aop/SensitiveWordAop.kt +++ /dev/null @@ -1,75 +0,0 @@ -package plus.maa.backend.common.aop - -import cn.hutool.dfa.WordTree -import com.fasterxml.jackson.databind.ObjectMapper -import org.aspectj.lang.JoinPoint -import org.aspectj.lang.annotation.Aspect -import org.aspectj.lang.annotation.Before -import org.aspectj.lang.reflect.MethodSignature -import org.springframework.core.DefaultParameterNameDiscoverer -import org.springframework.expression.EvaluationContext -import org.springframework.expression.spel.standard.SpelExpressionParser -import org.springframework.expression.spel.support.StandardEvaluationContext -import org.springframework.http.HttpStatus -import org.springframework.stereotype.Component -import plus.maa.backend.common.annotation.SensitiveWordDetection -import plus.maa.backend.controller.response.MaaResultException - -/** - * 敏感词处理程序

- * - * @author lixuhuilll - * Date: 2023-08-25 18:50 - */ -@Aspect -@Component -class SensitiveWordAop( - // 敏感词库 - private val wordTree: WordTree, - private val objectMapper: ObjectMapper, -) { - // SpEL 表达式解析器 - private val parser = SpelExpressionParser() - - // 用于获取方法参数名 - private val nameDiscoverer = DefaultParameterNameDiscoverer() - - fun getObjectBySpEL(spELString: String, joinPoint: JoinPoint): Any? { - // 获取被注解方法 - val signature = joinPoint.signature as? MethodSignature ?: return null - // 获取方法参数名数组 - val paramNames = nameDiscoverer.getParameterNames(signature.method) - // 解析 Spring 表达式对象 - val expression = parser.parseExpression(spELString) - // Spring 表达式上下文对象 - val context: EvaluationContext = StandardEvaluationContext() - // 通过 joinPoint 获取被注解方法的参数 - val args = joinPoint.args - // 给上下文赋值 - for (i in args.indices) { - if (paramNames != null) { - context.setVariable(paramNames[i], args[i]) - } - } - context.setVariable("objectMapper", objectMapper) - // 表达式从上下文中计算出实际参数值 - return expression.getValue(context) - } - - @Before("@annotation(annotation)") // 处理 SensitiveWordDetection 注解 - fun before(joinPoint: JoinPoint, annotation: SensitiveWordDetection) { - // 获取 SpEL 表达式 - val expressions = annotation.value - for (expression in expressions) { - // 解析 SpEL 表达式 - val value = getObjectBySpEL(expression, joinPoint) - // 校验 - if (value is String) { - val matchAll = wordTree.matchAll(value) - if (matchAll != null && matchAll.isNotEmpty()) { - throw MaaResultException(HttpStatus.BAD_REQUEST.value(), "包含敏感词:$matchAll") - } - } - } - } -} diff --git a/src/main/kotlin/plus/maa/backend/config/SensitiveWordConfig.kt b/src/main/kotlin/plus/maa/backend/config/SensitiveWordConfig.kt deleted file mode 100644 index 86452392..00000000 --- a/src/main/kotlin/plus/maa/backend/config/SensitiveWordConfig.kt +++ /dev/null @@ -1,61 +0,0 @@ -package plus.maa.backend.config - -import cn.hutool.dfa.WordTree -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.ApplicationContext -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStreamReader - -/** - * 敏感词配置类

- * - * @author lixuhuilll - * Date: 2023-08-25 18:50 - */ -@Configuration -class SensitiveWordConfig( - // 标准的 Spring 路径匹配语法,默认为 classpath:sensitive-word.txt - @Value("\${maa-copilot.sensitive-word.path:classpath:sensitive-word.txt}") private val sensitiveWordPath: String, -) { - private val log = KotlinLogging.logger {} - - /** - * 敏感词库初始化

- * 使用 Hutool 的 DFA 算法库,如果后续需要可转其他开源库或者使用付费的敏感词库

- * - * @return 敏感词库 - */ - @Bean - @Throws(IOException::class) - fun sensitiveWordInit(applicationContext: ApplicationContext): WordTree { - // Spring 上下文获取敏感词文件 - val sensitiveWordResource = applicationContext.getResource(sensitiveWordPath) - val wordTree = WordTree() - - // 获取载入用时 - val start = System.currentTimeMillis() - - // 以行为单位载入敏感词 - try { - BufferedReader( - InputStreamReader(sensitiveWordResource.inputStream), - ).use { bufferedReader -> - var line: String? - while ((bufferedReader.readLine().also { line = it }) != null) { - wordTree.addWord(line) - } - } - } catch (e: Exception) { - log.error { "敏感词库初始化失败:${e.message}" } - throw e - } - - log.info { "敏感词库初始化完成,耗时 ${System.currentTimeMillis() - start} ms" } - - return wordTree - } -} diff --git a/src/main/kotlin/plus/maa/backend/controller/CommentsAreaController.kt b/src/main/kotlin/plus/maa/backend/controller/CommentsAreaController.kt index c15a952b..d8ad9b9a 100644 --- a/src/main/kotlin/plus/maa/backend/controller/CommentsAreaController.kt +++ b/src/main/kotlin/plus/maa/backend/controller/CommentsAreaController.kt @@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController -import plus.maa.backend.common.annotation.SensitiveWordDetection import plus.maa.backend.config.doc.RequireJwt import plus.maa.backend.config.security.AuthenticationHelper import plus.maa.backend.controller.request.comments.CommentsAddDTO @@ -35,7 +34,6 @@ class CommentsAreaController( private val commentsAreaService: CommentsAreaService, private val authHelper: AuthenticationHelper, ) { - @SensitiveWordDetection("#comments.message") @PostMapping("/add") @Operation(summary = "发送评论") @ApiResponse(description = "发送评论结果") diff --git a/src/main/kotlin/plus/maa/backend/controller/CopilotController.kt b/src/main/kotlin/plus/maa/backend/controller/CopilotController.kt index 594a3c29..1572003e 100644 --- a/src/main/kotlin/plus/maa/backend/controller/CopilotController.kt +++ b/src/main/kotlin/plus/maa/backend/controller/CopilotController.kt @@ -16,7 +16,6 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController -import plus.maa.backend.common.annotation.SensitiveWordDetection import plus.maa.backend.config.doc.RequireJwt import plus.maa.backend.config.security.AuthenticationHelper import plus.maa.backend.controller.request.copilot.CopilotCUDRequest @@ -44,12 +43,10 @@ class CopilotController( @Operation(summary = "上传作业") @ApiResponse(description = "上传作业结果") @RequireJwt - @SensitiveWordDetection( - "#request.content != null ? #objectMapper.readTree(#request.content).get('doc')?.toString() : null", - ) @PostMapping("/upload") - fun uploadCopilot(@RequestBody @Valid request: CopilotCUDRequest): MaaResult = - success(copilotService.upload(helper.requireUserId(), request.content)) + fun uploadCopilot(@RequestBody @Valid request: CopilotCUDRequest): MaaResult { + return success(copilotService.upload(helper.requireUserId(), request.content)) + } @Operation(summary = "删除作业") @ApiResponse(description = "删除作业结果") @@ -81,9 +78,6 @@ class CopilotController( @Operation(summary = "更新作业") @ApiResponse(description = "更新结果") @RequireJwt - @SensitiveWordDetection( - "#copilotCUDRequest.content != null ? #objectMapper.readTree(#copilotCUDRequest.content).get('doc')?.toString() : null", - ) @PostMapping("/update") fun updateCopilot(@RequestBody @Valid copilotCUDRequest: CopilotCUDRequest): MaaResult { copilotService.update(helper.requireUserId(), copilotCUDRequest) diff --git a/src/main/kotlin/plus/maa/backend/handler/GlobalExceptionHandler.kt b/src/main/kotlin/plus/maa/backend/handler/GlobalExceptionHandler.kt index c9bc3056..d444f0a6 100644 --- a/src/main/kotlin/plus/maa/backend/handler/GlobalExceptionHandler.kt +++ b/src/main/kotlin/plus/maa/backend/handler/GlobalExceptionHandler.kt @@ -17,6 +17,7 @@ import org.springframework.web.servlet.NoHandlerFoundException import plus.maa.backend.controller.response.MaaResult import plus.maa.backend.controller.response.MaaResult.Companion.fail import plus.maa.backend.controller.response.MaaResultException +import plus.maa.backend.service.sensitiveword.SensitiveWordException private val log = KotlinLogging.logger { } @@ -132,6 +133,11 @@ class GlobalExceptionHandler { return fail(ex.statusCode.value(), ex.message) } + @ExceptionHandler(SensitiveWordException::class) + fun handleSensitiveWordException(ex: SensitiveWordException): MaaResult { + return fail(400, ex.message) + } + /** * @return plus.maa.backend.controller.response.MaaResult * @author john180 diff --git a/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt b/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt index e787c95b..53e69786 100644 --- a/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt +++ b/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt @@ -20,6 +20,7 @@ import plus.maa.backend.repository.entity.CommentsArea import plus.maa.backend.repository.entity.Copilot import plus.maa.backend.repository.entity.MaaUser import plus.maa.backend.service.model.RatingType +import plus.maa.backend.service.sensitiveword.SensitiveWordService import java.time.LocalDateTime /** @@ -33,6 +34,7 @@ class CommentsAreaService( private val copilotRepository: CopilotRepository, private val userRepository: UserRepository, private val emailService: EmailService, + private val sensitiveWordService: SensitiveWordService, ) { /** * 评论 @@ -42,6 +44,7 @@ class CommentsAreaService( * @param commentsAddDTO CommentsRequest */ fun addComments(userId: String, commentsAddDTO: CommentsAddDTO) { + sensitiveWordService.validate(commentsAddDTO.message) val copilotId = commentsAddDTO.copilotId val copilot = copilotRepository.findByCopilotId(copilotId).requireNotNull { "作业不存在" } diff --git a/src/main/kotlin/plus/maa/backend/service/CopilotService.kt b/src/main/kotlin/plus/maa/backend/service/CopilotService.kt index e38d0ea8..7f3b9736 100644 --- a/src/main/kotlin/plus/maa/backend/service/CopilotService.kt +++ b/src/main/kotlin/plus/maa/backend/service/CopilotService.kt @@ -16,6 +16,7 @@ import org.springframework.util.Assert import org.springframework.util.ObjectUtils import plus.maa.backend.common.utils.IdComponent import plus.maa.backend.common.utils.converter.CopilotConverter +import plus.maa.backend.common.utils.requireNotNull import plus.maa.backend.config.external.MaaCopilotProperties import plus.maa.backend.controller.request.copilot.CopilotCUDRequest import plus.maa.backend.controller.request.copilot.CopilotDTO @@ -36,6 +37,7 @@ import plus.maa.backend.repository.findByUsersId import plus.maa.backend.service.level.ArkLevelService import plus.maa.backend.service.model.RatingCache import plus.maa.backend.service.model.RatingType +import plus.maa.backend.service.sensitiveword.SensitiveWordService import java.math.RoundingMode import java.time.LocalDateTime import java.time.temporal.ChronoUnit @@ -67,6 +69,7 @@ class CopilotService( private val commentsAreaRepository: CommentsAreaRepository, private val properties: MaaCopilotProperties, private val copilotConverter: CopilotConverter, + private val sensitiveWordService: SensitiveWordService, ) { /** * 并修正前端的冗余部分 @@ -132,6 +135,7 @@ class CopilotService( */ fun upload(loginUserId: String, content: String?): Long { val copilotDTO = correctCopilot(parseToCopilotDto(content)) + sensitiveWordService.validate(copilotDTO.doc) // 将其转换为数据库存储对象 val copilot = copilotConverter.toCopilot( copilotDTO, @@ -363,9 +367,10 @@ class CopilotService( */ fun update(loginUserId: String, copilotCUDRequest: CopilotCUDRequest) { val content = copilotCUDRequest.content - val id = copilotCUDRequest.id - copilotRepository.findByCopilotId(id!!)?.let { copilot: Copilot -> + val id = copilotCUDRequest.id.requireNotNull { "id 不能为空" } + copilotRepository.findByCopilotId(id)?.let { copilot: Copilot -> val copilotDTO = correctCopilot(parseToCopilotDto(content)) + sensitiveWordService.validate(copilotDTO.doc) Assert.state(copilot.uploaderId == loginUserId, "您无法修改不属于您的作业") copilot.uploadTime = LocalDateTime.now() copilotConverter.updateCopilotFromDto(copilotDTO, content!!, copilot) diff --git a/src/main/kotlin/plus/maa/backend/service/sensitiveword/SensitiveWordException.kt b/src/main/kotlin/plus/maa/backend/service/sensitiveword/SensitiveWordException.kt new file mode 100644 index 00000000..037521de --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/sensitiveword/SensitiveWordException.kt @@ -0,0 +1,3 @@ +package plus.maa.backend.service.sensitiveword + +class SensitiveWordException(message: String?) : RuntimeException(message) diff --git a/src/main/kotlin/plus/maa/backend/service/sensitiveword/SensitiveWordService.kt b/src/main/kotlin/plus/maa/backend/service/sensitiveword/SensitiveWordService.kt new file mode 100644 index 00000000..91022266 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/sensitiveword/SensitiveWordService.kt @@ -0,0 +1,35 @@ +package plus.maa.backend.service.sensitiveword + +import cn.hutool.dfa.WordTree +import com.fasterxml.jackson.databind.ObjectMapper +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.context.ApplicationContext +import org.springframework.stereotype.Service +import plus.maa.backend.config.external.MaaCopilotProperties + +@Service +class SensitiveWordService( + private val ctx: ApplicationContext, + maaCopilotProperties: MaaCopilotProperties, + private val objectMapper: ObjectMapper, +) { + private val log = KotlinLogging.logger {} + private val wordTree = WordTree().apply { + val path = maaCopilotProperties.sensitiveWord.path + try { + ctx.getResource(path).inputStream.bufferedReader().use { it.lines().forEach(::addWord) } + log.info { "初始化敏感词库完成: $path" } + } catch (e: Exception) { + log.error { "初始化敏感词库失败: $path" } + throw e + } + } + + @Throws(SensitiveWordException::class) + fun validate(value: T) { + if (value == null) return + val text = if (value is String) value else objectMapper.writeValueAsString(value) + val detected = wordTree.matchAll(text) + if (detected.size > 0) throw SensitiveWordException("包含敏感词:$detected") + } +}