Repository: pig-mesh/pig
Branch: master
Commit: 6721dd17df59
Files: 490
Total size: 1.2 MB
Directory structure:
gitextract_fxd_qqx7/
├── .editorconfig
├── .gitee/
│ └── ISSUE_TEMPLATE/
│ ├── config.yml
│ └── issue.yml
├── .github/
│ ├── renovate.json
│ └── workflows/
│ ├── github-release.yml
│ ├── image.yml
│ ├── maven.yml
│ └── mirror.yml
├── .gitignore
├── LICENSE
├── README.md
├── db/
│ ├── Dockerfile
│ ├── pig.sql
│ └── pig_config.sql
├── docker-compose.yml
├── pig-auth/
│ ├── Dockerfile
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── pig4cloud/
│ │ └── pig/
│ │ └── auth/
│ │ ├── PigAuthApplication.java
│ │ ├── config/
│ │ │ └── AuthorizationServerConfiguration.java
│ │ ├── endpoint/
│ │ │ ├── ImageCodeEndpoint.java
│ │ │ └── PigTokenEndpoint.java
│ │ └── support/
│ │ ├── CustomeOAuth2AccessTokenGenerator.java
│ │ ├── base/
│ │ │ ├── OAuth2ResourceOwnerBaseAuthenticationConverter.java
│ │ │ ├── OAuth2ResourceOwnerBaseAuthenticationProvider.java
│ │ │ ├── OAuth2ResourceOwnerBaseAuthenticationToken.java
│ │ │ └── package-info.java
│ │ ├── core/
│ │ │ ├── CustomeOAuth2TokenCustomizer.java
│ │ │ ├── FormIdentityLoginConfigurer.java
│ │ │ └── PigDaoAuthenticationProvider.java
│ │ ├── filter/
│ │ │ ├── AuthSecurityConfigProperties.java
│ │ │ ├── PasswordDecoderFilter.java
│ │ │ └── ValidateCodeFilter.java
│ │ ├── handler/
│ │ │ ├── FormAuthenticationFailureHandler.java
│ │ │ ├── PigAuthenticationFailureEventHandler.java
│ │ │ ├── PigAuthenticationSuccessEventHandler.java
│ │ │ ├── PigLogoutSuccessEventHandler.java
│ │ │ └── SsoLogoutSuccessHandler.java
│ │ ├── password/
│ │ │ ├── OAuth2ResourceOwnerPasswordAuthenticationConverter.java
│ │ │ ├── OAuth2ResourceOwnerPasswordAuthenticationProvider.java
│ │ │ ├── OAuth2ResourceOwnerPasswordAuthenticationToken.java
│ │ │ └── package-info.java
│ │ └── sms/
│ │ ├── OAuth2ResourceOwnerSmsAuthenticationConverter.java
│ │ ├── OAuth2ResourceOwnerSmsAuthenticationProvider.java
│ │ ├── OAuth2ResourceOwnerSmsAuthenticationToken.java
│ │ └── package-info.java
│ └── resources/
│ ├── application.yml
│ ├── logback-spring.xml
│ └── templates/
│ └── ftl/
│ ├── confirm.ftl
│ ├── layout/
│ │ └── base.ftl
│ └── login.ftl
├── pig-boot/
│ ├── Dockerfile
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── pig4cloud/
│ │ └── pig/
│ │ └── PigBootApplication.java
│ └── resources/
│ ├── application-dev.yml
│ ├── application.yml
│ └── logback-spring.xml
├── pig-common/
│ ├── pig-common-bom/
│ │ └── pom.xml
│ ├── pig-common-core/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── common/
│ │ │ └── core/
│ │ │ ├── config/
│ │ │ │ ├── JacksonConfiguration.java
│ │ │ │ ├── RedisTemplateConfiguration.java
│ │ │ │ ├── RestTemplateConfiguration.java
│ │ │ │ └── WebMvcConfiguration.java
│ │ │ ├── constant/
│ │ │ │ ├── CacheConstants.java
│ │ │ │ ├── CommonConstants.java
│ │ │ │ ├── SecurityConstants.java
│ │ │ │ ├── ServiceNameConstants.java
│ │ │ │ └── enums/
│ │ │ │ ├── DictTypeEnum.java
│ │ │ │ ├── LoginTypeEnum.java
│ │ │ │ └── MenuTypeEnum.java
│ │ │ ├── exception/
│ │ │ │ ├── CheckedException.java
│ │ │ │ ├── ErrorCodes.java
│ │ │ │ ├── PigDeniedException.java
│ │ │ │ └── ValidateCodeException.java
│ │ │ ├── factory/
│ │ │ │ └── YamlPropertySourceFactory.java
│ │ │ ├── jackson/
│ │ │ │ └── PigJavaTimeModule.java
│ │ │ ├── servlet/
│ │ │ │ └── RepeatBodyRequestWrapper.java
│ │ │ └── util/
│ │ │ ├── ClassUtils.java
│ │ │ ├── MsgUtils.java
│ │ │ ├── R.java
│ │ │ ├── RedisUtils.java
│ │ │ ├── RetOps.java
│ │ │ ├── SpringContextHolder.java
│ │ │ └── WebUtils.java
│ │ └── resources/
│ │ ├── META-INF/
│ │ │ └── spring/
│ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ │ ├── banner.txt
│ │ ├── i18n/
│ │ │ └── messages_zh_CN.properties
│ │ └── logback-spring.xml
│ ├── pig-common-datasource/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ └── java/
│ │ └── com/
│ │ └── pig4cloud/
│ │ └── pig/
│ │ └── common/
│ │ └── datasource/
│ │ ├── DynamicDataSourceAutoConfiguration.java
│ │ ├── annotation/
│ │ │ └── EnableDynamicDataSource.java
│ │ ├── config/
│ │ │ ├── ClearTtlDataSourceFilter.java
│ │ │ ├── DataSourceProperties.java
│ │ │ ├── JdbcDynamicDataSourceProvider.java
│ │ │ ├── LastParamDsProcessor.java
│ │ │ └── MasterDataSourceProvider.java
│ │ ├── support/
│ │ │ └── DataSourceConstants.java
│ │ └── util/
│ │ ├── DsConfTypeEnum.java
│ │ └── DsJdbcUrlEnum.java
│ ├── pig-common-excel/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── common/
│ │ │ └── excel/
│ │ │ ├── ExcelAutoConfiguration.java
│ │ │ └── provider/
│ │ │ ├── RemoteDictApiService.java
│ │ │ └── RemoteDictDataProvider.java
│ │ └── resources/
│ │ └── META-INF/
│ │ └── spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ ├── pig-common-feign/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ ├── com/
│ │ │ │ └── pig4cloud/
│ │ │ │ └── pig/
│ │ │ │ └── common/
│ │ │ │ └── feign/
│ │ │ │ ├── PigFeignAutoConfiguration.java
│ │ │ │ ├── annotation/
│ │ │ │ │ ├── EnablePigFeignClients.java
│ │ │ │ │ └── NoToken.java
│ │ │ │ ├── core/
│ │ │ │ │ ├── PigFeignInnerRequestInterceptor.java
│ │ │ │ │ └── PigFeignRequestCloseInterceptor.java
│ │ │ │ └── sentinel/
│ │ │ │ ├── SentinelAutoConfiguration.java
│ │ │ │ ├── ext/
│ │ │ │ │ ├── PigSentinelFeign.java
│ │ │ │ │ └── PigSentinelInvocationHandler.java
│ │ │ │ ├── handle/
│ │ │ │ │ ├── GlobalBizExceptionHandler.java
│ │ │ │ │ └── PigUrlBlockHandler.java
│ │ │ │ └── parser/
│ │ │ │ └── PigHeaderRequestOriginParser.java
│ │ │ └── org/
│ │ │ └── springframework/
│ │ │ └── cloud/
│ │ │ └── openfeign/
│ │ │ └── PigFeignClientsRegistrar.java
│ │ └── resources/
│ │ └── META-INF/
│ │ └── spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ ├── pig-common-log/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── common/
│ │ │ └── log/
│ │ │ ├── LogAutoConfiguration.java
│ │ │ ├── annotation/
│ │ │ │ └── SysLog.java
│ │ │ ├── aspect/
│ │ │ │ └── SysLogAspect.java
│ │ │ ├── config/
│ │ │ │ └── PigLogProperties.java
│ │ │ ├── event/
│ │ │ │ ├── SysLogEvent.java
│ │ │ │ ├── SysLogEventSource.java
│ │ │ │ └── SysLogListener.java
│ │ │ ├── init/
│ │ │ │ └── ApplicationLoggerInitializer.java
│ │ │ └── util/
│ │ │ ├── LogTypeEnum.java
│ │ │ └── SysLogUtils.java
│ │ └── resources/
│ │ └── META-INF/
│ │ ├── spring/
│ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ │ ├── spring-configuration-metadata.json
│ │ └── spring.factories
│ ├── pig-common-mybatis/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── common/
│ │ │ └── mybatis/
│ │ │ ├── MybatisAutoConfiguration.java
│ │ │ ├── base/
│ │ │ │ └── BaseEntity.java
│ │ │ ├── config/
│ │ │ │ └── MybatisPlusMetaObjectHandler.java
│ │ │ ├── handler/
│ │ │ │ ├── JsonLongArrayTypeHandler.java
│ │ │ │ └── JsonStringArrayTypeHandler.java
│ │ │ ├── plugins/
│ │ │ │ └── PigPaginationInnerInterceptor.java
│ │ │ └── resolver/
│ │ │ └── SqlFilterArgumentResolver.java
│ │ └── resources/
│ │ └── META-INF/
│ │ └── spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ ├── pig-common-oss/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── common/
│ │ │ └── file/
│ │ │ ├── FileAutoConfiguration.java
│ │ │ ├── core/
│ │ │ │ ├── FileProperties.java
│ │ │ │ └── FileTemplate.java
│ │ │ ├── local/
│ │ │ │ ├── LocalFileAutoConfiguration.java
│ │ │ │ ├── LocalFileProperties.java
│ │ │ │ └── LocalFileTemplate.java
│ │ │ └── oss/
│ │ │ ├── OssAutoConfiguration.java
│ │ │ ├── OssProperties.java
│ │ │ ├── http/
│ │ │ │ └── OssEndpoint.java
│ │ │ └── service/
│ │ │ └── OssTemplate.java
│ │ └── resources/
│ │ └── META-INF/
│ │ └── spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ ├── pig-common-seata/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── common/
│ │ │ └── seata/
│ │ │ └── config/
│ │ │ └── SeataAutoConfiguration.java
│ │ └── resources/
│ │ ├── META-INF/
│ │ │ └── spring/
│ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ │ └── seata-config.yml
│ ├── pig-common-security/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── common/
│ │ │ └── security/
│ │ │ ├── annotation/
│ │ │ │ ├── EnablePigResourceServer.java
│ │ │ │ ├── HasPermission.java
│ │ │ │ └── Inner.java
│ │ │ ├── component/
│ │ │ │ ├── PermissionService.java
│ │ │ │ ├── PermitAllUrlProperties.java
│ │ │ │ ├── PigBearerTokenExtractor.java
│ │ │ │ ├── PigBootCorsProperties.java
│ │ │ │ ├── PigClientCredentialsOAuth2AuthenticatedPrincipal.java
│ │ │ │ ├── PigCustomOAuth2AccessTokenResponseHttpMessageConverter.java
│ │ │ │ ├── PigCustomOpaqueTokenIntrospector.java
│ │ │ │ ├── PigResourceServerAutoConfiguration.java
│ │ │ │ ├── PigResourceServerConfiguration.java
│ │ │ │ ├── PigSecurityInnerAspect.java
│ │ │ │ ├── PigSecurityMessageSourceConfiguration.java
│ │ │ │ └── ResourceAuthExceptionEntryPoint.java
│ │ │ ├── feign/
│ │ │ │ ├── PigFeignClientConfiguration.java
│ │ │ │ └── PigOAuthRequestInterceptor.java
│ │ │ ├── service/
│ │ │ │ ├── PigAppUserDetailsServiceImpl.java
│ │ │ │ ├── PigRedisOAuth2AuthorizationConsentService.java
│ │ │ │ ├── PigRedisOAuth2AuthorizationService.java
│ │ │ │ ├── PigRemoteRegisteredClientRepository.java
│ │ │ │ ├── PigUser.java
│ │ │ │ ├── PigUserDetailsService.java
│ │ │ │ └── PigUserDetailsServiceImpl.java
│ │ │ └── util/
│ │ │ ├── OAuth2EndpointUtils.java
│ │ │ ├── OAuth2ErrorCodesExpand.java
│ │ │ ├── OAuthClientException.java
│ │ │ ├── ScopeException.java
│ │ │ └── SecurityUtils.java
│ │ └── resources/
│ │ ├── META-INF/
│ │ │ └── spring/
│ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ │ └── i18n/
│ │ └── errors/
│ │ └── messages_zh_CN.properties
│ ├── pig-common-swagger/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── common/
│ │ │ └── swagger/
│ │ │ ├── annotation/
│ │ │ │ └── EnablePigDoc.java
│ │ │ ├── config/
│ │ │ │ ├── OpenAPIDefinition.java
│ │ │ │ ├── OpenAPIDefinitionImportSelector.java
│ │ │ │ └── OpenAPIMetadataConfiguration.java
│ │ │ └── support/
│ │ │ └── SwaggerProperties.java
│ │ └── resources/
│ │ └── openapi-config.yaml
│ ├── pig-common-websocket/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── common/
│ │ │ └── websocket/
│ │ │ ├── config/
│ │ │ │ ├── LocalMessageDistributorConfiguration.java
│ │ │ │ ├── MessageDistributorTypeConstants.java
│ │ │ │ ├── RedisMessageDistributorConfiguration.java
│ │ │ │ ├── WebSocketAutoConfiguration.java
│ │ │ │ ├── WebSocketHandlerConfig.java
│ │ │ │ ├── WebSocketMessageSender.java
│ │ │ │ └── WebSocketProperties.java
│ │ │ ├── custom/
│ │ │ │ ├── PigxSessionKeyGenerator.java
│ │ │ │ └── UserAttributeHandshakeInterceptor.java
│ │ │ ├── distribute/
│ │ │ │ ├── LocalMessageDistributor.java
│ │ │ │ ├── MessageDO.java
│ │ │ │ ├── MessageDistributor.java
│ │ │ │ ├── MessageSender.java
│ │ │ │ ├── RedisMessageDistributor.java
│ │ │ │ └── RedisWebsocketMessageListener.java
│ │ │ ├── handler/
│ │ │ │ ├── CustomPlanTextMessageHandler.java
│ │ │ │ ├── CustomWebSocketHandler.java
│ │ │ │ ├── JsonMessageHandler.java
│ │ │ │ ├── PingJsonMessageHandler.java
│ │ │ │ └── PlanTextMessageHandler.java
│ │ │ ├── holder/
│ │ │ │ ├── JsonMessageHandlerHolder.java
│ │ │ │ ├── MapSessionWebSocketHandlerDecorator.java
│ │ │ │ ├── SessionKeyGenerator.java
│ │ │ │ └── WebSocketSessionHolder.java
│ │ │ ├── message/
│ │ │ │ ├── AbstractJsonWebSocketMessage.java
│ │ │ │ ├── JsonWebSocketMessage.java
│ │ │ │ ├── PingJsonWebSocketMessage.java
│ │ │ │ ├── PongJsonWebSocketMessage.java
│ │ │ │ └── WebSocketMessageTypeEnum.java
│ │ │ └── package-info.java
│ │ └── resources/
│ │ └── META-INF/
│ │ └── spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ ├── pig-common-xss/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── common/
│ │ │ └── xss/
│ │ │ ├── PigXssAutoConfiguration.java
│ │ │ ├── config/
│ │ │ │ ├── PigXssProperties.java
│ │ │ │ └── package-info.java
│ │ │ ├── core/
│ │ │ │ ├── DefaultXssCleaner.java
│ │ │ │ ├── FormXssClean.java
│ │ │ │ ├── FromXssException.java
│ │ │ │ ├── JacksonXssClean.java
│ │ │ │ ├── JacksonXssException.java
│ │ │ │ ├── XssCleanDeserializer.java
│ │ │ │ ├── XssCleanDeserializerBase.java
│ │ │ │ ├── XssCleanIgnore.java
│ │ │ │ ├── XssCleanInterceptor.java
│ │ │ │ ├── XssCleaner.java
│ │ │ │ ├── XssException.java
│ │ │ │ ├── XssHolder.java
│ │ │ │ └── XssType.java
│ │ │ ├── package-info.java
│ │ │ └── utils/
│ │ │ ├── XssUtil.java
│ │ │ └── package-info.java
│ │ └── resources/
│ │ └── META-INF/
│ │ ├── spring/
│ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ │ └── spring-configuration-metadata.json
│ └── pom.xml
├── pig-gateway/
│ ├── Dockerfile
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── pig4cloud/
│ │ └── pig/
│ │ └── gateway/
│ │ ├── PigGatewayApplication.java
│ │ ├── config/
│ │ │ ├── GatewayConfiguration.java
│ │ │ ├── RateLimiterConfiguration.java
│ │ │ └── SpringDocConfiguration.java
│ │ ├── filter/
│ │ │ └── PigRequestGlobalFilter.java
│ │ └── handler/
│ │ └── GlobalExceptionHandler.java
│ └── resources/
│ ├── application.yml
│ └── logback-spring.xml
├── pig-register/
│ ├── Dockerfile
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── alibaba/
│ │ └── nacos/
│ │ └── bootstrap/
│ │ └── PigNacosApplication.java
│ └── resources/
│ ├── application.properties
│ └── logback-spring.xml
├── pig-upms/
│ ├── pig-upms-api/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── admin/
│ │ │ └── api/
│ │ │ ├── dto/
│ │ │ │ ├── RegisterUserDTO.java
│ │ │ │ ├── SysLogDTO.java
│ │ │ │ ├── UserDTO.java
│ │ │ │ └── UserInfo.java
│ │ │ ├── entity/
│ │ │ │ ├── SysDept.java
│ │ │ │ ├── SysDeptRelation.java
│ │ │ │ ├── SysDict.java
│ │ │ │ ├── SysDictItem.java
│ │ │ │ ├── SysFile.java
│ │ │ │ ├── SysLog.java
│ │ │ │ ├── SysMenu.java
│ │ │ │ ├── SysOauthClientDetails.java
│ │ │ │ ├── SysPost.java
│ │ │ │ ├── SysPublicParam.java
│ │ │ │ ├── SysRole.java
│ │ │ │ ├── SysRoleMenu.java
│ │ │ │ ├── SysUser.java
│ │ │ │ ├── SysUserPost.java
│ │ │ │ └── SysUserRole.java
│ │ │ ├── feign/
│ │ │ │ ├── RemoteClientDetailsService.java
│ │ │ │ ├── RemoteDictService.java
│ │ │ │ ├── RemoteLogService.java
│ │ │ │ ├── RemoteParamService.java
│ │ │ │ ├── RemoteTokenService.java
│ │ │ │ └── RemoteUserService.java
│ │ │ ├── util/
│ │ │ │ ├── DictResolver.java
│ │ │ │ └── ParamResolver.java
│ │ │ └── vo/
│ │ │ ├── DeptExcelVo.java
│ │ │ ├── PostExcelVO.java
│ │ │ ├── PreLogVO.java
│ │ │ ├── RoleExcelVO.java
│ │ │ ├── RoleVO.java
│ │ │ ├── TokenVo.java
│ │ │ ├── UserExcelVO.java
│ │ │ └── UserVO.java
│ │ └── resources/
│ │ └── META-INF/
│ │ └── spring/
│ │ └── org.springframework.cloud.openfeign.FeignClient.imports
│ ├── pig-upms-biz/
│ │ ├── Dockerfile
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── admin/
│ │ │ ├── PigAdminApplication.java
│ │ │ ├── controller/
│ │ │ │ ├── SysClientController.java
│ │ │ │ ├── SysDeptController.java
│ │ │ │ ├── SysDictController.java
│ │ │ │ ├── SysFileController.java
│ │ │ │ ├── SysLogController.java
│ │ │ │ ├── SysMenuController.java
│ │ │ │ ├── SysMobileController.java
│ │ │ │ ├── SysPostController.java
│ │ │ │ ├── SysPublicParamController.java
│ │ │ │ ├── SysRegisterController.java
│ │ │ │ ├── SysRoleController.java
│ │ │ │ ├── SysSystemInfoController.java
│ │ │ │ ├── SysTokenController.java
│ │ │ │ └── SysUserController.java
│ │ │ ├── mapper/
│ │ │ │ ├── SysDeptMapper.java
│ │ │ │ ├── SysDictItemMapper.java
│ │ │ │ ├── SysDictMapper.java
│ │ │ │ ├── SysFileMapper.java
│ │ │ │ ├── SysLogMapper.java
│ │ │ │ ├── SysMenuMapper.java
│ │ │ │ ├── SysOauthClientDetailsMapper.java
│ │ │ │ ├── SysPostMapper.java
│ │ │ │ ├── SysPublicParamMapper.java
│ │ │ │ ├── SysRoleMapper.java
│ │ │ │ ├── SysRoleMenuMapper.java
│ │ │ │ ├── SysUserMapper.java
│ │ │ │ ├── SysUserPostMapper.java
│ │ │ │ └── SysUserRoleMapper.java
│ │ │ └── service/
│ │ │ ├── SysDeptService.java
│ │ │ ├── SysDictItemService.java
│ │ │ ├── SysDictService.java
│ │ │ ├── SysFileService.java
│ │ │ ├── SysLogService.java
│ │ │ ├── SysMenuService.java
│ │ │ ├── SysMobileService.java
│ │ │ ├── SysOauthClientDetailsService.java
│ │ │ ├── SysPostService.java
│ │ │ ├── SysPublicParamService.java
│ │ │ ├── SysRoleMenuService.java
│ │ │ ├── SysRoleService.java
│ │ │ ├── SysUserRoleService.java
│ │ │ ├── SysUserService.java
│ │ │ └── impl/
│ │ │ ├── SysDeptServiceImpl.java
│ │ │ ├── SysDictItemServiceImpl.java
│ │ │ ├── SysDictServiceImpl.java
│ │ │ ├── SysFileServiceImpl.java
│ │ │ ├── SysLogServiceImpl.java
│ │ │ ├── SysMenuServiceImpl.java
│ │ │ ├── SysMobileServiceImpl.java
│ │ │ ├── SysOauthClientDetailsServiceImpl.java
│ │ │ ├── SysPostServiceImpl.java
│ │ │ ├── SysPublicParamServiceImpl.java
│ │ │ ├── SysRoleMenuServiceImpl.java
│ │ │ ├── SysRoleServiceImpl.java
│ │ │ ├── SysUserRoleServiceImpl.java
│ │ │ └── SysUserServiceImpl.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── file/
│ │ │ ├── approle.xlsx
│ │ │ ├── dept.xlsx
│ │ │ ├── post.xlsx
│ │ │ ├── role.xlsx
│ │ │ └── user.xlsx
│ │ ├── logback-spring.xml
│ │ └── mapper/
│ │ ├── SysDeptMapper.xml
│ │ ├── SysMenuMapper.xml
│ │ ├── SysPostMapper.xml
│ │ ├── SysRoleMapper.xml
│ │ └── SysUserMapper.xml
│ └── pom.xml
├── pig-visual/
│ ├── pig-codegen/
│ │ ├── Dockerfile
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── codegen/
│ │ │ ├── PigCodeGenApplication.java
│ │ │ ├── config/
│ │ │ │ └── PigCodeGenDefaultProperties.java
│ │ │ ├── controller/
│ │ │ │ ├── GenDsConfController.java
│ │ │ │ ├── GenFieldTypeController.java
│ │ │ │ ├── GenGroupController.java
│ │ │ │ ├── GenTableController.java
│ │ │ │ ├── GenTemplateController.java
│ │ │ │ ├── GenTemplateGroupController.java
│ │ │ │ └── GeneratorController.java
│ │ │ ├── entity/
│ │ │ │ ├── ColumnEntity.java
│ │ │ │ ├── GenConfig.java
│ │ │ │ ├── GenDatasourceConf.java
│ │ │ │ ├── GenFieldType.java
│ │ │ │ ├── GenGroupEntity.java
│ │ │ │ ├── GenTable.java
│ │ │ │ ├── GenTableColumnEntity.java
│ │ │ │ ├── GenTemplateEntity.java
│ │ │ │ ├── GenTemplateGroupEntity.java
│ │ │ │ └── TableEntity.java
│ │ │ ├── mapper/
│ │ │ │ ├── GenDatasourceConfMapper.java
│ │ │ │ ├── GenDynamicMapper.java
│ │ │ │ ├── GenFieldTypeMapper.java
│ │ │ │ ├── GenGroupMapper.java
│ │ │ │ ├── GenTableColumnMapper.java
│ │ │ │ ├── GenTableMapper.java
│ │ │ │ ├── GenTemplateGroupMapper.java
│ │ │ │ └── GenTemplateMapper.java
│ │ │ ├── service/
│ │ │ │ ├── GenDatasourceConfService.java
│ │ │ │ ├── GenFieldTypeService.java
│ │ │ │ ├── GenGroupService.java
│ │ │ │ ├── GenTableColumnService.java
│ │ │ │ ├── GenTableService.java
│ │ │ │ ├── GenTemplateGroupService.java
│ │ │ │ ├── GenTemplateService.java
│ │ │ │ ├── GeneratorService.java
│ │ │ │ └── impl/
│ │ │ │ ├── GenDatasourceConfServiceImpl.java
│ │ │ │ ├── GenFieldTypeServiceImpl.java
│ │ │ │ ├── GenGroupServiceImpl.java
│ │ │ │ ├── GenTableColumnServiceImpl.java
│ │ │ │ ├── GenTableServiceImpl.java
│ │ │ │ ├── GenTemplateGroupServiceImpl.java
│ │ │ │ ├── GenTemplateServiceImpl.java
│ │ │ │ └── GeneratorServiceImpl.java
│ │ │ └── util/
│ │ │ ├── AutoFillEnum.java
│ │ │ ├── BoolFillEnum.java
│ │ │ ├── CommonColumnFiledEnum.java
│ │ │ ├── DictTool.java
│ │ │ ├── GenKit.java
│ │ │ ├── GeneratorStyleEnum.java
│ │ │ ├── NamingCaseTool.java
│ │ │ ├── VelocityKit.java
│ │ │ └── vo/
│ │ │ ├── GenCreateTableVO.java
│ │ │ ├── GenTemplateFileVO.java
│ │ │ ├── GroupVO.java
│ │ │ ├── SqlDto.java
│ │ │ └── TemplateGroupDTO.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── logback-spring.xml
│ │ └── mapper/
│ │ ├── GenFieldTypeMapper.xml
│ │ ├── GenGroupMapper.xml
│ │ ├── GenTableMapper.xml
│ │ ├── GenTemplateGroupMapper.xml
│ │ └── GenTemplateMapper.xml
│ ├── pig-monitor/
│ │ ├── Dockerfile
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── monitor/
│ │ │ ├── PigMonitorApplication.java
│ │ │ ├── config/
│ │ │ │ ├── CustomCsrfFilter.java
│ │ │ │ └── SecuritySecureConfig.java
│ │ │ └── converter/
│ │ │ └── NacosServiceInstanceConverter.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── logback-spring.xml
│ ├── pig-quartz/
│ │ ├── Dockerfile
│ │ ├── SECURITY.md
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── pig4cloud/
│ │ │ └── pig/
│ │ │ └── daemon/
│ │ │ └── quartz/
│ │ │ ├── PigQuartzApplication.java
│ │ │ ├── config/
│ │ │ │ ├── AutowireCapableBeanJobFactory.java
│ │ │ │ ├── PigInitQuartzJob.java
│ │ │ │ ├── PigQuartzConfig.java
│ │ │ │ ├── PigQuartzCustomizerConfig.java
│ │ │ │ ├── PigQuartzFactory.java
│ │ │ │ └── PigQuartzInvokeFactory.java
│ │ │ ├── constants/
│ │ │ │ ├── JobTypeQuartzEnum.java
│ │ │ │ └── PigQuartzEnum.java
│ │ │ ├── controller/
│ │ │ │ ├── SysJobController.java
│ │ │ │ └── SysJobLogController.java
│ │ │ ├── entity/
│ │ │ │ ├── SysJob.java
│ │ │ │ └── SysJobLog.java
│ │ │ ├── event/
│ │ │ │ ├── SysJobEvent.java
│ │ │ │ ├── SysJobListener.java
│ │ │ │ ├── SysJobLogEvent.java
│ │ │ │ └── SysJobLogListener.java
│ │ │ ├── exception/
│ │ │ │ └── TaskException.java
│ │ │ ├── mapper/
│ │ │ │ ├── SysJobLogMapper.java
│ │ │ │ └── SysJobMapper.java
│ │ │ ├── service/
│ │ │ │ ├── SysJobLogService.java
│ │ │ │ ├── SysJobService.java
│ │ │ │ └── impl/
│ │ │ │ ├── SysJobLogServiceImpl.java
│ │ │ │ └── SysJobServiceImpl.java
│ │ │ ├── task/
│ │ │ │ ├── RestTaskDemo.java
│ │ │ │ └── SpringBeanTaskDemo.java
│ │ │ └── util/
│ │ │ ├── ClassNameValidator.java
│ │ │ ├── ITaskInvok.java
│ │ │ ├── JarTaskInvok.java
│ │ │ ├── JavaClassTaskInvok.java
│ │ │ ├── RestTaskInvok.java
│ │ │ ├── SpringBeanTaskInvok.java
│ │ │ ├── TaskInvokFactory.java
│ │ │ ├── TaskInvokUtil.java
│ │ │ └── TaskUtil.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── logback-spring.xml
│ │ └── quartz-config.yml
│ └── pom.xml
└── pom.xml
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*.{groovy,java,kt,xml}]
indent_style = tab
indent_size = 4
continuation_indent_size = 8
================================================
FILE: .gitee/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false # 不允许用户创建空白 Issue
contact_links:
- name: 遇到问题先去看文档!谢谢! # 外部网站名称
url: https://wiki.pig4cloud.com/ # 跳转的外部网站目标地址
about: 文档可以解决你80%的疑惑 # 跳转外部网站的描述说明
================================================
FILE: .gitee/ISSUE_TEMPLATE/issue.yml
================================================
name: 问题咨询
description: "请尽可能详细的描述问题,提供足够的上下文,一分钟的描述不需要期望别人花半小时帮你排查"
body:
- type: dropdown
id: version
attributes:
label: PIG版本(提问先右上角 Star ♥️)
options:
- "不处理PIGX或其他魔改版本"
- "3.9"
- "3.8"
- "3.7"
validations:
required: true
- type: checkboxes
validations:
required: true
attributes:
label: 架构
options:
- label: 微服务架构
- label: 单体架构
- type: textarea
id: desired-solution
attributes:
label: 问题描述,提供详细截图和报错
description: 详细问题,提供相应截图和日志,一分钟的描述不需要期望别人花半小时帮你排查
validations:
required: true
================================================
FILE: .github/renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"baseBranches": [
"jdk17-dev"
],
"extends": [
"config:recommended"
],
"rangeStrategy": "bump",
"packageRules": [
{
"matchPackagePatterns": [
"^com.amazonaws:aws-java-sdk-s3$",
"^tomcat$",
"^io.springboot:knife4j-openapi3-ui$",
"^com.alibaba:fastjson$",
"^org.anyline:anyline.*$",
"^org.apache.velocity:velocity-engine-core.*$"
],
"enabled": false
}
]
}
================================================
FILE: .github/workflows/github-release.yml
================================================
name: publish github release
on:
workflow_dispatch:
inputs:
releaseversion:
description: 'Release version'
required: true
default: '3.8.0'
jobs:
publish-github-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate changelog
id: changelog
uses: metcalfc/changelog-generator@v4.5.0
with:
myToken: ${{ secrets.GH_TOKEN }}
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
with:
tag_name: ${{ github.event.inputs.releaseversion }}
release_name: ${{ github.event.inputs.releaseversion }}
body: |
### Things that changed in this release
${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: ${{ contains(github.event.inputs.releaseversion, '-') }}
================================================
FILE: .github/workflows/image.yml
================================================
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Docker 镜像 构建
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: [ 17 ]
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'zulu'
- name: mvn clean install
run: mvn clean install -Pcloud
- name: Login to Docker Registry
run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} registry.cn-hangzhou.aliyuncs.com
- name: Build and push Docker images
run: |
docker compose build
registry="registry.cn-hangzhou.aliyuncs.com/pigx/"
for service in $(docker compose config --services); do
if [ "$service" != "pig-redis" ]; then
docker tag ${service}:latest ${registry}${service}:latest
docker push ${registry}${service}:latest
else
echo "Skipping pig-redis service"
fi
done
================================================
FILE: .github/workflows/maven.yml
================================================
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: maven 编译检查
on:
push:
branches: [ master,dev ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: [ 17,21,25 ]
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'zulu'
- name: mvn spring-javaformat:validate
id: validate
run: mvn spring-javaformat:validate -Dmaven.compiler.release=${{ matrix.java-version }}
continue-on-error: true
- name: Auto format code if validation fails
if: steps.validate.outcome == 'failure'
run: mvn spring-javaformat:apply -Dmaven.compiler.release=${{ matrix.java-version }}
- name: Create Pull Request for formatting changes
if: steps.validate.outcome == 'failure'
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'Auto-format code with spring-javaformat'
title: 'Auto-format: Fix code formatting issues'
body: |
This PR was automatically created because the spring-javaformat validation failed.
The following changes have been applied:
- Applied spring-javaformat:apply to fix formatting issues
Please review and merge if the changes look correct.
branch: auto-format-${{ github.run_number }}
delete-branch: true
- name: mvn clean install
run: mvn clean install -Pboot -Dmaven.compiler.release=${{ matrix.java-version }}
- name: mvn clean install
run: mvn clean install -Dmaven.compiler.release=${{ matrix.java-version }}
- name: failure
if: failure() && github.repository == 'pig-mesh/pig'
uses: chf007/action-wechat-work@master
env:
WECHAT_WORK_BOT_WEBHOOK: ${{secrets.WECHAT_WORK_BOT_WEBHOOK}}
with:
msgtype: markdown
content: |
# 💤🤷♀️ failure 🙅♂️💣 [pig-mesh/pig](https://github.com/pig-mesh/pig)
> Github Action: https://github.com/pig-mesh/pig failure
> (⋟﹏⋞) from github action message
================================================
FILE: .github/workflows/mirror.yml
================================================
name: 同步代码
on:
push:
branches: [ master,dev ]
pull_request:
branches: [ master,dev ]
jobs:
gitee:
runs-on: ubuntu-latest
container:
image: "centos:8"
steps:
- uses: wearerequired/git-mirror-action@master #同步至 gitee
env:
SSH_PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
with:
source-repo: "git@github.com:pig-mesh/pig.git"
destination-repo: "git@gitee.com:log4j/pig.git"
================================================
FILE: .gitignore
================================================
### gradle ###
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
.mvn/wrapper/maven-wrapper.jar
### STS ###
.settings/
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
bin/
### IntelliJ IDEA ###
!.idea/icon.png
.idea
*.iws
*.iml
*.ipr
rebel.xml
### NetBeans ###
nbproject/private/
build/
nbbuild/
nbdist/
.nb-gradle/
### maven ###
target/
*.war
*.ear
*.zip
*.tar
*.tar.gz
*.versionsBackup
### vscode ###
.vscode
### logs ###
/logs/
*.log
### temp ignore ###
*.cache
*.diff
*.patch
*.tmp
*.java~
*.properties~
*.xml~
### system ignore ###
.DS_Store
Thumbs.db
Servers
.metadata
.flattened-pom.xml
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
> **🚀 Spring Boot 4.0 版本来了**
>
> 分支 `boot4` 基于 Spring Boot 4.0 + Spring Cloud 2025.1 进行开发。
.*), /$\\{path}/$\\{path}/v3/api-docs', '53ace4035d810f07e3767d94e1e68379', '2025-01-30 16:50:04', '2025-05-30 08:36:27', 'nacos_namespace_migrate', '0:0:0:0:0:0:0:1', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` (`id`, `data_id`, `group_id`, `content`, `md5`, `gmt_create`, `gmt_modified`, `src_user`, `src_ip`, `app_name`, `tenant_id`, `c_desc`, `c_use`, `effect`, `type`, `c_schema`, `encrypted_data_key`) VALUES (5, 'pig-monitor-dev.yml', 'DEFAULT_GROUP', 'spring:\n autoconfigure:\n exclude: com.pig4cloud.pig.common.core.config.JacksonConfiguration\n # 安全配置\n security:\n user:\n name: ENC(8Hk2ILNJM8UTOuW/Xi75qg==) # pig\n password: ENC(o6cuPFfUevmTbkmBnE67Ow====) # pig\n', '650bdfa15f60f3faa84dfe6e6878b8cf', '2025-01-30 16:50:04', '2025-01-30 16:50:04', 'nacos', '127.0.0.1', '', 'public', NULL, NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` (`id`, `data_id`, `group_id`, `content`, `md5`, `gmt_create`, `gmt_modified`, `src_user`, `src_ip`, `app_name`, `tenant_id`, `c_desc`, `c_use`, `effect`, `type`, `c_schema`, `encrypted_data_key`) VALUES (6, 'pig-upms-biz-dev.yml', 'DEFAULT_GROUP', '# 数据源\nspring:\n datasource:\n type: com.zaxxer.hikari.HikariDataSource\n driver-class-name: com.mysql.cj.jdbc.Driver\n username: ${MYSQL_USERNAME:root}\n password: ${MYSQL_PASSWORD:root}\n url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3306}/${MYSQL_DB:pig}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true\n\n# 文件上传相关 支持阿里云、华为云、腾讯、minio\nfile:\n bucketName: s3demo \n local:\n enable: true\n base-path: /Users/lengleng/Downloads/img', '80cf3a9b7b490e32b03550c429dea33e', '2025-10-29 11:39:40', '2025-10-29 11:39:40', 'nacos', '10.25.25.2', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` (`id`, `data_id`, `group_id`, `content`, `md5`, `gmt_create`, `gmt_modified`, `src_user`, `src_ip`, `app_name`, `tenant_id`, `c_desc`, `c_use`, `effect`, `type`, `c_schema`, `encrypted_data_key`) VALUES (7, 'pig-quartz-dev.yml', 'DEFAULT_GROUP', 'spring:\n datasource:\n type: com.zaxxer.hikari.HikariDataSource\n driver-class-name: com.mysql.cj.jdbc.Driver\n username: ${MYSQL_USERNAME:root}\n password: ${MYSQL_PASSWORD:root}\n url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3306}/${MYSQL_DB:pig}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true\n', 'e0c9ce980fd14fd28f955852061970ca', '2025-10-29 11:39:40', '2025-10-29 11:39:40', 'nacos', '10.25.25.2', '', 'public', '', NULL, NULL, 'yaml', NULL, '');
COMMIT;
-- ----------------------------
-- Table structure for config_info_beta
-- ----------------------------
DROP TABLE IF EXISTS `config_info_beta`;
CREATE TABLE `config_info_beta` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
`group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
`app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name',
`content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) COLLATE utf8_bin DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COLLATE utf8_bin COMMENT 'source user',
`src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段',
`encrypted_data_key` varchar(1024) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT '密钥',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
-- ----------------------------
-- Records of config_info_beta
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for config_info_gray
-- ----------------------------
DROP TABLE IF EXISTS `config_info_gray`;
CREATE TABLE `config_info_gray` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`src_user` text COMMENT 'src_user',
`src_ip` varchar(100) DEFAULT NULL COMMENT 'src_ip',
`gmt_create` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_create',
`gmt_modified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_modified',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`gray_name` varchar(128) NOT NULL COMMENT 'gray_name',
`gray_rule` text NOT NULL COMMENT 'gray_rule',
`encrypted_data_key` varchar(256) NOT NULL DEFAULT '' COMMENT 'encrypted_data_key',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfogray_datagrouptenantgray` (`data_id`,`group_id`,`tenant_id`,`gray_name`),
KEY `idx_dataid_gmt_modified` (`data_id`,`gmt_modified`),
KEY `idx_gmt_modified` (`gmt_modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='config_info_gray';
-- ----------------------------
-- Records of config_info_gray
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for config_info_tag
-- ----------------------------
DROP TABLE IF EXISTS `config_info_tag`;
CREATE TABLE `config_info_tag` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
`group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name',
`content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content',
`md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COLLATE utf8_bin COMMENT 'source user',
`src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
-- ----------------------------
-- Records of config_info_tag
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for config_tags_relation
-- ----------------------------
DROP TABLE IF EXISTS `config_tags_relation`;
CREATE TABLE `config_tags_relation` (
`id` bigint NOT NULL COMMENT 'id',
`tag_name` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
`group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id',
`nid` bigint NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识',
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
-- ----------------------------
-- Records of config_tags_relation
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for group_capacity
-- ----------------------------
DROP TABLE IF EXISTS `group_capacity`;
CREATE TABLE `group_capacity` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
-- ----------------------------
-- Records of group_capacity
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for his_config_info
-- ----------------------------
DROP TABLE IF EXISTS `his_config_info`;
CREATE TABLE `his_config_info` (
`id` bigint unsigned NOT NULL COMMENT 'id',
`nid` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识',
`data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
`group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
`app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name',
`content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content',
`md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COLLATE utf8_bin COMMENT 'source user',
`src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip',
`op_type` char(10) COLLATE utf8_bin DEFAULT NULL COMMENT 'operation type',
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段',
`encrypted_data_key` varchar(1024) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT '密钥',
`publish_type` varchar(50) COLLATE utf8_bin DEFAULT 'formal' COMMENT 'publish type gray or formal',
`gray_name` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'gray name',
`ext_info` longtext COLLATE utf8_bin COMMENT 'ext info',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
-- ----------------------------
-- Table structure for permissions
-- ----------------------------
DROP TABLE IF EXISTS `permissions`;
CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL COMMENT 'role',
`resource` varchar(128) NOT NULL COMMENT 'resource',
`action` varchar(8) NOT NULL COMMENT 'action',
UNIQUE KEY `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of permissions
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for roles
-- ----------------------------
DROP TABLE IF EXISTS `roles`;
CREATE TABLE `roles` (
`username` varchar(50) NOT NULL COMMENT 'username',
`role` varchar(50) NOT NULL COMMENT 'role',
UNIQUE KEY `idx_user_role` (`username`,`role`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of roles
-- ----------------------------
BEGIN;
INSERT INTO `roles` (`username`, `role`) VALUES ('nacos', 'ROLE_ADMIN');
COMMIT;
-- ----------------------------
-- Table structure for tenant_capacity
-- ----------------------------
DROP TABLE IF EXISTS `tenant_capacity`;
CREATE TABLE `tenant_capacity` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
-- ----------------------------
-- Records of tenant_capacity
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for tenant_info
-- ----------------------------
DROP TABLE IF EXISTS `tenant_info`;
CREATE TABLE `tenant_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id',
`tenant_name` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) COLLATE utf8_bin DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint NOT NULL COMMENT '创建时间',
`gmt_modified` bigint NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
-- ----------------------------
-- Records of tenant_info
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`username` varchar(50) NOT NULL COMMENT 'username',
`password` varchar(500) NOT NULL COMMENT 'password',
`enabled` tinyint(1) NOT NULL COMMENT 'enabled',
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` (`username`, `password`, `enabled`) VALUES ('nacos', '$2a$10$W6PKgRTzXUp6R/NY853Kn.nRaIcX3whIMTZ/WWkNqo2MTOeSBjKJq', 1);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
================================================
FILE: docker-compose.yml
================================================
services:
pig-mysql:
build:
context: ./db
environment:
MYSQL_ROOT_HOST: "%"
MYSQL_ROOT_PASSWORD: root
restart: always
container_name: pig-mysql
image: pig-mysql
ports:
- 33306:3306
networks:
- spring_cloud_default
pig-redis:
image: registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/redis
ports:
- 36379:6379
restart: always
container_name: pig-redis
hostname: pig-redis
networks:
- spring_cloud_default
pig-register:
build:
context: ./pig-register
restart: always
ports:
- 8848:8848
- 9848:9848
- 8080:8080
environment:
MYSQL_HOST: pig-mysql
REDIS_HOST: pig-redis
container_name: pig-register
hostname: pig-register
image: pig-register
networks:
- spring_cloud_default
pig-gateway:
build:
context: ./pig-gateway
restart: always
ports:
- 9999:9999
container_name: pig-gateway
hostname: pig-gateway
image: pig-gateway
environment:
REDIS_HOST: pig-redis
NACOS_HOST: pig-register
networks:
- spring_cloud_default
pig-auth:
build:
context: ./pig-auth
restart: always
container_name: pig-auth
hostname: pig-auth
image: pig-auth
environment:
REDIS_HOST: pig-redis
NACOS_HOST: pig-register
networks:
- spring_cloud_default
pig-upms:
build:
context: ./pig-upms/pig-upms-biz
restart: always
container_name: pig-upms
hostname: pig-upms
image: pig-upms
environment:
MYSQL_HOST: pig-mysql
REDIS_HOST: pig-redis
NACOS_HOST: pig-register
networks:
- spring_cloud_default
pig-monitor:
build:
context: ./pig-visual/pig-monitor
restart: always
ports:
- 5001:5001
container_name: pig-monitor
hostname: pig-monitor
image: pig-monitor
environment:
NACOS_HOST: pig-register
networks:
- spring_cloud_default
pig-codegen:
build:
context: ./pig-visual/pig-codegen
restart: always
container_name: pig-codegen
hostname: pig-codegen
image: pig-codegen
environment:
MYSQL_HOST: pig-mysql
REDIS_HOST: pig-redis
NACOS_HOST: pig-register
networks:
- spring_cloud_default
pig-quartz:
build:
context: ./pig-visual/pig-quartz
restart: always
image: pig-quartz
container_name: pig-quartz
environment:
MYSQL_HOST: pig-mysql
REDIS_HOST: pig-redis
NACOS_HOST: pig-register
networks:
- spring_cloud_default
networks:
spring_cloud_default:
name: spring_cloud_default
driver: bridge
================================================
FILE: pig-auth/Dockerfile
================================================
FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/java:21-anolis
WORKDIR /pig-auth
ARG JAR_FILE=target/pig-auth.jar
COPY ${JAR_FILE} app.jar
EXPOSE 3000
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java $JAVA_OPTS -jar app.jar
================================================
FILE: pig-auth/pom.xml
================================================
4.0.0
com.pig4cloud
pig
${revision}
pig-auth
jar
pig 认证授权中心,基于 spring security oAuth2
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
com.pig4cloud
pig-common-feign
com.pig4cloud
pig-upms-api
com.pig4cloud
pig-common-security
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-freemarker
org.springframework.boot
spring-boot-starter-undertow
com.pig4cloud
pig-common-log
com.pig4cloud.plugin
captcha-core
cn.hutool
hutool-crypto
boot
cloud
true
org.springframework.boot
spring-boot-maven-plugin
io.fabric8
docker-maven-plugin
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/PigAuthApplication.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.auth;
import com.pig4cloud.pig.common.feign.annotation.EnablePigFeignClients;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 认证授权中心应用启动类
*
* @author lengleng
* @date 2025/05/30
*/
@EnablePigFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class PigAuthApplication {
public static void main(String[] args) {
SpringApplication.run(PigAuthApplication.class, args);
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfiguration.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.auth.config;
import com.pig4cloud.pig.auth.support.CustomeOAuth2AccessTokenGenerator;
import com.pig4cloud.pig.auth.support.core.CustomeOAuth2TokenCustomizer;
import com.pig4cloud.pig.auth.support.core.FormIdentityLoginConfigurer;
import com.pig4cloud.pig.auth.support.core.PigDaoAuthenticationProvider;
import com.pig4cloud.pig.auth.support.filter.PasswordDecoderFilter;
import com.pig4cloud.pig.auth.support.filter.ValidateCodeFilter;
import com.pig4cloud.pig.auth.support.handler.PigAuthenticationFailureEventHandler;
import com.pig4cloud.pig.auth.support.handler.PigAuthenticationSuccessEventHandler;
import com.pig4cloud.pig.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationConverter;
import com.pig4cloud.pig.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationProvider;
import com.pig4cloud.pig.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationConverter;
import com.pig4cloud.pig.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationProvider;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.security.component.PigBootCorsProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
/**
* 认证服务器配置类
*
* @author lengleng
* @date 2025/05/30
*/
@Configuration
@RequiredArgsConstructor
public class AuthorizationServerConfiguration {
private final OAuth2AuthorizationService authorizationService;
private final PasswordDecoderFilter passwordDecoderFilter;
private final ValidateCodeFilter validateCodeFilter;
private final PigBootCorsProperties pigBootCorsProperties;
/**
* Authorization Server 配置,仅对 /oauth2/** 的请求有效
* @param http http
* @return {@link SecurityFilterChain }
* @throws Exception 异常
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServer(HttpSecurity http) throws Exception {
// 配置授权服务器的安全策略,只有/oauth2/**的请求才会走如下的配置
http.securityMatcher("/oauth2/**");
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
// 增加验证码过滤器
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
// 增加密码解密过滤器
http.addFilterBefore(passwordDecoderFilter, UsernamePasswordAuthenticationFilter.class);
http.with(authorizationServerConfigurer.tokenEndpoint((tokenEndpoint) -> {// 个性化认证授权端点
tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter
.accessTokenResponseHandler(new PigAuthenticationSuccessEventHandler()) // 登录成功处理器
.errorResponseHandler(new PigAuthenticationFailureEventHandler());// 登录失败处理器
}).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证
oAuth2ClientAuthenticationConfigurer.errorResponseHandler(new PigAuthenticationFailureEventHandler()))// 处理客户端认证异常
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint// 授权码端点个性化confirm页面
.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI)), Customizer.withDefaults())
.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated());
// 设置 Token 存储的策略
http.with(authorizationServerConfigurer.authorizationService(authorizationService)// redis存储token的实现
.authorizationServerSettings(
AuthorizationServerSettings.builder().issuer(SecurityConstants.PROJECT_LICENSE).build()),
Customizer.withDefaults());
// 设置授权码模式登录页面
http.with(new FormIdentityLoginConfigurer(), Customizer.withDefaults());
// 配置 CORS 跨域资源共享
if (Boolean.TRUE.equals(pigBootCorsProperties.getEnabled())) {
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
}
DefaultSecurityFilterChain securityFilterChain = http.build();
// 注入自定义授权模式实现
addCustomOAuth2GrantAuthenticationProvider(http);
return securityFilterChain;
}
/**
* 令牌生成规则实现
* client:username:uuid
* @return OAuth2TokenGenerator
*/
@Bean
public OAuth2TokenGenerator oAuth2TokenGenerator() {
CustomeOAuth2AccessTokenGenerator accessTokenGenerator = new CustomeOAuth2AccessTokenGenerator();
// 注入Token 增加关联用户信息
accessTokenGenerator.setAccessTokenCustomizer(new CustomeOAuth2TokenCustomizer());
return new DelegatingOAuth2TokenGenerator(accessTokenGenerator, new OAuth2RefreshTokenGenerator());
}
/**
* request -> xToken 注入请求转换器
* @return DelegatingAuthenticationConverter
*/
@Bean
public AuthenticationConverter accessTokenRequestConverter() {
return new DelegatingAuthenticationConverter(Arrays.asList(
new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
new OAuth2ResourceOwnerSmsAuthenticationConverter(), new OAuth2RefreshTokenAuthenticationConverter(),
new OAuth2ClientCredentialsAuthenticationConverter(),
new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2AuthorizationCodeRequestAuthenticationConverter()));
}
/**
* 注入授权模式实现提供方
*
* 1. 密码模式
* 2. 短信登录
*/
private void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http) {
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
OAuth2ResourceOwnerPasswordAuthenticationProvider resourceOwnerPasswordAuthenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
OAuth2ResourceOwnerSmsAuthenticationProvider resourceOwnerSmsAuthenticationProvider = new OAuth2ResourceOwnerSmsAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
// 处理 UsernamePasswordAuthenticationToken
http.authenticationProvider(new PigDaoAuthenticationProvider());
// 处理 OAuth2ResourceOwnerPasswordAuthenticationToken
http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider);
// 处理 OAuth2ResourceOwnerSmsAuthenticationToken
http.authenticationProvider(resourceOwnerSmsAuthenticationProvider);
}
/**
* 配置 CORS 跨域资源共享
* @return UrlBasedCorsConfigurationSource CORS配置源
*/
private UrlBasedCorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 从配置文件读取允许的源模式
pigBootCorsProperties.getAllowedOriginPatterns().forEach(corsConfiguration::addAllowedOriginPattern);
// 从配置文件读取允许的请求头
pigBootCorsProperties.getAllowedHeaders().forEach(corsConfiguration::addAllowedHeader);
// 从配置文件读取允许的HTTP方法
pigBootCorsProperties.getAllowedMethods().forEach(corsConfiguration::addAllowedMethod);
// 从配置文件读取是否允许携带凭证
corsConfiguration.setAllowCredentials(pigBootCorsProperties.getAllowCredentials());
// 注册CORS配置到指定路径
source.registerCorsConfiguration(pigBootCorsProperties.getPathPattern(), corsConfiguration);
return source;
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/ImageCodeEndpoint.java
================================================
package com.pig4cloud.pig.auth.endpoint;
import cn.hutool.core.lang.Validator;
import com.pig4cloud.captcha.ArithmeticCaptcha;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.util.RedisUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* 验证码相关的接口
*
* @author lengleng
* @date 2022/6/27
*/
@RestController
@RequestMapping("/code")
@RequiredArgsConstructor
@Tag(description = "code", name = "验证码控制器管理模块")
public class ImageCodeEndpoint {
private static final Integer DEFAULT_IMAGE_WIDTH = 100;
private static final Integer DEFAULT_IMAGE_HEIGHT = 40;
/**
* 创建图形验证码并输出到响应流
* @param randomStr 随机字符串,用于缓存验证码
* @param response HTTP响应对象,用于输出验证码图片
*/
@SneakyThrows
@GetMapping("/image")
@Operation(summary = "创建图形验证码并输出到响应流", description = "创建图形验证码并输出到响应流")
public void image(String randomStr, HttpServletResponse response) {
ArithmeticCaptcha captcha = new ArithmeticCaptcha(DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT);
if (Validator.isMobile(randomStr)) {
return;
}
String result = captcha.text();
RedisUtils.set(CacheConstants.DEFAULT_CODE_KEY + randomStr, result, SecurityConstants.CODE_TIME,
TimeUnit.SECONDS);
// 转换流信息写出
captcha.out(response.getOutputStream());
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/PigTokenEndpoint.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.auth.endpoint;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.cache.CacheManager;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.event.LogoutSuccessEvent;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pig.admin.api.entity.SysOauthClientDetails;
import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService;
import com.pig4cloud.pig.admin.api.vo.TokenVo;
import com.pig4cloud.pig.auth.support.handler.PigAuthenticationFailureEventHandler;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.util.R;
import com.pig4cloud.pig.common.core.util.RedisUtils;
import com.pig4cloud.pig.common.core.util.RetOps;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.security.annotation.Inner;
import com.pig4cloud.pig.common.security.util.OAuth2EndpointUtils;
import com.pig4cloud.pig.common.security.util.OAuth2ErrorCodesExpand;
import com.pig4cloud.pig.common.security.util.OAuthClientException;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
/**
* OAuth2 令牌端点控制器,提供令牌相关操作
*
* @author lengleng
* @date 2025/05/30
*/
@RestController
@RequestMapping
@RequiredArgsConstructor
@Tag(description = "oauth", name = "OAuth2 令牌端点控制器管理模块")
public class PigTokenEndpoint {
private final HttpMessageConverter accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
private final AuthenticationFailureHandler authenticationFailureHandler = new PigAuthenticationFailureEventHandler();
private final OAuth2AuthorizationService authorizationService;
private final RemoteClientDetailsService clientDetailsService;
private final CacheManager cacheManager;
/**
* 授权码模式:认证页面
* @param modelAndView 视图模型对象
* @param error 表单登录失败处理回调的错误信息
* @return 包含登录页面视图和错误信息的ModelAndView对象
*/
@GetMapping("/token/login")
@Operation(summary = "授权码模式:认证页面", description = "授权码模式:认证页面")
public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
modelAndView.setViewName("ftl/login");
modelAndView.addObject("error", error);
return modelAndView;
}
/**
* 授权码模式:确认页面
* @param principal 用户主体信息
* @param modelAndView 模型和视图对象
* @param clientId 客户端ID
* @param scope 请求的权限范围
* @param state 状态参数
* @return 包含确认页面信息的ModelAndView对象
*/
@GetMapping("/oauth2/confirm_access")
@Operation(summary = "授权码模式:确认页面", description = "授权码模式:确认页面")
public ModelAndView confirm(Principal principal, ModelAndView modelAndView,
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
@RequestParam(OAuth2ParameterNames.STATE) String state) {
SysOauthClientDetails clientDetails = RetOps.of(clientDetailsService.getClientDetailsById(clientId))
.getData()
.orElseThrow(() -> new OAuthClientException("clientId 不合法"));
Set authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
modelAndView.addObject("clientId", clientId);
modelAndView.addObject("state", state);
modelAndView.addObject("scopeList", authorizedScopes);
modelAndView.addObject("principalName", principal.getName());
modelAndView.setViewName("ftl/confirm");
return modelAndView;
}
/**
* 注销并删除令牌
* @param authHeader 认证头信息,包含Bearer token
* @return 返回操作结果,包含布尔值表示是否成功
*/
@DeleteMapping("/token/logout")
@Operation(summary = "注销并删除令牌", description = "注销并删除令牌")
public R logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
if (StrUtil.isBlank(authHeader)) {
return R.ok();
}
String tokenValue = authHeader.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), StrUtil.EMPTY).trim();
return removeToken(tokenValue);
}
/**
* 检查令牌有效性
* @param token 待验证的令牌
* @param response HTTP响应对象
* @param request HTTP请求对象
* @throws InvalidBearerTokenException 令牌无效或缺失时抛出异常
*/
@SneakyThrows
@GetMapping("/token/check_token")
@Operation(summary = "检查令牌有效性", description = "检查令牌有效性")
public void checkToken(String token, HttpServletResponse response, HttpServletRequest request) {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
if (StrUtil.isBlank(token)) {
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
new InvalidBearerTokenException(OAuth2ErrorCodesExpand.TOKEN_MISSING));
return;
}
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
// 如果令牌不存在 返回401
if (authorization == null || authorization.getAccessToken() == null) {
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
new InvalidBearerTokenException(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN));
return;
}
Map claims = authorization.getAccessToken().getClaims();
OAuth2AccessTokenResponse sendAccessTokenResponse = OAuth2EndpointUtils.sendAccessTokenResponse(authorization,
claims);
this.accessTokenHttpResponseConverter.write(sendAccessTokenResponse, MediaType.APPLICATION_JSON, httpResponse);
}
/**
* 删除令牌
* @param token 令牌
* @return 删除结果
*/
@Inner
@DeleteMapping("/token/remove/{token}")
@Operation(summary = "删除令牌", description = "删除令牌")
public R removeToken(@PathVariable("token") String token) {
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
if (authorization == null) {
return R.ok();
}
OAuth2Authorization.Token accessToken = authorization.getAccessToken();
if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) {
return R.ok();
}
// 清空用户信息(立即删除)
cacheManager.getCache(CacheConstants.USER_DETAILS).evictIfPresent(authorization.getPrincipalName());
// 清空access token
authorizationService.remove(authorization);
// 处理自定义退出事件,保存相关日志
SpringContextHolder.publishEvent(new LogoutSuccessEvent(new PreAuthenticatedAuthenticationToken(
authorization.getPrincipalName(), authorization.getRegisteredClientId())));
return R.ok();
}
/**
* 分页查询令牌列表
* @param params 请求参数,包含分页参数current和size
* @return 分页结果,包含令牌信息列表
*/
@Inner
@PostMapping("/token/page")
@Operation(summary = "分页查询令牌列表", description = "分页查询令牌列表")
public R tokenList(@RequestBody Map params) {
// 根据分页参数获取对应数据
String username = MapUtil.getStr(params, SecurityConstants.USERNAME);
String pattern = String.format("%s::*", CacheConstants.PROJECT_OAUTH_ACCESS);
int current = MapUtil.getInt(params, CommonConstants.CURRENT);
int size = MapUtil.getInt(params, CommonConstants.SIZE);
Page result = new Page(current, size);
// 获取总数
List allKeys = RedisUtils.scan(pattern);
result.setTotal(allKeys.size());
List pageKeys = RedisUtils.findKeysForPage(pattern, current - 1, size);
List pagedAuthorizations = RedisUtils.multiGet(pageKeys);
// 转换为TokenVo
List tokenVoList = pagedAuthorizations.stream()
.filter(Objects::nonNull)
.map(this::convertToTokenVo)
.filter(tokenVo -> {
if (StrUtil.isBlank(username)) {
return true;
}
return StrUtil.startWithAnyIgnoreCase(tokenVo.getUsername(), username);
})
.toList();
if (StrUtil.isNotBlank(username)) {
result.setTotal(tokenVoList.size());
}
result.setRecords(tokenVoList);
return R.ok(result);
}
/**
* 将OAuth2Authorization转换为TokenVo
* @param authorization OAuth2授权对象
* @return TokenVo对象
*/
private TokenVo convertToTokenVo(OAuth2Authorization authorization) {
TokenVo tokenVo = new TokenVo();
tokenVo.setClientId(authorization.getRegisteredClientId());
tokenVo.setId(authorization.getId());
tokenVo.setUsername(authorization.getPrincipalName());
OAuth2Authorization.Token accessToken = authorization.getAccessToken();
tokenVo.setAccessToken(accessToken.getToken().getTokenValue());
String expiresAt = TemporalAccessorUtil.format(accessToken.getToken().getExpiresAt(),
DatePattern.NORM_DATETIME_PATTERN);
tokenVo.setExpiresAt(expiresAt);
String issuedAt = TemporalAccessorUtil.format(accessToken.getToken().getIssuedAt(),
DatePattern.NORM_DATETIME_PATTERN);
tokenVo.setIssuedAt(issuedAt);
return tokenVo;
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/CustomeOAuth2AccessTokenGenerator.java
================================================
package com.pig4cloud.pig.auth.support;
import org.springframework.lang.Nullable;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.Serial;
import java.time.Instant;
import java.util.*;
/**
* 自定义OAuth2访问令牌生成器
*
* @author lengleng
* @date 2025/05/30
*/
public class CustomeOAuth2AccessTokenGenerator implements OAuth2TokenGenerator {
private OAuth2TokenCustomizer accessTokenCustomizer;
private final StringKeyGenerator accessTokenGenerator = new Base64StringKeyGenerator(
Base64.getUrlEncoder().withoutPadding(), 96);
/**
* 生成OAuth2访问令牌
* @param context OAuth2令牌上下文
* @return 生成的访问令牌,如果令牌类型不是ACCESS_TOKEN或格式不是REFERENCE则返回null
* @see OAuth2TokenContext
* @see OAuth2AccessToken
*/
@Nullable
@Override
public OAuth2AccessToken generate(OAuth2TokenContext context) {
if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) || !OAuth2TokenFormat.REFERENCE
.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
return null;
}
String issuer = null;
if (context.getAuthorizationServerContext() != null) {
issuer = context.getAuthorizationServerContext().getIssuer();
}
RegisteredClient registeredClient = context.getRegisteredClient();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
// @formatter:off
OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
if (StringUtils.hasText(issuer)) {
claimsBuilder.issuer(issuer);
}
claimsBuilder
.subject(context.getPrincipal().getName())
.audience(Collections.singletonList(registeredClient.getClientId()))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.notBefore(issuedAt)
.id(UUID.randomUUID().toString());
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
}
// @formatter:on
if (this.accessTokenCustomizer != null) {
// @formatter:off
OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
.registeredClient(context.getRegisteredClient())
.principal(context.getPrincipal())
.authorizationServerContext(context.getAuthorizationServerContext())
.authorizedScopes(context.getAuthorizedScopes())
.tokenType(context.getTokenType())
.authorizationGrantType(context.getAuthorizationGrantType());
if (context.getAuthorization() != null) {
accessTokenContextBuilder.authorization(context.getAuthorization());
}
if (context.getAuthorizationGrant() != null) {
accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
}
// @formatter:on
OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
this.accessTokenCustomizer.customize(accessTokenContext);
}
OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
return new CustomeOAuth2AccessTokenGenerator.OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER,
this.accessTokenGenerator.generateKey(), accessTokenClaimsSet.getIssuedAt(),
accessTokenClaimsSet.getExpiresAt(), context.getAuthorizedScopes(), accessTokenClaimsSet.getClaims());
}
/**
* 设置用于定制{@link OAuth2AccessToken}的{@link OAuth2TokenClaimsContext#getClaims()}的{@link OAuth2TokenCustomizer}
* @param accessTokenCustomizer
* 用于定制{@code OAuth2AccessToken}声明的{@link OAuth2TokenCustomizer}
* @throws IllegalArgumentException 当accessTokenCustomizer为null时抛出
*/
public void setAccessTokenCustomizer(OAuth2TokenCustomizer accessTokenCustomizer) {
Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null");
this.accessTokenCustomizer = accessTokenCustomizer;
}
/**
* OAuth2访问令牌声明类,继承自OAuth2AccessToken并实现ClaimAccessor接口
*
* @author lengleng
* @date 2025/05/30
*/
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
@Serial
private static final long serialVersionUID = 1L;
private final Map claims;
/**
* 构造OAuth2访问令牌声明
* @param tokenType 令牌类型
* @param tokenValue 令牌值
* @param issuedAt 颁发时间
* @param expiresAt 过期时间
* @param scopes 权限范围集合
* @param claims 声明信息映射
*/
private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
Set scopes, Map claims) {
super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
this.claims = claims;
}
/**
* 获取claims集合
* @return claims键值对集合
*/
@Override
public Map getClaims() {
return this.claims;
}
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationConverter.java
================================================
package com.pig4cloud.pig.auth.support.base;
import com.pig4cloud.pig.common.security.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* OAuth2资源所有者基础认证转换器抽象类
*
* @param 继承自OAuth2ResourceOwnerBaseAuthenticationToken的泛型类型
* @author lengleng
* @date 2025/05/30
*/
public abstract class OAuth2ResourceOwnerBaseAuthenticationConverter
implements AuthenticationConverter {
/**
* 是否支持此convert
* @param grantType 授权类型
* @return
*/
public abstract boolean support(String grantType);
/**
* 校验参数
* @param request 请求
*/
public void checkParams(HttpServletRequest request) {
}
/**
* 构建具体类型的token
* @param clientPrincipal 客户端认证信息
* @param requestedScopes 请求的作用域集合
* @param additionalParameters 附加参数映射
* @return 构建完成的token对象
*/
public abstract T buildToken(Authentication clientPrincipal, Set requestedScopes,
Map additionalParameters);
/**
* 将HttpServletRequest转换为Authentication对象
* @param request HTTP请求对象
* @return 认证信息对象
* @throws OAuth2AuthenticationException 当请求参数不合法或客户端未认证时抛出异常
*/
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!support(grantType)) {
return null;
}
MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request);
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Set requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// 校验个性化参数
checkParams(request);
// 获取当前已经认证的客户端信息
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
if (clientPrincipal == null) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// 扩展信息
Map additionalParameters = parameters.entrySet()
.stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE)
&& !e.getKey().equals(OAuth2ParameterNames.SCOPE))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
// 创建token
return buildToken(clientPrincipal, requestedScopes, additionalParameters);
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java
================================================
package com.pig4cloud.pig.auth.support.base;
import cn.hutool.extra.spring.SpringUtil;
import com.pig4cloud.pig.common.security.util.OAuth2ErrorCodesExpand;
import com.pig4cloud.pig.common.security.util.ScopeException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.security.Principal;
import java.time.Instant;
import java.util.*;
import java.util.function.Supplier;
/**
* OAuth2资源所有者基础认证提供者抽象类,用于处理资源所有者密码凭证授权流程
*
* @param OAuth2资源所有者基础认证令牌类型
* @author lengleng
* @date 2025/05/30
*/
public abstract class OAuth2ResourceOwnerBaseAuthenticationProvider
implements AuthenticationProvider {
private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerBaseAuthenticationProvider.class);
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator extends OAuth2Token> tokenGenerator;
private final AuthenticationManager authenticationManager;
private final MessageSourceAccessor messages;
@Deprecated
private Supplier refreshTokenGenerator;
/**
* 构造一个基于资源所有者密码模式的OAuth2认证提供者
* @param authenticationManager 认证管理器
* @param authorizationService 授权服务
* @param tokenGenerator token生成器
* @throws IllegalArgumentException 当authorizationService或tokenGenerator为null时抛出
* @since 0.2.3
*/
public OAuth2ResourceOwnerBaseAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authenticationManager = authenticationManager;
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
// 国际化配置
this.messages = new MessageSourceAccessor(SpringUtil.getBean("securityMessageSource"), Locale.CHINA);
}
/**
* 设置刷新令牌生成器
* @param refreshTokenGenerator 刷新令牌生成器,不能为null
* @deprecated 该方法已废弃
*/
@Deprecated
public void setRefreshTokenGenerator(Supplier refreshTokenGenerator) {
Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null");
this.refreshTokenGenerator = refreshTokenGenerator;
}
/**
* 构建用户名密码认证令牌
* @param reqParameters 请求参数映射
* @return 用户名密码认证令牌
*/
public abstract UsernamePasswordAuthenticationToken buildToken(Map reqParameters);
/**
* 当前provider是否支持此令牌类型
* @param authentication
* @return
*/
@Override
public abstract boolean supports(Class> authentication);
/**
* 当前的请求客户端是否支持此模式
* @param registeredClient
*/
public abstract void checkClient(RegisteredClient registeredClient);
/**
* 执行认证操作,遵循与{@link AuthenticationManager#authenticate(Authentication)}相同的契约
* @param authentication 认证请求对象
* @return 包含凭证的完整认证对象,如果当前认证提供者无法处理传入的认证对象可能返回null
* @throws AuthenticationException 认证失败时抛出
* @throws OAuth2AuthenticationException 当scope无效或token生成失败时抛出
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
T resouceOwnerBaseAuthentication = (T) authentication;
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(
resouceOwnerBaseAuthentication);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
checkClient(registeredClient);
Set authorizedScopes;
// Default to configured scopes
if (!CollectionUtils.isEmpty(resouceOwnerBaseAuthentication.getScopes())) {
for (String requestedScope : resouceOwnerBaseAuthentication.getScopes()) {
if (!registeredClient.getScopes().contains(requestedScope)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
}
authorizedScopes = new LinkedHashSet<>(resouceOwnerBaseAuthentication.getScopes());
}
else {
authorizedScopes = new LinkedHashSet<>();
}
Map reqParameters = resouceOwnerBaseAuthentication.getAdditionalParameters();
try {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(reqParameters);
LOGGER.debug("got usernamePasswordAuthenticationToken=" + usernamePasswordAuthenticationToken);
Authentication usernamePasswordAuthentication = authenticationManager
.authenticate(usernamePasswordAuthenticationToken);
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
.authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType())
.authorizationGrant(resouceOwnerBaseAuthentication);
// @formatter:on
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization
.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType())
// 0.4.0 新增的方法
.authorizedScopes(authorizedScopes);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.id(accessToken.getTokenValue())
.token(accessToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
((ClaimAccessor) generatedAccessToken).getClaims()))
// 0.4.0 新增的方法
.authorizedScopes(authorizedScopes)
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
}
else {
authorizationBuilder.id(accessToken.getTokenValue()).accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// Do not issue refresh token to public client
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
if (this.refreshTokenGenerator != null) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive());
refreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);
}
else {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
}
authorizationBuilder.refreshToken(refreshToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
LOGGER.debug("returning OAuth2AccessTokenAuthenticationToken");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken,
refreshToken, Objects.requireNonNull(authorization.getAccessToken().getClaims()));
}
catch (Exception ex) {
LOGGER.error("problem in authenticate", ex);
throw oAuth2AuthenticationException(authentication, (AuthenticationException) ex);
}
}
/**
* 登录异常转换为oauth2异常
* @param authentication 身份验证
* @param authenticationException 身份验证异常
* @return {@link OAuth2AuthenticationException}
*/
private OAuth2AuthenticationException oAuth2AuthenticationException(Authentication authentication,
AuthenticationException authenticationException) {
if (authenticationException instanceof UsernameNotFoundException) {
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USERNAME_NOT_FOUND,
this.messages.getMessage("JdbcDaoImpl.notFound", new Object[] { authentication.getName() },
"Username {0} not found"),
""));
}
if (authenticationException instanceof BadCredentialsException) {
return new OAuth2AuthenticationException(
new OAuth2Error(OAuth2ErrorCodesExpand.BAD_CREDENTIALS, this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), ""));
}
if (authenticationException instanceof LockedException) {
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_LOCKED, this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"), ""));
}
if (authenticationException instanceof DisabledException) {
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_DISABLE,
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"),
""));
}
if (authenticationException instanceof AccountExpiredException) {
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_EXPIRED, this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"), ""));
}
if (authenticationException instanceof CredentialsExpiredException) {
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.CREDENTIALS_EXPIRED,
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
"User credentials have expired"),
""));
}
if (authenticationException instanceof ScopeException) {
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "invalid_scope"), ""));
}
return new OAuth2AuthenticationException(OAuth2ErrorCodesExpand.UN_KNOW_LOGIN_ERROR);
}
/**
* 获取已认证的客户端主体,否则抛出无效客户端异常
* @param authentication 认证信息
* @return 已认证的客户端主体
* @throws OAuth2AuthenticationException 客户端未认证时抛出异常
*/
private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(
Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationToken.java
================================================
package com.pig4cloud.pig.auth.support.base;
import lombok.Getter;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
import java.io.Serial;
import java.util.*;
/**
* OAuth2资源所有者基础认证令牌抽象类
*
* @author lengleng
* @date 2025/05/30
*/
public abstract class OAuth2ResourceOwnerBaseAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 1L;
@Getter
private final AuthorizationGrantType authorizationGrantType;
@Getter
private final Authentication clientPrincipal;
@Getter
private final Set scopes;
@Getter
private final Map additionalParameters;
public OAuth2ResourceOwnerBaseAuthenticationToken(AuthorizationGrantType authorizationGrantType,
Authentication clientPrincipal, @Nullable Set scopes,
@Nullable Map additionalParameters) {
super(Collections.emptyList());
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
this.authorizationGrantType = authorizationGrantType;
this.clientPrincipal = clientPrincipal;
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
this.additionalParameters = Collections.unmodifiableMap(
additionalParameters != null ? new HashMap<>(additionalParameters) : Collections.emptyMap());
}
/**
* 扩展模式一般不需要密码
*/
@Override
public Object getCredentials() {
return "";
}
/**
* 获取用户名
*/
@Override
public Object getPrincipal() {
return this.clientPrincipal;
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/base/package-info.java
================================================
/**
* 自定义认证模式接入的抽象实现
*/
package com.pig4cloud.pig.auth.support.base;
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/core/CustomeOAuth2TokenCustomizer.java
================================================
package com.pig4cloud.pig.auth.support.core;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.security.service.PigUser;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
/**
* OAuth2 Token 自定义增强实现类
*
* @author lengleng
* @date 2025/05/30
*/
public class CustomeOAuth2TokenCustomizer implements OAuth2TokenCustomizer {
/**
* 自定义OAuth 2.0 Token属性
* @param context 包含OAuth 2.0 Token属性的上下文
*/
@Override
public void customize(OAuth2TokenClaimsContext context) {
OAuth2TokenClaimsSet.Builder claims = context.getClaims();
claims.claim(SecurityConstants.DETAILS_LICENSE, SecurityConstants.PROJECT_LICENSE);
String clientId = context.getAuthorizationGrant().getName();
claims.claim(SecurityConstants.CLIENT_ID, clientId);
// 客户端模式不返回具体用户信息
if (SecurityConstants.CLIENT_CREDENTIALS.equals(context.getAuthorizationGrantType().getValue())) {
return;
}
PigUser pigUser = (PigUser) context.getPrincipal().getPrincipal();
claims.claim(SecurityConstants.DETAILS_USER, pigUser);
claims.claim(SecurityConstants.DETAILS_USER_ID, pigUser.getId());
claims.claim(SecurityConstants.USERNAME, pigUser.getUsername());
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/core/FormIdentityLoginConfigurer.java
================================================
package com.pig4cloud.pig.auth.support.core;
import com.pig4cloud.pig.auth.support.handler.FormAuthenticationFailureHandler;
import com.pig4cloud.pig.auth.support.handler.SsoLogoutSuccessHandler;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
/**
* 基于授权码模式的统一认证登录配置类,适用于Spring Security和SAS
*
* @author lengleng
* @date 2025/05/30
*/
public final class FormIdentityLoginConfigurer
extends AbstractHttpConfigurer {
@Override
public void init(HttpSecurity http) throws Exception {
http.formLogin(formLogin -> {
formLogin.loginPage("/token/login");
formLogin.loginProcessingUrl("/oauth2/form");
formLogin.failureHandler(new FormAuthenticationFailureHandler());
})
.logout(logout -> logout.logoutUrl("/oauth2/logout")
.logoutSuccessHandler(new SsoLogoutSuccessHandler())
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)) // SSO登出成功处理
.csrf(AbstractHttpConfigurer::disable);
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/core/PigDaoAuthenticationProvider.java
================================================
package com.pig4cloud.pig.auth.support.core;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.pig4cloud.pig.common.core.util.WebUtils;
import com.pig4cloud.pig.common.security.service.PigUserDetailsService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.SneakyThrows;
import org.springframework.core.Ordered;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.www.BasicAuthenticationConverter;
import org.springframework.util.Assert;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import static com.pig4cloud.pig.common.core.constant.SecurityConstants.PASSWORD;
/**
* 基于DAO的认证提供者实现,用于处理用户名密码认证
*
* @author lengleng
* @date 2025/05/30
*/
public class PigDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
/**
* 用户未找到时用于PasswordEncoder#matches(CharSequence, String)的明文密码,避免SEC-2056问题
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private final static BasicAuthenticationConverter basicConvert = new BasicAuthenticationConverter();
/**
* 密码编码器
*/
private PasswordEncoder passwordEncoder;
/**
* 用户未找到时的加密密码,用于避免SEC-2056问题,某些密码编码器在密码格式无效时会短路处理
*/
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public PigDaoAuthenticationProvider() {
setMessageSource(SpringUtil.getBean("securityMessageSource"));
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
/**
* 执行额外的身份验证检查
* @param userDetails 用户详细信息
* @param authentication 身份验证令牌
* @throws AuthenticationException 身份验证失败时抛出异常
*/
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 只有密码模式需要校验密码
String grantType = WebUtils.getRequest().get().getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!StrUtil.equals(PASSWORD, grantType)) {
return;
}
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
/**
* 根据用户名检索用户详情
* @param username 用户名
* @param authentication 认证令牌
* @return 用户详情信息
* @throws InternalAuthenticationServiceException
* 当无法获取请求、未注册UserDetailsService或加载用户失败时抛出
* @throws UsernameNotFoundException 当用户名不存在时抛出
*/
@SneakyThrows
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) {
prepareTimingAttackProtection();
HttpServletRequest request = WebUtils.getRequest()
.orElseThrow(
(Supplier) () -> new InternalAuthenticationServiceException("web request is empty"));
String grantType = WebUtils.getRequest().get().getParameter(OAuth2ParameterNames.GRANT_TYPE);
String clientId = WebUtils.getRequest().get().getParameter(OAuth2ParameterNames.CLIENT_ID);
if (StrUtil.isBlank(clientId)) {
clientId = Optional.ofNullable(basicConvert.convert(request))
.map(UsernamePasswordAuthenticationToken::getName)
.orElse(null);
}
Map userDetailsServiceMap = SpringUtil
.getBeansOfType(PigUserDetailsService.class);
String finalClientId = clientId;
Optional optional = userDetailsServiceMap.values()
.stream()
.filter(service -> service.support(finalClientId, grantType))
.max(Comparator.comparingInt(Ordered::getOrder));
if (optional.isEmpty()) {
throw new InternalAuthenticationServiceException("UserDetailsService error , not register");
}
try {
UserDetails loadedUser = optional.get().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
/**
* 创建认证成功后的Authentication对象
* @param principal 认证主体
* @param authentication 认证信息
* @param user 用户详情
* @return 认证成功后的Authentication对象
*/
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
/**
* 准备定时攻击保护,如果未找到用户编码密码为空则进行编码
*/
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}
}
/**
* 防止时序攻击的缓解措施
* @param authentication 用户名密码认证令牌
*/
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
/**
* 设置用于编码和验证密码的PasswordEncoder实例
* @param passwordEncoder 密码编码器实例,不能为null
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return this.passwordEncoder;
}
/**
* 设置用户详情服务
* @param userDetailsService 用户详情服务
*/
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
/**
* 设置用户详情密码服务
* @param userDetailsPasswordService 用户详情密码服务
*/
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/filter/AuthSecurityConfigProperties.java
================================================
package com.pig4cloud.pig.auth.support.filter;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 安全认证配置属性类
*
*
* 用于配置网关安全相关属性
*
*
* @author lengleng
* @date 2025/05/30
* @since 2020/10/4
*/
@Data
@Component
@RefreshScope
@ConfigurationProperties("security")
public class AuthSecurityConfigProperties {
/**
* 是否是微服务架构
*/
private boolean isMicro;
/**
* 网关解密登录前端密码 秘钥
*/
private String encodeKey;
/**
* 网关不需要校验验证码的客户端
*/
private List ignoreClients;
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/filter/PasswordDecoderFilter.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.auth.support.filter;
import java.io.IOException;
import java.util.Map;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.servlet.RepeatBodyRequestWrapper;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
/**
* 密码解密过滤器:用于处理登录请求中的密码解密
*
* @author lengleng
* @date 2025/05/30
*/
@Component
@RequiredArgsConstructor
public class PasswordDecoderFilter extends OncePerRequestFilter {
private final AuthSecurityConfigProperties authSecurityConfigProperties;
private static final String PASSWORD = "password";
private static final String KEY_ALGORITHM = "AES";
static {
// 关闭hutool 强制关闭Bouncy Castle库的依赖
SecureUtil.disableBouncyCastle();
}
/**
* 过滤器内部处理逻辑,用于处理登录请求中的密码解密
* @param request HTTP请求对象
* @param response HTTP响应对象
* @param chain 过滤器链
* @throws ServletException 如果发生servlet相关异常
* @throws IOException 如果发生I/O异常
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 不是登录请求,直接向下执行
if (!StrUtil.containsAnyIgnoreCase(request.getRequestURI(), SecurityConstants.OAUTH_TOKEN_URL)) {
chain.doFilter(request, response);
return;
}
// 将请求流转换为可多次读取的请求流
RepeatBodyRequestWrapper requestWrapper = new RepeatBodyRequestWrapper(request);
Map parameterMap = requestWrapper.getParameterMap();
// 构建前端对应解密AES 因子
AES aes = new AES(Mode.CFB, Padding.NoPadding,
new SecretKeySpec(authSecurityConfigProperties.getEncodeKey().getBytes(), KEY_ALGORITHM),
new IvParameterSpec(authSecurityConfigProperties.getEncodeKey().getBytes()));
parameterMap.forEach((k, v) -> {
String[] values = parameterMap.get(k);
if (!PASSWORD.equals(k) || ArrayUtil.isEmpty(values)) {
return;
}
// 解密密码
String decryptPassword = aes.decryptStr(values[0]);
parameterMap.put(k, new String[] { decryptPassword });
});
chain.doFilter(requestWrapper, response);
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/filter/ValidateCodeFilter.java
================================================
package com.pig4cloud.pig.auth.support.filter;
import java.io.IOException;
import java.util.Optional;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.exception.ValidateCodeException;
import com.pig4cloud.pig.common.core.util.RedisUtils;
import com.pig4cloud.pig.common.core.util.WebUtils;
/**
* 登录前处理器
*
* @author lengleng
* @date 2024/4/3
*/
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
/**
* 验证码过滤器:用于处理登录请求中的验证码校验
*
* @author lengleng
* @date 2025/05/30
*/
@Component
@RequiredArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter {
private final AuthSecurityConfigProperties authSecurityConfigProperties;
/**
* 过滤器内部处理逻辑,用于验证码校验
* @param request HTTP请求
* @param response HTTP响应
* @param filterChain 过滤器链
* @throws ServletException Servlet异常
* @throws IOException IO异常
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestUrl = request.getServletPath();
// 不是登录URL 请求直接跳过
if (!SecurityConstants.OAUTH_TOKEN_URL.equals(requestUrl)) {
filterChain.doFilter(request, response);
return;
}
// 如果登录URL 但是刷新token的请求,直接向下执行
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)) {
filterChain.doFilter(request, response);
return;
}
// 如果是密码模式 && 客户端不需要校验验证码
boolean isIgnoreClient = authSecurityConfigProperties.getIgnoreClients().contains(WebUtils.getClientId());
if (StrUtil.equalsAnyIgnoreCase(grantType, SecurityConstants.PASSWORD, SecurityConstants.CLIENT_CREDENTIALS,
SecurityConstants.AUTHORIZATION_CODE) && isIgnoreClient) {
filterChain.doFilter(request, response);
return;
}
// 校验验证码 1. 客户端开启验证码 2. 短信模式
try {
checkCode();
filterChain.doFilter(request, response);
}
catch (ValidateCodeException validateCodeException) {
throw new OAuth2AuthenticationException(validateCodeException.getMessage());
}
}
/**
* 校验验证码
*/
private void checkCode() throws ValidateCodeException {
Optional request = WebUtils.getRequest();
String code = request.get().getParameter("code");
if (StrUtil.isBlank(code)) {
throw new ValidateCodeException("验证码不能为空");
}
String randomStr = request.get().getParameter("randomStr");
// https://gitee.com/log4j/pig/issues/IWA0D
String mobile = request.get().getParameter("mobile");
if (StrUtil.isNotBlank(mobile)) {
randomStr = mobile;
}
String key = CacheConstants.DEFAULT_CODE_KEY + randomStr;
if (!RedisUtils.hasKey(key)) {
throw new ValidateCodeException("验证码不合法");
}
String saveCode = RedisUtils.get(key);
if (StrUtil.isBlank(saveCode)) {
RedisUtils.delete(key);
throw new ValidateCodeException("验证码不合法");
}
if (!StrUtil.equals(saveCode, code)) {
RedisUtils.delete(key);
throw new ValidateCodeException("验证码不合法");
}
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/handler/FormAuthenticationFailureHandler.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.auth.support.handler;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.HttpUtil;
import com.pig4cloud.pig.common.core.util.WebUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import java.io.IOException;
/**
* 表单登录失败处理逻辑
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
public class FormAuthenticationFailureHandler implements AuthenticationFailureHandler {
/**
* 当认证失败时调用
* @param request 认证尝试发生的请求
* @param response 响应对象
* @param exception 拒绝认证时抛出的异常
*/
@Override
@SneakyThrows
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
log.debug("表单登录失败:{}", exception.getLocalizedMessage());
// 获取当前请求的context-path
String contextPath = request.getContextPath();
// 构建重定向URL,加入context-path
String url = HttpUtil.encodeParams(
String.format("%s/token/login?error=%s", contextPath, exception.getMessage()),
CharsetUtil.CHARSET_UTF_8);
try {
WebUtils.getResponse().sendRedirect(url);
}
catch (IOException e) {
log.error("重定向失败", e);
}
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/handler/PigAuthenticationFailureEventHandler.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.auth.support.handler;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.admin.api.entity.SysLog;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.core.util.R;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.log.event.SysLogEvent;
import com.pig4cloud.pig.common.log.util.LogTypeEnum;
import com.pig4cloud.pig.common.log.util.SysLogUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import java.io.IOException;
/**
* 认证失败处理器:处理用户认证失败事件并记录日志
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
public class PigAuthenticationFailureEventHandler implements AuthenticationFailureHandler {
private final MappingJackson2HttpMessageConverter errorHttpResponseConverter = new MappingJackson2HttpMessageConverter();
/**
* 当认证失败时调用
* @param request 认证请求
* @param response 认证响应
* @param exception 认证失败的异常
*/
@Override
@SneakyThrows
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
String username = request.getParameter(OAuth2ParameterNames.USERNAME);
log.info("用户:{} 登录失败,异常:{}", username, exception.getLocalizedMessage());
SysLog logVo = SysLogUtils.getSysLog();
logVo.setTitle("登录失败");
logVo.setLogType(LogTypeEnum.ERROR.getType());
logVo.setException(exception.getLocalizedMessage());
// 发送异步日志事件
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
if (StrUtil.isNotBlank(startTimeStr)) {
Long startTime = Long.parseLong(startTimeStr);
Long endTime = System.currentTimeMillis();
logVo.setTime(endTime - startTime);
}
logVo.setCreateBy(username);
SpringContextHolder.publishEvent(new SysLogEvent(logVo));
// 写出错误信息
sendErrorResponse(request, response, exception);
}
/**
* 发送错误响应
* @param request HTTP请求
* @param response HTTP响应
* @param exception 认证异常
* @throws IOException 写入响应时发生IO异常
*/
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
String errorMessage;
if (exception instanceof OAuth2AuthenticationException) {
OAuth2AuthenticationException authorizationException = (OAuth2AuthenticationException) exception;
errorMessage = StrUtil.isBlank(authorizationException.getError().getDescription())
? authorizationException.getError().getErrorCode()
: authorizationException.getError().getDescription();
}
else {
errorMessage = exception.getLocalizedMessage();
}
this.errorHttpResponseConverter.write(R.failed(errorMessage), MediaType.APPLICATION_JSON, httpResponse);
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/handler/PigAuthenticationSuccessEventHandler.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.auth.support.handler;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.admin.api.entity.SysLog;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.log.event.SysLogEvent;
import com.pig4cloud.pig.common.log.util.SysLogUtils;
import com.pig4cloud.pig.common.security.component.PigCustomOAuth2AccessTokenResponseHttpMessageConverter;
import com.pig4cloud.pig.common.security.service.PigUser;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.Map;
/**
* 处理认证成功事件的处理器
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
public class PigAuthenticationSuccessEventHandler implements AuthenticationSuccessHandler {
private final HttpMessageConverter accessTokenHttpResponseConverter = new PigCustomOAuth2AccessTokenResponseHttpMessageConverter();
/**
* 用户认证成功时调用
* @param request 触发认证成功的请求
* @param response 响应对象
* @param authentication 认证过程中创建的认证对象
*/
@SneakyThrows
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
Map map = accessTokenAuthentication.getAdditionalParameters();
if (MapUtil.isNotEmpty(map)) {
// 发送异步日志事件
PigUser userInfo = (PigUser) map.get(SecurityConstants.DETAILS_USER);
log.info("用户:{} 登录成功", userInfo.getName());
SecurityContextHolder.getContext().setAuthentication(accessTokenAuthentication);
SysLog logVo = SysLogUtils.getSysLog();
logVo.setTitle("登录成功");
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
if (StrUtil.isNotBlank(startTimeStr)) {
Long startTime = Long.parseLong(startTimeStr);
Long endTime = System.currentTimeMillis();
logVo.setTime(endTime - startTime);
}
logVo.setCreateBy(userInfo.getName());
SpringContextHolder.publishEvent(new SysLogEvent(logVo));
}
// 输出token
sendAccessTokenResponse(request, response, authentication);
}
/**
* 发送访问令牌响应
* @param request HTTP请求
* @param response HTTP响应
* @param authentication 认证信息
* @throws IOException 写入响应时可能抛出IO异常
*/
private void sendAccessTokenResponse(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
Map additionalParameters = accessTokenAuthentication.getAdditionalParameters();
OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.scopes(accessToken.getScopes());
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
}
if (refreshToken != null) {
builder.refreshToken(refreshToken.getTokenValue());
}
if (!CollectionUtils.isEmpty(additionalParameters)) {
builder.additionalParameters(additionalParameters);
}
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
// 无状态 注意删除 context 上下文的信息
SecurityContextHolder.clearContext();
this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/handler/PigLogoutSuccessEventHandler.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.auth.support.handler;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.admin.api.entity.SysLog;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.core.util.WebUtils;
import com.pig4cloud.pig.common.log.event.SysLogEvent;
import com.pig4cloud.pig.common.log.util.SysLogUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.event.LogoutSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Component;
/**
* 处理用户退出成功事件处理器
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
@Component
public class PigLogoutSuccessEventHandler implements ApplicationListener {
/**
* 处理登出成功事件
* @param event 登出成功事件
*/
@Override
public void onApplicationEvent(LogoutSuccessEvent event) {
Authentication authentication = (Authentication) event.getSource();
if (authentication instanceof PreAuthenticatedAuthenticationToken) {
handle(authentication);
}
}
/**
* 处理退出成功方法
*
* 获取到登录的authentication 对象
* @param authentication 登录对象
*/
public void handle(Authentication authentication) {
log.info("用户:{} 退出成功", authentication.getPrincipal());
SysLog logVo = SysLogUtils.getSysLog();
logVo.setTitle("退出成功");
// 设置对应的token
WebUtils.getRequest().ifPresent(request -> {
logVo.setParams(request.getHeader(HttpHeaders.AUTHORIZATION));
// 计算请求耗时
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
if (StrUtil.isNotBlank(startTimeStr)) {
Long startTime = Long.parseLong(startTimeStr);
Long endTime = System.currentTimeMillis();
logVo.setTime(endTime - startTime);
}
});
// 这边设置ServiceId
if (authentication instanceof PreAuthenticatedAuthenticationToken) {
logVo.setServiceId(authentication.getCredentials().toString());
}
logVo.setCreateBy(authentication.getName());
// 发送异步日志事件
SpringContextHolder.publishEvent(new SysLogEvent(logVo));
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/handler/SsoLogoutSuccessHandler.java
================================================
package com.pig4cloud.pig.auth.support.handler;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import java.io.IOException;
/**
* SSO 登出成功处理器,根据客户端传入的跳转地址进行重定向
*
* @author lengleng
* @date 2025/05/30
*/
public class SsoLogoutSuccessHandler implements LogoutSuccessHandler {
private static final String REDIRECT_URL = "redirect_url";
/**
* 登出成功处理逻辑
* @param request HTTP请求
* @param response HTTP响应
* @param authentication 认证信息
* @throws IOException 重定向失败时抛出IO异常
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
if (response == null) {
return;
}
// 获取请求参数中是否包含 回调地址
String redirectUrl = request.getParameter(REDIRECT_URL);
if (StrUtil.isNotBlank(redirectUrl)) {
response.sendRedirect(redirectUrl);
}
else if (StrUtil.isNotBlank(request.getHeader(HttpHeaders.REFERER))) {
// 默认跳转referer 地址
String referer = request.getHeader(HttpHeaders.REFERER);
response.sendRedirect(referer);
}
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/password/OAuth2ResourceOwnerPasswordAuthenticationConverter.java
================================================
package com.pig4cloud.pig.auth.support.password;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationConverter;
import com.pig4cloud.pig.common.security.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.Set;
import static com.pig4cloud.pig.common.core.constant.SecurityConstants.PASSWORD;
/**
* OAuth2 资源所有者密码认证转换器
*
* @author lengleng
* @author jumuning
* @date 2025/05/30
*/
public class OAuth2ResourceOwnerPasswordAuthenticationConverter
extends OAuth2ResourceOwnerBaseAuthenticationConverter {
/**
* 支持密码模式
* @param grantType 授权类型
*/
@Override
public boolean support(String grantType) {
return PASSWORD.equals(grantType);
}
/**
* 构建OAuth2资源所有者密码认证令牌
* @param clientPrincipal 客户端主体认证信息
* @param requestedScopes 请求的作用域集合
* @param additionalParameters 附加参数映射
* @return 构建完成的OAuth2资源所有者密码认证令牌
*/
@Override
public OAuth2ResourceOwnerPasswordAuthenticationToken buildToken(Authentication clientPrincipal,
Set requestedScopes, Map additionalParameters) {
return new OAuth2ResourceOwnerPasswordAuthenticationToken(new AuthorizationGrantType(PASSWORD), clientPrincipal,
requestedScopes, additionalParameters);
}
/**
* 校验扩展参数 密码模式密码必须不为空
* @param request 参数列表
*/
@Override
public void checkParams(HttpServletRequest request) {
MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request);
// username (REQUIRED)
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// password (REQUIRED)
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/password/OAuth2ResourceOwnerPasswordAuthenticationProvider.java
================================================
package com.pig4cloud.pig.auth.support.password;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationProvider;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import java.util.Map;
import static com.pig4cloud.pig.common.core.constant.SecurityConstants.PASSWORD;
/**
* OAuth2 资源所有者密码认证提供者
*
* @author lengleng
* @author jumuning
* @date 2025/05/30
* @since 0.2.3
*/
public class OAuth2ResourceOwnerPasswordAuthenticationProvider
extends OAuth2ResourceOwnerBaseAuthenticationProvider {
private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerPasswordAuthenticationProvider.class);
/**
* 使用提供的参数构造一个OAuth2ResourceOwnerPasswordAuthenticationProvider
* @param authenticationManager 认证管理器
* @param authorizationService 授权服务
* @param tokenGenerator 令牌生成器
* @since 0.2.3
*/
public OAuth2ResourceOwnerPasswordAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator extends OAuth2Token> tokenGenerator) {
super(authenticationManager, authorizationService, tokenGenerator);
}
/**
* 构建用户名密码认证令牌
* @param reqParameters 请求参数映射,包含用户名和密码
* @return 用户名密码认证令牌
*/
@Override
public UsernamePasswordAuthenticationToken buildToken(Map reqParameters) {
String username = (String) reqParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) reqParameters.get(OAuth2ParameterNames.PASSWORD);
return new UsernamePasswordAuthenticationToken(username, password);
}
/**
* 判断是否支持指定的认证类型
* @param authentication 待验证的认证类型
* @return 如果支持该认证类型则返回true,否则返回false
*/
@Override
public boolean supports(Class> authentication) {
boolean supports = OAuth2ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication);
LOGGER.debug("supports authentication=" + authentication + " returning " + supports);
return supports;
}
/**
* 检查客户端是否支持密码授权模式
* @param registeredClient 已注册的客户端
* @throws OAuth2AuthenticationException 当客户端不支持密码授权模式时抛出异常
*/
@Override
public void checkClient(RegisteredClient registeredClient) {
assert registeredClient != null;
if (!registeredClient.getAuthorizationGrantTypes().contains(new AuthorizationGrantType(PASSWORD))) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/password/OAuth2ResourceOwnerPasswordAuthenticationToken.java
================================================
package com.pig4cloud.pig.auth.support.password;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import java.io.Serial;
import java.util.Map;
import java.util.Set;
/**
* OAuth2资源所有者密码认证令牌
*
* @author lengleng
* @author jumuning
* @date 2025/05/30
*/
public class OAuth2ResourceOwnerPasswordAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造OAuth2资源所有者密码认证令牌
* @param authorizationGrantType 授权类型
* @param clientPrincipal 客户端认证主体
* @param scopes 权限范围集合
* @param additionalParameters 附加参数映射
*/
public OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType authorizationGrantType,
Authentication clientPrincipal, Set scopes, Map additionalParameters) {
super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/password/package-info.java
================================================
/**
* 密码模式
*/
package com.pig4cloud.pig.auth.support.password;
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/sms/OAuth2ResourceOwnerSmsAuthenticationConverter.java
================================================
package com.pig4cloud.pig.auth.support.sms;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationConverter;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.security.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.Set;
/**
* @author lengleng
* @date 2022-05-31
*
* 短信登录转换器
*/
public class OAuth2ResourceOwnerSmsAuthenticationConverter
extends OAuth2ResourceOwnerBaseAuthenticationConverter {
/**
* 是否支持此convert
* @param grantType 授权类型
* @return
*/
@Override
public boolean support(String grantType) {
return SecurityConstants.MOBILE.equals(grantType);
}
@Override
public OAuth2ResourceOwnerSmsAuthenticationToken buildToken(Authentication clientPrincipal, Set requestedScopes,
Map additionalParameters) {
return new OAuth2ResourceOwnerSmsAuthenticationToken(new AuthorizationGrantType(SecurityConstants.MOBILE),
clientPrincipal, requestedScopes, additionalParameters);
}
/**
* 校验扩展参数 密码模式密码必须不为空
* @param request 参数列表
*/
@Override
public void checkParams(HttpServletRequest request) {
MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request);
// PHONE (REQUIRED)
String phone = parameters.getFirst(SecurityConstants.SMS_PARAMETER_NAME);
if (!StringUtils.hasText(phone) || parameters.get(SecurityConstants.SMS_PARAMETER_NAME).size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, SecurityConstants.SMS_PARAMETER_NAME,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/sms/OAuth2ResourceOwnerSmsAuthenticationProvider.java
================================================
package com.pig4cloud.pig.auth.support.sms;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationProvider;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import java.util.Map;
/**
* @author lengleng
* @date date
*
* 短信登录的核心处理
*/
public class OAuth2ResourceOwnerSmsAuthenticationProvider
extends OAuth2ResourceOwnerBaseAuthenticationProvider {
private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerSmsAuthenticationProvider.class);
/**
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the
* provided parameters.
* @param authenticationManager
* @param authorizationService the authorization service
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public OAuth2ResourceOwnerSmsAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator extends OAuth2Token> tokenGenerator) {
super(authenticationManager, authorizationService, tokenGenerator);
}
@Override
public boolean supports(Class> authentication) {
boolean supports = OAuth2ResourceOwnerSmsAuthenticationToken.class.isAssignableFrom(authentication);
LOGGER.debug("supports authentication=" + authentication + " returning " + supports);
return supports;
}
@Override
public void checkClient(RegisteredClient registeredClient) {
assert registeredClient != null;
if (!registeredClient.getAuthorizationGrantTypes()
.contains(new AuthorizationGrantType(SecurityConstants.MOBILE))) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
}
@Override
public UsernamePasswordAuthenticationToken buildToken(Map reqParameters) {
String phone = (String) reqParameters.get(SecurityConstants.SMS_PARAMETER_NAME);
return new UsernamePasswordAuthenticationToken(phone, null);
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/sms/OAuth2ResourceOwnerSmsAuthenticationToken.java
================================================
package com.pig4cloud.pig.auth.support.sms;
import java.io.Serial;
import java.util.Map;
import java.util.Set;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import com.pig4cloud.pig.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationToken;
/**
* @author lengleng
* @description 短信登录token信息
*/
public class OAuth2ResourceOwnerSmsAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {
@Serial
private static final long serialVersionUID = 1L;
public OAuth2ResourceOwnerSmsAuthenticationToken(AuthorizationGrantType authorizationGrantType,
Authentication clientPrincipal, Set scopes, Map additionalParameters) {
super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
}
}
================================================
FILE: pig-auth/src/main/java/com/pig4cloud/pig/auth/support/sms/package-info.java
================================================
/**
* 短信模式
*/
package com.pig4cloud.pig.auth.support.sms;
================================================
FILE: pig-auth/src/main/resources/application.yml
================================================
server:
port: 3000
spring:
application:
name: @artifactId@
cloud:
nacos:
username: @nacos.username@
password: @nacos.password@
discovery:
server-addr: ${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
config:
import:
- nacos:application-@profiles.active@.yml
- nacos:${spring.application.name}-@profiles.active@.yml
================================================
FILE: pig-auth/src/main/resources/logback-spring.xml
================================================
${CONSOLE_LOG_PATTERN}
${log.path}/debug.log
${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz
50MB
30
${FILE_LOG_PATTERN}
${log.path}/error.log
${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz
50MB
30
${FILE_LOG_PATTERN}
ERROR
================================================
FILE: pig-auth/src/main/resources/templates/ftl/confirm.ftl
================================================
<#assign content>
#assign>
<#include "layout/base.ftl">
================================================
FILE: pig-auth/src/main/resources/templates/ftl/layout/base.ftl
================================================
<#if title??>${title}<#else>Pig 统一身份认证#if>
<#if extraHead??>${extraHead}#if>
统一身份认证平台
为企业提供一套集中式的账号、权限、认证、审计工具,帮助企业打通身份数据孤岛,实现"一个账号、一次认证、多点通行"的效果,强化企业安全体系的同时,提升组织管理效率,助力企业数字化升级转型。
<#if content??>${content}#if>
Copyright © 2021-2025
PIGCLOUD
================================================
FILE: pig-auth/src/main/resources/templates/ftl/login.ftl
================================================
<#assign content>
#assign>
<#include "layout/base.ftl">
================================================
FILE: pig-boot/Dockerfile
================================================
FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/java:21-anolis
WORKDIR /pig-boot
ARG JAR_FILE=target/pig-boot.jar
COPY ${JAR_FILE} app.jar
EXPOSE 9999
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx1024m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java $JAVA_OPTS -jar app.jar
================================================
FILE: pig-boot/pom.xml
================================================
4.0.0
com.pig4cloud
pig
${revision}
pig-boot
jar
pig 单体版本启动
com.pig4cloud
pig-auth
${revision}
com.pig4cloud
pig-upms-biz
${revision}
com.pig4cloud
pig-codegen
${revision}
com.pig4cloud
pig-quartz
${revision}
com.pig4cloud
pig-common-security
org.springdoc
springdoc-openapi-starter-webmvc-ui
org.springdoc
springdoc-openapi-starter-webmvc-api
com.github.xiaoymin
knife4j-openapi3-ui
com.pig4cloud
pig-common-swagger
org.springframework.boot
spring-boot-starter-undertow
org.springframework.boot
spring-boot-maven-plugin
io.fabric8
docker-maven-plugin
================================================
FILE: pig-boot/src/main/java/com/pig4cloud/pig/PigBootApplication.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig;
import com.pig4cloud.pig.common.security.annotation.EnablePigResourceServer;
import com.pig4cloud.pig.common.swagger.annotation.EnablePigDoc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 单体版本启动器,运行此模块即可启动整个系统
*
* @author lengleng
* @date 2025/05/30
*/
@SpringBootApplication
@EnablePigResourceServer
@EnablePigDoc(value = "admin", isMicro = false)
public class PigBootApplication {
public static void main(String[] args) {
SpringApplication.run(PigBootApplication.class, args);
}
}
================================================
FILE: pig-boot/src/main/resources/application-dev.yml
================================================
spring:
cache:
type: redis # 缓存类型 Redis
data:
redis:
database: 5
host: 127.0.0.1
# 数据库相关配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&nullCatalogMeansCurrent=true
# 本地文件系统
file:
local:
enable: true
base-path: /Users/lengleng/Downloads/img
## 登录配置
security:
# 登录报文加密根密钥 ,必须是16位
encodeKey: thanks,pig4cloud
# 跳过验证码校验的客户端
ignore-clients:
- test
# 配置文件加密根密码
jasypt:
encryptor:
password: pig # 加密根密码
algorithm: PBEWithMD5AndDES # 加密算法
iv-generator-classname: org.jasypt.iv.NoIvGenerator # 无向量生成器
# 短信插件配置:https://www.yuque.com/vxixfq/pig/zw8udk
sms:
is-print: false # 是否打印日志
config-type: yaml # 配置类型,yaml
================================================
FILE: pig-boot/src/main/resources/application.yml
================================================
server:
port: 9999 # 项目端口
servlet:
context-path: /admin # 项目访问路径
spring:
application:
name: @project.artifactId@ # 服务名称,取 pom.xml 中的 artifactId
# 上传文件大小限制
servlet:
multipart:
max-file-size: 100MB # 单个文件最大
max-request-size: 100MB # 接收的最大请求大小
cloud:
nacos: # 单机版本关闭nacos 服务发现和配置管理的能力
config:
enabled: false
discovery:
enabled: false
freemarker: # freemarker 配置,授权码模式页面渲染使用
suffix: .ftl
template-loader-path: classpath:/templates/
request-context-attribute: request
main:
allow-bean-definition-overriding: true # 允许覆盖bean定义
profiles:
active: dev # 激活dev,对应 application-dev.yml
## spring security 对外暴露接口设置(不鉴权直接可访问)
security:
micro: false
oauth2:
ignore:
urls:
- /webjars/**
- /v3/api-docs/**
- /doc.html
- /swagger-ui.html
- /swagger-ui/**
- /swagger-resources
- /code/image
- /error
- /token/**
- /actuator/**
#--------------如下配置尽量不要变动-------------
# swagger 配置
swagger:
token-url: ${swagger.gateway}/admin/oauth2/token
# mybatis-plus 配置
mybatis-plus:
mapper-locations: classpath*:/mapper/*Mapper.xml # mapper文件位置
global-config:
banner: false # 是否打印 mybatis-plus banner
db-config:
id-type: auto # 主键类型
where-strategy: not_empty # where 条件策略
insert-strategy: not_empty # 插入策略
update-strategy: not_null # 更新策略
type-handlers-package: com.pig4cloud.pig.common.mybatis.handler # 类型处理器包
configuration:
jdbc-type-for-null: 'null' # 是否设置字段为null
call-setters-on-nulls: true # 是否调用set方法时传入null值
shrink-whitespaces-in-sql: true # 去掉sql中多余的空格
================================================
FILE: pig-boot/src/main/resources/logback-spring.xml
================================================
${CONSOLE_LOG_PATTERN}
${log.path}/debug.log
${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz
50MB
30
${CONSOLE_LOG_PATTERN}
${log.path}/error.log
${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz
50MB
30
${CONSOLE_LOG_PATTERN}
ERROR
================================================
FILE: pig-common/pig-common-bom/pom.xml
================================================
4.0.0
com.pig4cloud
pig-common-bom
${revision}
pom
pig-common-bom
pig cloud parent
pig cloud parent
3.9.2
7.1
3.3.5
2.29.45
9.2.0
1.7.0
3.4.3
0.0.3
5.8.42
2.4
2.2.5
4.5.0
4.3.2
1.8.4
9.0.1
2.8.14
2.18.0
4.5.0
3.0.0
17
17
3.1.0
3.1
1.10
2.2.39
3.5.16
5.4.1
1.2.83_noneautotype
0.0.47
1.6.0
UTF-8
com.pig4cloud
pig-common-core
${revision}
com.pig4cloud
pig-common-datasource
${revision}
com.pig4cloud
pig-common-log
${revision}
com.pig4cloud
pig-common-mybatis
${revision}
com.pig4cloud
pig-common-oss
${revision}
com.pig4cloud
pig-common-security
${revision}
com.pig4cloud
pig-common-feign
${revision}
com.pig4cloud
pig-common-swagger
${revision}
com.pig4cloud
pig-common-xss
${revision}
com.pig4cloud
pig-common-excel
${revision}
com.pig4cloud
pig-common-websocket
${revision}
com.pig4cloud
pig-upms-api
${revision}
com.mysql
mysql-connector-j
${mysql.version}
org.springdoc
springdoc-openapi-starter-webflux-ui
${springdoc.version}
org.springdoc
springdoc-openapi-starter-webmvc-ui
${springdoc.version}
org.springdoc
springdoc-openapi-starter-webmvc-api
${springdoc.version}
io.swagger.core.v3
swagger-annotations-jakarta
${swagger.core.version}
com.alibaba
fastjson
${fastjson.version}
org.apache.velocity
velocity-engine-core
${velocity.version}
org.apache.velocity.tools
velocity-tools-generic
${velocity.tool.version}
com.pig4cloud.plugin
captcha-core
${captcha.version}
com.pig4cloud.excel
excel-spring-boot-starter
${excel.version}
commons-io
commons-io
${common.io.version}
org.apache.shardingsphere
shardingsphere-jdbc-core
${shardingsphere.version}
com.baomidou
dynamic-datasource-spring-boot3-starter
${dynamic-ds.version}
org.dromara.sms4j
sms4j-spring-boot-starter
${sms.version}
org.springframework.cloud
spring-cloud-gateway-server
${gateway.version}
com.github.xiaoymin
knife4j-openapi3-ui
${knife4j.version}
software.amazon.awssdk
s3
${aws.version}
com.alibaba.nacos
nacos-client
${nacos.client.version}
com.baomidou
mybatis-plus-bom
${mybatis-plus.version}
pom
import
cn.hutool
hutool-bom
${hutool.version}
pom
import
io.github.git-commit-id
git-commit-id-maven-plugin
${git.commit.plugin}
io.spring.javaformat
spring-javaformat-maven-plugin
${spring.checkstyle.plugin}
org.codehaus.mojo
flatten-maven-plugin
${flatten-maven-plugin.version}
resolveCiFriendliesOnly
true
flatten
process-resources
flatten
flatten.clean
clean
clean
================================================
FILE: pig-common/pig-common-core/pom.xml
================================================
4.0.0
com.pig4cloud
pig-common
${revision}
pig-common-core
jar
pig 公共工具类核心包
cn.hutool
hutool-core
org.springframework.boot
spring-boot-starter-data-redis
jakarta.servlet
jakarta.servlet-api
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-json
org.springframework
spring-webmvc
provided
org.springframework.cloud
spring-cloud-commons
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/config/JacksonConfiguration.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.config;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.pig4cloud.pig.common.core.jackson.PigJavaTimeModule;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.context.annotation.Bean;
import java.time.ZoneId;
import java.util.Locale;
import java.util.TimeZone;
/**
* Jackson配置类,用于自定义Jackson的ObjectMapper配置
*
* @author lengleng
* @author L.cm
* @author lishangbu
* @date 2025/05/30
*/
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
@AutoConfigureBefore(JacksonAutoConfiguration.class)
public class JacksonConfiguration {
/**
* 自定义Jackson2ObjectMapperBuilder配置
* @return Jackson2ObjectMapperBuilderCustomizer实例,包含以下配置: 1. 设置地区为中国 2. 设置系统默认时区 3.
* 设置默认日期时间格式 4. 配置Long类型序列化为字符串 5. 注册自定义时间模块
*/
@Bean
@ConditionalOnMissingBean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
builder.locale(Locale.CHINA);
builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
builder.simpleDateFormat(DatePattern.NORM_DATETIME_PATTERN);
builder.serializerByType(Long.class, ToStringSerializer.instance);
builder.modules(new PigJavaTimeModule());
};
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/config/RedisTemplateConfiguration.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Redis 配置类
*
* @author lengleng
* @date 2025/05/30
*/
@EnableCaching
@AutoConfiguration
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisTemplateConfiguration {
/**
* 创建并配置RedisTemplate实例
* @param factory Redis连接工厂
* @return 配置好的RedisTemplate实例
*/
@Bean
@Primary
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.setHashValueSerializer(RedisSerializer.java());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
/**
* 创建并返回HashOperations实例
* @param redisTemplate Redis模板
* @return HashOperations实例
*/
@Bean
public HashOperations hashOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForHash();
}
/**
* 创建并返回用于操作Redis String类型数据的ValueOperations实例
* @param redisTemplate Redis模板,用于操作Redis
* @return ValueOperations实例,提供对Redis String类型数据的操作
*/
@Bean
public ValueOperations valueOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForValue();
}
/**
* 创建并返回ListOperations实例
* @param redisTemplate Redis模板
* @return ListOperations实例
*/
@Bean
public ListOperations listOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForList();
}
/**
* 创建并返回SetOperations实例
* @param redisTemplate Redis模板
* @return SetOperations实例
*/
@Bean
public SetOperations setOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForSet();
}
/**
* 创建并返回ZSetOperations实例
* @param redisTemplate Redis模板对象
* @return ZSetOperations实例
*/
@Bean
public ZSetOperations zSetOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForZSet();
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/config/RestTemplateConfiguration.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate 自动配置类
*
* @author lengleng
* @date 2025/05/30
*/
@AutoConfiguration
public class RestTemplateConfiguration {
/**
* 创建动态REST模板
* @return {@link RestTemplate} REST模板实例
*/
@Bean
@LoadBalanced
@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.enabled", havingValue = "true", matchIfMissing = true)
public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* 创建支持负载均衡的REST客户端构建器
* @return {@link RestClient.Builder} REST客户端构建器
*/
@Bean
@LoadBalanced
@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.enabled", havingValue = "true", matchIfMissing = true)
RestClient.Builder restClientBuilder() {
return RestClient.builder();
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/config/WebMvcConfiguration.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.config;
import cn.hutool.core.date.DatePattern;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET;
/**
* WebMvc配置类:用于自定义Spring MVC配置
*
* 包含GET请求参数时间类型转换和系统国际化配置
*
* @author lengleng
* @date 2025/05/30
*/
@AutoConfiguration
@ConditionalOnWebApplication(type = SERVLET)
public class WebMvcConfiguration implements WebMvcConfigurer {
/**
* 增加GET请求参数中时间类型转换
* @param registry 格式化注册器
*/
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setTimeFormatter(DatePattern.NORM_TIME_FORMATTER);
registrar.setDateFormatter(DatePattern.NORM_DATE_FORMATTER);
registrar.setDateTimeFormatter(DatePattern.NORM_DATETIME_FORMATTER);
registrar.registerFormatters(registry);
}
/**
* 创建并配置国际化消息源
* @return 可重载的资源包消息源
*/
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:i18n/messages");
return messageSource;
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/constant/CacheConstants.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.constant;
/**
* @author lengleng
* @date 2020年01月01日
*
* 缓存的key 常量
*/
public interface CacheConstants {
/**
* oauth 缓存前缀
*/
String PROJECT_OAUTH_ACCESS = "token::access_token";
/**
* 验证码前缀
*/
String DEFAULT_CODE_KEY = "DEFAULT_CODE_KEY:";
/**
* 菜单信息缓存
*/
String MENU_DETAILS = "menu_details";
/**
* 用户信息缓存
*/
String USER_DETAILS = "user_details";
/**
* 字典信息缓存
*/
String DICT_DETAILS = "dict_details";
/**
* 角色信息缓存
*/
String ROLE_DETAILS = "role_details";
/**
* oauth 客户端信息
*/
String CLIENT_DETAILS_KEY = "client:details";
/**
* 参数缓存
*/
String PARAMS_DETAILS = "params_details";
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/constant/CommonConstants.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.constant;
/**
* @author lengleng
* @date 2019/2/1
*/
public interface CommonConstants {
/**
* 删除
*/
String STATUS_DEL = "1";
/**
* 正常
*/
String STATUS_NORMAL = "0";
/**
* 锁定
*/
String STATUS_LOCK = "9";
/**
* 菜单树根节点
*/
Long MENU_TREE_ROOT_ID = -1L;
/**
* 菜单
*/
String MENU = "0";
/**
* 编码
*/
String UTF8 = "UTF-8";
/**
* JSON 资源
*/
String CONTENT_TYPE = "application/json; charset=utf-8";
/**
* 前端工程名
*/
String FRONT_END_PROJECT = "pig-ui";
/**
* 后端工程名
*/
String BACK_END_PROJECT = "pig";
/**
* 成功标记
*/
Integer SUCCESS = 0;
/**
* 失败标记
*/
Integer FAIL = 1;
/**
* 当前页
*/
String CURRENT = "current";
/**
* size
*/
String SIZE = "size";
/**
* 请求开始时间
*/
String REQUEST_START_TIME = "REQUEST-START-TIME";
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/constant/SecurityConstants.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.constant;
/**
* @author lengleng
* @date 2019/2/1
*/
public interface SecurityConstants {
/**
* 角色前缀
*/
String ROLE = "ROLE_";
/**
* 前缀
*/
String PROJECT_PREFIX = "pig";
/**
* 项目的license
*/
String PROJECT_LICENSE = "https://pig4cloud.com";
/**
* 内部
*/
String FROM_IN = "Y";
/**
* 标志
*/
String FROM = "from";
/**
* 默认登录URL
*/
String OAUTH_TOKEN_URL = "/oauth2/token";
/**
* grant_type
*/
String REFRESH_TOKEN = "refresh_token";
/**
* password 模式
*/
String PASSWORD = "password";
/**
* 授权码
*/
String AUTHORIZATION_CODE = "authorization_code";
/**
* 手机号登录
*/
String MOBILE = "mobile";
/**
* {bcrypt} 加密的特征码
*/
String BCRYPT = "{bcrypt}";
/**
* {noop} 加密的特征码
*/
String NOOP = "{noop}";
/**
* 用户名
*/
String USERNAME = "username";
/**
* 用户信息
*/
String DETAILS_USER = "user_info";
/**
* 用户ID
*/
String DETAILS_USER_ID = "user_id";
/**
* 协议字段
*/
String DETAILS_LICENSE = "license";
/**
* 验证码有效期,默认 60秒
*/
long CODE_TIME = 60;
/**
* 验证码长度
*/
String CODE_SIZE = "6";
/**
* 客户端模式
*/
String CLIENT_CREDENTIALS = "client_credentials";
/**
* 客户端ID
*/
String CLIENT_ID = "clientId";
/**
* 短信登录 参数名称
*/
String SMS_PARAMETER_NAME = "mobile";
/**
* 授权码模式confirm
*/
String CUSTOM_CONSENT_PAGE_URI = "/oauth2/confirm_access";
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/constant/ServiceNameConstants.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.constant;
/**
* @author lengleng
* @date 2018年06月22日16:41:01 服务名称
*/
public interface ServiceNameConstants {
/**
* 认证服务的SERVICEID
*/
String AUTH_SERVICE = "pig-auth";
/**
* UPMS模块
*/
String UPMS_SERVICE = "pig-upms-biz";
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/constant/enums/DictTypeEnum.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.constant.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @author lengleng
* @date 2019-05-16
*
* 字典类型
*/
@Getter
@RequiredArgsConstructor
public enum DictTypeEnum {
/**
* 字典类型-系统内置(不可修改)
*/
SYSTEM("1", "系统内置"),
/**
* 字典类型-业务类型
*/
BIZ("0", "业务类");
/**
* 类型
*/
private final String type;
/**
* 描述
*/
private final String description;
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/constant/enums/LoginTypeEnum.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.constant.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @author lengleng
* @date 2018/8/15 社交登录类型
*/
@Getter
@RequiredArgsConstructor
public enum LoginTypeEnum {
/**
* 账号密码登录
*/
PWD("PWD", "账号密码登录"),
/**
* 验证码登录
*/
SMS("SMS", "验证码登录");
/**
* 类型
*/
private final String type;
/**
* 描述
*/
private final String description;
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/constant/enums/MenuTypeEnum.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.constant.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @author lengleng
* @date 2020-02-17
*
* 菜单类型
*/
@Getter
@RequiredArgsConstructor
public enum MenuTypeEnum {
/**
* 左侧菜单
*/
LEFT_MENU("0", "left"),
/**
* 顶部菜单
*/
TOP_MENU("2", "top"),
/**
* 按钮
*/
BUTTON("1", "button");
/**
* 类型
*/
private final String type;
/**
* 描述
*/
private final String description;
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/exception/CheckedException.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.exception;
import lombok.NoArgsConstructor;
/**
* 受检异常类,继承自RuntimeException
*
* @author lengleng
* @date 2025/05/30
*/
@NoArgsConstructor
public class CheckedException extends RuntimeException {
private static final long serialVersionUID = 1L;
public CheckedException(String message) {
super(message);
}
public CheckedException(Throwable cause) {
super(cause);
}
public CheckedException(String message, Throwable cause) {
super(message, cause);
}
public CheckedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/exception/ErrorCodes.java
================================================
package com.pig4cloud.pig.common.core.exception;
/**
* 错误编码
*
* @author lengleng
* @date 2022/3/30
*/
public interface ErrorCodes {
/**
* 系统编码错误
*/
String SYS_PARAM_CONFIG_ERROR = "sys.param.config.error";
/**
* 系统内置参数不能删除
*/
String SYS_PARAM_DELETE_SYSTEM = "sys.param.delete.system";
/**
* 用户已存在
*/
String SYS_USER_EXISTING = "sys.user.existing";
/**
* 用户名已存在
*/
String SYS_USER_USERNAME_EXISTING = "sys.user.username.existing";
/**
* 用户原密码错误,修改失败
*/
String SYS_USER_UPDATE_PASSWORDERROR = "sys.user.update.passwordError";
/**
* 用户信息为空
*/
String SYS_USER_USERINFO_EMPTY = "sys.user.userInfo.empty";
/**
* 获取当前用户信息失败
*/
String SYS_USER_QUERY_ERROR = "sys.user.query.error";
/**
* 部门名称不存在
*/
String SYS_DEPT_DEPTNAME_INEXISTENCE = "sys.dept.deptName.inexistence";
/**
* 岗位名称不存在
*/
String SYS_POST_POSTNAME_INEXISTENCE = "sys.post.postName.inexistence";
/**
* 岗位名称或编码已经存在
*/
String SYS_POST_NAMEORCODE_EXISTING = "sys.post.nameOrCode.existing";
/**
* 角色名称不存在
*/
String SYS_ROLE_ROLENAME_INEXISTENCE = "sys.role.roleName.inexistence";
/**
* 角色名或角色编码已经存在
*/
String SYS_ROLE_NAMEORCODE_EXISTING = "sys.role.nameOrCode.existing";
/**
* 菜单存在下级节点 删除失败
*/
String SYS_MENU_DELETE_EXISTING = "sys.menu.delete.existing";
/**
* 系统内置字典不允许删除
*/
String SYS_DICT_DELETE_SYSTEM = "sys.dict.delete.system";
/**
* 系统内置字典不能修改
*/
String SYS_DICT_UPDATE_SYSTEM = "sys.dict.update.system";
/**
* 验证码发送频繁
*/
String SYS_APP_SMS_OFTEN = "sys.app.sms.often";
/**
* 验证码错误
*/
String SYS_APP_SMS_ERROR = "sys.app.sms.error";
/**
* 手机号未注册
*/
String SYS_APP_PHONE_UNREGISTERED = "sys.app.phone.unregistered";
/**
* 未注册用户的短信混合系统配置键
*/
String SYS_SMS_BLEND_UNREGISTERED = "sys.app.sms.blend.unregistered";
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/exception/PigDeniedException.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.exception;
import lombok.NoArgsConstructor;
/**
* 授权拒绝异常类
*
* @author lengleng
* @date 2018/06/22
*/
@NoArgsConstructor
public class PigDeniedException extends RuntimeException {
private static final long serialVersionUID = 1L;
public PigDeniedException(String message) {
super(message);
}
public PigDeniedException(Throwable cause) {
super(cause);
}
public PigDeniedException(String message, Throwable cause) {
super(message, cause);
}
public PigDeniedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/exception/ValidateCodeException.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.exception;
/**
* 验证码异常类
*
* @author lengleng
* @date 2018/06/22
*/
public class ValidateCodeException extends RuntimeException {
private static final long serialVersionUID = -7285211528095468156L;
public ValidateCodeException() {
}
public ValidateCodeException(String msg) {
super(msg);
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/factory/YamlPropertySourceFactory.java
================================================
package com.pig4cloud.pig.common.core.factory;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import org.springframework.lang.Nullable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
/**
* YAML属性源工厂类:用于读取自定义YAML文件并转换为属性源
*
* @author lengleng
* @date 2025/05/30
*/
public class YamlPropertySourceFactory implements PropertySourceFactory {
/**
* 创建属性源
* @param name 属性源名称,可为空
* @param resource 编码资源
* @return 属性源对象
* @throws IOException 读取资源时可能抛出IO异常
*/
@Override
public PropertySource> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
Properties propertiesFromYaml = loadYamlIntoProperties(resource);
String sourceName = name != null ? name : resource.getResource().getFilename();
return new PropertiesPropertySource(sourceName, propertiesFromYaml);
}
/**
* 将YAML资源加载为Properties对象
* @param resource 编码后的资源对象
* @return 加载后的Properties对象
* @throws FileNotFoundException 当资源文件不存在时抛出
*/
private Properties loadYamlIntoProperties(EncodedResource resource) throws FileNotFoundException {
try {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
factory.afterPropertiesSet();
return factory.getObject();
}
catch (IllegalStateException e) {
Throwable cause = e.getCause();
if (cause instanceof FileNotFoundException) {
throw (FileNotFoundException) e.getCause();
}
throw e;
}
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/jackson/PigJavaTimeModule.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.jackson;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.PackageVersion;
import com.fasterxml.jackson.datatype.jsr310.deser.*;
import com.fasterxml.jackson.datatype.jsr310.ser.*;
import java.io.Serial;
import java.time.*;
import java.time.format.DateTimeFormatter;
/**
* Java 8 时间默认序列化模块
*
* @author L.cm
* @author lishanbu
* @author lengleng
* @date 2025/05/30
*/
public class PigJavaTimeModule extends SimpleModule {
@Serial
private static final long serialVersionUID = 1L;
/**
* PigJavaTimeModule构造函数,用于初始化时间序列化和反序列化规则
*/
public PigJavaTimeModule() {
super(PackageVersion.VERSION);
// ======================= 时间序列化规则 ===============================
// yyyy-MM-dd HH:mm:ss
this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMATTER));
// yyyy-MM-dd
this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE));
// HH:mm:ss
this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME));
// Instant 类型序列化
this.addSerializer(Instant.class, InstantSerializer.INSTANCE);
// Duration 类型序列化
this.addSerializer(Duration.class, DurationSerializer.INSTANCE);
// ======================= 时间反序列化规则 ==============================
// yyyy-MM-dd HH:mm:ss
this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMATTER));
// yyyy-MM-dd
this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE));
// HH:mm:ss
this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME));
// Instant 反序列化
this.addDeserializer(Instant.class, InstantDeserializer.INSTANT);
// Duration 反序列化
this.addDeserializer(Duration.class, DurationDeserializer.INSTANCE);
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/servlet/RepeatBodyRequestWrapper.java
================================================
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.servlet;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StreamUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
/**
* Request包装类:允许body重复读取
*
* @author lengleng
* @date 2025/05/30
*/
@Slf4j
public class RepeatBodyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] bodyByteArray;
private final Map parameterMap;
public RepeatBodyRequestWrapper(HttpServletRequest request) {
super(request);
this.bodyByteArray = getByteBody(request);
// 使用 HashMap 以便后续可以修改
this.parameterMap = new HashMap<>(request.getParameterMap());
}
/**
* 获取BufferedReader对象
* @return 如果bodyByteArray为空则返回null,否则返回对应的BufferedReader
*/
@Override
public BufferedReader getReader() {
return ObjectUtils.isEmpty(this.bodyByteArray) ? null
: new BufferedReader(new InputStreamReader(getInputStream()));
}
/**
* 获取Servlet输入流
* @return ServletInputStream 基于bodyByteArray的输入流
*/
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.bodyByteArray);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true; // 可以读取
}
@Override
public void setReadListener(ReadListener readListener) {
// doNothing
}
@Override
public int read() {
return byteArrayInputStream.read();
}
};
}
/**
* 从HttpServletRequest中获取字节数组形式的请求体
* @param request HTTP请求对象
* @return 请求体字节数组,解析失败时返回空数组
*/
private static byte[] getByteBody(HttpServletRequest request) {
byte[] body = new byte[0];
try {
body = StreamUtils.copyToByteArray(request.getInputStream());
}
catch (IOException e) {
log.error("解析流中数据异常", e);
}
return body;
}
/**
* 获取参数映射表
* @return 可变的参数映射表
*/
@Override
public Map getParameterMap() {
return this.parameterMap; // 返回可变的 parameterMap
}
/**
* 设置新的参数映射
* @param parameterMap 新的参数映射,将替换现有参数映射
*/
public void setParameterMap(Map parameterMap) {
this.parameterMap.clear();
this.parameterMap.putAll(parameterMap);
}
/**
* 根据参数名获取参数值
* @param name 参数名
* @return 参数值,如果不存在则返回null
*/
@Override
public String getParameter(String name) {
String[] values = parameterMap.get(name);
return (values != null && values.length > 0) ? values[0] : null;
}
/**
* 根据参数名获取参数值数组
* @param name 参数名
* @return 参数值数组,如果不存在则返回null
*/
@Override
public String[] getParameterValues(String name) {
return parameterMap.get(name);
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/ClassUtils.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.util;
import lombok.experimental.UtilityClass;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.web.method.HandlerMethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
/**
* 类工具类
*
* @author L.cm
*/
@UtilityClass
public class ClassUtils extends org.springframework.util.ClassUtils {
private final ParameterNameDiscoverer PARAMETERNAMEDISCOVERER = new DefaultParameterNameDiscoverer();
/**
* 获取方法参数信息
* @param constructor 构造器
* @param parameterIndex 参数序号
* @return {MethodParameter}
*/
public MethodParameter getMethodParameter(Constructor> constructor, int parameterIndex) {
MethodParameter methodParameter = new SynthesizingMethodParameter(constructor, parameterIndex);
methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER);
return methodParameter;
}
/**
* 获取方法参数信息
* @param method 方法
* @param parameterIndex 参数序号
* @return {MethodParameter}
*/
public MethodParameter getMethodParameter(Method method, int parameterIndex) {
MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex);
methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER);
return methodParameter;
}
/**
* 获取Annotation
* @param method Method
* @param annotationType 注解类
* @param 泛型标记
* @return {Annotation}
*/
public A getAnnotation(Method method, Class annotationType) {
Class> targetClass = method.getDeclaringClass();
// The method may be on an interface, but we need attributes from the target
// class.
// If the target class is null, the method will be unchanged.
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
// If we are dealing with method with generic parameters, find the original
// method.
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
// 先找方法,再找方法上的类
A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationType);
if (null != annotation) {
return annotation;
}
// 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类
return AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), annotationType);
}
/**
* 获取Annotation
* @param handlerMethod HandlerMethod
* @param annotationType 注解类
* @param 泛型标记
* @return {Annotation}
*/
public A getAnnotation(HandlerMethod handlerMethod, Class annotationType) {
// 先找方法,再找方法上的类
A annotation = handlerMethod.getMethodAnnotation(annotationType);
if (null != annotation) {
return annotation;
}
// 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类
Class> beanType = handlerMethod.getBeanType();
return AnnotatedElementUtils.findMergedAnnotation(beanType, annotationType);
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/MsgUtils.java
================================================
package com.pig4cloud.pig.common.core.util;
import lombok.experimental.UtilityClass;
import org.springframework.context.MessageSource;
import java.util.Locale;
/**
* i18n 工具类
*
* @author lengleng
* @date 2022/3/30
*/
@UtilityClass
public class MsgUtils {
/**
* 根据错误码获取中文错误信息
* @param code 错误码
* @return 对应的中文错误信息
*/
public String getMessage(String code) {
MessageSource messageSource = SpringContextHolder.getBean("messageSource");
return messageSource.getMessage(code, null, Locale.CHINA);
}
/**
* 通过错误码和参数获取中文错误信息
* @param code 错误码
* @param objects 格式化参数
* @return 格式化后的中文错误信息
*/
public String getMessage(String code, Object... objects) {
MessageSource messageSource = SpringContextHolder.getBean("messageSource");
return messageSource.getMessage(code, objects, Locale.CHINA);
}
/**
* 通过错误码和参数获取中文错误信息
* @param code 错误码
* @param objects 格式化参数
* @return 格式化后的中文错误信息
*/
public String getSecurityMessage(String code, Object... objects) {
MessageSource messageSource = SpringContextHolder.getBean("securityMessageSource");
return messageSource.getMessage(code, objects, Locale.CHINA);
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/R.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.util;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import lombok.*;
import lombok.experimental.Accessors;
import lombok.experimental.FieldNameConstants;
import java.io.Serializable;
/**
* 响应信息主体
*
* @param
* @author lengleng
*/
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@FieldNameConstants
public class R implements Serializable {
private static final long serialVersionUID = 1L;
@Getter
@Setter
private int code;
@Getter
@Setter
private String msg;
@Getter
@Setter
private T data;
public static R ok() {
return restResult(null, CommonConstants.SUCCESS, null);
}
public static R ok(T data) {
return restResult(data, CommonConstants.SUCCESS, null);
}
public static R ok(T data, String msg) {
return restResult(data, CommonConstants.SUCCESS, msg);
}
public static R failed() {
return restResult(null, CommonConstants.FAIL, null);
}
public static R failed(String msg) {
return restResult(null, CommonConstants.FAIL, msg);
}
public static R failed(T data) {
return restResult(data, CommonConstants.FAIL, null);
}
public static R failed(T data, String msg) {
return restResult(data, CommonConstants.FAIL, msg);
}
public static R restResult(T data, int code, String msg) {
R apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/RedisUtils.java
================================================
package com.pig4cloud.pig.common.core.util;
import cn.hutool.core.convert.Convert;
import lombok.experimental.UtilityClass;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 缓存工具类,注意这里都是基于RedisTemplate 来操作的
*
* @author XX
* @date 2023/05/12
*/
@UtilityClass
public class RedisUtils {
private static final Long SUCCESS = 1L;
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
Optional.ofNullable(redisTemplate)
.filter(template -> time > 0)
.ifPresent(template -> template.expire(key, time, TimeUnit.SECONDS));
return true;
}
/**
* 根据 key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return Optional.ofNullable(redisTemplate)
.map(template -> template.getExpire(key, TimeUnit.SECONDS))
.orElse(-1L);
}
/**
* 查找匹配key
* @param pattern key
* @return /
*/
public List scan(String pattern) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
return Optional.ofNullable(redisTemplate).map(template -> {
RedisConnectionFactory factory = template.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor cursor = rc.keyCommands().scan(options);
List result = new ArrayList<>();
while (cursor.hasNext()) {
result.add(new String(cursor.next()));
}
RedisConnectionUtils.releaseConnection(rc, factory);
return result;
}).orElse(Collections.emptyList());
}
/**
* 查找匹配key (使用KEYS命令)
* @param pattern key模式,支持通配符 * ? [] 等
* @return 匹配的key列表
* @apiNote 注意:KEYS命令会阻塞Redis服务器,生产环境建议使用scan方法
*/
public Set keys(String pattern) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return Optional.ofNullable(redisTemplate)
.map(template -> template.keys(pattern))
.orElse(Collections.emptySet());
}
/**
* 分页查询 key
* @param patternKey key
* @param page 页码
* @param size 每页数目
* @return /
*/
public List findKeysForPage(String patternKey, int page, int size) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor cursor = rc.keyCommands().scan(options);
List result = new ArrayList<>(size);
int tmpIndex = 0;
int fromIndex = page * size;
int toIndex = page * size + size;
while (cursor.hasNext()) {
if (tmpIndex >= fromIndex && tmpIndex < toIndex) {
result.add(new String(cursor.next()));
tmpIndex++;
continue;
}
// 获取到满足条件的数据后,就可以退出了
if (tmpIndex >= toIndex) {
break;
}
tmpIndex++;
cursor.next();
}
RedisConnectionUtils.releaseConnection(rc, factory);
return result;
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return Optional.ofNullable(redisTemplate).map(template -> template.hasKey(key)).orElse(false);
}
/**
* 删除缓存
* @param keys 可以传一个值 或多个
*/
public void delete(String... keys) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
if (keys != null) {
Arrays.stream(keys).forEach(redisTemplate::delete);
}
}
/**
* 获取锁
* @param lockKey 锁key
* @param value value
* @param expireTime:单位-秒
* @return boolean
*/
public boolean getLock(String lockKey, String value, int expireTime) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return Optional.ofNullable(redisTemplate)
.map(template -> template.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS))
.orElse(false);
}
/**
* 释放锁
* @param lockKey 锁key
* @param value value
* @return boolean
*/
public boolean releaseLock(String lockKey, String value) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript redisScript = new DefaultRedisScript<>(script, Long.class);
return Optional.ofNullable(redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value))
.map(Convert::toLong)
.filter(SUCCESS::equals)
.isPresent();
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public T get(String key) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForValue().get(key);
}
/**
* 批量获取
* @param keys
* @return
*/
public List multiGet(List keys) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForValue().multiGet(keys);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
Optional.ofNullable(redisTemplate).map(template -> {
template.opsForValue().set(key, value);
return true;
});
return true;
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return Optional.ofNullable(redisTemplate).map(template -> {
if (time > 0) {
template.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}
else {
template.opsForValue().set(key, value);
}
return true;
}).orElse(false);
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间
* @param timeUnit 类型
* @return true成功 false 失败
*/
public boolean set(String key, T value, long time, TimeUnit timeUnit) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
Optional.ofNullable(redisTemplate).map(template -> {
if (time > 0) {
template.opsForValue().set(key, value, time, timeUnit);
}
else {
template.opsForValue().set(key, value);
}
return true;
});
return true;
}
/**
* 执行 Redis 命令回调
* @param callback Redis回调函数
* @return 执行结果
*/
public T execute(RedisCallback callback) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return (T) redisTemplate.execute(callback);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param hashKey 项 不能为null
* @return 值
*/
public HV hget(String key, HK hashKey) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForHash().get(key, hashKey);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map hmget(String key) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map map) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
Optional.ofNullable(redisTemplate).map(template -> {
template.opsForHash().putAll(key, map);
return true;
});
return true;
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map map, long time) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
Optional.ofNullable(redisTemplate).map(template -> {
template.opsForHash().putAll(key, map);
if (time > 0) {
template.expire(key, time, TimeUnit.SECONDS);
}
return true;
});
return true;
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return Optional.ofNullable(redisTemplate).map(template -> {
template.opsForHash().put(key, item, value);
return true;
}).orElse(false);
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return Optional.ofNullable(redisTemplate).map(template -> {
template.opsForHash().put(key, item, value);
if (time > 0) {
template.expire(key, time, TimeUnit.SECONDS);
}
return true;
}).orElse(false);
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set sGet(String key) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForSet().members(key);
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForSet().isMember(key, value);
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForSet().add(key, values);
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForSet().size(key);
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
}
/**
* 获集合key1和集合key2的差集元素
* @param key 键
* @return
*/
public Set sDifference(String key, String otherKey) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForSet().difference(key, otherKey);
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List lGet(String key, long start, long end) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForList().size(key);
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForList().index(key, index);
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
redisTemplate.opsForList().rightPush(key, value);
return true;
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
Optional.ofNullable(redisTemplate).ifPresent(template -> template.expire(key, time, TimeUnit.SECONDS));
}
return true;
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List value) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
redisTemplate.opsForList().rightPushAll(key, value);
return true;
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List value, long time) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return /
*/
public boolean lUpdateIndex(String key, long index, Object value) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
redisTemplate.opsForList().set(key, index, value);
return true;
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForList().remove(key, count, value);
}
/**
* 将zSet数据放入缓存
* @param key
* @param time
* @param tuples
* @return
*/
public long zSetAndTime(String key, long time, Set> tuples) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
Long count = redisTemplate.opsForZSet().add(key, tuples);
if (time > 0) {
expire(key, time);
}
return count;
}
/**
* Sorted set:有序集合获取
* @param key
* @param min
* @param max
* @return
*/
public Set zRangeByScore(String key, double min, double max) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
ZSetOperations zset = redisTemplate.opsForZSet();
return zset.rangeByScore(key, min, max);
}
/**
* Sorted set:有序集合获取 正序
* @param key
* @param start
* @param end
* @return
*/
public Set zRange(String key, long start, long end) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
ZSetOperations zset = redisTemplate.opsForZSet();
return zset.range(key, start, end);
}
/**
* Sorted set:有序集合获取 倒叙
* @param key
* @param start
* @param end
* @return
*/
public Set zReverseRange(String key, long start, long end) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
ZSetOperations zset = redisTemplate.opsForZSet();
return zset.reverseRange(key, start, end);
}
/**
* 获取zSet缓存的长度
* @param key 键
* @return
*/
public long zGetSetSize(String key) {
RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
return redisTemplate.opsForZSet().size(key);
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/RetOps.java
================================================
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pig.common.core.util;
import cn.hutool.core.util.ObjectUtil;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* 简化{@code R} 的访问操作,例子
* R result = R.ok(0);
* // 使用场景1: 链式操作: 断言然后消费
* RetOps.of(result)
* .assertCode(-1,r -> new RuntimeException("error "+r.getCode()))
* .assertDataNotEmpty(r -> new IllegalStateException("oops!"))
* .useData(System.out::println);
*
* // 使用场景2: 读取原始值(data),这里返回的是Optional
* RetOps.of(result).getData().orElse(null);
*
* // 使用场景3: 类型转换
* R s = RetOps.of(result)
* .assertDataNotNull(r -> new IllegalStateException("nani??"))
* .map(i -> Integer.toHexString(i))
* .peek();
*
*
* @author CJ (power4j@outlook.com)
* @date 2022/5/12
* @since 4.4
*/
public class RetOps {
/** 状态码为成功 */
public static final Predicate> CODE_SUCCESS = r -> CommonConstants.SUCCESS == r.getCode();
/** 数据有值 */
public static final Predicate> HAS_DATA = r -> ObjectUtil.isNotEmpty(r.getData());
/** 数据有值,并且包含元素 */
public static final Predicate> HAS_ELEMENT = r -> ObjectUtil.isNotEmpty(r.getData());
/** 状态码为成功并且有值 */
public static final Predicate> DATA_AVAILABLE = CODE_SUCCESS.and(HAS_DATA);
private final R original;
// ~ 初始化
// ===================================================================================================
RetOps(R original) {
this.original = original;
}
public static RetOps of(R original) {
return new RetOps<>(Objects.requireNonNull(original));
}
// ~ 杂项方法
// ===================================================================================================
/**
* 观察原始值
* @return R
*/
public R peek() {
return original;
}
/**
* 读取{@code code}的值
* @return 返回code的值
*/
public int getCode() {
return original.getCode();
}
/**
* 读取{@code data}的值
* @return 返回 Optional 包装的data
*/
public Optional getData() {
return Optional.ofNullable(original.getData());
}
/**
* 有条件地读取{@code data}的值
* @param predicate 断言函数
* @return 返回 Optional 包装的data,如果断言失败返回empty
*/
public Optional getDataIf(Predicate super R>> predicate) {
return predicate.test(original) ? getData() : Optional.empty();
}
/**
* 读取{@code msg}的值
* @return 返回Optional包装的 msg
*/
public Optional getMsg() {
return Optional.of(original.getMsg());
}
/**
* 对{@code code}的值进行相等性测试
* @param value 基准值
* @return 返回ture表示相等
*/
public boolean codeEquals(int value) {
return original.getCode() == value;
}
/**
* 对{@code code}的值进行相等性测试
* @param value 基准值
* @return 返回ture表示不相等
*/
public boolean codeNotEquals(int value) {
return !codeEquals(value);
}
/**
* 是否成功
* @return 返回ture表示成功
* @see CommonConstants#SUCCESS
*/
public boolean isSuccess() {
return codeEquals(CommonConstants.SUCCESS);
}
/**
* 是否失败
* @return 返回ture表示失败
*/
public boolean notSuccess() {
return !isSuccess();
}
// ~ 链式操作
// ===================================================================================================
/**
* 断言{@code code}的值
* @param expect 预期的值
* @param func 用户函数,负责创建异常对象
* @param 异常类型
* @return 返回实例,以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public RetOps assertCode(int expect, Function super R, ? extends Ex> func)
throws Ex {
if (codeNotEquals(expect)) {
throw func.apply(original);
}
return this;
}
/**
* 断言成功
* @param func 用户函数,负责创建异常对象
* @param 异常类型
* @return 返回实例,以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public RetOps assertSuccess(Function super R, ? extends Ex> func) throws Ex {
return assertCode(CommonConstants.SUCCESS, func);
}
/**
* 断言业务数据有值
* @param func 用户函数,负责创建异常对象
* @param 异常类型
* @return 返回实例,以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public RetOps assertDataNotNull(Function super R, ? extends Ex> func) throws Ex {
if (Objects.isNull(original.getData())) {
throw func.apply(original);
}
return this;
}
/**
* 断言业务数据有值,并且包含元素
* @param func 用户函数,负责创建异常对象
* @param 异常类型
* @return 返回实例,以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public RetOps assertDataNotEmpty(Function super R, ? extends Ex> func) throws Ex {
if (ObjectUtil.isNotEmpty(original.getData())) {
throw func.apply(original);
}
return this;
}
/**
* 对业务数据(data)转换
* @param mapper 业务数据转换函数
* @param 数据类型
* @return 返回新实例,以便于继续进行链式操作
*/
public RetOps map(Function super T, ? extends U> mapper) {
R result = R.restResult(mapper.apply(original.getData()), original.getCode(), original.getMsg());
return of(result);
}
/**
* 对业务数据(data)转换
* @param predicate 断言函数
* @param mapper 业务数据转换函数
* @param 数据类型
* @return 返回新实例,以便于继续进行链式操作
* @see RetOps#CODE_SUCCESS
* @see RetOps#HAS_DATA
* @see RetOps#HAS_ELEMENT
* @see RetOps#DATA_AVAILABLE
*/
public RetOps mapIf(Predicate super R> predicate, Function super T, ? extends U> mapper) {
R result = R.restResult(mapper.apply(original.getData()), original.getCode(), original.getMsg());
return of(result);
}
// ~ 数据消费
// ===================================================================================================
/**
* 消费数据,注意此方法保证数据可用
* @param consumer 消费函数
*/
public void useData(Consumer super T> consumer) {
consumer.accept(original.getData());
}
/**
* 条件消费(错误代码匹配某个值)
* @param consumer 消费函数
* @param codes 错误代码集合,匹配任意一个则调用消费函数
*/
public void useDataOnCode(Consumer super T> consumer, int... codes) {
useDataIf(o -> Arrays.stream(codes).filter(c -> original.getCode() == c).findFirst().isPresent(), consumer);
}
/**
* 条件消费(错误代码表示成功)
* @param consumer 消费函数
*/
public void useDataIfSuccess(Consumer super T> consumer) {
useDataIf(CODE_SUCCESS, consumer);
}
/**
* 条件消费
* @param predicate 断言函数
* @param consumer 消费函数,断言函数返回{@code true}时被调用
* @see RetOps#CODE_SUCCESS
* @see RetOps#HAS_DATA
* @see RetOps#HAS_ELEMENT
* @see RetOps#DATA_AVAILABLE
*/
public void useDataIf(Predicate super R> predicate, Consumer super T> consumer) {
if (predicate.test(original)) {
consumer.accept(original.getData());
}
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/SpringContextHolder.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.util;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
/**
* @author lengleng
* @date 2019/2/1 Spring 工具类
*/
@Slf4j
@Service
@Lazy(false)
public class SpringContextHolder implements ApplicationContextAware, EnvironmentAware, DisposableBean {
private static ApplicationContext applicationContext = null;
private static Environment environment = null;
/**
* 取得存储在静态变量中的ApplicationContext.
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 获取环境
* @return {@link Environment }
*/
public static Environment getEnvironment() {
return environment;
}
/**
* 实现ApplicationContextAware接口, 注入Context到静态变量中.
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
SpringContextHolder.applicationContext = applicationContext;
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static T getBean(String name) {
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static T getBean(Class requiredType) {
return applicationContext.getBean(requiredType);
}
/**
* 清除SpringContextHolder中的ApplicationContext为Null.
*/
public static void clearHolder() {
if (log.isDebugEnabled()) {
log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
}
applicationContext = null;
}
/**
* 发布事件
* @param event
*/
public static void publishEvent(ApplicationEvent event) {
if (applicationContext == null) {
return;
}
applicationContext.publishEvent(event);
}
/**
* 是否是微服务
* @return boolean
*/
public static boolean isMicro() {
return environment.getProperty("spring.cloud.nacos.discovery.enabled", Boolean.class, true);
}
/**
* 实现DisposableBean接口, 在Context关闭时清理静态变量.
*/
@Override
@SneakyThrows
public void destroy() {
SpringContextHolder.clearHolder();
}
@Override
public void setEnvironment(Environment environment) {
SpringContextHolder.environment = environment;
}
}
================================================
FILE: pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/WebUtils.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.core.util;
import cn.hutool.core.codec.Base64;
import com.pig4cloud.pig.common.core.exception.CheckedException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.method.HandlerMethod;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
/**
* Miscellaneous utilities for web applications.
*
* @author L.cm
*/
@UtilityClass
public class WebUtils extends org.springframework.web.util.WebUtils {
private final String BASIC_ = "Basic ";
/**
* 判断是否ajax请求 spring ajax 返回含有 ResponseBody 或者 RestController注解
* @param handlerMethod HandlerMethod
* @return 是否ajax请求
*/
public boolean isBody(HandlerMethod handlerMethod) {
ResponseBody responseBody = ClassUtils.getAnnotation(handlerMethod, ResponseBody.class);
return responseBody != null;
}
/**
* 读取cookie
* @param name cookie name
* @return cookie value
*/
public String getCookieVal(String name) {
if (WebUtils.getRequest().isPresent()) {
return getCookieVal(WebUtils.getRequest().get(), name);
}
return null;
}
/**
* 读取cookie
* @param request HttpServletRequest
* @param name cookie name
* @return cookie value
*/
public String getCookieVal(HttpServletRequest request, String name) {
Cookie cookie = getCookie(request, name);
return cookie != null ? cookie.getValue() : null;
}
/**
* 清除 某个指定的cookie
* @param response HttpServletResponse
* @param key cookie key
*/
public void removeCookie(HttpServletResponse response, String key) {
setCookie(response, key, null, 0);
}
/**
* 设置cookie
* @param response HttpServletResponse
* @param name cookie name
* @param value cookie value
* @param maxAgeInSeconds maxage
*/
public void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setMaxAge(maxAgeInSeconds);
cookie.setHttpOnly(true);
response.addCookie(cookie);
}
/**
* 获取 HttpServletRequest
* @return {HttpServletRequest}
*/
public Optional getRequest() {
return Optional
.ofNullable(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
}
/**
* 获取 HttpServletResponse
* @return {HttpServletResponse}
*/
public HttpServletResponse getResponse() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
}
/**
* 从request 获取CLIENT_ID
* @return
*/
@SneakyThrows
public String getClientId(ServerHttpRequest request) {
String header = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
return splitClient(header)[0];
}
@SneakyThrows
public String getClientId() {
if (WebUtils.getRequest().isPresent()) {
String header = WebUtils.getRequest().get().getHeader(HttpHeaders.AUTHORIZATION);
return splitClient(header)[0];
}
return null;
}
@NotNull
private static String[] splitClient(String header) {
if (header == null || !header.startsWith(BASIC_)) {
throw new CheckedException("请求头中client信息为空");
}
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
}
catch (IllegalArgumentException e) {
throw new CheckedException("Failed to decode basic authentication token");
}
String token = new String(decoded, StandardCharsets.UTF_8);
int delim = token.indexOf(":");
if (delim == -1) {
throw new CheckedException("Invalid basic authentication token");
}
return new String[] { token.substring(0, delim), token.substring(delim + 1) };
}
}
================================================
FILE: pig-common/pig-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
================================================
com.pig4cloud.pig.common.core.config.JacksonConfiguration
com.pig4cloud.pig.common.core.config.RedisTemplateConfiguration
com.pig4cloud.pig.common.core.config.RestTemplateConfiguration
com.pig4cloud.pig.common.core.util.SpringContextHolder
com.pig4cloud.pig.common.core.config.WebMvcConfiguration
================================================
FILE: pig-common/pig-common-core/src/main/resources/banner.txt
================================================
${AnsiColor.BRIGHT_YELLOW}
::::::::: ::::::::::: ::::::::
:+: :+: :+: :+: :+:
+:+ +:+ +:+ +:+
+#++:++#+ +#+ :#:
+#+ +#+ +#+ +#+#
#+# #+# #+# #+#
### ########### ########
www.pig4cloud.com
Pig Microservice Architecture
${AnsiColor.DEFAULT}
================================================
FILE: pig-common/pig-common-core/src/main/resources/i18n/messages_zh_CN.properties
================================================
sys.user.update.passwordError=\u539F\u5BC6\u7801\u9519\u8BEF\uFF0C\u4FEE\u6539\u5931\u8D25
sys.user.query.error=\u83B7\u53D6\u5F53\u524D\u7528\u6237\u4FE1\u606F\u5931\u8D25
sys.user.existing=\u7528\u6237\u5DF2\u5B58\u5728
sys.user.username.existing={0} \u7528\u6237\u540D\u5DF2\u5B58\u5728
sys.user.userInfo.empty={0} \u7528\u6237\u4FE1\u606F\u4E3A\u7A7A
sys.dept.deptName.inexistence={0} \u90E8\u95E8\u540D\u79F0\u4E0D\u5B58\u5728
sys.post.postName.inexistence={0} \u5C97\u4F4D\u540D\u79F0\u4E0D\u5B58\u5728
sys.post.nameOrCode.existing={0} {1} \u5C97\u4F4D\u540D\u6216\u5C97\u4F4D\u7F16\u7801\u5DF2\u7ECF\u5B58\u5728
sys.role.roleName.inexistence={0} \u89D2\u8272\u540D\u79F0\u4E0D\u5B58\u5728
sys.role.nameOrCode.existing={0} {1} \u89D2\u8272\u540D\u6216\u89D2\u8272\u7F16\u7801\u5DF2\u7ECF\u5B58\u5728
sys.param.delete.system=\u7CFB\u7EDF\u5185\u7F6E\u53C2\u6570\u4E0D\u80FD\u5220\u9664
sys.param.config.error={0} \u7CFB\u7EDF\u53C2\u6570\u914D\u7F6E\u9519\u8BEF
sys.menu.delete.existing=\u83DC\u5355\u542B\u6709\u4E0B\u7EA7\u4E0D\u80FD\u5220\u9664
sys.app.sms.often=\u9A8C\u8BC1\u7801\u53D1\u9001\u8FC7\u9891\u7E41
sys.app.sms.error=\u9A8C\u8BC1\u7801\u9519\u8BEF
sys.app.phone.unregistered={0} \u624B\u673A\u53F7\u672A\u6CE8\u518C
sys.app.sms.blend.unregistered=\u77ED\u4FE1\u6E20\u9053\u672A\u914D\u7F6E\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
sys.dict.delete.system=\u7CFB\u7EDF\u5185\u7F6E\u5B57\u5178\u9879\u76EE\u4E0D\u80FD\u5220\u9664
sys.dict.update.system=\u7CFB\u7EDF\u5185\u7F6E\u5B57\u5178\u9879\u76EE\u4E0D\u80FD\u4FEE\u6539
================================================
FILE: pig-common/pig-common-core/src/main/resources/logback-spring.xml
================================================
${CONSOLE_LOG_PATTERN}
${log.path}/debug.log
${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz
50MB
30
${FILE_LOG_PATTERN}
${log.path}/error.log
${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz
50MB
30
${FILE_LOG_PATTERN}
ERROR
================================================
FILE: pig-common/pig-common-datasource/pom.xml
================================================
pig-common
com.pig4cloud
${revision}
4.0.0
pig-common-datasource
jar
pig 动态切换数据源
com.baomidou
dynamic-datasource-spring-boot3-starter
jakarta.servlet
jakarta.servlet-api
================================================
FILE: pig-common/pig-common-datasource/src/main/java/com/pig4cloud/pig/common/datasource/DynamicDataSourceAutoConfiguration.java
================================================
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pig.common.datasource;
import com.baomidou.dynamic.datasource.creator.DataSourceCreator;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.creator.hikaricp.HikariDataSourceCreator;
import com.baomidou.dynamic.datasource.processor.DsJakartaHeaderProcessor;
import com.baomidou.dynamic.datasource.processor.DsJakartaSessionProcessor;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import com.pig4cloud.pig.common.datasource.config.*;
import lombok.RequiredArgsConstructor;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.expression.BeanFactoryResolver;
import java.util.ArrayList;
import java.util.List;
/**
* 动态数据源切换配置
*
* @author lengleng
* @date 2020-02-06
*/
@Configuration
@RequiredArgsConstructor
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DynamicDataSourceAutoConfiguration {
/**
* 获取动态数据源提供者
* @param defaultDataSourceCreator 默认数据源创建器
* @param stringEncryptor 字符串加密器
* @param properties 数据源属性
* @return 动态数据源提供者
*/
@Bean
public DynamicDataSourceProvider dynamicDataSourceProvider(DefaultDataSourceCreator defaultDataSourceCreator,
StringEncryptor stringEncryptor, DataSourceProperties properties) {
return new JdbcDynamicDataSourceProvider(defaultDataSourceCreator, stringEncryptor, properties);
}
/**
* 主数据源提供程序
* @param defaultDataSourceCreator 默认数据源创建者
* @param properties 性能
* @return {@link DynamicDataSourceProvider }
*/
@Bean
public DynamicDataSourceProvider masterDataSourceProvider(DefaultDataSourceCreator defaultDataSourceCreator,
DataSourceProperties properties) {
return new MasterDataSourceProvider(defaultDataSourceCreator, properties);
}
/**
* 获取默认数据源创建器
* @param druidDataSourceCreator Druid数据源创建器
* @return 默认数据源创建器
*/
@Bean
public DefaultDataSourceCreator defaultDataSourceCreator(HikariDataSourceCreator druidDataSourceCreator) {
DefaultDataSourceCreator defaultDataSourceCreator = new DefaultDataSourceCreator();
List creators = new ArrayList<>();
creators.add(druidDataSourceCreator);
defaultDataSourceCreator.setCreators(creators);
return defaultDataSourceCreator;
}
/**
* 获取数据源处理器
* @return 数据源处理器
*/
@Bean
public DsProcessor dsProcessor(BeanFactory beanFactory) {
DsProcessor lastParamDsProcessor = new LastParamDsProcessor();
DsProcessor headerProcessor = new DsJakartaHeaderProcessor();
DsProcessor sessionProcessor = new DsJakartaSessionProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));
lastParamDsProcessor.setNextProcessor(headerProcessor);
headerProcessor.setNextProcessor(sessionProcessor);
sessionProcessor.setNextProcessor(spelExpressionProcessor);
return lastParamDsProcessor;
}
/**
* 获取清除TTL数据源过滤器
* @return 清除TTL数据源过滤器
*/
@Bean
public ClearTtlDataSourceFilter clearTtlDsFilter() {
return new ClearTtlDataSourceFilter();
}
}
================================================
FILE: pig-common/pig-common-datasource/src/main/java/com/pig4cloud/pig/common/datasource/annotation/EnableDynamicDataSource.java
================================================
package com.pig4cloud.pig.common.datasource.annotation;
import com.pig4cloud.pig.common.datasource.DynamicDataSourceAutoConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* 开启动态数据源注解
*
* 用于启用动态数据源自动配置功能
*
* @author lengleng
* @date 2025/07/14
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(DynamicDataSourceAutoConfiguration.class)
public @interface EnableDynamicDataSource {
}
================================================
FILE: pig-common/pig-common-datasource/src/main/java/com/pig4cloud/pig/common/datasource/config/ClearTtlDataSourceFilter.java
================================================
package com.pig4cloud.pig.common.datasource.config;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.springframework.core.Ordered;
import org.springframework.web.filter.GenericFilterBean;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
/**
* 清空上文的DS 设置避免污染当前线程
*
* @author lengleng
* @date 2020/12/11
*/
public class ClearTtlDataSourceFilter extends GenericFilterBean implements Ordered {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
DynamicDataSourceContextHolder.clear();
filterChain.doFilter(servletRequest, servletResponse);
DynamicDataSourceContextHolder.clear();
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}
================================================
FILE: pig-common/pig-common-datasource/src/main/java/com/pig4cloud/pig/common/datasource/config/DataSourceProperties.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.datasource.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 数据源配置属性
*
* @author lengleng
* @date 2025/07/14
*/
@Data
@ConfigurationProperties("spring.datasource")
public class DataSourceProperties {
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* jdbcurl
*/
private String url;
/**
* 驱动类型
*/
private String driverClassName;
/**
* 查询数据源的SQL
*/
private String queryDsSql = "select * from gen_datasource_conf where del_flag = '0'";
}
================================================
FILE: pig-common/pig-common-datasource/src/main/java/com/pig4cloud/pig/common/datasource/config/JdbcDynamicDataSourceProvider.java
================================================
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pig.common.datasource.config;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.provider.AbstractJdbcDataSourceProvider;
import com.pig4cloud.pig.common.datasource.support.DataSourceConstants;
import com.pig4cloud.pig.common.datasource.util.DsConfTypeEnum;
import com.pig4cloud.pig.common.datasource.util.DsJdbcUrlEnum;
import lombok.extern.slf4j.Slf4j;
import org.jasypt.encryption.StringEncryptor;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
/**
* JDBC动态数据源提供者:从数据源中获取配置信息
*
* @author lengleng
* @date 2025/07/14
*/
@Slf4j
public class JdbcDynamicDataSourceProvider extends AbstractJdbcDataSourceProvider {
private final DataSourceProperties properties;
private final StringEncryptor stringEncryptor;
public JdbcDynamicDataSourceProvider(DefaultDataSourceCreator defaultDataSourceCreator,
StringEncryptor stringEncryptor, DataSourceProperties properties) {
super(defaultDataSourceCreator, properties.getDriverClassName(), properties.getUrl(), properties.getUsername(),
properties.getPassword());
this.stringEncryptor = stringEncryptor;
this.properties = properties;
}
/**
* 执行语句获得数据源参数
* @param statement 语句
* @return 数据源参数
* @throws SQLException sql异常
*/
@Override
protected Map executeStmt(Statement statement) throws SQLException {
Map map = new HashMap<>(8);
try {
ResultSet rs = statement.executeQuery(properties.getQueryDsSql());
while (rs.next()) {
String name = rs.getString(DataSourceConstants.NAME);
String username = rs.getString(DataSourceConstants.DS_USER_NAME);
String password = rs.getString(DataSourceConstants.DS_USER_PWD);
Integer confType = rs.getInt(DataSourceConstants.DS_CONFIG_TYPE);
String dsType = rs.getString(DataSourceConstants.DS_TYPE);
DataSourceProperty property = new DataSourceProperty();
property.setUsername(username);
property.setPassword(stringEncryptor.decrypt(password));
String url;
// JDBC 配置形式
DsJdbcUrlEnum urlEnum = DsJdbcUrlEnum.get(dsType);
if (DsConfTypeEnum.JDBC.getType().equals(confType)) {
url = rs.getString(DataSourceConstants.DS_JDBC_URL);
}
else {
String host = rs.getString(DataSourceConstants.DS_HOST);
String port = rs.getString(DataSourceConstants.DS_PORT);
String dsName = rs.getString(DataSourceConstants.DS_NAME);
url = String.format(urlEnum.getUrl(), host, port, dsName);
}
property.setUrl(url);
map.put(name, property);
}
}
catch (Exception e) {
log.warn("动态数据源配置表异常:{}", e.getMessage());
}
return map;
}
}
================================================
FILE: pig-common/pig-common-datasource/src/main/java/com/pig4cloud/pig/common/datasource/config/LastParamDsProcessor.java
================================================
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pig.common.datasource.config;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.aopalliance.intercept.MethodInvocation;
/**
* 参数数据源解析 @DS("#last")
*
* @author lengleng
* @date 2020/2/6
*/
public class LastParamDsProcessor extends DsProcessor {
private static final String LAST_PREFIX = "#last";
/**
* 抽象匹配条件 匹配才会走当前执行器否则走下一级执行器
* @param key DS注解里的内容
* @return 是否匹配
*/
@Override
public boolean matches(String key) {
if (key.startsWith(LAST_PREFIX)) {
// https://github.com/baomidou/dynamic-datasource-spring-boot-starter/issues/213
DynamicDataSourceContextHolder.clear();
return true;
}
return false;
}
/**
* 抽象最终决定数据源
* @param invocation 方法执行信息
* @param key DS注解里的内容
* @return 数据源名称
*/
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
Object[] arguments = invocation.getArguments();
return String.valueOf(arguments[arguments.length - 1]);
}
}
================================================
FILE: pig-common/pig-common-datasource/src/main/java/com/pig4cloud/pig/common/datasource/config/MasterDataSourceProvider.java
================================================
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pig.common.datasource.config;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.provider.AbstractDataSourceProvider;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import static com.pig4cloud.pig.common.datasource.support.DataSourceConstants.DS_MASTER;
/**
* 主数据源提供者,用于保证原有配置有效性并扩展其他数据源,和原有spring.datasource配置兼容。
*
* @author lengleng
* @date 2025/07/14
*/
public class MasterDataSourceProvider extends AbstractDataSourceProvider {
private final DataSourceProperties properties;
private final DefaultDataSourceCreator defaultDataSourceCreator;
public MasterDataSourceProvider(DefaultDataSourceCreator defaultDataSourceCreator,
DataSourceProperties properties) {
super(defaultDataSourceCreator);
this.properties = properties;
this.defaultDataSourceCreator = defaultDataSourceCreator;
}
/**
* 加载所有数据源
* @return 所有数据源,key为数据源名称
*/
@Override
public Map loadDataSources() {
Map map = new HashMap<>();
// 添加默认主数据源
DataSourceProperty property = new DataSourceProperty();
property.setUsername(properties.getUsername());
property.setPassword(properties.getPassword());
property.setUrl(properties.getUrl());
map.put(DS_MASTER, defaultDataSourceCreator.createDataSource(property));
return map;
}
}
================================================
FILE: pig-common/pig-common-datasource/src/main/java/com/pig4cloud/pig/common/datasource/support/DataSourceConstants.java
================================================
package com.pig4cloud.pig.common.datasource.support;
/**
* 数据源相关常量
*
* @author lengleng
* @date 2019-04-01
*/
public interface DataSourceConstants {
/**
* 数据源名称
*/
String NAME = "name";
/**
* 默认数据源(master)
*/
String DS_MASTER = "master";
/**
* jdbcurl
*/
String DS_JDBC_URL = "url";
/**
* 配置类型
*/
String DS_CONFIG_TYPE = "conf_type";
/**
* 用户名
*/
String DS_USER_NAME = "username";
/**
* 密码
*/
String DS_USER_PWD = "password";
/**
* 数据库类型
*/
String DS_TYPE = "ds_type";
/**
* 数据库名称
*/
String DS_NAME = "ds_name";
/**
* 主机类型
*/
String DS_HOST = "host";
/**
* 端口
*/
String DS_PORT = "port";
/**
* 实例名称
*/
String DS_INSTANCE = "instance";
}
================================================
FILE: pig-common/pig-common-datasource/src/main/java/com/pig4cloud/pig/common/datasource/util/DsConfTypeEnum.java
================================================
package com.pig4cloud.pig.common.datasource.util;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 数据源配置类型
*
* @author lengleng
* @date 2020/12/11
*/
@Getter
@AllArgsConstructor
public enum DsConfTypeEnum {
/**
* 主机链接
*/
HOST(0, "主机链接"),
/**
* JDBC链接
*/
JDBC(1, "JDBC链接");
private final Integer type;
private final String description;
}
================================================
FILE: pig-common/pig-common-datasource/src/main/java/com/pig4cloud/pig/common/datasource/util/DsJdbcUrlEnum.java
================================================
package com.pig4cloud.pig.common.datasource.util;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* JDBC URL 枚举
*
* @author lengleng
* @date 2020/12/11
*/
@Getter
@AllArgsConstructor
public enum DsJdbcUrlEnum {
/**
* mysql 数据库
*/
MYSQL("mysql",
"jdbc:mysql://%s:%s/%s?characterEncoding=utf8"
+ "&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true"
+ "&useLegacyDatetimeCode=false&allowMultiQueries=true&allowPublicKeyRetrieval=true",
"select 1", "mysql8 链接"),
/**
* pg 数据库
*/
PG("pg", "jdbc:postgresql://%s:%s/%s", "select 1", "postgresql 链接"),
/**
* SQL SERVER
*/
MSSQL("mssql", "jdbc:sqlserver://%s:%s;database=%s;characterEncoding=UTF-8", "select 1", "sqlserver 链接"),
/**
* oracle
*/
ORACLE("oracle", "jdbc:oracle:thin:@%s:%s:%s", "select 1 from dual", "oracle 链接"),
/**
* db2
*/
DB2("db2", "jdbc:db2://%s:%s/%s", "select 1 from sysibm.sysdummy1", "DB2 TYPE4 连接"),
/**
* 达梦
*/
DM("dm", "jdbc:dm://%s:%s/%s", "select 1 from dual", "达梦连接"),
/**
* pg 数据库
*/
HIGHGO("highgo", "jdbc:highgo://%s:%s/%s", "select 1", "highgo 链接");
private final String dbName;
private final String url;
private final String validationQuery;
private final String description;
public static DsJdbcUrlEnum get(String dsType) {
return Arrays.stream(DsJdbcUrlEnum.values())
.filter(dsJdbcUrlEnum -> dsType.equals(dsJdbcUrlEnum.getDbName()))
.findFirst()
.get();
}
}
================================================
FILE: pig-common/pig-common-excel/pom.xml
================================================
4.0.0
com.pig4cloud
pig-common
${revision}
pig-common-excel
jar
excel 导入导出处理模块
com.pig4cloud
pig-common-core
com.pig4cloud.excel
excel-spring-boot-starter
================================================
FILE: pig-common/pig-common-excel/src/main/java/com/pig4cloud/pig/common/excel/ExcelAutoConfiguration.java
================================================
package com.pig4cloud.pig.common.excel;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.constant.ServiceNameConstants;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.excel.provider.RemoteDictApiService;
import com.pig4cloud.pig.common.excel.provider.RemoteDictDataProvider;
import com.pig4cloud.plugin.excel.handler.DictDataProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import java.util.Optional;
/**
* Excel 自动装配类
*
* @author lengleng
* @date 2025/05/31
*/
@AutoConfiguration
public class ExcelAutoConfiguration {
/**
* 创建远程字典API服务实例
* @param restClientBuilderOptional RestClient构建器的可选对象
* @return {@link RemoteDictApiService} 远程字典API服务实例
*/
@Bean
@ConditionalOnMissingBean
public RemoteDictApiService remoteDictApiService(Optional restClientBuilderOptional) {
RestClient client = restClientBuilderOptional.orElseGet(RestClient::builder)
.baseUrl(getBaseUrl())
.defaultHeader(SecurityConstants.FROM, SecurityConstants.FROM_IN)
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client)).build();
return factory.createClient(RemoteDictApiService.class);
}
/**
* 创建字典数据提供程序
* @param remoteDictApiService 远程字典API服务
* @return 字典数据提供程序实例
*/
@Bean
@ConditionalOnMissingBean
public DictDataProvider dictDataProvider(RemoteDictApiService remoteDictApiService) {
return new RemoteDictDataProvider(remoteDictApiService);
}
/**
* 获取基础URL
* @return 根据当前架构模式组装的基础URL字符串
*/
private String getBaseUrl() {
// 根据当前架构模式,组装URL
if (SpringContextHolder.isMicro()) {
return String.format("http://%s", ServiceNameConstants.UPMS_SERVICE);
}
else {
return String.format("http://%s", SpringContextHolder.getEnvironment()
.resolvePlaceholders("127.0.0.1:${server.port}${server.servlet.context-path}"));
}
}
}
================================================
FILE: pig-common/pig-common-excel/src/main/java/com/pig4cloud/pig/common/excel/provider/RemoteDictApiService.java
================================================
package com.pig4cloud.pig.common.excel.provider;
import com.pig4cloud.pig.common.core.util.R;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;
import java.util.List;
import java.util.Map;
/**
* 远程字典API服务接口,基于RestClient GetExchange实现
*
* @author lengleng
* @date 2025/05/31
*/
public interface RemoteDictApiService {
/**
* 根据类型获取字典数据
* @param type 字典类型
* @return 包含字典数据的响应对象,字典数据以Map列表形式返回
*/
@GetExchange("/dict/remote/type/{type}")
R>> getDictByType(@PathVariable String type);
}
================================================
FILE: pig-common/pig-common-excel/src/main/java/com/pig4cloud/pig/common/excel/provider/RemoteDictDataProvider.java
================================================
package com.pig4cloud.pig.common.excel.provider;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import com.pig4cloud.pig.common.core.util.R;
import com.pig4cloud.plugin.excel.handler.DictDataProvider;
import com.pig4cloud.plugin.excel.vo.DictEnum;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* 远程字典数据提供程序实现类
*
* @author lengleng
* @date 2025/05/31
*/
@RequiredArgsConstructor
public class RemoteDictDataProvider implements DictDataProvider {
private final RemoteDictApiService remoteDictApiService;
/**
* 根据类型获取字典枚举数组
* @param type 字典类型
* @return 字典枚举数组,无数据时返回空数组
*/
@Override
public DictEnum[] getDict(String type) {
R>> dictDataListR = remoteDictApiService.getDictByType(type);
List> dictDataList = dictDataListR.getData();
if (CollUtil.isEmpty(dictDataList)) {
return new DictEnum[0];
}
// 构建 DictEnum 数组
DictEnum.Builder dictEnumBuilder = DictEnum.builder();
for (Map dictData : dictDataList) {
String value = MapUtil.getStr(dictData, "value");
String label = MapUtil.getStr(dictData, "label");
dictEnumBuilder.add(value, label);
}
return dictEnumBuilder.build();
}
}
================================================
FILE: pig-common/pig-common-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
================================================
com.pig4cloud.pig.common.excel.ExcelAutoConfiguration
================================================
FILE: pig-common/pig-common-feign/pom.xml
================================================
com.pig4cloud
pig-common
${revision}
4.0.0
jar
pig-common-feign
feign-sentinel服务降级熔断、限流组件
com.pig4cloud
pig-common-core
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
org.springframework.cloud
spring-cloud-starter-openfeign
io.github.openfeign
feign-okhttp
org.springframework.cloud
spring-cloud-starter-loadbalancer
com.github.ben-manes.caffeine
caffeine
org.springframework.security
spring-security-core
org.springframework
spring-webmvc
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/PigFeignAutoConfiguration.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.feign;
import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration;
import com.pig4cloud.pig.common.feign.core.PigFeignInnerRequestInterceptor;
import com.pig4cloud.pig.common.feign.core.PigFeignRequestCloseInterceptor;
import com.pig4cloud.pig.common.feign.sentinel.ext.PigSentinelFeign;
import feign.Feign;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.openfeign.PigFeignClientsRegistrar;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
/**
* Sentinel Feign 自动配置类
*
* @author lengleng
* @date 2025/05/31
*/
@Configuration(proxyBeanMethods = false)
@Import(PigFeignClientsRegistrar.class)
@AutoConfigureBefore(SentinelFeignAutoConfiguration.class)
public class PigFeignAutoConfiguration {
/**
* 创建Feign.Builder实例,支持Sentinel功能
* @return Feign.Builder实例
* @ConditionalOnMissingBean 当容器中不存在该类型bean时创建
* @ConditionalOnProperty 当配置feign.sentinel.enabled为true时生效
* @Scope 指定bean作用域为prototype
*/
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.sentinel.enabled")
public Feign.Builder feignSentinelBuilder() {
return PigSentinelFeign.builder();
}
/**
* 创建并返回PigFeignRequestCloseInterceptor实例
* @return PigFeignRequestCloseInterceptor实例
*/
@Bean
public PigFeignRequestCloseInterceptor pigFeignRequestCloseInterceptor() {
return new PigFeignRequestCloseInterceptor();
}
/**
* 创建并返回PigFeignInnerRequestInterceptor实例
* @return PigFeignInnerRequestInterceptor 内部请求拦截器实例
*/
@Bean
public PigFeignInnerRequestInterceptor pigFeignInnerRequestInterceptor() {
return new PigFeignInnerRequestInterceptor();
}
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/annotation/EnablePigFeignClients.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.feign.annotation;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* 启用Pig Feign客户端注解
*
* @author lengleng
* @date 2025/05/31
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableFeignClients
public @interface EnablePigFeignClients {
/**
* {@link #basePackages()}属性的别名。允许更简洁的注解声明
* @return 'basePackages'数组
*/
String[] value() default {};
/**
* 扫描注解组件的基础包路径
*
* 与{@link #value()}互为别名且互斥
*
* 对于基于字符串的包名,可使用{@link #basePackageClasses()}作为类型安全的替代方案
* @return 基础包路径数组
*/
@AliasFor(annotation = EnableFeignClients.class, attribute = "basePackages")
String[] basePackages() default { "com.pig4cloud.pig" };
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/annotation/NoToken.java
================================================
package com.pig4cloud.pig.common.feign.annotation;
import java.lang.annotation.*;
/**
* 服务无token调用声明注解
*
* 只有发起方没有 token 时候才需要添加此注解, @NoToken + @Inner
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoToken {
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/core/PigFeignInnerRequestInterceptor.java
================================================
package com.pig4cloud.pig.common.feign.core;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.feign.annotation.NoToken;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.core.Ordered;
import java.lang.reflect.Method;
/**
* PigFeign 内部请求拦截器,用于处理 Feign 请求的 Token 校验
*
* @author lengleng
* @date 2025/05/31
*/
public class PigFeignInnerRequestInterceptor implements RequestInterceptor, Ordered {
/**
* 为每个请求调用,使用提供的{@link RequestTemplate}方法添加数据
* @param template 请求模板
*/
@Override
public void apply(RequestTemplate template) {
Method method = template.methodMetadata().method();
NoToken noToken = method.getAnnotation(NoToken.class);
if (noToken != null) {
template.header(SecurityConstants.FROM, SecurityConstants.FROM_IN);
}
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/core/PigFeignRequestCloseInterceptor.java
================================================
package com.pig4cloud.pig.common.feign.core;
import feign.RequestInterceptor;
import org.springframework.http.HttpHeaders;
/**
* Feign请求连接关闭拦截器
*
* 用于设置HTTP连接为关闭状态
*
* @author lengleng
* @date 2025/05/31
*/
public class PigFeignRequestCloseInterceptor implements RequestInterceptor {
/**
* 设置连接关闭
* @param template 请求模板
*/
@Override
public void apply(feign.RequestTemplate template) {
template.header(HttpHeaders.CONNECTION, "close");
}
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/sentinel/SentinelAutoConfiguration.java
================================================
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pig.common.feign.sentinel;
import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.RequestOriginParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pig4cloud.pig.common.feign.sentinel.ext.PigSentinelFeign;
import com.pig4cloud.pig.common.feign.sentinel.handle.PigUrlBlockHandler;
import com.pig4cloud.pig.common.feign.sentinel.parser.PigHeaderRequestOriginParser;
import feign.Feign;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/**
* Sentinel 自动配置类
*
* @author lengleng
* @date 2025/05/31
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(SentinelFeignAutoConfiguration.class)
public class SentinelAutoConfiguration {
/**
* 创建Feign Sentinel构建器
* @return Feign.Builder实例
* @ConditionalOnMissingBean 当容器中不存在该类型bean时创建
* @ConditionalOnProperty 当配置项spring.cloud.openfeign.sentinel.enabled为true时生效
* @Scope 指定bean作用域为prototype
*/
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "spring.cloud.openfeign.sentinel.enabled")
public Feign.Builder feignSentinelBuilder() {
return PigSentinelFeign.builder();
}
/**
* 创建默认的BlockExceptionHandler bean
* @param objectMapper 对象映射器
* @return PigUrlBlockHandler实例
* @ConditionalOnMissingBean 当容器中不存在该类型bean时创建
*/
@Bean
@ConditionalOnMissingBean
public BlockExceptionHandler blockExceptionHandler(ObjectMapper objectMapper) {
return new PigUrlBlockHandler(objectMapper);
}
/**
* 创建并返回一个RequestOriginParser bean,当容器中不存在该类型的bean时生效
* @return 默认的PigHeaderRequestOriginParser实例
*/
@Bean
@ConditionalOnMissingBean
public RequestOriginParser requestOriginParser() {
return new PigHeaderRequestOriginParser();
}
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/sentinel/ext/PigSentinelFeign.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.feign.sentinel.ext;
import com.alibaba.cloud.sentinel.feign.SentinelContractHolder;
import feign.Contract;
import feign.Feign;
import feign.InvocationHandlerFactory;
import feign.Target;
import org.springframework.beans.BeansException;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.FeignClientFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
/**
* 支持自动降级注入的Feign构建器,重写 {@link com.alibaba.cloud.sentinel.feign.SentinelFeign}
*
* @author lengleng
* @date 2025/05/31
*/
public final class PigSentinelFeign {
private PigSentinelFeign() {
}
public static PigSentinelFeign.Builder builder() {
return new PigSentinelFeign.Builder();
}
public static final class Builder extends Feign.Builder implements ApplicationContextAware {
private Contract contract = new Contract.Default();
private ApplicationContext applicationContext;
private FeignClientFactory feignClientFactory;
@Override
public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
throw new UnsupportedOperationException();
}
@Override
public PigSentinelFeign.Builder contract(Contract contract) {
this.contract = contract;
return this;
}
@Override
public Feign internalBuild() {
super.invocationHandlerFactory(new InvocationHandlerFactory() {
@Override
public InvocationHandler create(Target target, Map dispatch) {
// 查找 FeignClient 上的 降级策略
FeignClient feignClient = AnnotationUtils.findAnnotation(target.type(), FeignClient.class);
Class> fallback = feignClient.fallback();
Class> fallbackFactory = feignClient.fallbackFactory();
String beanName = feignClient.contextId();
if (!StringUtils.hasText(beanName)) {
beanName = feignClient.name();
}
Object fallbackInstance;
FallbackFactory> fallbackFactoryInstance;
if (void.class != fallback) {
fallbackInstance = getFromContext(beanName, "fallback", fallback, target.type());
return new PigSentinelInvocationHandler(target, dispatch,
new FallbackFactory.Default(fallbackInstance));
}
if (void.class != fallbackFactory) {
fallbackFactoryInstance = (FallbackFactory>) getFromContext(beanName, "fallbackFactory",
fallbackFactory, FallbackFactory.class);
return new PigSentinelInvocationHandler(target, dispatch, fallbackFactoryInstance);
}
return new PigSentinelInvocationHandler(target, dispatch);
}
private Object getFromContext(String name, String type, Class> fallbackType, Class> targetType) {
Object fallbackInstance = feignClientFactory.getInstance(name, fallbackType);
if (fallbackInstance == null) {
throw new IllegalStateException(String
.format("No %s instance of type %s found for feign client %s", type, fallbackType, name));
}
if (!targetType.isAssignableFrom(fallbackType)) {
throw new IllegalStateException(String.format(
"Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
type, fallbackType, targetType, name));
}
return fallbackInstance;
}
});
super.contract(new SentinelContractHolder(contract));
return super.internalBuild();
}
/**
* private Object getFieldValue(Object instance, String fieldName) { Field field =
* ReflectionUtils.findField(instance.getClass(), fieldName);
* field.setAccessible(true); try { return field.get(instance); } catch
* (IllegalAccessException e) { // ignore } return null; }
**/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
this.feignClientFactory = this.applicationContext.getBean(FeignClientFactory.class);
}
}
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/sentinel/ext/PigSentinelInvocationHandler.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.feign.sentinel.ext;
import com.alibaba.cloud.sentinel.feign.SentinelContractHolder;
import com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.pig4cloud.pig.common.core.util.R;
import feign.Feign;
import feign.InvocationHandlerFactory;
import feign.MethodMetadata;
import feign.Target;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
import java.util.Map;
import static feign.Util.checkNotNull;
/**
* 支持自动降级注入的Sentinel调用处理器,重写{@link SentinelInvocationHandler}
*
* @author lengleng
* @date 2025/05/31
*/
@Slf4j
public class PigSentinelInvocationHandler implements InvocationHandler {
public static final String EQUALS = "equals";
public static final String HASH_CODE = "hashCode";
public static final String TO_STRING = "toString";
private final Target> target;
private final Map dispatch;
private FallbackFactory> fallbackFactory;
private Map fallbackMethodMap;
PigSentinelInvocationHandler(Target> target, Map dispatch,
FallbackFactory> fallbackFactory) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallbackFactory = fallbackFactory;
this.fallbackMethodMap = toFallbackMethod(dispatch);
}
PigSentinelInvocationHandler(Target> target, Map dispatch) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
if (EQUALS.equals(method.getName())) {
try {
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
}
catch (IllegalArgumentException e) {
return false;
}
}
else if (HASH_CODE.equals(method.getName())) {
return hashCode();
}
else if (TO_STRING.equals(method.getName())) {
return toString();
}
Object result;
InvocationHandlerFactory.MethodHandler methodHandler = this.dispatch.get(method);
// only handle by HardCodedTarget
if (target instanceof Target.HardCodedTarget) {
Target.HardCodedTarget> hardCodedTarget = (Target.HardCodedTarget) target;
MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP
.get(hardCodedTarget.type().getName() + Feign.configKey(hardCodedTarget.type(), method));
// resource default is HttpMethod:protocol://url
if (methodMetadata == null) {
result = methodHandler.invoke(args);
}
else {
String resourceName = methodMetadata.template().method().toUpperCase() + ':' + hardCodedTarget.url()
+ methodMetadata.template().path();
Entry entry = null;
try {
ContextUtil.enter(resourceName);
entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
result = methodHandler.invoke(args);
}
catch (Throwable ex) {
// fallback handle
if (!BlockException.isBlockException(ex)) {
Tracer.trace(ex);
}
if (fallbackFactory != null) {
try {
return fallbackMethodMap.get(method).invoke(fallbackFactory.create(ex), args);
}
catch (IllegalAccessException e) {
// shouldn't happen as method is public due to being an
// interface
throw new AssertionError(e);
}
catch (InvocationTargetException e) {
throw new AssertionError(e.getCause());
}
}
else {
// 若是R类型 执行自动降级返回R
if (R.class == method.getReturnType()) {
log.error("feign 服务间调用异常", ex);
return R.failed(ex.getLocalizedMessage());
}
else {
throw ex;
}
}
}
finally {
if (entry != null) {
entry.exit(1, args);
}
ContextUtil.exit();
}
}
}
else {
// other target type using default strategy
result = methodHandler.invoke(args);
}
return result;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SentinelInvocationHandler) {
PigSentinelInvocationHandler other = (PigSentinelInvocationHandler) obj;
return target.equals(other.target);
}
return false;
}
@Override
public int hashCode() {
return target.hashCode();
}
@Override
public String toString() {
return target.toString();
}
static Map toFallbackMethod(Map dispatch) {
Map result = new LinkedHashMap<>();
for (Method method : dispatch.keySet()) {
method.setAccessible(true);
result.put(method, method);
}
return result;
}
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/sentinel/handle/GlobalBizExceptionHandler.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.feign.sentinel.handle;
import com.alibaba.csp.sentinel.Tracer;
import com.pig4cloud.pig.common.core.util.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
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.servlet.resource.NoResourceFoundException;
import java.util.List;
/**
* 全局业务异常处理器,结合Sentinel处理系统异常
*
* 注意:全局异常处理器不能作用在OAuth Server
*
*
* @author lengleng
* @date 2025/05/31
*/
@Slf4j
@Order(10000)
@RestControllerAdvice
@ConditionalOnExpression("!'${security.oauth2.client.clientId}'.isEmpty()")
public class GlobalBizExceptionHandler {
/**
* 全局异常.
* @param e the e
* @return R
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handleGlobalException(Exception e) {
log.error("全局异常信息 ex={}", e.getMessage(), e);
// 业务异常交由 sentinel 记录
Tracer.trace(e);
return R.failed(e.getLocalizedMessage());
}
/**
* 处理业务校验过程中碰到的非法参数异常 该异常基本由{@link org.springframework.util.Assert}抛出
* @param exception 参数校验异常
* @return API返回结果对象包装后的错误输出结果
* @see Assert#hasLength(String, String)
* @see Assert#hasText(String, String)
* @see Assert#isTrue(boolean, String)
* @see Assert#isNull(Object, String)
* @see Assert#notNull(Object, String)
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.OK)
public R handleIllegalArgumentException(IllegalArgumentException exception) {
log.error("非法参数,ex = {}", exception.getMessage(), exception);
return R.failed(exception.getMessage());
}
/**
* AccessDeniedException
* @param e the e
* @return R
*/
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public R handleAccessDeniedException(AccessDeniedException e) {
String msg = SpringSecurityMessageSource.getAccessor()
.getMessage("AbstractAccessDecisionManager.accessDenied", e.getMessage());
log.warn("拒绝授权异常信息 ex={}", msg);
return R.failed(msg);
}
/**
* validation Exception
* @param exception
* @return R
*/
@ExceptionHandler({ MethodArgumentNotValidException.class })
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleBodyValidException(MethodArgumentNotValidException exception) {
List fieldErrors = exception.getBindingResult().getFieldErrors();
log.warn("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage());
return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
}
/**
* validation Exception (以form-data形式传参)
* @param exception
* @return R
*/
@ExceptionHandler({ BindException.class })
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R bindExceptionHandler(BindException exception) {
List fieldErrors = exception.getBindingResult().getFieldErrors();
log.warn("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage());
return R.failed(fieldErrors.get(0).getDefaultMessage());
}
/**
* 保持和低版本请求路径不存在的行为一致
*
* [Spring Boot
* 3.2.0] 404 Not Found behavior #38733
* @param exception
* @return R
*/
@ExceptionHandler({ NoResourceFoundException.class })
@ResponseStatus(HttpStatus.NOT_FOUND)
public R notFoundExceptionHandler(NoResourceFoundException exception) {
log.debug("请求路径 404 {}", exception.getMessage());
return R.failed(exception.getMessage());
}
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/sentinel/handle/PigUrlBlockHandler.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.feign.sentinel.handle;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pig4cloud.pig.common.core.util.R;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
/**
* Sentinel统一降级限流策略处理器
*
* 实现BlockExceptionHandler接口,处理Sentinel限流降级异常
*
* @author lengleng
* @date 2025/05/31
*/
@Slf4j
@RequiredArgsConstructor
public class PigUrlBlockHandler implements BlockExceptionHandler {
private final ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException e)
throws Exception {
log.error("sentinel 降级 资源名称{}", resourceName, e);
response.setContentType(MediaType.APPLICATION_JSON.getType());
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().print(objectMapper.writeValueAsString(R.failed(e.getMessage())));
}
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/sentinel/parser/PigHeaderRequestOriginParser.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.feign.sentinel.parser;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.RequestOriginParser;
import jakarta.servlet.http.HttpServletRequest;
/**
* Sentinel 请求头解析判断实现类,用于从HTTP请求头中获取Allow字段值
*
* @author lengleng
* @date 2025/05/31
*/
public class PigHeaderRequestOriginParser implements RequestOriginParser {
/**
* 请求头获取allow
*/
private static final String ALLOW = "Allow";
/**
* 解析HTTP请求中的来源信息
* @param request HTTP请求对象
* @return 解析出的来源信息
*/
@Override
public String parseOrigin(HttpServletRequest request) {
return request.getHeader(ALLOW);
}
}
================================================
FILE: pig-common/pig-common-feign/src/main/java/org/springframework/cloud/openfeign/PigFeignClientsRegistrar.java
================================================
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package org.springframework.cloud.openfeign;
import lombok.Getter;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.context.annotation.ImportCandidates;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Feign客户端注册器,用于自动配置Feign客户端
*
* @author lengleng
* @date 2025/05/31
* @see ImportBeanDefinitionRegistrar
* @see BeanClassLoaderAware
* @see EnvironmentAware
*/
public class PigFeignClientsRegistrar implements ImportBeanDefinitionRegistrar, BeanClassLoaderAware, EnvironmentAware {
private final static String BASE_URL = "http://127.0.0.1:${server.port}${server.servlet.context-path}";
@Getter
private ClassLoader beanClassLoader;
@Getter
private Environment environment;
/**
* 注册Bean定义
* @param metadata 注解元数据
* @param registry Bean定义注册器
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerFeignClients(registry);
}
/**
* 设置Bean类加载器
* @param classLoader 要设置的类加载器
*/
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
/**
* 注册Feign客户端到Spring容器
* @param registry Bean定义注册器,用于注册Bean定义
*/
private void registerFeignClients(BeanDefinitionRegistry registry) {
List feignClients = new ArrayList<>();
// 支持 springboot 2.7 + 最新版本的配置方式
ImportCandidates.load(FeignClient.class, getBeanClassLoader()).forEach(feignClients::add);
// 如果 spring.factories 里为空
if (feignClients.isEmpty()) {
return;
}
for (String className : feignClients) {
try {
Class> clazz = beanClassLoader.loadClass(className);
AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(clazz,
FeignClient.class);
if (attributes == null) {
continue;
}
// 如果是单体项目自动注入 & url 为空
Boolean isMicro = environment.getProperty("spring.cloud.nacos.discovery.enabled", Boolean.class, true);
// 如果已经存在该 bean,支持原生的 Feign
if (registry.containsBeanDefinition(className) && isMicro) {
continue;
}
registerClientConfiguration(registry, getClientName(attributes), className,
attributes.get("configuration"));
validate(attributes);
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
// 兼容最新版本的 spring-cloud-openfeign,尚未发布
StringBuilder aliasBuilder = new StringBuilder(18);
if (attributes.containsKey("contextId")) {
String contextId = getContextId(attributes);
aliasBuilder.append(contextId);
definition.addPropertyValue("contextId", contextId);
}
else {
aliasBuilder.append(name);
}
definition.addPropertyValue("type", className);
definition.addPropertyValue("dismiss404",
Boolean.parseBoolean(String.valueOf(attributes.get("dismiss404"))));
Object fallbackFactory = attributes.get("fallbackFactory");
if (fallbackFactory != null) {
definition.addPropertyValue("fallbackFactory", fallbackFactory instanceof Class ? fallbackFactory
: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
}
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
// alias
String alias = aliasBuilder.append("FeignClient").toString();
// has a default, won't be null
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
/**
* 验证Feign客户端配置属性
* @param attributes 配置属性Map
*/
private void validate(Map attributes) {
AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes);
// This blows up if an aliased property is overspecified
FeignClientsRegistrar.validateFallback(annotation.getClass("fallback"));
FeignClientsRegistrar.validateFallbackFactory(annotation.getClass("fallbackFactory"));
}
/**
* 从属性Map中获取名称
* @param attributes 属性Map
* @return 解析后的名称
*/
private String getName(Map attributes) {
String name = (String) attributes.get("serviceId");
if (!StringUtils.hasText(name)) {
name = (String) attributes.get("name");
}
if (!StringUtils.hasText(name)) {
name = (String) attributes.get("value");
}
name = resolve(name);
return FeignClientsRegistrar.getName(name);
}
/**
* 获取上下文ID
* @param attributes 属性Map
* @return 解析后的上下文ID,如果contextId为空则返回名称
*/
private String getContextId(Map attributes) {
String contextId = (String) attributes.get("contextId");
if (!StringUtils.hasText(contextId)) {
return getName(attributes);
}
contextId = resolve(contextId);
return FeignClientsRegistrar.getName(contextId);
}
/**
* 解析字符串中的占位符
* @param value 待解析的字符串
* @return 解析后的字符串,若输入为空则返回原值
*/
private String resolve(String value) {
if (StringUtils.hasText(value)) {
return this.environment.resolvePlaceholders(value);
}
return value;
}
/**
* 获取URL地址
* @param attributes 属性集合
* @return 解析后的URL地址,微服务环境下返回null
*/
private String getUrl(Map attributes) {
// 如果是单体项目自动注入 & url 为空
Boolean isMicro = environment.getProperty("spring.cloud.nacos.discovery.enabled", Boolean.class, true);
if (isMicro) {
return null;
}
Object objUrl = attributes.get("url");
String url = "";
if (StringUtils.hasText(objUrl.toString())) {
url = resolve(objUrl.toString());
}
else {
url = resolve(BASE_URL);
}
return FeignClientsRegistrar.getUrl(url);
}
/**
* 获取路径
* @param attributes 属性Map,包含路径信息
* @return 解析后的路径
*/
private String getPath(Map attributes) {
String path = resolve((String) attributes.get("path"));
return FeignClientsRegistrar.getPath(path);
}
/**
* 从客户端信息中获取qualifier字段值
* @param client 客户端信息映射表,可能为null
* @return qualifier字段值,若无有效值则返回null
*/
@Nullable
private String getQualifier(@Nullable Map client) {
if (client == null) {
return null;
}
String qualifier = (String) client.get("qualifier");
if (StringUtils.hasText(qualifier)) {
return qualifier;
}
return null;
}
/**
* 获取客户端名称,依次从contextId、value、name、serviceId字段中获取
* @param client 客户端信息Map,可为null
* @return 客户端名称,可能为null
* @throws IllegalStateException 当无法从client中获取到有效名称时抛出
*/
@Nullable
private String getClientName(@Nullable Map client) {
if (client == null) {
return null;
}
String value = (String) client.get("contextId");
if (!StringUtils.hasText(value)) {
value = (String) client.get("value");
}
if (!StringUtils.hasText(value)) {
value = (String) client.get("name");
}
if (!StringUtils.hasText(value)) {
value = (String) client.get("serviceId");
}
if (StringUtils.hasText(value)) {
return value;
}
throw new IllegalStateException(
"Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName());
}
/**
* 注册客户端配置到BeanDefinitionRegistry
* @param registry Bean定义注册器
* @param name 客户端名称
* @param className 类名
* @param configuration 配置对象
*/
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object className,
Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(className);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
/**
* 设置环境变量
* @param environment 要设置的环境变量对象
*/
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
================================================
FILE: pig-common/pig-common-feign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
================================================
com.pig4cloud.pig.common.feign.PigFeignAutoConfiguration
com.pig4cloud.pig.common.feign.sentinel.SentinelAutoConfiguration
com.pig4cloud.pig.common.feign.sentinel.handle.GlobalBizExceptionHandler
================================================
FILE: pig-common/pig-common-log/pom.xml
================================================
4.0.0
com.pig4cloud
pig-common
${revision}
pig-common-log
jar
pig 日志服务
com.pig4cloud
pig-common-core
cn.hutool
hutool-extra
cn.hutool
hutool-http
com.pig4cloud
pig-upms-api
org.springframework.security
spring-security-core
org.springframework.security
spring-security-oauth2-core
================================================
FILE: pig-common/pig-common-log/src/main/java/com/pig4cloud/pig/common/log/LogAutoConfiguration.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.log;
import com.pig4cloud.pig.admin.api.feign.RemoteLogService;
import com.pig4cloud.pig.common.log.aspect.SysLogAspect;
import com.pig4cloud.pig.common.log.config.PigLogProperties;
import com.pig4cloud.pig.common.log.event.SysLogListener;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* 日志自动配置类,用于配置系统日志相关功能
*
* @author lengleng
* @date 2025/05/31
*/
@EnableAsync
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PigLogProperties.class)
@ConditionalOnProperty(value = "security.log.enabled", matchIfMissing = true)
public class LogAutoConfiguration {
/**
* 创建并返回SysLogListener的Bean实例
* @param logProperties 日志属性配置
* @param remoteLogService 远程日志服务
* @return SysLogListener实例
*/
@Bean
public SysLogListener sysLogListener(PigLogProperties logProperties, RemoteLogService remoteLogService) {
return new SysLogListener(remoteLogService, logProperties);
}
/**
* 创建并返回SysLogAspect的Bean实例
* @return SysLogAspect实例
*/
@Bean
public SysLogAspect sysLogAspect() {
return new SysLogAspect();
}
}
================================================
FILE: pig-common/pig-common-log/src/main/java/com/pig4cloud/pig/common/log/annotation/SysLog.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.log.annotation;
import java.lang.annotation.*;
/**
* 系统日志注解:用于标记需要记录操作日志的方法
*
* @author lengleng
* @date 2025/05/31
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
/**
* 描述
* @return {String}
*/
String value() default "";
/**
* spel 表达式
* @return 日志描述
*/
String expression() default "";
}
================================================
FILE: pig-common/pig-common-log/src/main/java/com/pig4cloud/pig/common/log/aspect/SysLogAspect.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.log.aspect;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.log.event.SysLogEvent;
import com.pig4cloud.pig.common.log.event.SysLogEventSource;
import com.pig4cloud.pig.common.log.util.LogTypeEnum;
import com.pig4cloud.pig.common.log.util.SysLogUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.EvaluationContext;
/**
* 系统日志切面类,通过Spring AOP实现操作日志的异步记录
*
* @author lengleng
* @date 2025/05/31
*/
@Aspect
@Slf4j
@RequiredArgsConstructor
public class SysLogAspect {
/**
* 环绕通知方法,用于处理系统日志记录
* @param point 连接点对象
* @param sysLog 系统日志注解
* @return 目标方法执行结果
* @throws Throwable 目标方法执行可能抛出的异常
*/
@Around("@annotation(sysLog)")
@SneakyThrows
public Object around(ProceedingJoinPoint point, com.pig4cloud.pig.common.log.annotation.SysLog sysLog) {
String strClassName = point.getTarget().getClass().getName();
String strMethodName = point.getSignature().getName();
log.debug("[类名]:{},[方法]:{}", strClassName, strMethodName);
String value = sysLog.value();
String expression = sysLog.expression();
// 当前表达式存在 SPEL,会覆盖 value 的值
if (StrUtil.isNotBlank(expression)) {
// 解析SPEL
MethodSignature signature = (MethodSignature) point.getSignature();
EvaluationContext context = SysLogUtils.getContext(point.getArgs(), signature.getMethod());
try {
value = SysLogUtils.getValue(context, expression, String.class);
}
catch (Exception e) {
// SPEL 表达式异常,获取 value 的值
log.error("@SysLog 解析SPEL {} 异常", expression);
}
}
SysLogEventSource logVo = SysLogUtils.getSysLog();
logVo.setTitle(value);
// 获取请求body参数
if (StrUtil.isBlank(logVo.getParams())) {
logVo.setBody(point.getArgs());
}
// 发送异步日志事件
Long startTime = System.currentTimeMillis();
Object obj;
try {
obj = point.proceed();
}
catch (Exception e) {
logVo.setLogType(LogTypeEnum.ERROR.getType());
logVo.setException(e.getMessage());
throw e;
}
finally {
Long endTime = System.currentTimeMillis();
logVo.setTime(endTime - startTime);
SpringContextHolder.publishEvent(new SysLogEvent(logVo));
}
return obj;
}
}
================================================
FILE: pig-common/pig-common-log/src/main/java/com/pig4cloud/pig/common/log/config/PigLogProperties.java
================================================
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
*
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.gnu.org/licenses/lgpl.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.log.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* 日志配置类
*
* @author lengleng
* @date 2025/05/31
*/
@Getter
@Setter
@ConfigurationProperties(PigLogProperties.PREFIX)
public class PigLogProperties {
public static final String PREFIX = "security.log";
/**
* 开启日志记录
*/
private boolean enabled = true;
/**
* 放行字段,password,mobile,idcard,phone
*/
@Value("${security.log.exclude-fields:password,mobile,idcard,phone}")
private List excludeFields;
/**
* 请求报文最大存储长度
*/
private Integer maxLength = 2000;
}
================================================
FILE: pig-common/pig-common-log/src/main/java/com/pig4cloud/pig/common/log/event/SysLogEvent.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.log.event;
import java.io.Serial;
import org.springframework.context.ApplicationEvent;
import com.pig4cloud.pig.admin.api.entity.SysLog;
/**
* 系统日志事件类
*
* @author lengleng
* @date 2025/05/31
*/
public class SysLogEvent extends ApplicationEvent {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造方法,根据源SysLog对象创建SysLogEvent
* @param source 源SysLog对象
*/
public SysLogEvent(SysLog source) {
super(source);
}
}
================================================
FILE: pig-common/pig-common-log/src/main/java/com/pig4cloud/pig/common/log/event/SysLogEventSource.java
================================================
package com.pig4cloud.pig.common.log.event;
import java.io.Serial;
import com.pig4cloud.pig.admin.api.entity.SysLog;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 系统日志事件源类,继承自SysLog
*
* @author lengleng
* @date 2025/05/31
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class SysLogEventSource extends SysLog {
@Serial
private static final long serialVersionUID = 1L;
/**
* 参数重写成object
*/
private Object body;
}
================================================
FILE: pig-common/pig-common-log/src/main/java/com/pig4cloud/pig/common/log/event/SysLogListener.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.log.event;
import java.util.Objects;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.pig4cloud.pig.admin.api.entity.SysLog;
import com.pig4cloud.pig.admin.api.feign.RemoteLogService;
import com.pig4cloud.pig.common.core.jackson.PigJavaTimeModule;
import com.pig4cloud.pig.common.log.config.PigLogProperties;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
/**
* 系统日志监听器:异步处理系统日志事件
*
* @author lengleng
* @date 2025/05/31
*/
@RequiredArgsConstructor
public class SysLogListener implements InitializingBean {
// new 一个 避免日志脱敏策略影响全局ObjectMapper
private final static ObjectMapper objectMapper = new ObjectMapper();
private final RemoteLogService remoteLogService;
private final PigLogProperties logProperties;
/**
* 异步保存系统日志
* @param event 系统日志事件
*/
@SneakyThrows
@Async
@Order
@EventListener(SysLogEvent.class)
public void saveSysLog(SysLogEvent event) {
SysLogEventSource source = (SysLogEventSource) event.getSource();
SysLog sysLog = new SysLog();
BeanUtils.copyProperties(source, sysLog);
// json 格式刷参数放在异步中处理,提升性能
if (Objects.nonNull(source.getBody())) {
String params = objectMapper.writeValueAsString(source.getBody());
sysLog.setParams(StrUtil.subPre(params, logProperties.getMaxLength()));
}
remoteLogService.saveLog(sysLog);
}
@Override
public void afterPropertiesSet() {
objectMapper.addMixIn(Object.class, PropertyFilterMixIn.class);
String[] ignorableFieldNames = logProperties.getExcludeFields().toArray(new String[0]);
FilterProvider filters = new SimpleFilterProvider().addFilter("filter properties by name",
SimpleBeanPropertyFilter.serializeAllExcept(ignorableFieldNames));
objectMapper.setFilterProvider(filters);
objectMapper.registerModule(new PigJavaTimeModule());
}
/**
* 属性过滤混合类:用于通过名称过滤属性
*
* @author lengleng
* @date 2025/05/31
*/
@JsonFilter("filter properties by name")
class PropertyFilterMixIn {
}
}
================================================
FILE: pig-common/pig-common-log/src/main/java/com/pig4cloud/pig/common/log/init/ApplicationLoggerInitializer.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.log.init;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* 应用日志初始化类:通过环境变量注入 logging.file 自动维护 Spring Boot Admin Logger Viewer
*
* @author lengleng
* @date 2025/05/31
*/
public class ApplicationLoggerInitializer implements EnvironmentPostProcessor, Ordered {
/**
* 后处理环境配置,设置日志路径和相关系统属性
* @param environment 可配置的环境对象
* @param application Spring应用对象
*/
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
String appName = environment.getProperty("spring.application.name");
String logBase = environment.getProperty("LOGGING_PATH", "logs");
// spring boot admin 直接加载日志
System.setProperty("logging.file.name", String.format("%s/%s/debug.log", logBase, appName));
// 避免各种依赖的地方组件造成 BeanPostProcessorChecker 警告
System.setProperty("logging.level.org.springframework.context.support.PostProcessorRegistrationDelegate",
"ERROR");
// 避免 sentinel 1.8.4+ 心跳日志过大
System.setProperty("csp.sentinel.log.level", "OFF");
// 避免 sentinel 健康检查 server
System.setProperty("management.health.sentinel.enabled", "false");
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
================================================
FILE: pig-common/pig-common-log/src/main/java/com/pig4cloud/pig/common/log/util/LogTypeEnum.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.log.util;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 日志类型枚举
*
* @author lengleng
* @date 2025/05/31
*/
@Getter
@RequiredArgsConstructor
public enum LogTypeEnum {
/**
* 正常日志类型
*/
NORMAL("0", "正常日志"),
/**
* 错误日志类型
*/
ERROR("9", "错误日志");
/**
* 类型
*/
private final String type;
/**
* 描述
*/
private final String description;
}
================================================
FILE: pig-common/pig-common-log/src/main/java/com/pig4cloud/pig/common/log/util/SysLogUtils.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.log.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpUtil;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.log.config.PigLogProperties;
import com.pig4cloud.pig.common.log.event.SysLogEventSource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.experimental.UtilityClass;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 系统日志工具类
*
* @author lengleng
* @date 2025/05/31
*/
@UtilityClass
public class SysLogUtils {
/**
* 获取系统日志事件源
* @return 系统日志事件源对象
*/
public SysLogEventSource getSysLog() {
HttpServletRequest request = ((ServletRequestAttributes) Objects
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
SysLogEventSource sysLog = new SysLogEventSource();
sysLog.setLogType(LogTypeEnum.NORMAL.getType());
sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
sysLog.setMethod(request.getMethod());
sysLog.setRemoteAddr(JakartaServletUtil.getClientIP(request));
sysLog.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
sysLog.setCreateBy(getUsername());
sysLog.setServiceId(SpringUtil.getProperty("spring.application.name"));
// get 参数脱敏
PigLogProperties logProperties = SpringContextHolder.getBean(PigLogProperties.class);
Map paramsMap = MapUtil.removeAny(new HashMap<>(request.getParameterMap()),
ArrayUtil.toArray(logProperties.getExcludeFields(), String.class));
sysLog.setParams(HttpUtil.toParams(paramsMap));
return sysLog;
}
/**
* 获取用户名称
* @return username
*/
private String getUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return null;
}
return authentication.getName();
}
/**
* 获取spel 定义的参数值
* @param context 参数容器
* @param key key
* @param clazz 需要返回的类型
* @param 返回泛型
* @return 参数值
*/
public T getValue(EvaluationContext context, String key, Class clazz) {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expression = spelExpressionParser.parseExpression(key);
return expression.getValue(context, clazz);
}
/**
* 获取参数容器
* @param arguments 方法的参数列表
* @param signatureMethod 被执行的方法体
* @return 装载参数的容器
*/
public EvaluationContext getContext(Object[] arguments, Method signatureMethod) {
String[] parameterNames = new StandardReflectionParameterNameDiscoverer().getParameterNames(signatureMethod);
EvaluationContext context = new StandardEvaluationContext();
if (parameterNames == null) {
return context;
}
for (int i = 0; i < arguments.length; i++) {
context.setVariable(parameterNames[i], arguments[i]);
}
return context;
}
}
================================================
FILE: pig-common/pig-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
================================================
com.pig4cloud.pig.common.log.LogAutoConfiguration
================================================
FILE: pig-common/pig-common-log/src/main/resources/META-INF/spring-configuration-metadata.json
================================================
{
"groups": [
{
"name": "security.log",
"type": "com.pig4cloud.pig.common.log.config.PigLogProperties",
"sourceType": "com.pig4cloud.pig.common.log.config.PigLogProperties"
}
],
"properties": [
{
"name": "security.log.enabled",
"type": "java.lang.Boolean",
"description": "开启日志记录",
"sourceType": "com.pig4cloud.pig.common.log.config.PigLogProperties"
},
{
"name": "security.log.exclude-fields",
"type": "java.util.List",
"description": "放行字段,password,mobile,idcard,phone",
"sourceType": "com.pig4cloud.pig.common.log.config.PigLogProperties"
},
{
"name": "security.log.max-length",
"type": "java.lang.Integer",
"description": "请求报文最大存储长度",
"sourceType": "com.pig4cloud.pig.common.log.config.PigLogProperties"
}
],
"hints": []
}
================================================
FILE: pig-common/pig-common-log/src/main/resources/META-INF/spring.factories
================================================
org.springframework.boot.env.EnvironmentPostProcessor=\
com.pig4cloud.pig.common.log.init.ApplicationLoggerInitializer
================================================
FILE: pig-common/pig-common-mybatis/pom.xml
================================================
4.0.0
com.pig4cloud
pig-common
${revision}
pig-common-mybatis
jar
pig mybatis 封装
cn.hutool
hutool-core
com.baomidou
mybatis-plus-spring
com.baomidou
mybatis-plus-jsqlparser
io.swagger.core.v3
swagger-annotations-jakarta
jakarta.servlet
jakarta.servlet-api
provided
org.springframework
spring-webmvc
true
org.springframework.security
spring-security-core
true
com.pig4cloud
pig-common-core
================================================
FILE: pig-common/pig-common-mybatis/src/main/java/com/pig4cloud/pig/common/mybatis/MybatisAutoConfiguration.java
================================================
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pig.common.mybatis;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.pig4cloud.pig.common.mybatis.config.MybatisPlusMetaObjectHandler;
import com.pig4cloud.pig.common.mybatis.plugins.PigPaginationInnerInterceptor;
import com.pig4cloud.pig.common.mybatis.resolver.SqlFilterArgumentResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* MyBatis Plus 统一自动配置类
*
* 提供SQL过滤器、分页插件及审计字段自动填充等配置
*
* @author lengleng
* @date 2025/05/31
*/
@Configuration(proxyBeanMethods = false)
public class MybatisAutoConfiguration implements WebMvcConfigurer {
/**
* 添加SQL过滤器参数解析器,避免SQL注入
* @param argumentResolvers 方法参数解析器列表
*/
@Override
public void addArgumentResolvers(List argumentResolvers) {
argumentResolvers.add(new SqlFilterArgumentResolver());
}
/**
* 创建并配置MybatisPlus分页拦截器
* @return 配置好的MybatisPlus拦截器实例
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PigPaginationInnerInterceptor());
return interceptor;
}
/**
* 创建并返回MybatisPlusMetaObjectHandler实例,用于审计字段自动填充
* @return MybatisPlusMetaObjectHandler实例
*/
@Bean
public MybatisPlusMetaObjectHandler mybatisPlusMetaObjectHandler() {
return new MybatisPlusMetaObjectHandler();
}
}
================================================
FILE: pig-common/pig-common-mybatis/src/main/java/com/pig4cloud/pig/common/mybatis/base/BaseEntity.java
================================================
package com.pig4cloud.pig.common.mybatis.base;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
/**
* 基础实体抽象类,包含通用实体字段
*
* @author lengleng
* @date 2025/05/31
*/
@Getter
@Setter
public class BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 创建者
*/
@Schema(description = "创建人")
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 创建时间
*/
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新者
*/
@Schema(description = "更新人")
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 更新时间
*/
@Schema(description = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
================================================
FILE: pig-common/pig-common-mybatis/src/main/java/com/pig4cloud/pig/common/mybatis/config/MybatisPlusMetaObjectHandler.java
================================================
package com.pig4cloud.pig.common.mybatis.config;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.ClassUtils;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.util.Optional;
/**
* MybatisPlus 自动填充处理器,用于实体类字段的自动填充
*
* @author lengleng
* @date 2025/05/31
*/
@Slf4j
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
/**
* 插入时自动填充字段
* @param metaObject 元对象,用于操作实体类属性
*/
@Override
public void insertFill(MetaObject metaObject) {
log.debug("mybatis plus start insert fill ....");
LocalDateTime now = LocalDateTime.now();
fillValIfNullByName("createTime", now, metaObject, true);
fillValIfNullByName("updateTime", now, metaObject, true);
fillValIfNullByName("createBy", getUserName(), metaObject, true);
fillValIfNullByName("updateBy", getUserName(), metaObject, true);
// 删除标记自动填充
fillValIfNullByName("delFlag", CommonConstants.STATUS_NORMAL, metaObject, true);
}
/**
* 更新时自动填充字段
* @param metaObject 元对象
*/
@Override
public void updateFill(MetaObject metaObject) {
log.debug("mybatis plus start update fill ....");
fillValIfNullByName("updateTime", LocalDateTime.now(), metaObject, true);
fillValIfNullByName("updateBy", getUserName(), metaObject, true);
}
/**
* 填充值,先判断是否有手动设置,优先手动设置的值,例如:job必须手动设置
* @param fieldName 属性名
* @param fieldVal 属性值
* @param metaObject MetaObject
* @param isCover 是否覆盖原有值,避免更新操作手动入参
*/
private static void fillValIfNullByName(String fieldName, Object fieldVal, MetaObject metaObject, boolean isCover) {
// 0. 如果填充值为空
if (fieldVal == null) {
return;
}
// 1. 没有 set 方法
if (!metaObject.hasSetter(fieldName)) {
return;
}
// 2. 如果用户有手动设置的值
Object userSetValue = metaObject.getValue(fieldName);
String setValueStr = StrUtil.str(userSetValue, Charset.defaultCharset());
if (StrUtil.isNotBlank(setValueStr) && !isCover) {
return;
}
// 3. field 类型相同时设置
Class> getterType = metaObject.getGetterType(fieldName);
if (ClassUtils.isAssignableValue(getterType, fieldVal)) {
metaObject.setValue(fieldName, fieldVal);
}
}
/**
* 获取 spring security 当前的用户名
* @return 当前用户名
*/
private String getUserName() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 匿名接口直接返回
if (authentication instanceof AnonymousAuthenticationToken) {
return null;
}
if (Optional.ofNullable(authentication).isPresent()) {
return authentication.getName();
}
return null;
}
}
================================================
FILE: pig-common/pig-common-mybatis/src/main/java/com/pig4cloud/pig/common/mybatis/handler/JsonLongArrayTypeHandler.java
================================================
package com.pig4cloud.pig.common.mybatis.handler;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import lombok.SneakyThrows;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* MyBatis 长整型数组与字符串类型转换处理器
*
* 实现数据库VARCHAR类型与Java Long数组类型的相互转换
*
* @author lengleng
* @date 2025/05/31
* @see MappedTypes 映射的Java类型为Long[]
* @see MappedJdbcTypes 映射的JDBC类型为VARCHAR
*/
@MappedTypes(value = { Long[].class })
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class JsonLongArrayTypeHandler extends BaseTypeHandler {
/**
* 设置非空参数到PreparedStatement中
* @param ps PreparedStatement对象
* @param i 参数位置
* @param parameter 长整型数组参数
* @param jdbcType JDBC类型
* @throws SQLException 数据库操作异常
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Long[] parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, ArrayUtil.join(parameter, StrUtil.COMMA));
}
/**
* 从ResultSet中获取指定列名的长整型数组结果
* @param rs 结果集
* @param columnName 列名
* @return 转换后的长整型数组,可能为null
* @throws SQLException 数据库访问错误时抛出
*/
@Override
@SneakyThrows
public Long[] getNullableResult(ResultSet rs, String columnName) {
String reString = rs.getString(columnName);
return Convert.toLongArray(reString);
}
/**
* 从ResultSet中获取指定列的长整型数组
* @param rs 结果集
* @param columnIndex 列索引
* @return 长整型数组,可能为null
* @throws SQLException 数据库访问错误时抛出
*/
@Override
@SneakyThrows
public Long[] getNullableResult(ResultSet rs, int columnIndex) {
String reString = rs.getString(columnIndex);
return Convert.toLongArray(reString);
}
/**
* 从CallableStatement中获取指定列的长整型数组
* @param cs CallableStatement对象
* @param columnIndex 列索引
* @return 转换后的长整型数组,可能为null
* @throws Exception 数据库访问出错或转换异常时抛出
*/
@Override
@SneakyThrows
public Long[] getNullableResult(CallableStatement cs, int columnIndex) {
String reString = cs.getString(columnIndex);
return Convert.toLongArray(reString);
}
}
================================================
FILE: pig-common/pig-common-mybatis/src/main/java/com/pig4cloud/pig/common/mybatis/handler/JsonStringArrayTypeHandler.java
================================================
package com.pig4cloud.pig.common.mybatis.handler;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import lombok.SneakyThrows;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* MyBatis 字符串数组类型处理器,用于数据库VARCHAR类型与Java字符串数组的相互转换
*
* @author lengleng
* @date 2025/05/31
* @see MappedTypes 指定处理的Java类型
* @see MappedJdbcTypes 指定处理的JDBC类型
*/
@MappedTypes(value = { String[].class })
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class JsonStringArrayTypeHandler extends BaseTypeHandler {
/**
* 设置非空参数到PreparedStatement
* @param ps PreparedStatement对象
* @param i 参数位置
* @param parameter 字符串数组参数
* @param jdbcType JDBC类型
* @throws SQLException 数据库操作异常
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String[] parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, ArrayUtil.join(parameter, StrUtil.COMMA));
}
/**
* 从ResultSet中获取指定列名的字符串数组结果,允许为null
* @param rs 结果集
* @param columnName 列名
* @return 转换后的字符串数组,可能为null
* @throws SQLException 数据库访问错误时抛出
*/
@Override
@SneakyThrows
public String[] getNullableResult(ResultSet rs, String columnName) {
String reString = rs.getString(columnName);
return Convert.toStrArray(reString);
}
/**
* 从ResultSet中获取指定列的可空字符串数组结果
* @param rs 结果集
* @param columnIndex 列索引
* @return 转换后的字符串数组,可能为null
* @throws SQLException 数据库访问错误时抛出
*/
@Override
@SneakyThrows
public String[] getNullableResult(ResultSet rs, int columnIndex) {
String reString = rs.getString(columnIndex);
return Convert.toStrArray(reString);
}
/**
* 从CallableStatement中获取指定列的可空字符串结果并转换为字符串数组
* @param cs CallableStatement对象
* @param columnIndex 列索引
* @return 转换后的字符串数组
* @throws Exception 如果操作过程中发生错误
*/
@Override
@SneakyThrows
public String[] getNullableResult(CallableStatement cs, int columnIndex) {
String reString = cs.getString(columnIndex);
return Convert.toStrArray(reString);
}
}
================================================
FILE: pig-common/pig-common-mybatis/src/main/java/com/pig4cloud/pig/common/mybatis/plugins/PigPaginationInnerInterceptor.java
================================================
package com.pig4cloud.pig.common.mybatis.plugins;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ParameterUtils;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.dialects.IDialect;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 分页拦截器实现类,用于处理分页查询逻辑
*
* 当分页大小小于0时自动设置为0,防止全表查询
*
* @author lengleng
* @date 2025/05/31
* @since 2021年10月11日
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class PigPaginationInnerInterceptor extends PaginationInnerInterceptor {
/**
* 数据库类型
*
* 查看 {@link #findIDialect(Executor)} 逻辑
*/
private DbType dbType;
/**
* 方言实现类
*
* 查看 {@link #findIDialect(Executor)} 逻辑
*/
private IDialect dialect;
public PigPaginationInnerInterceptor(DbType dbType) {
this.dbType = dbType;
}
public PigPaginationInnerInterceptor(IDialect dialect) {
this.dialect = dialect;
}
/**
* 在执行查询前处理分页参数
* @param executor 执行器
* @param ms 映射语句
* @param parameter 参数对象
* @param rowBounds 行边界
* @param resultHandler 结果处理器
* @param boundSql 绑定SQL
*/
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) {
IPage> page = ParameterUtils.findPage(parameter).orElse(null);
// size 小于 0 直接设置为 0 , 即不查询任何数据
if (null != page && page.getSize() < 0) {
page.setSize(0);
}
super.beforeQuery(executor, ms, page, rowBounds, resultHandler, boundSql);
}
}
================================================
FILE: pig-common/pig-common-mybatis/src/main/java/com/pig4cloud/pig/common/mybatis/resolver/SqlFilterArgumentResolver.java
================================================
/*
*
* * Copyright (c) 2019-2020, 冷冷 (wangiegie@gmail.com).
* *
* * Licensed under the GNU Lesser General Public License 3.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * https://www.gnu.org/licenses/lgpl.html
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package com.pig4cloud.pig.common.mybatis.resolver;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
* Mybatis Plus Order By SQL注入问题解决类
*
* @author lengleng
* @date 2019-06-24
*/
public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 判断Controller方法参数是否为Page类型
* @param parameter 方法参数
* @return 如果参数类型是Page则返回true,否则返回false
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Page.class);
}
/**
* 解析分页参数并构建Page对象
* @param parameter 方法参数信息
* @param mavContainer 模型和视图容器
* @param webRequest web请求对象
* @param binderFactory 数据绑定工厂
* @return 包含分页和排序信息的Page对象
* @throws NumberFormatException 当分页参数转换失败时抛出
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
String[] ascs = request.getParameterValues("ascs");
String[] descs = request.getParameterValues("descs");
String current = request.getParameter("current");
String size = request.getParameter("size");
Page> page = new Page<>();
if (StrUtil.isNotBlank(current)) {
page.setCurrent(Convert.toLong(current, 0L));
}
if (StrUtil.isNotBlank(size)) {
page.setSize(Convert.toLong(size, 10L));
}
List orderItemList = new ArrayList<>();
Optional.ofNullable(ascs)
.ifPresent(s -> orderItemList
.addAll(Arrays.stream(s).filter(asc -> !SqlInjectionUtils.check(asc)).map(OrderItem::asc).toList()));
Optional.ofNullable(descs)
.ifPresent(s -> orderItemList
.addAll(Arrays.stream(s).filter(desc -> !SqlInjectionUtils.check(desc)).map(OrderItem::desc).toList()));
page.addOrder(orderItemList);
return page;
}
}
================================================
FILE: pig-common/pig-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
================================================
com.pig4cloud.pig.common.mybatis.MybatisAutoConfiguration
================================================
FILE: pig-common/pig-common-oss/pom.xml
================================================
4.0.0