Initial commit

This commit is contained in:
jackyu66git
2026-03-23 11:52:51 +08:00
commit ea8d889fbe
1114 changed files with 92438 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>oneone-common</artifactId>
<groupId>com.oneone.cloud</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-web</artifactId>
<dependencies>
<dependency>
<groupId>com.oneone.cloud</groupId>
<artifactId>common-core</artifactId>
</dependency>
<dependency>
<groupId>com.oneone.cloud</groupId>
<artifactId>oneone-upms-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--配置文件处理器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--undertow容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId> <!-- 接口文档 -->
<artifactId>springdoc-openapi-ui</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.16.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-core</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
</dependency>
</dependencies>
</project>
@@ -0,0 +1,14 @@
package com.oneone.common.web.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@Component
@RefreshScope
@Data
public class Configs {
@Value("${innerToken:xx}")
private String innerToken;
}
@@ -0,0 +1,95 @@
package com.oneone.common.web.config;
import feign.RequestInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.DispatcherServlet;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import static com.oneone.common.constant.GlobalConstants.FEIGN_TOKEN;
import static com.oneone.common.constant.GlobalConstants.USER_INFO;
/**
* Feign相关配置类
*
* @author oneone
* @since 2021-08-06 9:47
*/
//@ComponentScan("com.oneone.common.ruoyi.api")
//@EnableFeignClients(clients = {RemoteAuthService.class
//})
//@EnableFeignClients("com.oneone.common.ruoyi.api")
@Configuration
public class FeignConfig {
@Resource
Configs configs;
/**
* 让DispatcherServlet向子线程传递RequestContext
*
* @param servlet servlet
* @return 注册bean
*/
@Bean
public ServletRegistrationBean<DispatcherServlet> dispatcherRegistration(DispatcherServlet servlet) {
servlet.setThreadContextInheritable(true);
return new ServletRegistrationBean<>(servlet, "/**" );
}
/**
* 覆写拦截器,在feign发送请求前取出原来的header并转发
*
* @return 拦截器
*/
@Bean
public RequestInterceptor requestInterceptor() {
return (template) -> {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (servletRequestAttributes == null) {
return;
}
HttpServletRequest request = servletRequestAttributes.getRequest();
if (request == null) {
return;
}
// 标注feign请求头
template.header(FEIGN_TOKEN, configs.getInnerToken());
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
if (checkEssential(name)) {
template.header(name, values);
}
}
}
};
}
List<String> headers = Arrays.asList(USER_INFO);
/**
* 请求头必要传递参数过滤,只携带必要请求头信息
*
* @param name
* @return
*/
private boolean checkEssential(String name) {
return headers.stream().anyMatch(e -> e.equalsIgnoreCase(name));
}
}
@@ -0,0 +1,16 @@
package com.oneone.common.web.config;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HttpClientConfig {
@Bean
public HttpClient httpClient() {
return HttpClientBuilder.create().build();
}
}
@@ -0,0 +1,26 @@
package com.oneone.common.web.config;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
/**
* Metrics 配置类
*
*/
@AutoConfiguration
@ConditionalOnClass({MeterRegistryCustomizer.class})
@ConditionalOnProperty(prefix = "oneone.metrics", value = "enable", matchIfMissing = true) // 允许使用 oneone.metrics.enable=false 禁用 Metrics
public class MetricsAutoConfiguration {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags(
@Value("${spring.application.name}") String applicationName) {
return registry -> registry.config().commonTags("application", applicationName);
}
}
@@ -0,0 +1,18 @@
package com.oneone.common.web.config;
import com.oneone.common.web.security.service.SecurityService;
import com.oneone.common.web.security.service.SecurityServiceImpl;
import com.oneone.upms.api.permission.PermissionApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class SecurityConfiguration {
@Bean("securityService")
public SecurityService securityService(PermissionApi permissionApi) {
return new SecurityServiceImpl(permissionApi);
}
}
@@ -0,0 +1,35 @@
package com.oneone.common.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author mice
* @version 1.0
* @date 2022-04-14 21:00
*/
@Configuration
public class ThreadPoolConfig {
@Bean("asyncExecutor")
public ThreadPoolTaskExecutor asyncExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//创建核心线程数量
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
//最大核心线程数
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
//队列长度(超过会创建工作线程)
executor.setQueueCapacity(10240);
//线程名称前缀,用于监控识别
executor.setThreadNamePrefix("async-task-");
//设置线程保持活跃的时间(默认:60)
executor.setKeepAliveSeconds(60);
//设置任务拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
@@ -0,0 +1,29 @@
package com.oneone.common.web.config;
import com.oneone.common.web.trace.TraceFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
/**
* Tracer 配置类
*
*/
@AutoConfiguration
@ConditionalOnProperty(prefix = "oneone.tracer", value = "enable", matchIfMissing = true)
public class TracerAutoConfiguration {
/**
* 创建 TraceFilter 过滤器,响应 header 设置 traceId
*/
@Bean
public FilterRegistrationBean<TraceFilter> traceFilter() {
FilterRegistrationBean<TraceFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new TraceFilter());
registrationBean.setOrder(1);
return registrationBean;
}
}
@@ -0,0 +1,84 @@
package com.oneone.common.web.config;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.oneone.common.web.interceptor.AuthInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.TimeZone;
@Configuration
@Slf4j
@EnableConfigurationProperties(WebProperties.class)
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private AuthInterceptor authInterceptor;
@Resource
private WebProperties webProperties;
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
// 后台Long值传递给前端精度丢失问题(JS最大精度整数是Math.pow(2,53)
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
// simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
// simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
converters.add(1, jackson2HttpMessageConverter);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.authInterceptor).addPathPatterns("/**").excludePathPatterns("/swagger-resources/**" , "/webjars/**" , "/v2/api-docs" , "/doc.html/**");
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurePathMatch(configurer, webProperties.getAdminApi());
configurePathMatch(configurer, webProperties.getAppApi());
}
/**
* 设置 API 前缀,仅仅匹配 controller 包下的
*
* @param configurer 配置
* @param api API 配置
*/
private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class)
&& antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(4);
}
}
@@ -0,0 +1,65 @@
package com.oneone.common.web.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@ConfigurationProperties(prefix = "oneone.web")
@Validated
@Data
public class WebProperties {
@NotNull(message = "APP API 不能为空")
private Api appApi = new Api("/app-api", "**.controller.app.**");
@NotNull(message = "Admin API 不能为空")
private Api adminApi = new Api("/admin-api", "**.controller.admin.**");
@NotNull(message = "Admin UI 不能为空")
private Ui adminUi;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Valid
public static class Api {
/**
* API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
*
*
* 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
* 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
*
*/
@NotEmpty(message = "API 前缀不能为空")
private String prefix;
/**
* Controller 所在包的 Ant 路径规则
*
* 主要目的是,给该 Controller 设置指定的 {@link #prefix}
*/
@NotEmpty(message = "Controller 所在包不能为空")
private String controller;
}
@Data
@Valid
public static class Ui {
/**
* 访问地址
*/
private String url;
}
}
@@ -0,0 +1,231 @@
package com.oneone.common.web.exception;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.oneone.common.exception.BusinessException;
import com.oneone.common.result.Result;
import com.oneone.common.result.ResultCode;
import feign.FeignException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.servlet.ServletException;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.sql.SQLSyntaxErrorException;
import java.util.concurrent.CompletionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 全局系统异常处理
* 调整异常处理的HTTP状态码,丰富异常处理类型
*
**/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 表单绑定到 java bean 出错时抛出 BindException 异常
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(BindException.class)
public <T> Result<T> processException(BindException e) {
log.error(e.getMessage(), e);
JSONObject msg = new JSONObject();
e.getAllErrors().forEach(error -> {
if (error instanceof FieldError) {
FieldError fieldError = (FieldError) error;
msg.set(fieldError.getField(),
fieldError.getDefaultMessage());
} else {
msg.set(error.getObjectName(),
error.getDefaultMessage());
}
});
return Result.failed(ResultCode.PARAM_ERROR, msg.toString());
}
/**
* 普通参数(非 java bean)校验出错时抛出 ConstraintViolationException 异常
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(ConstraintViolationException.class)
public <T> Result<T> processException(ConstraintViolationException e) {
log.error(e.getMessage(), e);
JSONObject msg = new JSONObject();
e.getConstraintViolations().forEach(constraintViolation -> {
String template = constraintViolation.getMessage();
String path = constraintViolation.getPropertyPath().toString();
msg.set(path, template);
});
return Result.failed(ResultCode.PARAM_ERROR, msg.toString());
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(ValidationException.class)
public <T> Result<T> processException(ValidationException e) {
log.error(e.getMessage(), e);
return Result.failed(ResultCode.PARAM_ERROR, "参数校验失败");
}
/**
* NoHandlerFoundException
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NoHandlerFoundException.class)
public <T> Result<T> processException(NoHandlerFoundException e) {
log.error(e.getMessage(), e);
return Result.failed(ResultCode.RESOURCE_NOT_FOUND);
}
/**
* MissingServletRequestParameterException
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(MissingServletRequestParameterException.class)
public <T> Result<T> processException(MissingServletRequestParameterException e) {
log.error(e.getMessage(), e);
return Result.failed(ResultCode.PARAM_IS_NULL);
}
/**
* MethodArgumentTypeMismatchException
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
log.error(e.getMessage(), e);
return Result.failed(ResultCode.PARAM_ERROR, "类型错误");
}
/**
* ServletException
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(ServletException.class)
public <T> Result<T> processException(ServletException e) {
log.error(e.getMessage(), e);
return Result.failed(e.getMessage());
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(IllegalArgumentException.class)
public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
log.error("非法参数异常,异常原因:{}", e.getMessage(), e);
return Result.failed(e.getMessage());
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(JsonProcessingException.class)
public <T> Result<T> handleJsonProcessingException(JsonProcessingException e) {
log.error("Json转换异常,异常原因:{}", e.getMessage(), e);
return Result.failed(e.getMessage());
}
/**
* HttpMessageNotReadableException
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(HttpMessageNotReadableException.class)
public <T> Result<T> processException(HttpMessageNotReadableException e) {
log.error(e.getMessage(), e);
String errorMessage = "请求体不可为空";
Throwable cause = e.getCause();
if (cause != null) {
errorMessage = convertMessage(cause);
}
return Result.failed(errorMessage);
}
/**
* TypeMismatchException
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(TypeMismatchException.class)
public <T> Result<T> processException(TypeMismatchException e) {
log.error(e.getMessage(), e);
return Result.failed(e.getMessage());
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(SQLSyntaxErrorException.class)
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
log.error(e.getMessage(), e);
String errorMsg = e.getMessage();
if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) {
return Result.failed("数据库用户无操作权限,建议本地搭建数据库环境");
} else {
return Result.failed(e.getMessage());
}
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(CompletionException.class)
public <T> Result<T> processException(CompletionException e) {
if (e.getMessage().startsWith("feign.FeignException")) {
return Result.failed("微服务调用异常");
}
return handleException(e);
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(FeignException.BadRequest.class)
public <T> Result<T> processException(FeignException.BadRequest e) {
log.error("微服务feign调用异常:", e);
return Result.failed(e.getMessage());
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(BusinessException.class)
public <T> Result<T> handleBizException(BusinessException e) {
log.error("业务异常,异常原因:{}", e.getMessage(), e);
if (e.getResultCode() != null) {
return Result.failed(e.getResultCode());
}
if (e.getCode() != null) {
return Result.failed(e.getCode(), e.getMessage());
}
return Result.failed(e.getMessage());
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(Exception.class)
public <T> Result<T> handleException(Exception e) {
log.error("==>系统异常{}", e.getMessage(), e);
return Result.failed();
}
/**
* 传参类型错误时,用于消息转换
*
* @param throwable 异常
* @return 错误信息
*/
private String convertMessage(Throwable throwable) {
String error = throwable.toString();
String regulation = "\\[\"(.*?)\"]+";
Pattern pattern = Pattern.compile(regulation);
Matcher matcher = pattern.matcher(error);
String group = "";
if (matcher.find()) {
String matchString = matcher.group();
matchString = matchString
.replace("[", "")
.replace("]", "");
matchString = matchString.replaceAll("\\\"", "") + "字段类型错误";
group += matchString;
}
return group;
}
}
@@ -0,0 +1,50 @@
package com.oneone.common.web.interceptor;
import com.oneone.common.base.LocalUser;
import com.oneone.common.constant.SecurityConstants;
import com.oneone.common.util.JsonUtils;
import com.oneone.common.web.util.UserContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLDecoder;
import static com.oneone.common.constant.GlobalConstants.USER_INFO;
/**
* @author mice
* @version 1.0
* @date 2021/11/26 23:26
*/
@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String user_info = request.getHeader(USER_INFO);
if (StringUtils.isNotEmpty(user_info)){
user_info = URLDecoder.decode(user_info, "UTF-8");
LocalUser user = JsonUtils.strToClass(user_info,LocalUser.class);
UserContext.setUser(user);
}else{
String sysUserId = request.getHeader(SecurityConstants.DETAILS_USER_ID);
if(StringUtils.isNotEmpty(sysUserId)){
LocalUser user=new LocalUser();
user.setUserId(new Long(sysUserId));
UserContext.setUser(user);
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
UserContext.clear();
}
}
@@ -0,0 +1,60 @@
package com.oneone.common.web.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @description: 优雅停机 提前从nacos主动下线服务
* @author: liubo
* @create: 2023/04/23 10:00
*/
@Slf4j
@Component
public class NacosShutdownListener implements ApplicationListener<ContextClosedEvent>, PriorityOrdered {
@Resource
private AbstractAutoServiceRegistration autoServiceRegistration;
private AtomicInteger contextClosedEventCount = new AtomicInteger();
@Value("${nacos-discovery.per-shutdown-time:35}")
private int waitTime;
@Override
public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
if (contextClosedEventCount.incrementAndGet() > 1) {
log.info("Frequent request");
return;
}
log.info("Nacos initiative DOWN Start!");
try {
autoServiceRegistration.stop();
//responseEntity = serviceRegistryEndpoint.setStatus("DOWN");
} catch (Exception e) {
log.error("Nacos initiative DOWN ServiceRegistryEndpoint setStatus error:{}", e);
}
log.info("Nacos initiative DOWN End!");
log.info("Start sleep {}S for cache!", waitTime);
try {
TimeUnit.SECONDS.sleep(waitTime);
} catch (InterruptedException e) {
log.warn("End sleep {}S for cache!", waitTime, e);
}
log.info("End sleep {}S for cache!", waitTime);
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}
@@ -0,0 +1,20 @@
package com.oneone.common.web.security.annotation;
/**
* 权限注解的验证模式
*
* @author ruoyi
*
*/
public enum Logical
{
/**
* 必须具有所有的元素
*/
AND,
/**
* 只需具有其中一个元素
*/
OR
}
@@ -0,0 +1,17 @@
package com.oneone.common.web.security.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 登录认证:只有登录之后才能进入该方法
*
* @author ruoyi
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresLogin {
}
@@ -0,0 +1,25 @@
package com.oneone.common.web.security.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 权限认证:必须具有指定权限才能进入该方法
*
* @author ruoyi
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresPermissions {
/**
* 需要校验的权限码
*/
String[] value() default {};
/**
* 验证模式:AND | OR,默认AND
*/
Logical logical() default Logical.AND;
}
@@ -0,0 +1,26 @@
package com.oneone.common.web.security.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 角色认证:必须具有指定角色标识才能进入该方法
*
* @author ruoyi
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresRoles
{
/**
* 需要校验的角色标识
*/
String[] value() default {};
/**
* 验证逻辑:AND | OR,默认AND
*/
Logical logical() default Logical.AND;
}
@@ -0,0 +1,117 @@
package com.oneone.common.web.security.aspect;
import com.oneone.common.exception.BusinessException;
import com.oneone.common.web.config.Configs;
import com.oneone.common.web.security.annotation.Logical;
import com.oneone.common.web.security.annotation.RequiresLogin;
import com.oneone.common.web.security.annotation.RequiresPermissions;
import com.oneone.common.web.security.annotation.RequiresRoles;
import com.oneone.common.web.security.service.SecurityService;
import com.oneone.common.web.util.UserContext;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
/**
* 基于 Spring Aop 的注解鉴权
*
* @author kong
*/
@Aspect
@Component
public class PreAuthorizeAspect {
@Resource
private SecurityService securityService;
@Resource
Configs configs;
/**
* 构建
*/
public PreAuthorizeAspect() {
}
/**
* 定义AOP签名 (切入所有使用鉴权注解的方法)
*/
public static final String POINTCUT_SIGN = " @annotation(com.oneone.common.web.security.annotation.RequiresLogin) || "
+ "@annotation(com.oneone.common.web.security.annotation.RequiresPermissions) || "
+ "@annotation(com.oneone.common.web.security.annotation.RequiresRoles)";
/**
* 声明AOP签名
*/
@Pointcut(POINTCUT_SIGN)
public void pointcut() {
}
/**
* 环绕切入
*
* @param joinPoint 切面对象
* @return 底层方法执行后的返回值
* @throws Throwable 底层方法抛出的异常
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 注解鉴权
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
checkMethodAnnotation(signature.getMethod());
try {
// 执行原有逻辑
Object obj = joinPoint.proceed();
return obj;
} catch (Throwable e) {
throw e;
}
}
/**
* 对一个Method对象进行注解检查
*/
public void checkMethodAnnotation(Method method) {
// 校验 @RequiresLogin 注解
RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
if (requiresLogin != null) {
if (UserContext.getUserIdIfPresentDefault().equals(UserContext.DEFAULT_VALUE)){
throw new BusinessException("权限校验失败");
}
return;
}
boolean hasAuth = false;
// 校验 @RequiresRoles 注解
RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
if (requiresRoles != null) {
if (requiresRoles.logical() == Logical.AND) {
hasAuth = securityService.hasRole(requiresRoles.value()[0]);
}else {
hasAuth = securityService.hasAnyRoles(requiresRoles.value());
}
if (!hasAuth){
throw new BusinessException("权限校验失败");
}
}
// 校验 @RequiresPermissions 注解
RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
if (requiresPermissions != null) {
if (requiresPermissions.logical() == Logical.AND) {
hasAuth = securityService.hasPermission(requiresPermissions.value()[0]);
}else {
hasAuth = securityService.hasAnyPermissions(requiresPermissions.value());
}
if (!hasAuth){
throw new BusinessException("权限校验失败");
}
}
}
}
@@ -0,0 +1,58 @@
package com.oneone.common.web.security.service;
/**
* Security 框架 Service 接口,定义权限相关的校验操作
*
*/
public interface SecurityService {
/**
* 判断是否有权限
*
* @param permission 权限
* @return 是否
*/
boolean hasPermission(String permission);
/**
* 判断是否有权限,任一一个即可
*
* @param permissions 权限
* @return 是否
*/
boolean hasAnyPermissions(String... permissions);
/**
* 判断是否有角色
*
* 注意,角色使用的是 SysRoleDO 的 code 标识
*
* @param role 角色
* @return 是否
*/
boolean hasRole(String role);
/**
* 判断是否有角色,任一一个即可
*
* @param roles 角色数组
* @return 是否
*/
boolean hasAnyRoles(String... roles);
/**
* 判断是否有授权
*
* @param scope 授权
* @return 是否
*/
boolean hasScope(String scope);
/**
* 判断是否有授权范围,任一一个即可
*
* @param scope 授权范围数组
* @return 是否
*/
boolean hasAnyScopes(String... scope);
}
@@ -0,0 +1,86 @@
package com.oneone.common.web.security.service;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.oneone.common.base.KeyValue;
import com.oneone.common.util.CacheUtils;
import com.oneone.common.web.util.UserContext;
import com.oneone.upms.api.permission.PermissionApi;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
/**
* 默认的 {@link SecurityService} 实现类
*
*/
@AllArgsConstructor
public class SecurityServiceImpl implements SecurityService {
private final PermissionApi permissionApi;
/**
* 针对 {@link #hasAnyRoles(String...)} 的缓存
*/
private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyRolesCache = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<KeyValue<Long, List<String>>, Boolean>() {
@Override
public Boolean load(KeyValue<Long, List<String>> key) {
return permissionApi.hasAnyRoles(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData();
}
});
/**
* 针对 {@link #hasAnyPermissions(String...)} 的缓存
*/
private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyPermissionsCache = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<KeyValue<Long, List<String>>, Boolean>() {
@Override
public Boolean load(KeyValue<Long, List<String>> key) {
return permissionApi.hasAnyPermissions(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData();
}
});
@Override
public boolean hasPermission(String permission) {
return hasAnyPermissions(permission);
}
@Override
@SneakyThrows
public boolean hasAnyPermissions(String... permissions) {
return hasAnyPermissionsCache.get(new KeyValue<>(UserContext.getUserId(), Arrays.asList(permissions)));
}
@Override
public boolean hasRole(String role) {
return hasAnyRoles(role);
}
@Override
@SneakyThrows
public boolean hasAnyRoles(String... roles) {
return hasAnyRolesCache.get(new KeyValue<>(UserContext.getUserId(), Arrays.asList(roles)));
}
@Override
public boolean hasScope(String scope) {
return hasAnyScopes(scope);
}
@Override
public boolean hasAnyScopes(String... scope) {
// TODO 暂未设计
return true;
}
}
@@ -0,0 +1,81 @@
package com.oneone.common.web.swagger.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.*;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Swagger 自动配置类,基于 OpenAPI + Springdoc 实现。
*
* 友情提示:
* 1. Springdoc 文档地址:<a href="https://github.com/springdoc/springdoc-openapi">仓库</a>
* 2. Swagger 规范,于 2015 更名为 OpenAPI 规范,本质是一个东西
*
*
*/
@AutoConfiguration
@ConditionalOnClass({OpenAPI.class})
@EnableConfigurationProperties(SwaggerProperties.class)
@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
public class SwaggerAutoConfiguration {
// ========== 全局 OpenAPI 配置 ==========
@Bean
public OpenAPI createApi(SwaggerProperties properties) {
OpenAPI openAPI = new OpenAPI()
.info(buildInfo(properties));
return openAPI;
}
/**
* API 摘要信息
*/
private Info buildInfo(SwaggerProperties properties) {
return new Info()
.title(properties.getTitle())
.description(properties.getDescription())
.version(properties.getVersion())
.contact(new Contact().name(properties.getAuthor()).url(properties.getUrl()).email(properties.getEmail()))
.license(new License().name(properties.getLicense()).url(properties.getLicenseUrl()));
}
/**
* 所有模块的 API 分组
*/
@Bean
public GroupedOpenApi allGroupedOpenApi() {
return buildGroupedOpenApi("all", "");
}
public static GroupedOpenApi buildGroupedOpenApi(String group, String path) {
return GroupedOpenApi.builder()
.group(group)
.pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**")
.build();
}
}
@@ -0,0 +1,60 @@
package com.oneone.common.web.swagger.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.validation.constraints.NotEmpty;
/**
* Swagger 配置属性
*
*
*/
@ConfigurationProperties("oneone.swagger")
@Data
public class SwaggerProperties {
/**
* 标题
*/
@NotEmpty(message = "标题不能为空")
private String title;
/**
* 描述
*/
@NotEmpty(message = "描述不能为空")
private String description;
/**
* 作者
*/
@NotEmpty(message = "作者不能为空")
private String author;
/**
* 版本
*/
@NotEmpty(message = "版本不能为空")
private String version;
/**
* url
*/
@NotEmpty(message = "扫描的 package 不能为空")
private String url;
/**
* email
*/
@NotEmpty(message = "扫描的 email 不能为空")
private String email;
/**
* license
*/
@NotEmpty(message = "扫描的 license 不能为空")
private String license;
/**
* license-url
*/
@NotEmpty(message = "扫描的 license-url 不能为空")
private String licenseUrl;
}
@@ -0,0 +1,33 @@
package com.oneone.common.web.trace;
import com.oneone.common.util.TracerUtils;
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Trace 过滤器,打印 traceId 到 header 中返回
*
*/
public class TraceFilter extends OncePerRequestFilter {
/**
* Header 名 - 链路追踪编号
*/
private static final String HEADER_NAME_TRACE_ID = "trace-id";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 设置响应 traceId
response.addHeader(HEADER_NAME_TRACE_ID, TracerUtils.getTraceId());
// 继续过滤
chain.doFilter(request, response);
}
}
@@ -0,0 +1,207 @@
package com.oneone.common.web.util;
import cn.hutool.core.net.URLEncodeUtil;
import com.oneone.common.util.JsonUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Map;
/**
* http请求工具类
*/
@Component
public class HttpClientUtil {
private static final String EMPTY_STR = "";
private static final String UTF_8 = "UTF-8";
@Resource
private HttpClient httpClient;
/**
* @param url
* @return
*/
public String doGet(String url) {
HttpGet httpGet = new HttpGet(url);
return getResult(httpGet);
}
public String doGet(String url, Map<String, Object> params) throws URISyntaxException {
URIBuilder ub = new URIBuilder();
ub.setPath(url);
ArrayList<NameValuePair> pairs = covertParams2NVPS(params);
ub.setParameters(pairs);
HttpGet httpGet = new HttpGet(ub.build());
return getResult(httpGet);
}
public String doGet(String url, Map<String, Object> headers, Map<String, Object> params)
throws URISyntaxException {
URIBuilder ub = new URIBuilder();
ub.setPath(url);
ArrayList<NameValuePair> pairs = covertParams2NVPS(params);
ub.setParameters(pairs);
HttpGet httpGet = new HttpGet(ub.build());
for (Map.Entry<String, Object> param : headers.entrySet()) {
httpGet.addHeader(param.getKey(), String.valueOf(param.getValue()));
}
return getResult(httpGet);
}
public String doPost(String url) {
HttpPost httpPost = new HttpPost(url);
return getResult(httpPost);
}
public String doPost(String url, Map<String, Object> params) throws UnsupportedEncodingException {
HttpPost httpPost = new HttpPost(url);
ArrayList<NameValuePair> pairs = covertParams2NVPS(params);
httpPost.setEntity(new UrlEncodedFormEntity(pairs, UTF_8));
return getResult(httpPost);
}
public String doPost(String url, Object param) {
HttpPost httpPost = new HttpPost(url);
//设置数据为utf-8编码
StringEntity entity = new StringEntity(JsonUtils.toJSONString(param), "utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置请求类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
//请求并得到结果
return getResult(httpPost);
}
public String doPost(String url, Map<String, Object> headers, Object obj) {
HttpPost httpPost = new HttpPost(url);
for (Map.Entry<String, Object> param : headers.entrySet()) {
httpPost.addHeader(param.getKey(), String.valueOf(param.getValue()));
}
//设置数据为utf-8编码
StringEntity entity = new StringEntity(JsonUtils.toJSONString(obj), "utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置请求类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
//请求并得到结果
return getResult(httpPost);
}
public String doPost(String url, MultipartFile file) {
HttpPost httpPost = new HttpPost(url);
// 创建 MultipartEntityBuilder 并添加文件部分
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
try {
builder.addBinaryBody("upload", file.getInputStream(), ContentType.APPLICATION_JSON, URLEncodeUtil.encode(file.getOriginalFilename()));
} catch (IOException e) {
throw new RuntimeException(e);
}
// 构建 HttpEntity
HttpEntity entity = builder.build();
httpPost.setEntity(entity);
return getResult(httpPost);
}
public HttpResponse doPostResp(String url, Map<String, Object> headers, Object obj) {
HttpPost httpPost = new HttpPost(url);
if (!CollectionUtils.isEmpty(headers)) {
for (Map.Entry<String, Object> param : headers.entrySet()) {
httpPost.addHeader(param.getKey(), String.valueOf(param.getValue()));
}
}
//设置数据为utf-8编码
StringEntity entity = new StringEntity(JsonUtils.toJSONString(obj), "utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置请求类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
//请求并得到结果
HttpResponse response = null;
try {
response = httpClient.execute(httpPost);
} catch (IOException e) {
throw new RuntimeException(e);
}
return response;
}
public String doPost(String url, Map<String, Object> headers, Map<String, Object> params)
throws UnsupportedEncodingException {
HttpPost httpPost = new HttpPost(url);
if (!CollectionUtils.isEmpty(headers)) {
for (Map.Entry<String, Object> param : headers.entrySet()) {
httpPost.addHeader(param.getKey(), String.valueOf(param.getValue()));
}
}
ArrayList<NameValuePair> pairs = covertParams2NVPS(params);
httpPost.setEntity(new UrlEncodedFormEntity(pairs, UTF_8));
return getResult(httpPost);
}
private ArrayList<NameValuePair> covertParams2NVPS(Map<String, Object> params) {
ArrayList<NameValuePair> pairs = new ArrayList<NameValuePair>();
for (Map.Entry<String, Object> param : params.entrySet()) {
pairs.add(new BasicNameValuePair(param.getKey(), String.valueOf(param.getValue())));
}
return pairs;
}
/**
* 处理Http请求
*
* @param request
* @return
*/
private String getResult(HttpRequestBase request) {
try {
HttpResponse response = httpClient.execute(request);
HttpEntity entity = response.getEntity();
if (entity != null) {
String result = EntityUtils.toString(entity, UTF_8);
return result;
}
} catch (Exception e) {
e.printStackTrace();
}
return EMPTY_STR;
}
}
@@ -0,0 +1,179 @@
package com.oneone.common.web.util;
import cn.hutool.core.io.resource.ResourceUtil;
import com.oneone.common.base.Area;
import com.oneone.common.util.AreaUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.*;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
@Slf4j
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
private static final String IP_UTILS_FLAG = ",";
private static final String UNKNOWN = "unknown";
private static final String LOCALHOST_IP = "0:0:0:0:0:0:0:1";
private static final String LOCALHOST_IP1 = "127.0.0.1";
/**
* 获取IP地址
* <p>
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddrByServlet() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = null;
try {
//以下两个获取在k8s中,将真实的客户端IP,放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。
ip = request.getHeader("X-Original-Forwarded-For");
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
//获取nginx等代理的ip
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("x-forwarded-for");
}
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
//兼容k8s集群获取ip
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {
//根据网卡取本机配置的IP
InetAddress iNet = null;
try {
iNet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
logger.error("getClientIp error: {}", e);
}
ip = iNet.getHostAddress();
}
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
//使用代理,则获取第一个IP地址
if (!StringUtils.isEmpty(ip) && ip.indexOf(IP_UTILS_FLAG) > 0) {
ip = ip.substring(0, ip.indexOf(IP_UTILS_FLAG));
}
return ip;
}
public static String getIpAddress(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
String ip = headers.getFirst("x-forwarded-for");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if (ip.indexOf(",") != -1) {
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = headers.getFirst("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = headers.getFirst("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = headers.getFirst("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = headers.getFirst("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddress().getAddress().getHostAddress();
}
return ip;
}
public static String getLocalIpv4() {
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
InetAddress ip = null;
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
//排除虚拟接口和没有启动运行的接口
if (netInterface.isVirtual() || !netInterface.isUp()) {
continue;
} else {
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
ip = addresses.nextElement();
if (ip != null && ip instanceof Inet4Address && !ip.isLoopbackAddress() && !ip.getHostAddress().contains(":")) {
return ip.getHostAddress();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static Set<String> getLocalIpAddress() {
Set<String> ipList = new HashSet<>();
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
InetAddress ip = null;
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
//排除虚拟接口和没有启动运行的接口
if (netInterface.isVirtual() || !netInterface.isUp()) {
continue;
} else {
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
ip = addresses.nextElement();
if (ip != null && (ip instanceof Inet4Address || ip instanceof Inet6Address)) {
ipList.add(ip.getHostAddress());
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return ipList;
}
public static String getUserAgent() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
if (request == null) {
return null;
}
String ua = request.getHeader("User-Agent");
return ua != null ? ua : "";
}
}
@@ -0,0 +1,37 @@
package com.oneone.common.web.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.oneone.common.constant.SecurityConstants;
import com.oneone.common.result.ResultCode;
import com.oneone.common.exception.BusinessException;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
/**
* JWT工具类
*
* @author oneone
* @date 2022/2/5
*/
@Slf4j
public class JwtUtils {
@SneakyThrows
public static JSONObject getJwtPayload() {
JSONObject jsonObject = null;
String payload = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader(SecurityConstants.JWT_PAYLOAD_KEY);
if (StrUtil.isNotBlank(payload)) {
jsonObject = JSONUtil.parseObj(URLDecoder.decode(payload, StandardCharsets.UTF_8.name()));
}else {
throw new BusinessException(ResultCode.TOKEN_INVALID_OR_EXPIRED);
}
return jsonObject;
}
}
@@ -0,0 +1,94 @@
package com.oneone.common.web.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* spring 工具类
*/
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
private static String host;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (this.applicationContext == null) {
this.applicationContext = applicationContext;
}
}
/**
* 获取applicationContext
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通过name获取 Bean.
*/
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* 通过class获取Bean.
*/
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
/**
* 获取指定注解的所有bean
*
* @param clazz
* @return
*/
public static Map<String, Object> getBeansWithAnnotation(Class clazz) {
return getApplicationContext().getBeansWithAnnotation(clazz);
}
public static Map<String, Object> getBeansOfType(Class clazz) {
return getApplicationContext().getBeansOfType(clazz);
}
/**
* 发布事件
*
* @param event
*/
public static void publishEvent(Object event) {
getApplicationContext().publishEvent(event);
}
public static String getHost() {
if (StringUtils.hasLength(host)) {
return host;
}
InetUtils inetUtils = SpringUtil.getBean(InetUtils.class);
inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
String port = applicationContext.getEnvironment().getProperty("server.port");
host = "http://" + inetUtils.findFirstNonLoopbackHostInfo().getIpAddress() + ":" + port;
return host;
}
}
@@ -0,0 +1,88 @@
package com.oneone.common.web.util;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.oneone.common.base.LocalUser;
import com.oneone.common.exception.BusinessException;
import com.oneone.common.result.ResultCode;
import java.util.List;
/**
* @author oneone
* @date 2022/2/12 20:14
*/
public class UserContext {
private static final ThreadLocal<LocalUser> THREAD_LOCAL = new TransmittableThreadLocal<>();
public static final Long DEFAULT_VALUE = -1L;
/**
* 获取当前登录管理员的ID
*
*/
public static Long getUserId() {
LocalUser localUser = THREAD_LOCAL.get();
if (localUser == null){
throw new BusinessException(ResultCode.TOKEN_INVALID_OR_EXPIRED);
}
return localUser.getUserId();
}
/**
* 获取当前登录管理员的ID 未登录返回默认值
*
*/
public static Long getUserIdIfPresentDefault() {
LocalUser localUser = THREAD_LOCAL.get();
if (localUser == null){
return DEFAULT_VALUE;
}
return localUser.getUserId();
}
public static Long getUserIdIfPresentNull() {
LocalUser localUser = THREAD_LOCAL.get();
if (localUser == null){
return null;
}
return localUser.getUserId();
}
/**
* 获取当前登录管理员的ID
*
*/
public static Long getMemberId() {
LocalUser localUser = THREAD_LOCAL.get();
if (localUser == null){
throw new BusinessException(ResultCode.TOKEN_INVALID_OR_EXPIRED);
}
return localUser.getUserId();
}
/**
* 获取当前登录管理员的ID 未登录返回默认值
*
*/
public static Long getMemberIdIfPresentDefault() {
LocalUser localUser = THREAD_LOCAL.get();
if (localUser == null){
return DEFAULT_VALUE;
}
return localUser.getUserId();
}
public static void setUser(LocalUser user){
THREAD_LOCAL.set(user);
}
public static LocalUser getUser(){
return THREAD_LOCAL.get();
}
public static void clear(){
THREAD_LOCAL.remove();
}
}
@@ -0,0 +1,89 @@
package com.oneone.common.web.util;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONObject;
import com.oneone.common.constant.SecurityConstants;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.List;
/**
* JWT工具类
*
* @author oneone
* @date 2022/2/5
*/
@Slf4j
public class UserUtils {
/**
* 解析JWT获取用户ID
*
* @return
*/
public static Long getUserId() {
Long userId = null;
JSONObject jwtPayload = JwtUtils.getJwtPayload();
if (jwtPayload != null) {
userId = jwtPayload.getLong("userId");
}
return userId;
}
public static Long getMemberId() {
Long memberId = null;
JSONObject jwtPayload = JwtUtils.getJwtPayload();
if (jwtPayload != null) {
memberId = jwtPayload.getLong("memberId");
}
return memberId;
}
/**
* 解析JWT获取用户ID
*
* @return
*/
public static Long getDeptId() {
Long id = JwtUtils.getJwtPayload().getLong("deptId");
return id;
}
/**
* 解析JWT获取获取用户名
*
* @return
*/
public static String getUsername() {
String username = JwtUtils.getJwtPayload().getStr(SecurityConstants.USER_NAME_KEY);
return username;
}
/**
* JWT获取用户角色列表
*
* @return 角色列表
*/
public static List<String> getRoles() {
List<String> roles;
JSONObject payload = JwtUtils.getJwtPayload();
if (payload.containsKey(SecurityConstants.JWT_AUTHORITIES_KEY)) {
roles = payload.getJSONArray(SecurityConstants.JWT_AUTHORITIES_KEY).toList(String.class);
} else {
roles = Collections.emptyList();
}
return roles;
}
/**
* 是否「超级管理员」
*
* @return
*/
public static boolean isRoot() {
List<String> roles = getRoles();
return CollectionUtil.isNotEmpty(roles) && roles.contains("ROOT");
}
}
@@ -0,0 +1,159 @@
package com.oneone.common.web.util;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.oneone.common.util.Func;
import org.apache.commons.codec.Charsets;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Enumeration;
public class WebUtil extends org.springframework.web.util.WebUtils{
public static final String USER_AGENT_HEADER = "user-agent";
public static final String UN_KNOWN = "unknown";
public static HttpServletRequest getRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return (requestAttributes == null) ? null : ((ServletRequestAttributes) requestAttributes).getRequest();
}
/***
* 获取 request 中 json 字符串的内容
*
* @param request request
* @return 字符串内容
*/
public static String getRequestParamString(HttpServletRequest request) {
try {
return getRequestStr(request);
} catch (Exception ex) {
return StringPool.EMPTY;
}
}
/**
* 获取 request 请求内容
*
* @param request request
* @return String
* @throws IOException IOException
*/
public static String getRequestStr(HttpServletRequest request) throws IOException {
String queryString = request.getQueryString();
if (Func.isEmpty(queryString)) {
return new String(queryString.getBytes(Charsets.ISO_8859_1), Charsets.UTF_8).replaceAll("&amp;", "&").replaceAll("%22", "\"");
}
return getRequestStr(request, getRequestBytes(request));
}
/**
* 获取 request 请求的 byte[] 数组
*
* @param request request
* @return byte[]
* @throws IOException IOException
*/
public static byte[] getRequestBytes(HttpServletRequest request) throws IOException {
int contentLength = request.getContentLength();
if (contentLength < 0) {
return null;
}
byte[] buffer = new byte[contentLength];
for (int i = 0; i < contentLength; ) {
int readlen = request.getInputStream().read(buffer, i, contentLength - i);
if (readlen == -1) {
break;
}
i += readlen;
}
return buffer;
}
/**
* 获取 request 请求内容
*
* @param request request
* @param buffer buffer
* @return String
* @throws IOException IOException
*/
public static String getRequestStr(HttpServletRequest request, byte[] buffer) throws IOException {
String charEncoding = request.getCharacterEncoding();
if (charEncoding == null) {
charEncoding = StringPool.UTF_8;
}
String str = new String(buffer, charEncoding).trim();
if (Func.isEmpty(str)) {
StringBuilder sb = new StringBuilder();
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String key = parameterNames.nextElement();
String value = request.getParameter(key);
Func.appendBuilder(sb, key, "=", value, "&");
}
str = Func.removeSuffix(sb.toString(), "&");
}
return str.replaceAll("&amp;", "&");
}
/**
* 获取ip
*
* @param request HttpServletRequest
* @return {String}
*/
@Nullable
public static String getIP(HttpServletRequest request) {
Assert.notNull(request, "HttpServletRequest is null");
String ip = request.getHeader("X-Requested-For");
if (Func.isEmpty(ip) || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (Func.isEmpty(ip) || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (Func.isEmpty(ip) || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (Func.isEmpty(ip) || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (Func.isEmpty(ip) || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (Func.isEmpty(ip) || UN_KNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return Func.isEmpty(ip) ? null : ip.split(",")[0];
}
/**
* 获取url路径
*
* @param uriStr 路径
* @return url路径
*/
public static String getPath(String uriStr) {
URI uri;
try {
uri = new URI(uriStr);
} catch (URISyntaxException var3) {
throw new RuntimeException(var3);
}
return uri.getPath();
}
}
@@ -0,0 +1,11 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.oneone.common.web.config.WebMvcConfig,\
com.oneone.common.web.config.HttpClientConfig,\
com.oneone.common.web.config.Configs,\
com.oneone.common.web.security.aspect.PreAuthorizeAspect,\
com.oneone.common.web.config.FeignConfig,\
com.oneone.common.web.exception.GlobalExceptionHandler,\
com.oneone.common.web.interceptor.AuthInterceptor,\
com.oneone.common.web.util.SpringUtil,\
com.oneone.common.web.util.HttpClientUtil,\
com.oneone.common.web.config.ThreadPoolConfig
@@ -0,0 +1,4 @@
com.oneone.common.web.swagger.config.SwaggerAutoConfiguration
com.oneone.common.web.config.SecurityConfiguration
com.oneone.common.web.config.MetricsAutoConfiguration
com.oneone.common.web.config.TracerAutoConfiguration