认证退出与操作日志、密码规则,以及运输业务多项修正

This commit is contained in:
2026-09-22 12:03:59 +08:00
parent 5254a46de8
commit 14c1d9dff0
21 changed files with 790 additions and 26 deletions
+10
View File
@@ -31,6 +31,16 @@
的 WebFlux 网关里启动即崩。业务服务经 blade-core-boot 自带 starter-log,运行时不受影响。 -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-auto</artifactId>
@@ -0,0 +1,145 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.common.aspect;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springblade.core.log.annotation.ApiLog;
import org.springblade.core.log.constant.EventConstant;
import org.springblade.core.log.event.ApiLogEvent;
import org.springblade.core.log.model.LogApi;
import org.springblade.core.log.utils.LogAbstractUtil;
import org.springblade.core.tool.constant.BladeConstant;
import org.springblade.core.tool.utils.SpringUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* 增删改操作日志切面
* 自动拦截 Controller 中的 save/submit/update/remove/delete 等方法,写入 blade_log_api
* 已标注 {@link ApiLog} 的方法跳过,避免重复记录
*
* @author BladeX
*/
@Slf4j
@Aspect
public class OperationApiLogAspect {
@Around("""
execution(* org.springblade..controller..*.save*(..))
|| execution(* org.springblade..controller..*.submit*(..))
|| execution(* org.springblade..controller..*.update*(..))
|| execution(* org.springblade..controller..*.remove*(..))
|| execution(* org.springblade..controller..*.delete*(..))
""")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
if (!shouldRecord(method)) {
return point.proceed();
}
String className = point.getTarget().getClass().getName();
String methodName = method.getName();
String title = resolveTitle(point.getTarget().getClass(), method);
long beginTime = System.currentTimeMillis();
Object result = point.proceed();
long time = System.currentTimeMillis() - beginTime;
try {
publishEvent(methodName, className, title, time);
} catch (Exception e) {
log.warn("记录操作日志失败: {}#{} - {}", className, methodName, e.getMessage());
}
return result;
}
private boolean shouldRecord(Method method) {
if (method.getAnnotation(ApiLog.class) != null) {
return false;
}
HttpServletRequest request = WebUtil.getRequest();
if (request == null) {
return true;
}
String httpMethod = request.getMethod();
return !"GET".equalsIgnoreCase(httpMethod)
&& !"HEAD".equalsIgnoreCase(httpMethod)
&& !"OPTIONS".equalsIgnoreCase(httpMethod);
}
private String resolveTitle(Class<?> targetClass, Method method) {
Tag tag = AnnotationUtils.findAnnotation(targetClass, Tag.class);
Operation operation = AnnotationUtils.findAnnotation(method, Operation.class);
String module = tag == null ? null : firstNonBlank(tag.name(), tag.description());
String action = operation == null ? null : firstNonBlank(operation.summary(), operation.description());
if (StringUtil.isNotBlank(module) && StringUtil.isNotBlank(action)) {
return module + "-" + action;
}
if (StringUtil.isNotBlank(action)) {
return action;
}
String simpleName = targetClass.getSimpleName().replace("Controller", "");
return simpleName + "-" + method.getName();
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (StringUtil.isNotBlank(value)) {
return value.trim();
}
}
return null;
}
private void publishEvent(String methodName, String methodClass, String title, long time) {
HttpServletRequest request = WebUtil.getRequest();
LogApi logApi = new LogApi();
logApi.setType(BladeConstant.LOG_NORMAL_TYPE);
logApi.setTitle(title);
logApi.setTime(String.valueOf(time));
logApi.setMethodClass(methodClass);
logApi.setMethodName(methodName);
LogAbstractUtil.addRequestInfoToLog(request, logApi);
Map<String, Object> event = new HashMap<>(16);
event.put(EventConstant.EVENT_LOG, logApi);
SpringUtil.publishEvent(new ApiLogEvent(event));
}
}
@@ -26,16 +26,27 @@
package org.springblade.common.config;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springblade.common.aspect.OperationApiLogAspect;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
/**
* 公共封装包配置类
*
* @author Chill
*/
@Configuration(proxyBeanMethods = false)
@AllArgsConstructor
@AutoConfiguration
public class BladeCommonConfiguration {
@Bean
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(name = "org.springblade.core.log.event.ApiLogEvent")
@ConditionalOnProperty(value = "blade.log.operation.enabled", havingValue = "true", matchIfMissing = true)
public OperationApiLogAspect operationApiLogAspect() {
return new OperationApiLogAspect();
}
}
@@ -0,0 +1,106 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.common.utils;
import org.springblade.core.tool.utils.StringUtil;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
/**
* 登录密码强度规则工具
* 规则:大于8位,同时包含字母、数字、特殊字符(.!@#$%^&*
*
* @author BladeX
*/
public final class PasswordRuleUtil {
/**
* 允许的特殊字符集合
*/
public static final String SPECIAL_CHARS = ".!@#$%^&*";
/**
* 最小长度(大于 8 位 => 至少 9 位)
*/
public static final int MIN_LENGTH = 9;
/**
* 自动生成密码长度
*/
public static final int GENERATE_LENGTH = 10;
/**
* 规则提示文案
*/
public static final String RULE_MESSAGE = "密码须大于8位,且同时包含字母、数字和特殊字符(.!@#$%^&*";
private static final String LETTERS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
private static final String DIGITS = "23456789";
private static final String ALL_CHARS = LETTERS + DIGITS + SPECIAL_CHARS;
private static final Pattern PASSWORD_PATTERN = Pattern.compile(
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[.!@#$%^&*]).{" + MIN_LENGTH + ",}$"
);
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private PasswordRuleUtil() {
}
/**
* 校验密码是否符合强度规则
*
* @param password 明文密码
* @return true-符合
*/
public static boolean isValid(String password) {
return StringUtil.isNotBlank(password) && PASSWORD_PATTERN.matcher(password).matches();
}
/**
* 生成符合规则的随机密码
*
* @return 随机密码
*/
public static String generate() {
List<Character> chars = new ArrayList<>(GENERATE_LENGTH);
chars.add(LETTERS.charAt(SECURE_RANDOM.nextInt(LETTERS.length())));
chars.add(DIGITS.charAt(SECURE_RANDOM.nextInt(DIGITS.length())));
chars.add(SPECIAL_CHARS.charAt(SECURE_RANDOM.nextInt(SPECIAL_CHARS.length())));
for (int i = chars.size(); i < GENERATE_LENGTH; i++) {
chars.add(ALL_CHARS.charAt(SECURE_RANDOM.nextInt(ALL_CHARS.length())));
}
Collections.shuffle(chars, SECURE_RANDOM);
StringBuilder password = new StringBuilder(GENERATE_LENGTH);
for (Character ch : chars) {
password.append(ch);
}
return password.toString();
}
}
@@ -0,0 +1 @@
org.springblade.common.config.BladeCommonConfiguration