first commit
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
package top.ysoft.admin.common.config.doc;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.PathItem;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import top.continew.starter.apidoc.autoconfigure.SpringDocExtensionProperties;
|
||||
import top.continew.starter.auth.satoken.autoconfigure.SaTokenExtensionProperties;
|
||||
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局鉴权参数定制器
|
||||
*
|
||||
* @author echo
|
||||
* @since 2024/12/31 13:36
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GlobalAuthenticationCustomizer implements GlobalOpenApiCustomizer {
|
||||
|
||||
private final SpringDocExtensionProperties properties;
|
||||
private final SaTokenExtensionProperties saTokenExtensionProperties;
|
||||
private final ApplicationContext context;
|
||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
/**
|
||||
* 定制 OpenAPI 文档
|
||||
*
|
||||
* @param openApi 当前 OpenAPI 对象
|
||||
*/
|
||||
@Override
|
||||
public void customise(OpenAPI openApi) {
|
||||
if (MapUtil.isEmpty(openApi.getPaths())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 收集需要排除的路径(包括 Sa-Token 配置中的排除路径和 @SaIgnore 注解路径)
|
||||
Set<String> excludedPaths = collectExcludedPaths();
|
||||
|
||||
// 遍历所有路径,为需要鉴权的路径添加安全认证配置
|
||||
openApi.getPaths().forEach((path, pathItem) -> {
|
||||
if (isPathExcluded(path, excludedPaths)) {
|
||||
// 路径在排除列表中,跳过处理
|
||||
return;
|
||||
}
|
||||
// 为路径添加安全认证参数
|
||||
addAuthenticationParameters(pathItem);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集所有需要排除的路径
|
||||
*
|
||||
* @return 排除路径集合
|
||||
*/
|
||||
private Set<String> collectExcludedPaths() {
|
||||
Set<String> excludedPaths = new HashSet<>();
|
||||
excludedPaths.addAll(Arrays.asList(saTokenExtensionProperties.getSecurity().getExcludes()));
|
||||
excludedPaths.addAll(resolveSaIgnorePaths());
|
||||
return excludedPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为路径项添加认证参数
|
||||
*
|
||||
* @param pathItem 当前路径项
|
||||
*/
|
||||
private void addAuthenticationParameters(PathItem pathItem) {
|
||||
Components components = properties.getComponents();
|
||||
if (components == null || MapUtil.isEmpty(components.getSecuritySchemes())) {
|
||||
return;
|
||||
}
|
||||
Map<String, SecurityScheme> securitySchemes = components.getSecuritySchemes();
|
||||
List<String> schemeNames = securitySchemes.values().stream().map(SecurityScheme::getName).toList();
|
||||
pathItem.readOperations().forEach(operation -> {
|
||||
SecurityRequirement securityRequirement = new SecurityRequirement();
|
||||
schemeNames.forEach(securityRequirement::addList);
|
||||
operation.addSecurityItem(securityRequirement);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析所有带有 @SaIgnore 注解的路径
|
||||
*
|
||||
* @return 被忽略的路径集合
|
||||
*/
|
||||
private Set<String> resolveSaIgnorePaths() {
|
||||
// 获取所有标注 @RestController 的 Bean
|
||||
Map<String, Object> controllers = context.getBeansWithAnnotation(RestController.class);
|
||||
Set<String> ignoredPaths = new HashSet<>();
|
||||
|
||||
// 遍历所有控制器,解析 @SaIgnore 注解路径
|
||||
controllers.values().forEach(controllerBean -> {
|
||||
Class<?> controllerClass = AopUtils.getTargetClass(controllerBean);
|
||||
List<String> classPaths = getClassPaths(controllerClass);
|
||||
|
||||
// 类级别的 @SaIgnore 注解
|
||||
if (controllerClass.isAnnotationPresent(SaIgnore.class)) {
|
||||
classPaths.forEach(classPath -> ignoredPaths.add(classPath + "/**"));
|
||||
}
|
||||
|
||||
// 方法级别的 @SaIgnore 注解
|
||||
Arrays.stream(controllerClass.getDeclaredMethods())
|
||||
.filter(method -> method.isAnnotationPresent(SaIgnore.class))
|
||||
.forEach(method -> ignoredPaths.addAll(combinePaths(classPaths, getMethodPaths(method))));
|
||||
});
|
||||
|
||||
return ignoredPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类上的所有路径
|
||||
*
|
||||
* @param controller 控制器类
|
||||
* @return 类路径列表
|
||||
*/
|
||||
private List<String> getClassPaths(Class<?> controller) {
|
||||
List<String> classPaths = new ArrayList<>();
|
||||
// 处理 @RequestMapping 注解
|
||||
if (controller.isAnnotationPresent(RequestMapping.class)) {
|
||||
RequestMapping mapping = controller.getAnnotation(RequestMapping.class);
|
||||
classPaths.addAll(Arrays.asList(mapping.value()));
|
||||
}
|
||||
// 处理 @CrudRequestMapping 注解
|
||||
if (controller.isAnnotationPresent(CrudRequestMapping.class)) {
|
||||
CrudRequestMapping mapping = controller.getAnnotation(CrudRequestMapping.class);
|
||||
if (!mapping.value().isEmpty()) {
|
||||
classPaths.add(mapping.value());
|
||||
}
|
||||
}
|
||||
return classPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取方法上的所有路径
|
||||
*
|
||||
* @param method 控制器方法
|
||||
* @return 方法路径列表
|
||||
*/
|
||||
private List<String> getMethodPaths(Method method) {
|
||||
List<String> methodPaths = new ArrayList<>();
|
||||
|
||||
// 检查方法上的各种映射注解
|
||||
if (method.isAnnotationPresent(GetMapping.class)) {
|
||||
methodPaths.addAll(Arrays.asList(method.getAnnotation(GetMapping.class).value()));
|
||||
} else if (method.isAnnotationPresent(PostMapping.class)) {
|
||||
methodPaths.addAll(Arrays.asList(method.getAnnotation(PostMapping.class).value()));
|
||||
} else if (method.isAnnotationPresent(PutMapping.class)) {
|
||||
methodPaths.addAll(Arrays.asList(method.getAnnotation(PutMapping.class).value()));
|
||||
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
|
||||
methodPaths.addAll(Arrays.asList(method.getAnnotation(DeleteMapping.class).value()));
|
||||
} else if (method.isAnnotationPresent(RequestMapping.class)) {
|
||||
methodPaths.addAll(Arrays.asList(method.getAnnotation(RequestMapping.class).value()));
|
||||
} else if (method.isAnnotationPresent(PatchMapping.class)) {
|
||||
methodPaths.addAll(Arrays.asList(method.getAnnotation(PatchMapping.class).value()));
|
||||
}
|
||||
|
||||
return methodPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组合类路径和方法路径
|
||||
*
|
||||
* @param classPaths 类路径列表
|
||||
* @param methodPaths 方法路径列表
|
||||
* @return 完整路径集合
|
||||
*/
|
||||
private Set<String> combinePaths(List<String> classPaths, List<String> methodPaths) {
|
||||
return classPaths.stream()
|
||||
.flatMap(classPath -> methodPaths.stream().map(methodPath -> classPath + methodPath))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否在排除列表中
|
||||
*
|
||||
* @param path 当前路径
|
||||
* @param excludedPaths 排除路径集合,支持通配符
|
||||
* @return 是否匹配排除规则
|
||||
*/
|
||||
private boolean isPathExcluded(String path, Set<String> excludedPaths) {
|
||||
return excludedPaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, path));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package top.ysoft.admin.common.config.doc;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.swagger.v3.oas.models.Operation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springdoc.core.customizers.GlobalOperationCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 全局描述定制器 - 处理 sa-token 的注解权限码
|
||||
*
|
||||
* @author echo
|
||||
* @since 2025/1/24 14:59
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GlobalDescriptionCustomizer implements GlobalOperationCustomizer {
|
||||
|
||||
@Override
|
||||
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
|
||||
// 将 sa-token 注解数据添加到 operation 的描述中
|
||||
// 权限
|
||||
List<String> noteList = new ArrayList<>(new OperationDescriptionCustomizer().getPermission(handlerMethod));
|
||||
|
||||
// 如果注解数据列表为空,直接返回原 operation
|
||||
if (noteList.isEmpty()) {
|
||||
return operation;
|
||||
}
|
||||
// 拼接注解数据为字符串
|
||||
String noteStr = StrUtil.join("<br/>", noteList);
|
||||
// 获取原描述
|
||||
String originalDescription = operation.getDescription();
|
||||
// 根据原描述是否为空,更新描述
|
||||
String newDescription = StringUtils.isNotEmpty(originalDescription)
|
||||
? originalDescription + "<br/>" + noteStr
|
||||
: noteStr;
|
||||
|
||||
// 设置新描述
|
||||
operation.setDescription(newDescription);
|
||||
return operation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package top.ysoft.admin.common.config.doc;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.extension.crud.annotation.CrudApi;
|
||||
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
|
||||
import top.continew.starter.extension.crud.enums.Api;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Operation 描述定制器 处理 sa-token 鉴权标识符
|
||||
*
|
||||
* @author echo
|
||||
* @since 2024/6/14 11:18
|
||||
*/
|
||||
public class OperationDescriptionCustomizer {
|
||||
|
||||
/**
|
||||
* 获取 sa-token 注解信息
|
||||
*
|
||||
* @param handlerMethod 处理程序方法
|
||||
* @return 包含权限和角色校验信息的列表
|
||||
*/
|
||||
public List<String> getPermission(HandlerMethod handlerMethod) {
|
||||
List<String> values = new ArrayList<>();
|
||||
|
||||
// 获取权限校验信息
|
||||
String permissionInfo = getAnnotationInfo(handlerMethod, SaCheckPermission.class, "权限校验:");
|
||||
if (!permissionInfo.isEmpty()) {
|
||||
values.add(permissionInfo);
|
||||
}
|
||||
|
||||
// 获取角色校验信息
|
||||
String roleInfo = getAnnotationInfo(handlerMethod, SaCheckRole.class, "角色校验:");
|
||||
if (!roleInfo.isEmpty()) {
|
||||
values.add(roleInfo);
|
||||
}
|
||||
|
||||
// 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息
|
||||
String crudPermissionInfo = getCrudPermissionInfo(handlerMethod);
|
||||
if (!crudPermissionInfo.isEmpty()) {
|
||||
values.add(crudPermissionInfo);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类和方法上指定注解的信息
|
||||
*
|
||||
* @param handlerMethod 处理程序方法
|
||||
* @param annotationClass 注解类
|
||||
* @param title 信息标题
|
||||
* @param <A> 注解类型
|
||||
* @return 拼接好的注解信息字符串
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private <A extends Annotation> String getAnnotationInfo(HandlerMethod handlerMethod,
|
||||
Class<A> annotationClass,
|
||||
String title) {
|
||||
StringBuilder infoBuilder = new StringBuilder();
|
||||
|
||||
// 获取类上的注解
|
||||
A classAnnotation = handlerMethod.getBeanType().getAnnotation(annotationClass);
|
||||
if (classAnnotation != null) {
|
||||
appendAnnotationInfo(infoBuilder, "类:", classAnnotation);
|
||||
}
|
||||
|
||||
// 获取方法上的注解
|
||||
A methodAnnotation = handlerMethod.getMethodAnnotation(annotationClass);
|
||||
if (methodAnnotation != null) {
|
||||
appendAnnotationInfo(infoBuilder, "方法:", methodAnnotation);
|
||||
}
|
||||
|
||||
// 如果有注解信息,添加标题
|
||||
if (!infoBuilder.isEmpty()) {
|
||||
infoBuilder.insert(0, "<font style=\"color:red\" class=\"light-red\">" + title + "</font></br>");
|
||||
}
|
||||
|
||||
return infoBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接注解信息到 StringBuilder 中
|
||||
*
|
||||
* @param builder 用于拼接信息的 StringBuilder
|
||||
* @param prefix 前缀信息,如 "类:" 或 "方法:"
|
||||
* @param annotation 注解对象
|
||||
*/
|
||||
private void appendAnnotationInfo(StringBuilder builder, String prefix, Annotation annotation) {
|
||||
String[] values = null;
|
||||
SaMode mode = null;
|
||||
String type = "";
|
||||
String[] orRole = new String[0];
|
||||
|
||||
if (annotation instanceof SaCheckPermission checkPermission) {
|
||||
values = checkPermission.value();
|
||||
mode = checkPermission.mode();
|
||||
type = checkPermission.type();
|
||||
orRole = checkPermission.orRole();
|
||||
} else if (annotation instanceof SaCheckRole checkRole) {
|
||||
values = checkRole.value();
|
||||
mode = checkRole.mode();
|
||||
type = checkRole.type();
|
||||
}
|
||||
|
||||
if (values != null && mode != null) {
|
||||
builder.append("<font style=\"color:red\" class=\"light-red\">");
|
||||
builder.append(prefix);
|
||||
if (!type.isEmpty()) {
|
||||
builder.append("(类型:").append(type).append(")");
|
||||
}
|
||||
builder.append(getAnnotationNote(values, mode));
|
||||
if (orRole.length > 0) {
|
||||
builder.append(" 或 角色校验(").append(getAnnotationNote(orRole, mode)).append(")");
|
||||
}
|
||||
builder.append("</font></br>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据注解的模式拼接注解值
|
||||
*
|
||||
* @param values 注解的值数组
|
||||
* @param mode 注解的模式(AND 或 OR)
|
||||
* @return 拼接好的注解值字符串
|
||||
*/
|
||||
private String getAnnotationNote(String[] values, SaMode mode) {
|
||||
if (mode.equals(SaMode.AND)) {
|
||||
return String.join(" 且 ", values);
|
||||
} else {
|
||||
return String.join(" 或 ", values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息
|
||||
*
|
||||
* @param handlerMethod 处理程序方法
|
||||
* @return 拼接好的权限信息字符串
|
||||
*/
|
||||
private String getCrudPermissionInfo(HandlerMethod handlerMethod) {
|
||||
CrudRequestMapping crudRequestMapping = handlerMethod.getBeanType().getAnnotation(CrudRequestMapping.class);
|
||||
CrudApi crudApi = handlerMethod.getMethodAnnotation(CrudApi.class);
|
||||
|
||||
if (crudRequestMapping == null || crudApi == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String path = crudRequestMapping.value();
|
||||
String prefix = String.join(StringConstants.COLON, CharSequenceUtil.splitTrim(path, StringConstants.SLASH));
|
||||
Api api = crudApi.value();
|
||||
String apiName = Api.PAGE.equals(api) || Api.TREE.equals(api) ? Api.LIST.name() : api.name();
|
||||
String permission = "%s:%s".formatted(prefix, apiName.toLowerCase());
|
||||
|
||||
return "<font style=\"color:red\" class=\"light-red\">Crud 权限校验:</font></br><font style=\"color:red\" class=\"light-red\">方法:</font><font style=\"color:red\" class=\"light-red\">" + permission + "</font>";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package top.ysoft.admin.common.config.exception;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MultipartException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import top.continew.starter.core.exception.BadRequestException;
|
||||
import top.continew.starter.core.exception.BusinessException;
|
||||
import top.continew.starter.web.model.R;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author echo
|
||||
* @since 2024/8/7 20:21
|
||||
*/
|
||||
@Slf4j
|
||||
@Order(99)
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 拦截业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public R handleBusinessException(BusinessException e, HttpServletRequest request) {
|
||||
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return R.fail(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截自定义验证异常-错误请求
|
||||
*/
|
||||
@ExceptionHandler(BadRequestException.class)
|
||||
public R handleBadRequestException(BadRequestException e, HttpServletRequest request) {
|
||||
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截校验异常-方法参数类型不匹配异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e,
|
||||
HttpServletRequest request) {
|
||||
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), "参数 '%s' 类型不匹配".formatted(e.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截文件上传异常-超过上传大小限制
|
||||
*/
|
||||
@ExceptionHandler(MultipartException.class)
|
||||
public R handleMultipartException(MultipartException e, HttpServletRequest request) {
|
||||
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
|
||||
String msg = e.getMessage();
|
||||
R defaultFail = R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), msg);
|
||||
if (CharSequenceUtil.isBlank(msg)) {
|
||||
return defaultFail;
|
||||
}
|
||||
String sizeLimit;
|
||||
Throwable cause = e.getCause();
|
||||
if (null != cause) {
|
||||
msg = msg.concat(cause.getMessage().toLowerCase());
|
||||
}
|
||||
if (msg.contains("larger than")) {
|
||||
sizeLimit = CharSequenceUtil.subAfter(msg, "larger than ", true);
|
||||
} else if (msg.contains("size") && msg.contains("exceed")) {
|
||||
sizeLimit = CharSequenceUtil.subBetween(msg, "the maximum size ", " for");
|
||||
} else {
|
||||
return defaultFail;
|
||||
}
|
||||
return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), "请上传小于 %s 的文件".formatted(FileUtil
|
||||
.readableFileSize(Long.parseLong(sizeLimit))));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截请求 URL 不存在异常
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public R handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
|
||||
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return R.fail(String.valueOf(HttpStatus.NOT_FOUND.value()), "请求 URL '%s' 不存在".formatted(request
|
||||
.getRequestURI()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截不支持的 HTTP 请求方法异常
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public R handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e,
|
||||
HttpServletRequest request) {
|
||||
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return R.fail(String.valueOf(HttpStatus.METHOD_NOT_ALLOWED.value()), "请求方式 '%s' 不支持".formatted(e.getMethod()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package top.ysoft.admin.common.config.exception;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.exception.NotPermissionException;
|
||||
import cn.dev33.satoken.exception.NotRoleException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import top.continew.starter.web.model.R;
|
||||
|
||||
/**
|
||||
* 全局 SaToken 异常处理器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2024/8/7 20:21
|
||||
*/
|
||||
@Slf4j
|
||||
@Order(99)
|
||||
@RestControllerAdvice
|
||||
public class GlobalSaTokenExceptionHandler {
|
||||
|
||||
/**
|
||||
* 认证异常-登录认证
|
||||
*/
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
public R handleNotLoginException(NotLoginException e, HttpServletRequest request) {
|
||||
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
|
||||
String errorMsg = switch (e.getType()) {
|
||||
case NotLoginException.KICK_OUT -> "您已被踢下线";
|
||||
case NotLoginException.BE_REPLACED_MESSAGE -> "您已被顶下线";
|
||||
default -> "您的登录状态已过期,请重新登录";
|
||||
};
|
||||
return R.fail(String.valueOf(HttpStatus.UNAUTHORIZED.value()), errorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证异常-权限认证
|
||||
*/
|
||||
@ExceptionHandler(NotPermissionException.class)
|
||||
public R handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
|
||||
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return R.fail(String.valueOf(HttpStatus.FORBIDDEN.value()), "没有访问权限,请联系管理员授权");
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证异常-角色认证
|
||||
*/
|
||||
@ExceptionHandler(NotRoleException.class)
|
||||
public R handleNotRoleException(NotRoleException e, HttpServletRequest request) {
|
||||
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return R.fail(String.valueOf(HttpStatus.FORBIDDEN.value()), "没有访问权限,请联系管理员授权");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package top.ysoft.admin.common.config.mybatis;
|
||||
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import top.continew.starter.security.crypto.encryptor.IEncryptor;
|
||||
|
||||
/**
|
||||
* BCrypt 加/解密处理器(不可逆)
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2024/2/8 22:29
|
||||
*/
|
||||
public class BCryptEncryptor implements IEncryptor {
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public BCryptEncryptor(PasswordEncoder passwordEncoder) {
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encrypt(String plaintext, String password, String publicKey) throws Exception {
|
||||
return passwordEncoder.encode(plaintext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String ciphertext, String password, String privateKey) throws Exception {
|
||||
return ciphertext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package top.ysoft.admin.common.config.mybatis;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import top.continew.starter.data.mp.base.BaseMapper;
|
||||
import top.continew.starter.extension.datapermission.annotation.DataPermission;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据权限 Mapper 基类
|
||||
*
|
||||
* @param <T> 实体类
|
||||
* @author Charles7c
|
||||
* @since 2023/9/3 21:50
|
||||
*/
|
||||
public interface DataPermissionMapper<T> extends BaseMapper<T> {
|
||||
|
||||
/**
|
||||
* 根据 entity 条件,查询全部记录
|
||||
*
|
||||
* @param queryWrapper 实体对象封装操作类(可以为 null)
|
||||
* @return 全部记录
|
||||
*/
|
||||
@Override
|
||||
@DataPermission
|
||||
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
|
||||
|
||||
/**
|
||||
* 根据 entity 条件,查询全部记录(并翻页)
|
||||
*
|
||||
* @param page 分页查询条件
|
||||
* @param queryWrapper 实体对象封装操作类(可以为 null)
|
||||
* @return 全部记录(并翻页)
|
||||
*/
|
||||
@Override
|
||||
@DataPermission
|
||||
List<T> selectList(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package top.ysoft.admin.common.config.mybatis;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import top.ysoft.admin.common.context.UserContextHolder;
|
||||
import top.continew.starter.extension.datapermission.enums.DataScope;
|
||||
import top.continew.starter.extension.datapermission.filter.DataPermissionUserContextProvider;
|
||||
import top.continew.starter.extension.datapermission.model.RoleContext;
|
||||
import top.continew.starter.extension.datapermission.model.UserContext;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 数据权限用户上下文提供者
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/12/21 21:19
|
||||
*/
|
||||
public class DefaultDataPermissionUserContextProvider implements DataPermissionUserContextProvider {
|
||||
|
||||
@Override
|
||||
public boolean isFilter() {
|
||||
return !UserContextHolder.isAdmin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserContext getUserContext() {
|
||||
top.ysoft.admin.common.context.UserContext context = UserContextHolder.getContext();
|
||||
UserContext userContext = new UserContext();
|
||||
userContext.setUserId(Convert.toStr(context.getId()));
|
||||
userContext.setDeptId(Convert.toStr(context.getDeptId()));
|
||||
userContext.setRoles(context.getRoles()
|
||||
.stream()
|
||||
.map(r -> new RoleContext(Convert.toStr(r.getId()), DataScope.valueOf(r.getDataScope().name())))
|
||||
.collect(Collectors.toSet()));
|
||||
return userContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package top.ysoft.admin.common.config.mybatis;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import top.ysoft.admin.common.context.UserContextHolder;
|
||||
import top.ysoft.admin.common.model.entity.BaseDO;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis Plus 元对象处理器配置(插入或修改时自动填充)
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/22 19:52
|
||||
*/
|
||||
public class MyBatisPlusMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private static final String CREATE_USER = "createUser";
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private static final String CREATE_TIME = "createTime";
|
||||
/**
|
||||
* 修改人
|
||||
*/
|
||||
private static final String UPDATE_USER = "updateUser";
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private static final String UPDATE_TIME = "updateTime";
|
||||
|
||||
/**
|
||||
* 插入数据时填充
|
||||
*
|
||||
* @param metaObject 元对象
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
if (null == metaObject) {
|
||||
return;
|
||||
}
|
||||
Long createUser = UserContextHolder.getUserId();
|
||||
LocalDateTime createTime = LocalDateTime.now();
|
||||
if (metaObject.getOriginalObject() instanceof BaseDO baseDO) {
|
||||
// 继承了 BaseDO 的类,填充创建信息字段
|
||||
baseDO.setCreateUser(ObjectUtil.defaultIfNull(baseDO.getCreateUser(), createUser));
|
||||
baseDO.setCreateTime(ObjectUtil.defaultIfNull(baseDO.getCreateTime(), createTime));
|
||||
} else {
|
||||
// 未继承 BaseDO 的类,如存在创建信息字段则进行填充
|
||||
this.fillFieldValue(metaObject, CREATE_USER, createUser, false);
|
||||
this.fillFieldValue(metaObject, CREATE_TIME, createTime, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改数据时填充
|
||||
*
|
||||
* @param metaObject 元对象
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
if (null == metaObject) {
|
||||
return;
|
||||
}
|
||||
Long updateUser = UserContextHolder.getUserId();
|
||||
LocalDateTime updateTime = LocalDateTime.now();
|
||||
if (metaObject.getOriginalObject() instanceof BaseDO baseDO) {
|
||||
// 继承了 BaseDO 的类,填充修改信息
|
||||
baseDO.setUpdateUser(updateUser);
|
||||
baseDO.setUpdateTime(updateTime);
|
||||
} else {
|
||||
// 未继承 BaseDO 的类,根据类中拥有的修改信息字段进行填充,不存在修改信息字段不进行填充
|
||||
this.fillFieldValue(metaObject, UPDATE_USER, updateUser, true);
|
||||
this.fillFieldValue(metaObject, UPDATE_TIME, updateTime, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充字段值
|
||||
*
|
||||
* @param metaObject 元数据对象
|
||||
* @param fieldName 要填充的字段名
|
||||
* @param fillFieldValue 要填充的字段值
|
||||
* @param isOverride 如果字段值不为空,是否覆盖(true:覆盖;false:不覆盖)
|
||||
*/
|
||||
private void fillFieldValue(MetaObject metaObject, String fieldName, Object fillFieldValue, boolean isOverride) {
|
||||
if (metaObject.hasSetter(fieldName)) {
|
||||
Object fieldValue = metaObject.getValue(fieldName);
|
||||
setFieldValByName(fieldName, null != fieldValue && !isOverride ? fieldValue : fillFieldValue, metaObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package top.ysoft.admin.common.config.mybatis;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import top.continew.starter.extension.datapermission.filter.DataPermissionUserContextProvider;
|
||||
|
||||
/**
|
||||
* MyBatis Plus 配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/22 19:51
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfiguration {
|
||||
|
||||
/**
|
||||
* 元对象处理器配置(插入或修改时自动填充)
|
||||
*/
|
||||
@Bean
|
||||
public MetaObjectHandler metaObjectHandler() {
|
||||
return new MyBatisPlusMetaObjectHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据权限用户上下文提供者
|
||||
*/
|
||||
@Bean
|
||||
public DataPermissionUserContextProvider dataPermissionUserContextProvider() {
|
||||
return new DefaultDataPermissionUserContextProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* BCrypt 加/解密处理器
|
||||
*/
|
||||
@Bean
|
||||
public BCryptEncryptor bCryptEncryptor(PasswordEncoder passwordEncoder) {
|
||||
return new BCryptEncryptor(passwordEncoder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package top.ysoft.admin.common.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 验证码配置属性
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/11 13:35
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "captcha")
|
||||
public class CaptchaProperties {
|
||||
|
||||
/**
|
||||
* 图形验证码过期时间
|
||||
*/
|
||||
@Value("${continew-starter.captcha.graphic.expirationInMinutes}")
|
||||
private long expirationInMinutes;
|
||||
|
||||
/**
|
||||
* 邮箱验证码配置
|
||||
*/
|
||||
private CaptchaMail mail;
|
||||
|
||||
/**
|
||||
* 短信验证码配置
|
||||
*/
|
||||
private CaptchaSms sms;
|
||||
|
||||
/**
|
||||
* 邮箱验证码配置
|
||||
*/
|
||||
@Data
|
||||
public static class CaptchaMail {
|
||||
/**
|
||||
* 内容长度
|
||||
*/
|
||||
private int length;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
private long expirationInMinutes;
|
||||
|
||||
/**
|
||||
* 模板路径
|
||||
*/
|
||||
private String templatePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码配置
|
||||
*/
|
||||
@Data
|
||||
public static class CaptchaSms {
|
||||
/**
|
||||
* 内容长度
|
||||
*/
|
||||
private int length;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
private long expirationInMinutes;
|
||||
|
||||
/**
|
||||
* 模板 ID
|
||||
*/
|
||||
private String templateId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package top.ysoft.admin.common.config.properties;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
|
||||
/**
|
||||
* RSA 配置属性
|
||||
*
|
||||
* @author Zheng Jie(ELADMIN)
|
||||
* @author Charles7c
|
||||
* @since 2022/12/21 20:21
|
||||
*/
|
||||
public class RsaProperties {
|
||||
|
||||
/**
|
||||
* 私钥
|
||||
*/
|
||||
public static final String PRIVATE_KEY;
|
||||
public static final String PUBLIC_KEY;
|
||||
|
||||
static {
|
||||
PRIVATE_KEY = SpringUtil.getProperty("continew-starter.security.crypto.private-key");
|
||||
PUBLIC_KEY = SpringUtil.getProperty("continew-starter.security.crypto.public-key");
|
||||
}
|
||||
|
||||
private RsaProperties() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package top.ysoft.admin.common.config.websocket;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.continew.starter.core.exception.BusinessException;
|
||||
import top.continew.starter.messaging.websocket.core.WebSocketClientService;
|
||||
|
||||
/**
|
||||
* 当前登录用户 Provider
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2024/6/4 22:13
|
||||
*/
|
||||
@Component
|
||||
public class WebSocketClientServiceImpl implements WebSocketClientService {
|
||||
|
||||
@Override
|
||||
public String getClientId(ServletServerHttpRequest request) {
|
||||
HttpServletRequest servletRequest = request.getServletRequest();
|
||||
String token = servletRequest.getParameter("token");
|
||||
if (null == StpUtil.getLoginIdByToken(token)) {
|
||||
throw new BusinessException("登录已过期,请重新登录");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user