Merge branch 'dev'

This commit is contained in:
jiojio 2021-09-23 10:46:04 +08:00
commit dae419582f
69 changed files with 2652 additions and 198 deletions

View File

@ -13,37 +13,51 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
- name: mvn clean install
run: mvn clean install
- name: mvn spring-javaformat:validate
run: mvn spring-javaformat:validate
- name: mvn spring-javaformat:validate
run: mvn spring-javaformat:validate
- name: mvn clean install
run: mvn clean install
- name: success
if: ${{ success() }}
uses: fifsky/dingtalk-action@master
with:
url: ${{ secrets.DINGTALK_WEBHOOK}}
type: markdown
content: |
# 💯👨‍💻 Success 🎉🎉🎉
> Github Action: https://github.com/pig-mesh/pig success
> ^_^ from github action message
- name: Start containers
run: docker-compose build && docker-compose up -d
- name: failure
if: ${{ failure() }}
uses: fifsky/dingtalk-action@master
with:
url: ${{ secrets.DINGTALK_WEBHOOK}}
type: markdown
content: |
# 💤🤷‍♀️ failure 🙅‍♂️💣
> Github Action: https://github.com/pig-mesh/pig failure
> (⋟﹏⋞) from github action message
- name: success
if: success() && 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: |
# 💯👨‍💻 Success 🎉🎉🎉 [pig-mesh/pig](https://github.com/pig-mesh/pig)
> Github Action: https://github.com/pig-mesh/pig success
> ^_^ from github action message
- 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
- uses: shaowenchen/debugger-action@v2
if: github.repository == 'pig-mesh/pig'
name: debugger
continue-on-error: true
with:
frp_server_addr: ${{ secrets.FRP_SERVER_ADDR }}
frp_server_port: ${{ secrets.FRP_SERVER_PORT }}
frp_token: ${{ secrets.FRP_TOKEN }}
ssh_port: ${{ secrets.SSH_PORT }}

View File

@ -273,8 +273,8 @@ INSERT INTO `sys_oauth_client_details` VALUES ('app', NULL, 'app', 'server', 'pa
INSERT INTO `sys_oauth_client_details` VALUES ('ASD', '', 'ASD', 'ASD', 'ASDddddxxxxxxxxxxxx', '', '', NULL, NULL, '', 'false', '2021-08-09 14:19:21', '2021-08-09 14:35:29', 'admin', 'admin');
INSERT INTO `sys_oauth_client_details` VALUES ('daemon', NULL, 'daemon', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('gen', NULL, 'gen', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('pig', NULL, 'pig', 'server', 'password,refresh_token,authorization_code,client_credentials', 'http://localhost:4040/sso1/login,http://localhost:4041/sso1/login', NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('test', NULL, 'test', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('pig', NULL, 'pig', 'server', 'password,phone,refresh_token,authorization_code,client_credentials', 'http://localhost:4040/sso1/login,http://localhost:4041/sso1/login', NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('test', NULL, 'test', 'server', 'password,phone,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('zxc', '', 'zxc', 'zxcxzc', 'cxz', 'cxz', 'zcxzxcxcxzccxz', NULL, NULL, 'zxc', 'true', '2021-08-09 14:37:45', '2021-08-09 14:37:55', 'admin', 'admin');
COMMIT;

View File

@ -41,7 +41,7 @@ INSERT INTO `config_info` VALUES (2, 'pig-auth-dev.yml', 'DEFAULT_GROUP', '# 数
INSERT INTO `config_info` VALUES (3, 'pig-codegen-dev.yml', 'DEFAULT_GROUP', '## spring security 配置\nsecurity:\n oauth2:\n client:\n client-id: ENC(27v1agvAug87ANOVnbKdsw==)\n client-secret: ENC(VbnkopxrwgbFVKp+UxJ2pg==)\n scope: server\n\n# 数据源配置\nspring:\n datasource:\n type: com.zaxxer.hikari.HikariDataSource\n driver-class-name: com.mysql.cj.jdbc.Driver\n username: root\n password: root\n url: jdbc:mysql://pig-mysql:3306/pig_codegen?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai\n resources:\n static-locations: classpath:/static/,classpath:/views/\n\n# 直接放行URL\nignore:\n urls:\n - /v2/api-docs\n - /actuator/**\n', 'abc702838b34d11b46e96143ccd9f367', '2019-11-29 16:32:12', '2019-11-29 16:32:12', NULL, '127.0.0.1', '', '', '代码生成配置', NULL, NULL, 'yaml', NULL);
INSERT INTO `config_info` VALUES (4, 'pig-gateway-dev.yml', 'DEFAULT_GROUP', 'spring:\n cloud:\n gateway:\n locator:\n enabled: true\n routes:\n # 认证中心\n - id: pig-auth\n uri: lb://pig-auth\n predicates:\n - Path=/auth/**\n filters:\n # 验证码处理\n - ValidateCodeGatewayFilter\n # 前端密码解密\n - PasswordDecoderFilter\n #UPMS 模块\n - id: pig-upms-biz\n uri: lb://pig-upms-biz\n predicates:\n - Path=/admin/**\n filters:\n # 限流配置\n - name: RequestRateLimiter\n args:\n key-resolver: \'#{@remoteAddrKeyResolver}\'\n redis-rate-limiter.replenishRate: 100\n redis-rate-limiter.burstCapacity: 200\n # 代码生成模块\n - id: pig-codegen\n uri: lb://pig-codegen\n predicates:\n - Path=/gen/**\n\n\ngateway:\n encode-key: \'thanks,pig4cloud\'\n ignore-clients:\n - test\n\nswagger:\n ignore-providers:\n - pig-auth\n - pig-codegen\n', '5cd71b235930c78e700819b944a14446', '2019-11-29 16:32:42', '2020-10-09 17:10:45', NULL, '0:0:0:0:0:0:0:1', '', '', '网关配置', '', '', 'yaml', '');
INSERT INTO `config_info` VALUES (5, 'pig-monitor-dev.yml', 'DEFAULT_GROUP', 'spring:\n # 安全配置\n security:\n user:\n name: ENC(8Hk2ILNJM8UTOuW/Xi75qg==) # pig\n password: ENC(o6cuPFfUevmTbkmBnE67Ow====) # pig\n', '85509c6f8c67c364dc78301896274f26', '2019-11-29 16:33:05', '2019-11-29 16:33:05', NULL, '127.0.0.1', '', '', '监控配置', NULL, NULL, 'yaml', NULL);
INSERT INTO `config_info` VALUES (6, 'pig-upms-biz-dev.yml', 'DEFAULT_GROUP', 'security:\n oauth2:\n client:\n client-id: ENC(imENTO7M8bLO38LFSIxnzw==)\n client-secret: ENC(i3cDFhs26sa2Ucrfz2hnQw==)\n scope: server\n\n# 数据源\nspring:\n datasource:\n type: com.zaxxer.hikari.HikariDataSource\n driver-class-name: com.mysql.cj.jdbc.Driver\n username: root\n password: root\n url: jdbc:mysql://pig-mysql:3306/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai\n\n# 文件上传相关 支持阿里云、华为云、腾讯、minio\noss:\n endpoint: https://play.min.io:9000\n access-key: Q3AM3UQ867SPQQA43P2F\n secret-key: zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG\n bucket-name: test-oss', '5041ac486e18aa0dd0bf624bb83806de', '2019-11-29 16:52:32', '2021-09-11 14:19:55', '', '127.0.0.1', '', '', '统一权限', 'null', 'null', 'yaml', 'null');
INSERT INTO `config_info` VALUES (6, 'pig-upms-biz-dev.yml', 'DEFAULT_GROUP', 'security:\n oauth2:\n client:\n client-id: ENC(imENTO7M8bLO38LFSIxnzw==)\n client-secret: ENC(i3cDFhs26sa2Ucrfz2hnQw==)\n scope: server\n\n# 数据源\nspring:\n datasource:\n type: com.zaxxer.hikari.HikariDataSource\n driver-class-name: com.mysql.cj.jdbc.Driver\n username: root\n password: root\n url: jdbc:mysql://pig-mysql:3306/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai\n\n# 文件上传相关 支持阿里云、华为云、腾讯、minio\noss:\n endpoint: https://play.min.io:9000\n accessKey: Q3AM3UQ867SPQQA43P2F\n secretKey: zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG\n bucket-name: test-oss', '5041ac486e18aa0dd0bf624bb83806de', '2019-11-29 16:52:32', '2021-09-11 14:19:55', '', '127.0.0.1', '', '', '统一权限', 'null', 'null', 'yaml', 'null');
COMMIT;
-- ----------------------------

View File

@ -1,20 +1,15 @@
FROM moxm/java:1.8-full as builder
WORKDIR /build
ARG JAR_FILE=target/pig-auth.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract && rm app.jar
FROM moxm/java:1.8-full
LABEL maintainer="jclazz@outlook.com"
ENV TZ=Asia/Shanghai
ENV JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
WORKDIR pig-auth
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/snapshot-dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/application/ ./
RUN mkdir -p /pig-auth
WORKDIR /pig-auth
ARG JAR_FILE=target/pig-auth.jar
COPY ${JAR_FILE} app.jar
EXPOSE 3000
CMD sleep 60; java $JAVA_OPTS org.springframework.boot.loader.JarLauncher
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java -jar app.jar $JAVA_OPTS

View File

@ -16,6 +16,8 @@
package com.pig4cloud.pig.auth.config;
import com.pig4cloud.pig.auth.converter.CustomAccessTokenConverter;
import com.pig4cloud.pig.common.security.grant.ResourceOwnerPhoneTokenGranter;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.security.component.PigWebResponseExceptionTranslator;
@ -35,11 +37,15 @@ import org.springframework.security.oauth2.config.annotation.web.configuration.A
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@ -63,10 +69,7 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
@Override
@SneakyThrows
public void configure(ClientDetailsServiceConfigurer clients) {
PigClientDetailsService clientDetailsService = new PigClientDetailsService(dataSource);
clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT);
clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT);
clients.withClientDetails(clientDetailsService);
clients.withClientDetails(pigClientDetailsService());
}
@Override
@ -80,7 +83,21 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
.tokenEnhancer(tokenEnhancer()).userDetailsService(userDetailsService)
.authenticationManager(authenticationManager).reuseRefreshTokens(false)
.pathMapping("/oauth/confirm_access", "/token/confirm_access")
.exceptionTranslator(new PigWebResponseExceptionTranslator());
.exceptionTranslator(new PigWebResponseExceptionTranslator())
.accessTokenConverter(new CustomAccessTokenConverter(pigClientDetailsService()));
setTokenGranter(endpoints);
}
private void setTokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
// 获取默认授权类型
TokenGranter tokenGranter = endpoints.getTokenGranter();
ArrayList<TokenGranter> tokenGranters = new ArrayList<>(Arrays.asList(tokenGranter));
ResourceOwnerPhoneTokenGranter resourceOwnerPhoneTokenGranter = new ResourceOwnerPhoneTokenGranter(
authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(),
endpoints.getOAuth2RequestFactory());
tokenGranters.add(resourceOwnerPhoneTokenGranter);
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(tokenGranters);
endpoints.tokenGranter(compositeTokenGranter);
}
@Bean
@ -104,4 +121,12 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
};
}
@Bean
public PigClientDetailsService pigClientDetailsService() {
PigClientDetailsService clientDetailsService = new PigClientDetailsService(dataSource);
clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT);
clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT);
return clientDetailsService;
}
}

View File

@ -16,8 +16,10 @@
package com.pig4cloud.pig.auth.config;
import com.pig4cloud.pig.common.security.grant.PhoneAuthenticationProvider;
import com.pig4cloud.pig.common.security.handler.FormAuthenticationFailureHandler;
import com.pig4cloud.pig.common.security.handler.SsoLogoutSuccessHandler;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -27,6 +29,7 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@ -39,18 +42,31 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
@Primary
@Order(90)
@Configuration
@AllArgsConstructor
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
@Override
@SneakyThrows
protected void configure(HttpSecurity http) {
http.formLogin().loginPage("/token/login").loginProcessingUrl("/token/form")
.failureHandler(authenticationFailureHandler()).and().logout()
http.authenticationProvider(phoneAuthenticationProvider()).formLogin().loginPage("/token/login")
.loginProcessingUrl("/token/form").failureHandler(authenticationFailureHandler()).and().logout()
.logoutSuccessHandler(logoutSuccessHandler()).deleteCookies("JSESSIONID").invalidateHttpSession(true)
.and().authorizeRequests().antMatchers("/token/**", "/actuator/**", "/mobile/**").permitAll()
.anyRequest().authenticated().and().csrf().disable();
}
/**
* 不要直接使用@Bean注入 会导致默认的提供者无法注入DaoAuthenticationProvider
*/
private PhoneAuthenticationProvider phoneAuthenticationProvider() {
PhoneAuthenticationProvider phoneAuthenticationProvider = new PhoneAuthenticationProvider();
phoneAuthenticationProvider.setPasswordEncoder(passwordEncoder());
phoneAuthenticationProvider.setUserDetailsService(userDetailsService);
return phoneAuthenticationProvider;
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/css/**");

View File

@ -0,0 +1,37 @@
package com.pig4cloud.pig.auth.converter;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.security.service.PigClientDetailsService;
import com.pig4cloud.pig.common.security.service.PigUser;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import java.util.Map;
/**
* @author hccake
*/
@RequiredArgsConstructor
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {
final PigClientDetailsService pigClientDetailsService;
@Override
@SuppressWarnings("unchecked")
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
Map<String, Object> response = (Map<String, Object>) super.convertAccessToken(token, authentication);
ClientDetails clientDetails = pigClientDetailsService
.loadClientByClientId(authentication.getOAuth2Request().getClientId());
if (clientDetails != null && clientDetails.getScope().contains("read_data_scope")) {
PigUser principal = (PigUser) authentication.getPrincipal();
response.put(SecurityConstants.DETAILS_USER_DATA_SCOPE, principal.getUserDataScope());
}
return response;
}
}

View File

@ -25,6 +25,9 @@
<fastjson.version>1.2.75</fastjson.version>
<swagger.core.version>1.5.24</swagger.core.version>
<mybatis-plus.version>3.4.3.3</mybatis-plus.version>
<mybatis.version>3.5.7</mybatis.version>
<jsqlparser.version>4.1</jsqlparser.version>
<rocksdbjni.version>5.18.3</rocksdbjni.version>
<nacos.version>2.0.3</nacos.version>
<excel.version>1.0.0</excel.version>
<oss.version>1.0.1</oss.version>
@ -38,6 +41,11 @@
<artifactId>pig-common-core</artifactId>
<version>${pig.common.version}</version>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pig-common-datascope</artifactId>
<version>${pig.common.version}</version>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pig-common-datasource</artifactId>
@ -123,12 +131,22 @@
<artifactId>oss-spring-boot-starter</artifactId>
<version>${oss.version}</version>
</dependency>
<!--mybatis-plus-->
<!--orm 相关-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser.version}</version>
</dependency>
<!--web 模块-->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -57,6 +57,11 @@ public interface SecurityConstants {
*/
String REFRESH_TOKEN = "refresh_token";
/**
* 手机号登录
*/
String PHONE = "phone";
/**
* {bcrypt} 加密的特征码
*/
@ -109,9 +114,19 @@ public interface SecurityConstants {
*/
String DETAILS_LICENSE = "license";
/**
* 用户数据权限信息
*/
String DETAILS_USER_DATA_SCOPE = "user_data_scope";
/**
* 验证码有效期,默认 60秒
*/
long CODE_TIME = 60;
/**
* 验证码长度
*/
String CODE_SIZE = "6";
}

View File

@ -0,0 +1,49 @@
package com.pig4cloud.pig.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 数据权限范围类型
* @author hccake
*/
@Getter
@AllArgsConstructor
public enum DataScopeTypeEnum {
/**
* 查询全部数据
*/
ALL(0),
/**
* 本人
*/
SELF(1),
/**
* 本人及子级
*/
SELF_CHILD_LEVEL(2),
/**
* 本级
*/
LEVEL(3),
/**
* 本级及子级
*/
LEVEL_CHILD_LEVEL(4),
/**
* 自定义
*/
CUSTOM(5);
/**
* 类型
*/
private final Integer type;
}

View File

@ -33,14 +33,9 @@ public enum LoginTypeEnum {
PWD("PWD", "账号密码登录"),
/**
* QQ登录
* 验证码登录
*/
QQ("QQ", "QQ登录"),
/**
* 微信登录
*/
WECHAT("WX", "微信登录");
SMS("SMS", "验证码登录");
/**
* 类型

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>pig-common</artifactId>
<groupId>com.pig4cloud</groupId>
<version>3.3.4</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>pig-common-datascope</artifactId>
<dependencies>
<!-- slf4j日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,36 @@
package com.pigcloud.pig.common.datascope;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import java.util.Collection;
/**
* @author Hccake 2020/9/28
* @version 1.0
*/
public interface DataScope {
/**
* 数据所对应的资源
* @return 资源标识
*/
String getResource();
/**
* 该资源相关的所有表推荐使用 Set 类型 <br/>
* 如需忽略表名大小写判断则可以使用 TreeSet并设置忽略大小写的自定义Comparator <br/>
* eg. new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
* @return tableNames
*/
Collection<String> getTableNames();
/**
* 根据表名和表别名动态生成的 where/or 筛选条件
* @param tableName 表名
* @param tableAlias 表别名可能为空
* @return 数据规则表达式
*/
Expression getExpression(String tableName, Alias tableAlias);
}

View File

@ -0,0 +1,54 @@
package com.pigcloud.pig.common.datascope;
import com.pigcloud.pig.common.datascope.handler.DataPermissionHandler;
import com.pigcloud.pig.common.datascope.handler.DefaultDataPermissionHandler;
import com.pigcloud.pig.common.datascope.interceptor.DataPermissionAnnotationAdvisor;
import com.pigcloud.pig.common.datascope.interceptor.DataPermissionInterceptor;
import com.pigcloud.pig.common.datascope.processor.DataScopeSqlProcessor;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
* @author hccake
*/
@RequiredArgsConstructor
@ConditionalOnBean(DataScope.class)
public class DataScopeAutoConfiguration {
/**
* 数据权限处理器
* @param dataScopeList 需要控制的数据范围集合
* @return DataPermissionHandler
*/
@Bean
@ConditionalOnMissingBean
public DataPermissionHandler dataPermissionHandler(List<DataScope> dataScopeList) {
return new DefaultDataPermissionHandler(dataScopeList);
}
/**
* 数据权限注解 Advisor用于处理数据权限的链式调用关系
* @return DataPermissionAnnotationAdvisor
*/
@Bean
@ConditionalOnMissingBean(DataPermissionAnnotationAdvisor.class)
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
return new DataPermissionAnnotationAdvisor();
}
/**
* mybatis 拦截器用于拦截处理 sql
* @param dataPermissionHandler 数据权限处理器
* @return DataPermissionInterceptor
*/
@Bean
@ConditionalOnMissingBean
public DataPermissionInterceptor dataPermissionInterceptor(DataPermissionHandler dataPermissionHandler) {
return new DataPermissionInterceptor(new DataScopeSqlProcessor(), dataPermissionHandler);
}
}

View File

@ -0,0 +1,35 @@
package com.pigcloud.pig.common.datascope.annotation;
import java.lang.annotation.*;
/**
* 数据权限注解注解在 Mapper类 或者 对应方法上 用于提供该 mapper 对应表所需控制的实体信息
* @author Hccake 2020/9/27
* @version 1.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
/**
* 当前类或方法是否忽略数据权限
* @return boolean 默认返回 false
*/
boolean ignore() default false;
/**
* 仅对指定资源类型进行数据权限控制只在开启情况下有效当该数组有值时exclude不生效
* @see DataPermission#excludeResources
* @return 资源类型数组
*/
String[] includeResources() default {};
/**
* 对指定资源类型跳过数据权限控制只在开启情况下有效当该includeResources有值时exclude不生效
* @see DataPermission#includeResources
* @return 资源类型数组
*/
String[] excludeResources() default {};
}

View File

@ -0,0 +1,35 @@
package com.pigcloud.pig.common.datascope.handler;
import com.pigcloud.pig.common.datascope.DataScope;
import java.util.List;
/**
* 数据权限处理器
*
* @author Hccake 2020/9/28
* @version 1.0
*/
public interface DataPermissionHandler {
/**
* 系统配置的所有的数据范围
* @return 数据范围集合
*/
List<DataScope> dataScopes();
/**
* 根据权限注解过滤后的数据范围集合
* @param mappedStatementId Mapper方法ID
* @return 数据范围集合
*/
List<DataScope> filterDataScopes(String mappedStatementId);
/**
* 是否忽略权限控制用于及早的忽略控制例如管理员直接放行而不必等到DataScope中再进行过滤处理提升效率
* @return boolean true: 忽略false: 进行权限控制
* @param mappedStatementId Mapper方法ID
*/
boolean ignorePermissionControl(String mappedStatementId);
}

View File

@ -0,0 +1,76 @@
package com.pigcloud.pig.common.datascope.handler;
import com.pigcloud.pig.common.datascope.DataScope;
import com.pigcloud.pig.common.datascope.annotation.DataPermission;
import com.pigcloud.pig.common.datascope.holder.DataPermissionAnnotationHolder;
import lombok.RequiredArgsConstructor;
import java.util.*;
import java.util.stream.Collectors;
/**
* 默认的数据权限控制处理器
*
* @author Hccake 2021/1/27
* @version 1.0
*/
@RequiredArgsConstructor
public class DefaultDataPermissionHandler implements DataPermissionHandler {
private final List<DataScope> dataScopes;
/**
* 系统配置的所有的数据范围
* @return 数据范围集合
*/
@Override
public List<DataScope> dataScopes() {
return dataScopes;
}
/**
* 系统配置的所有的数据范围
* @param mappedStatementId Mapper方法ID
* @return 数据范围集合
*/
@Override
public List<DataScope> filterDataScopes(String mappedStatementId) {
if (this.dataScopes == null || this.dataScopes.isEmpty()) {
return new ArrayList<>();
}
// 获取当前方法对应的权限注解根据注解进行数据范围控制的过滤
DataPermission dataPermission = DataPermissionAnnotationHolder.peek();
if (dataPermission == null) {
return dataScopes;
}
if (dataPermission.ignore()) {
return new ArrayList<>();
}
// 当指定了只包含的资源时只对该资源的DataScope
if (dataPermission.includeResources().length > 0) {
Set<String> a = new HashSet<>(Arrays.asList(dataPermission.includeResources()));
return dataScopes.stream().filter(x -> a.contains(x.getResource())).collect(Collectors.toList());
}
// 当未指定只包含的资源且指定了排除的资源时则排除此部分资源的 DataScope
if (dataPermission.excludeResources().length > 0) {
Set<String> a = new HashSet<>(Arrays.asList(dataPermission.excludeResources()));
return dataScopes.stream().filter(x -> !a.contains(x.getResource())).collect(Collectors.toList());
}
return dataScopes;
}
/**
* 是否忽略权限控制默认不忽略
* @param mappedStatementId Mapper方法ID
* @return always false
*/
@Override
public boolean ignorePermissionControl(String mappedStatementId) {
return false;
}
}

View File

@ -0,0 +1,58 @@
package com.pigcloud.pig.common.datascope.holder;
import com.pigcloud.pig.common.datascope.annotation.DataPermission;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* 数据权限注解的持有者使用栈存储调用链中各方法对应数据权限注解
*
* @author hccake
*/
public final class DataPermissionAnnotationHolder {
private DataPermissionAnnotationHolder() {
}
/**
* 使用栈存储 DataPermission便于在方法嵌套调用时使用不同的数据权限控制
*/
private static final ThreadLocal<Deque<DataPermission>> DATA_PERMISSIONS = ThreadLocal.withInitial(ArrayDeque::new);
/**
* 获取当前的 DataPermission 注解
* @return DataPermission
*/
public static DataPermission peek() {
return DATA_PERMISSIONS.get().peek();
}
/**
* 入栈一个 DataPermission 注解
* @return DataPermission
*/
public static DataPermission push(DataPermission dataPermission) {
DATA_PERMISSIONS.get().push(dataPermission);
return dataPermission;
}
/**
* 弹出最顶部 DataPermission
*/
public static void poll() {
Deque<DataPermission> deque = DATA_PERMISSIONS.get();
// 当没有元素时清空 ThreadLocal
if (deque.poll() == null) {
DATA_PERMISSIONS.remove();
}
}
/**
* 清除 TreadLocal
*/
public static void clear() {
DATA_PERMISSIONS.remove();
}
}

View File

@ -0,0 +1,41 @@
package com.pigcloud.pig.common.datascope.holder;
import com.pigcloud.pig.common.datascope.DataScope;
import java.util.List;
/**
* DataScope 持有者 方便解析 SQL 时的参数透传
*
* @author hccake
*/
public final class DataScopeHolder {
private DataScopeHolder() {
}
private static final ThreadLocal<List<DataScope>> DATA_SCOPES = new ThreadLocal<>();
/**
* get dataScope
* @return dataScopes
*/
public static List<DataScope> get() {
return DATA_SCOPES.get();
}
/**
* 添加 dataScope
*/
public static void set(List<DataScope> dataScopes) {
DATA_SCOPES.set(dataScopes);
}
/**
* 删除 dataScope
*/
public static void remove() {
DATA_SCOPES.remove();
}
}

View File

@ -0,0 +1,34 @@
package com.pigcloud.pig.common.datascope.interceptor;
import com.pigcloud.pig.common.datascope.annotation.DataPermission;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
/**
* @author hccake
*/
@Getter
@EqualsAndHashCode(callSuper = true)
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
private final Advice advice;
private final Pointcut pointcut;
public DataPermissionAnnotationAdvisor() {
this.advice = new DataPermissionAnnotationInterceptor();
this.pointcut = buildPointcut();
}
protected Pointcut buildPointcut() {
Pointcut cpc = new AnnotationMatchingPointcut(DataPermission.class, true);
Pointcut mpc = new AnnotationMatchingPointcut(null, DataPermission.class, true);
return new ComposablePointcut(cpc).union(mpc);
}
}

View File

@ -0,0 +1,35 @@
package com.pigcloud.pig.common.datascope.interceptor;
import com.pigcloud.pig.common.datascope.annotation.DataPermission;
import com.pigcloud.pig.common.datascope.holder.DataPermissionAnnotationHolder;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.lang.reflect.Method;
/**
* DataPermission注解的拦截器在执行方法前将当前方法的对应注解压栈执行后弹出注解
*
* @author hccake
*/
public class DataPermissionAnnotationInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
// 当前方法
Method method = methodInvocation.getMethod();
// 获取执行类
Object invocationThis = methodInvocation.getThis();
Class<?> clazz = invocationThis != null ? invocationThis.getClass() : method.getDeclaringClass();
// 寻找对应的 DataPermission 注解属性
DataPermission dataPermission = DataPermissionFinder.findDataPermission(method, clazz);
DataPermissionAnnotationHolder.push(dataPermission);
try {
return methodInvocation.proceed();
}
finally {
DataPermissionAnnotationHolder.poll();
}
}
}

View File

@ -0,0 +1,65 @@
package com.pigcloud.pig.common.datascope.interceptor;
import com.pigcloud.pig.common.datascope.annotation.DataPermission;
import org.springframework.core.MethodClassKey;
import org.springframework.core.annotation.AnnotatedElementUtils;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* {@link DataPermission} 注解的查找者用于查询当前方法对应的 DataPermission 注解环境当方法上没有找到时会去类上寻找
*
* @author hccake
*/
@DataPermission
public final class DataPermissionFinder {
private DataPermissionFinder() {
}
private static final Map<Object, DataPermission> DATA_PERMISSION_CACHE = new ConcurrentHashMap<>(1024);
/**
* 提供一个默认的空值注解用于缓存空值占位使用
*/
private static final DataPermission EMPTY_DATA_PERMISSION = DataPermissionFinder.class
.getAnnotation(DataPermission.class);
/**
* 缓存的 key
* @param method 方法
* @param clazz
* @return key
*/
private static Object getCacheKey(Method method, Class<?> clazz) {
return new MethodClassKey(method, clazz);
}
/**
* 从缓存中获取数据权限注解 优先获取方法上的注解再获取类上的注解
* @param method 当前方法
* @param clazz 当前类
* @return 当前方法有效的数据权限注解
*/
public static DataPermission findDataPermission(Method method, Class<?> clazz) {
Object methodKey = getCacheKey(method, clazz);
if (DATA_PERMISSION_CACHE.containsKey(methodKey)) {
DataPermission dataPermission = DATA_PERMISSION_CACHE.get(methodKey);
// 判断是否和缓存的空注解是同一个对象
return EMPTY_DATA_PERMISSION == dataPermission ? null : dataPermission;
}
// 先查方法如果方法上没有则使用类上
DataPermission dataPermission = AnnotatedElementUtils.findMergedAnnotation(method, DataPermission.class);
if (dataPermission == null) {
dataPermission = AnnotatedElementUtils.findMergedAnnotation(clazz, DataPermission.class);
}
DATA_PERMISSION_CACHE.put(methodKey, dataPermission == null ? EMPTY_DATA_PERMISSION : dataPermission);
return dataPermission;
}
}

View File

@ -0,0 +1,72 @@
package com.pigcloud.pig.common.datascope.interceptor;
import com.pigcloud.pig.common.datascope.DataScope;
import com.pigcloud.pig.common.datascope.handler.DataPermissionHandler;
import com.pigcloud.pig.common.datascope.processor.DataScopeSqlProcessor;
import com.pigcloud.pig.common.datascope.util.PluginUtils;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.List;
/**
* 数据权限拦截器
*
* @author Hccake 2020/9/28
* @version 1.0
*/
@RequiredArgsConstructor
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class DataPermissionInterceptor implements Interceptor {
private final DataScopeSqlProcessor dataScopeSqlProcessor;
private final DataPermissionHandler dataPermissionHandler;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 第一版测试用
Object target = invocation.getTarget();
StatementHandler sh = (StatementHandler) target;
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
SqlCommandType sct = ms.getSqlCommandType();
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
String mappedStatementId = ms.getId();
// 根据用户权限判断是否需要拦截例如管理员可以查看所有则直接放行
if (dataPermissionHandler.ignorePermissionControl(mappedStatementId)) {
return invocation.proceed();
}
List<DataScope> dataScopes = dataPermissionHandler.filterDataScopes(mappedStatementId);
if (dataScopes == null || dataScopes.isEmpty()) {
return invocation.proceed();
}
// 根据 DataScopes 进行数据权限的 sql 处理
if (sct == SqlCommandType.SELECT) {
mpBs.sql(dataScopeSqlProcessor.parserSingle(mpBs.sql(), dataScopes));
}
else if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
mpBs.sql(dataScopeSqlProcessor.parserMulti(mpBs.sql(), dataScopes));
}
// 执行 sql
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
}

View File

@ -0,0 +1,114 @@
package com.pigcloud.pig.common.datascope.parser;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
/**
* https://github.com/JSQLParser/JSqlParser
*
* @author miemie hccake
* @since 2020-06-22
*/
@Slf4j
public abstract class JsqlParserSupport {
public String parserSingle(String sql, Object obj) {
if (log.isDebugEnabled()) {
log.debug("original SQL: " + sql);
}
try {
Statement statement = CCJSqlParserUtil.parse(sql);
return processParser(statement, 0, sql, obj);
}
catch (JSQLParserException e) {
throw new RuntimeException(String.format("Failed to process, Error SQL: %s", sql), e);
}
}
public String parserMulti(String sql, Object obj) {
if (log.isDebugEnabled()) {
log.debug("original SQL: " + sql);
}
try {
// fixed github pull/295
StringBuilder sb = new StringBuilder();
Statements statements = CCJSqlParserUtil.parseStatements(sql);
int i = 0;
for (Statement statement : statements.getStatements()) {
if (i > 0) {
sb.append(";");
}
sb.append(processParser(statement, i, sql, obj));
i++;
}
return sb.toString();
}
catch (JSQLParserException e) {
throw new RuntimeException(String.format("Failed to process, Error SQL: %s", sql), e);
}
}
/**
* 执行 SQL 解析
* @param statement JsqlParser Statement
* @return sql
*/
protected String processParser(Statement statement, int index, String sql, Object obj) {
if (log.isDebugEnabled()) {
log.debug("SQL to parse, SQL: " + sql);
}
if (statement instanceof Insert) {
this.processInsert((Insert) statement, index, sql, obj);
}
else if (statement instanceof Select) {
this.processSelect((Select) statement, index, sql, obj);
}
else if (statement instanceof Update) {
this.processUpdate((Update) statement, index, sql, obj);
}
else if (statement instanceof Delete) {
this.processDelete((Delete) statement, index, sql, obj);
}
sql = statement.toString();
if (log.isDebugEnabled()) {
log.debug("parse the finished SQL: " + sql);
}
return sql;
}
/**
* 新增
*/
protected void processInsert(Insert insert, int index, String sql, Object obj) {
throw new UnsupportedOperationException();
}
/**
* 删除
*/
protected void processDelete(Delete delete, int index, String sql, Object obj) {
throw new UnsupportedOperationException();
}
/**
* 更新
*/
protected void processUpdate(Update update, int index, String sql, Object obj) {
throw new UnsupportedOperationException();
}
/**
* 查询
*/
protected void processSelect(Select select, int index, String sql, Object obj) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,321 @@
package com.pigcloud.pig.common.datascope.processor;
import com.pigcloud.pig.common.datascope.DataScope;
import com.pigcloud.pig.common.datascope.holder.DataScopeHolder;
import com.pigcloud.pig.common.datascope.parser.JsqlParserSupport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import java.util.List;
import java.util.Objects;
/**
* 数据权限 sql 处理器 参考 mybatis-plus 租户拦截器解析 sql where 部分进行查询表达式注入
*
* @author Hccake 2020/9/26
* @version 1.0
*/
@RequiredArgsConstructor
@Slf4j
public class DataScopeSqlProcessor extends JsqlParserSupport {
/**
* select 类型SQL处理
* @param select jsqlparser Statement Select
*/
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
List<DataScope> dataScopes = (List<DataScope>) obj;
try {
// dataScopes 放入 ThreadLocal 方便透传
DataScopeHolder.set(dataScopes);
processSelectBody(select.getSelectBody());
List<WithItem> withItemsList = select.getWithItemsList();
if (withItemsList != null && !withItemsList.isEmpty()) {
withItemsList.forEach(this::processSelectBody);
}
}
finally {
// 必须清空 ThreadLocal
DataScopeHolder.remove();
}
}
protected void processSelectBody(SelectBody selectBody) {
if (selectBody == null) {
return;
}
if (selectBody instanceof PlainSelect) {
processPlainSelect((PlainSelect) selectBody);
}
else if (selectBody instanceof WithItem) {
WithItem withItem = (WithItem) selectBody;
processSelectBody(withItem.getSubSelect().getSelectBody());
}
else {
SetOperationList operationList = (SetOperationList) selectBody;
List<SelectBody> selectBodys = operationList.getSelects();
if (selectBodys != null && !selectBodys.isEmpty()) {
selectBodys.forEach(this::processSelectBody);
}
}
}
/**
* insert 类型SQL处理
* @param insert jsqlparser Statement Insert
*/
@Override
protected void processInsert(Insert insert, int index, String sql, Object obj) {
// insert 暂时不处理
}
/**
* update 类型SQL处理
* @param update jsqlparser Statement Update
*/
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
List<DataScope> dataScopes = (List<DataScope>) obj;
try {
// dataScopes 放入 ThreadLocal 方便透传
DataScopeHolder.set(dataScopes);
update.setWhere(this.injectExpression(update.getWhere(), update.getTable()));
}
finally {
// 必须清空 ThreadLocal
DataScopeHolder.remove();
}
}
/**
* delete 类型SQL处理
* @param delete jsqlparser Statement Delete
*/
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
List<DataScope> dataScopes = (List<DataScope>) obj;
try {
// dataScopes 放入 ThreadLocal 方便透传
DataScopeHolder.set(dataScopes);
delete.setWhere(this.injectExpression(delete.getWhere(), delete.getTable()));
}
finally {
// 必须清空 ThreadLocal
DataScopeHolder.remove();
}
}
/**
* 处理 PlainSelect
*/
protected void processPlainSelect(PlainSelect plainSelect) {
FromItem fromItem = plainSelect.getFromItem();
Expression where = plainSelect.getWhere();
processWhereSubSelect(where);
if (fromItem instanceof Table) {
Table fromTable = (Table) fromItem;
// #1186 github
plainSelect.setWhere(injectExpression(where, fromTable));
}
else {
processFromItem(fromItem);
}
// #3087 github
List<SelectItem> selectItems = plainSelect.getSelectItems();
if (selectItems != null && !selectItems.isEmpty()) {
selectItems.forEach(this::processSelectItem);
}
List<Join> joins = plainSelect.getJoins();
if (joins != null && !joins.isEmpty()) {
joins.forEach(j -> {
processJoin(j);
processFromItem(j.getRightItem());
});
}
}
/**
* 处理where条件内的子查询
* <p>
* 支持如下: 1. in 2. = 3. > 4. < 5. >= 6. <= 7. <> 8. EXISTS 9. NOT EXISTS
* <p>
* 前提条件: 1. 子查询必须放在小括号中 2. 子查询一般放在比较操作符的右边
* @param where where 条件
*/
protected void processWhereSubSelect(Expression where) {
if (where == null) {
return;
}
if (where instanceof FromItem) {
processFromItem((FromItem) where);
return;
}
if (where.toString().indexOf("SELECT") > 0) {
// 有子查询
if (where instanceof BinaryExpression) {
// 比较符号 , and , or , 等等
BinaryExpression expression = (BinaryExpression) where;
processWhereSubSelect(expression.getLeftExpression());
processWhereSubSelect(expression.getRightExpression());
}
else if (where instanceof InExpression) {
// in
InExpression expression = (InExpression) where;
ItemsList itemsList = expression.getRightItemsList();
if (itemsList instanceof SubSelect) {
processSelectBody(((SubSelect) itemsList).getSelectBody());
}
}
else if (where instanceof ExistsExpression) {
// exists
ExistsExpression expression = (ExistsExpression) where;
processWhereSubSelect(expression.getRightExpression());
}
else if (where instanceof NotExpression) {
// not exists
NotExpression expression = (NotExpression) where;
processWhereSubSelect(expression.getExpression());
}
else if (where instanceof Parenthesis) {
Parenthesis expression = (Parenthesis) where;
processWhereSubSelect(expression.getExpression());
}
}
}
protected void processSelectItem(SelectItem selectItem) {
if (selectItem instanceof SelectExpressionItem) {
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
if (selectExpressionItem.getExpression() instanceof SubSelect) {
processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody());
}
else if (selectExpressionItem.getExpression() instanceof Function) {
processFunction((Function) selectExpressionItem.getExpression());
}
}
}
/**
* 处理函数
* <p>
* 支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)
* <p>
* <p>
* fixed gitee pulls/141
* </p>
* @param function
*/
protected void processFunction(Function function) {
ExpressionList parameters = function.getParameters();
if (parameters != null) {
parameters.getExpressions().forEach(expression -> {
if (expression instanceof SubSelect) {
processSelectBody(((SubSelect) expression).getSelectBody());
}
else if (expression instanceof Function) {
processFunction((Function) expression);
}
});
}
}
/**
* 处理子查询等
*/
protected void processFromItem(FromItem fromItem) {
if (fromItem instanceof SubJoin) {
SubJoin subJoin = (SubJoin) fromItem;
if (subJoin.getJoinList() != null) {
subJoin.getJoinList().forEach(this::processJoin);
}
if (subJoin.getLeft() != null) {
processFromItem(subJoin.getLeft());
}
}
else if (fromItem instanceof SubSelect) {
SubSelect subSelect = (SubSelect) fromItem;
if (subSelect.getSelectBody() != null) {
processSelectBody(subSelect.getSelectBody());
}
}
else if (fromItem instanceof ValuesList) {
log.debug("Perform a subquery, if you do not give us feedback");
}
else if (fromItem instanceof LateralSubSelect) {
LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
if (lateralSubSelect.getSubSelect() != null) {
SubSelect subSelect = lateralSubSelect.getSubSelect();
if (subSelect.getSelectBody() != null) {
processSelectBody(subSelect.getSelectBody());
}
}
}
}
/**
* 处理联接语句
*/
protected void processJoin(Join join) {
if (join.getRightItem() instanceof Table) {
Table fromTable = (Table) join.getRightItem();
join.setOnExpression(injectExpression(join.getOnExpression(), fromTable));
}
}
/**
* 根据 DataScope 将数据过滤的表达式注入原本的 where/or 条件
* @param currentExpression Expression where/or
* @param table 表信息
* @return 修改后的 where/or 条件
*/
private Expression injectExpression(Expression currentExpression, Table table) {
String tableName = table.getName();
List<DataScope> dataScopes = DataScopeHolder.get();
Expression dataFilterExpression = dataScopes.stream().filter(x -> x.getTableNames().contains(tableName))
.map(x -> x.getExpression(tableName, table.getAlias())).filter(Objects::nonNull)
.reduce(AndExpression::new).orElse(null);
if (currentExpression == null) {
return dataFilterExpression;
}
if (dataFilterExpression == null) {
return currentExpression;
}
if (currentExpression instanceof OrExpression) {
return new AndExpression(new Parenthesis(currentExpression), dataFilterExpression);
}
else {
return new AndExpression(currentExpression, dataFilterExpression);
}
}
/**
* 根据当前表是否有别名动态对字段名前添加表别名 eg. 表名 table_1 as t 原始字段column1 返回 t.column1
* @param table 表信息
* @param column 字段名
* @return 原始字段名或者添加了表别名的字段名
*/
protected Column getAliasColumn(Table table, String column) {
StringBuilder columnBuilder = new StringBuilder();
if (table.getAlias() != null) {
columnBuilder.append(table.getAlias().getName()).append(".");
}
columnBuilder.append(column);
return new Column(columnBuilder.toString());
}
}

View File

@ -0,0 +1,64 @@
package com.pigcloud.pig.common.datascope.util;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
/**
* @author Hccake 2021/1/27
* @version 1.0
*/
public final class AnnotationUtil {
private AnnotationUtil() {
}
/**
* 获取数据权限注解 优先获取方法上的注解再获取类上的注解
* @param mappedStatementId 类名.方法名
* @return 数据权限注解
*/
public static <A extends Annotation> A findAnnotationByMappedStatementId(String mappedStatementId,
Class<A> aClass) {
if (mappedStatementId == null || "".equals(mappedStatementId)) {
return null;
}
// 1.得到类路径和方法路径
int lastIndexOfDot = mappedStatementId.lastIndexOf(".");
if (lastIndexOfDot < 0) {
return null;
}
String className = mappedStatementId.substring(0, lastIndexOfDot);
String methodName = mappedStatementId.substring(lastIndexOfDot + 1);
if ("".equals(className) || "".equals(methodName)) {
return null;
}
// 2.字节码
Class<?> clazz = null;
try {
clazz = Class.forName(className);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (clazz == null) {
return null;
}
A annotation = null;
// 3.得到方法上的注解
Method[] methods = clazz.getMethods();
for (Method method : methods) {
String name = method.getName();
if (methodName.equals(name)) {
annotation = method.getAnnotation(aClass);
break;
}
}
if (annotation == null) {
annotation = clazz.getAnnotation(aClass);
}
return annotation;
}
}

View File

@ -0,0 +1,163 @@
/*
* Copyright (c) 2011-2020, baomidou (jobob@qq.com).
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.pigcloud.pig.common.datascope.util;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 插件工具类
*
* @author TaoYu , hubin
* @since 2017-06-20
*/
public abstract class PluginUtils {
public static final String DELEGATE_BOUNDSQL_SQL = "delegate.boundSql.sql";
/**
* 获得真正的处理对象,可能多层代理.
*/
@SuppressWarnings("unchecked")
public static <T> T realTarget(Object target) {
if (Proxy.isProxyClass(target.getClass())) {
MetaObject metaObject = SystemMetaObject.forObject(target);
return realTarget(metaObject.getValue("h.target"));
}
return (T) target;
}
/**
* BoundSql 设置 additionalParameters
* @param boundSql BoundSql
* @param additionalParameters additionalParameters
*/
public static void setAdditionalParameter(BoundSql boundSql, Map<String, Object> additionalParameters) {
additionalParameters.forEach(boundSql::setAdditionalParameter);
}
public static MPBoundSql mpBoundSql(BoundSql boundSql) {
return new MPBoundSql(boundSql);
}
public static MPStatementHandler mpStatementHandler(StatementHandler statementHandler) {
statementHandler = realTarget(statementHandler);
MetaObject object = SystemMetaObject.forObject(statementHandler);
return new MPStatementHandler(SystemMetaObject.forObject(object.getValue("delegate")));
}
/**
* {@link org.apache.ibatis.executor.statement.BaseStatementHandler}
*/
public static class MPStatementHandler {
private final MetaObject statementHandler;
MPStatementHandler(MetaObject statementHandler) {
this.statementHandler = statementHandler;
}
public ParameterHandler parameterHandler() {
return get("parameterHandler");
}
public MappedStatement mappedStatement() {
return get("mappedStatement");
}
public Executor executor() {
return get("executor");
}
public MPBoundSql mPBoundSql() {
return new MPBoundSql(boundSql());
}
public BoundSql boundSql() {
return get("boundSql");
}
public Configuration configuration() {
return get("configuration");
}
@SuppressWarnings("unchecked")
private <T> T get(String property) {
return (T) statementHandler.getValue(property);
}
}
/**
* {@link BoundSql}
*/
public static class MPBoundSql {
private final MetaObject boundSql;
private final BoundSql delegate;
MPBoundSql(BoundSql boundSql) {
this.delegate = boundSql;
this.boundSql = SystemMetaObject.forObject(boundSql);
}
public String sql() {
return delegate.getSql();
}
public void sql(String sql) {
boundSql.setValue("sql", sql);
}
public List<ParameterMapping> parameterMappings() {
List<ParameterMapping> parameterMappings = delegate.getParameterMappings();
return new ArrayList<>(parameterMappings);
}
public void parameterMappings(List<ParameterMapping> parameterMappings) {
boundSql.setValue("parameterMappings", Collections.unmodifiableList(parameterMappings));
}
public Object parameterObject() {
return get("parameterObject");
}
public Map<String, Object> additionalParameters() {
return get("additionalParameters");
}
@SuppressWarnings("unchecked")
private <T> T get(String property) {
return (T) boundSql.getValue(property);
}
}
}

View File

@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pigcloud.pig.common.datascope.DataScopeAutoConfiguration

View File

@ -0,0 +1,75 @@
package com.pigcloud.pig.common.datascope.test;
import com.pigcloud.pig.common.datascope.DataScope;
import com.pigcloud.pig.common.datascope.handler.DefaultDataPermissionHandler;
import com.pigcloud.pig.common.datascope.handler.DataPermissionHandler;
import com.pigcloud.pig.common.datascope.processor.DataScopeSqlProcessor;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import org.junit.jupiter.api.Test;
import org.springframework.util.Assert;
import java.util.*;
/**
* @author Hccake 2020/9/28
* @version 1.0
*/
class SqlParseTest {
@Test
void test() {
DataScope dataScope = new DataScope() {
final String columnId = "order_id";
@Override
public String getResource() {
return "order";
}
@Override
public Collection<String> getTableNames() {
Set<String> tableNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
tableNames.addAll(Arrays.asList("t_order", "t_order_info"));
return tableNames;
}
@Override
public Expression getExpression(String tableName, Alias tableAlias) {
Column column = new Column(tableAlias == null ? columnId : tableAlias.getName() + "." + columnId);
ExpressionList expressionList = new ExpressionList();
expressionList.setExpressions(Arrays.asList(new StringValue("1"), new StringValue("2")));
return new InExpression(column, expressionList);
}
};
List<DataScope> dataScopes = new ArrayList<>();
dataScopes.add(dataScope);
DataPermissionHandler dataPermissionHandler = new DefaultDataPermissionHandler(dataScopes) {
@Override
public boolean ignorePermissionControl(String mappedStatementId) {
return false;
}
};
DataScopeSqlProcessor dataScopeSqlProcessor = new DataScopeSqlProcessor();
// DataScopeHolder.putDataScope("order", dataScope);
String sql = "select o.order_id,o.order_name,oi.order_price "
+ "from t_ORDER o left join t_order_info oi on o.order_id = oi.order_id "
+ "where oi.order_price > 100";
String parseSql = dataScopeSqlProcessor.parserSingle(sql, dataPermissionHandler.dataScopes());
System.out.println(parseSql);
String trueSql = "SELECT o.order_id, o.order_name, oi.order_price FROM t_ORDER o LEFT JOIN t_order_info oi ON o.order_id = oi.order_id AND oi.order_id IN ('1', '2') WHERE oi.order_price > 100 AND o.order_id IN ('1', '2')";
Assert.isTrue(trueSql.equals(parseSql), "sql 数据权限解析异常");
}
}

View File

@ -16,7 +16,9 @@
package com.pig4cloud.pig.common.security.component;
import cn.hutool.core.map.MapUtil;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.security.datascope.UserDataScope;
import com.pig4cloud.pig.common.security.service.PigUser;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@ -25,9 +27,7 @@ import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.*;
/**
* @author lengleng
@ -69,7 +69,19 @@ public class PigUserAuthenticationConverter implements UserAuthenticationConvert
String username = (String) map.get(SecurityConstants.DETAILS_USERNAME);
Integer id = (Integer) map.get(SecurityConstants.DETAILS_USER_ID);
Integer deptId = (Integer) map.get(SecurityConstants.DETAILS_DEPT_ID);
PigUser user = new PigUser(id, deptId, username, N_A, true, true, true, true, authorities);
UserDataScope userDataScope = new UserDataScope();
Object value = map.get(SecurityConstants.DETAILS_USER_DATA_SCOPE);
if (value != null) {
Map<String, ?> userDataScopeMap = (Map) value;
userDataScope.setAllScope(MapUtil.getBool(userDataScopeMap, "allScope"));
userDataScope.setOnlySelf(MapUtil.getBool(userDataScopeMap, "onlySelf"));
userDataScope.setScopeUserIds(new HashSet<>((List) userDataScopeMap.get("scopeUserIds")));
userDataScope.setScopeUserIds(new HashSet<>((List) userDataScopeMap.get("scopeUserIds")));
userDataScope.setScopeDeptIds(new HashSet<>((List) userDataScopeMap.get("scopeDeptIds")));
}
PigUser user = new PigUser(id, deptId, username, N_A, true, true, true, true, authorities, userDataScope);
return new UsernamePasswordAuthenticationToken(user, N_A, authorities);
}
return null;

View File

@ -0,0 +1,21 @@
package com.pig4cloud.pig.common.security.datascope;
import com.pig4cloud.pig.admin.api.entity.SysRole;
import com.pig4cloud.pig.admin.api.entity.SysUser;
import java.util.List;
/**
* @author hccake
*/
public interface DataScopeProcessor {
/**
* 根据用户和角色信息合并用户最终的数据权限
* @param user 用户
* @param roles 角色列表
* @return UserDataScope
*/
UserDataScope mergeScopeType(SysUser user, List<SysRole> roles);
}

View File

@ -0,0 +1,119 @@
package com.pig4cloud.pig.common.security.datascope;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.admin.api.entity.SysRole;
import com.pig4cloud.pig.admin.api.entity.SysUser;
import com.pig4cloud.pig.admin.api.feign.RemoteDeptService;
import com.pig4cloud.pig.admin.api.feign.RemoteUserService;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.constant.enums.DataScopeTypeEnum;
import com.pig4cloud.pig.common.core.util.R;
import lombok.RequiredArgsConstructor;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author hccake
*/
@RequiredArgsConstructor
public class PigDataScopeProcessor implements DataScopeProcessor {
private final RemoteDeptService remoteDeptService;
private final RemoteUserService remoteUserService;
/**
* 合并角色的数据权限类型排除相同的权限后大的权限覆盖小的
* @param user 用户
* @param roles 角色列表
* @return List<Integer> 合并后的权限
*/
@Override
public UserDataScope mergeScopeType(SysUser user, List<SysRole> roles) {
UserDataScope userDataScope = new UserDataScope();
Set<Integer> scopeUserIds = userDataScope.getScopeUserIds();
Set<Integer> scopeDeptIds = userDataScope.getScopeDeptIds();
// 任何用户都应该可以看到自己的数据
Integer userId = user.getUserId();
scopeUserIds.add(userId);
if (CollectionUtil.isEmpty(roles)) {
return userDataScope;
}
// 根据角色的权限返回进行分组
Map<Integer, List<SysRole>> map = roles.stream().collect(Collectors.groupingBy(SysRole::getScopeType));
// 如果有全部权限直接返回
if (map.containsKey(DataScopeTypeEnum.ALL.getType())) {
userDataScope.setAllScope(true);
return userDataScope;
}
// 如果有本级及子级删除其包含的几类数据权限
boolean hasLevelChildLevel = map.containsKey(DataScopeTypeEnum.LEVEL_CHILD_LEVEL.getType());
if (hasLevelChildLevel) {
map.remove(DataScopeTypeEnum.SELF.getType());
map.remove(DataScopeTypeEnum.SELF_CHILD_LEVEL.getType());
map.remove(DataScopeTypeEnum.LEVEL.getType());
}
// 是否有本人及子级权限
boolean hasSelfChildLevel = map.containsKey(DataScopeTypeEnum.SELF_CHILD_LEVEL.getType());
// 是否有本级权限
boolean hasLevel = map.containsKey(DataScopeTypeEnum.LEVEL.getType());
if (hasSelfChildLevel || hasLevel) {
// 如果有本人及子级或者本级都删除本人的数据权限
map.remove(DataScopeTypeEnum.SELF.getType());
// 如果同时拥有则等于本级及子级权限
if (hasSelfChildLevel && hasLevel) {
map.remove(DataScopeTypeEnum.SELF_CHILD_LEVEL.getType());
map.remove(DataScopeTypeEnum.LEVEL.getType());
map.put(DataScopeTypeEnum.LEVEL_CHILD_LEVEL.getType(), new ArrayList<>());
}
}
// 这时如果仅仅只能看个人的直接返回
if (map.size() == 1 && map.containsKey(DataScopeTypeEnum.SELF.getType())) {
userDataScope.setOnlySelf(true);
return userDataScope;
}
// 如果有 本级及子级 或者 本级都把自己的 deptId 加进去
Integer deptId = user.getDeptId();
if (hasLevelChildLevel || hasLevel) {
scopeDeptIds.add(deptId);
}
// 如果有 本级及子级 或者 本人及子级都把下级组织的 deptId 加进去
if (hasLevelChildLevel || hasSelfChildLevel) {
List<Integer> childDeptIdList = remoteDeptService.listChildDeptId(deptId, SecurityConstants.FROM_IN)
.getData();
if (CollectionUtil.isNotEmpty(childDeptIdList)) {
scopeDeptIds.addAll(childDeptIdList);
}
}
// 自定义部门
List<SysRole> sysRoles = map.get(DataScopeTypeEnum.CUSTOM.getType());
if (CollectionUtil.isNotEmpty(sysRoles)) {
Set<Integer> customDeptIds = sysRoles.stream().map(SysRole::getScopeResources).filter(Objects::nonNull)
.flatMap(x -> Arrays.stream(x.split(StrUtil.COMMA))).map(Integer::parseInt)
.collect(Collectors.toSet());
scopeDeptIds.addAll(customDeptIds);
}
// 把部门对应的用户id都放入集合中
if (CollectionUtil.isNotEmpty(scopeDeptIds)) {
R<List<Integer>> r = remoteUserService.listUserIdByDeptIds(scopeDeptIds, SecurityConstants.FROM_IN);
List<Integer> userIds = r.getData();
if (CollectionUtil.isNotEmpty(userIds)) {
scopeUserIds.addAll(userIds);
}
}
return userDataScope;
}
}

View File

@ -0,0 +1,35 @@
package com.pig4cloud.pig.common.security.datascope;
import lombok.Data;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
/**
* @author hccake
*/
@Data
public class UserDataScope implements Serializable {
/**
* 是否是全部数据权限
*/
private boolean allScope = false;
/**
* 是否仅能看自己
*/
private boolean onlySelf = false;
/**
* 数据权限范围用户所能查看的用户id 集合
*/
private Set<Integer> scopeUserIds = new HashSet<>();
/**
* 数据权限范围用户所能查看的部门id 集合
*/
private Set<Integer> scopeDeptIds = new HashSet<>();
}

View File

@ -0,0 +1,82 @@
package com.pig4cloud.pig.common.security.grant;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.common.security.service.PigUserDetailsServiceImpl;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.security.authentication.BadCredentialsException;
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.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author hzq
* @since 2021-09-14
*/
@Slf4j
public class PhoneAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Setter
private UserDetailsService userDetailsService;
@Setter
private PasswordEncoder passwordEncoder;
/**
* 校验 请求信息userDetails
* @param userDetails 用户信息
* @param authentication 认证信息
* @throws AuthenticationException
*/
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
log.debug("Failed to authenticate since no credentials provided");
throw new BeanCreationException("Bad credentials");
}
// 手机号
String phone = authentication.getName();
if (StrUtil.equals(phone, "17034642999")) {
throw new UsernameNotFoundException(phone);
}
String code = authentication.getCredentials().toString();
UserDetails userDetails = ((PigUserDetailsServiceImpl) userDetailsService).loadUserByPhone(phone);
PhoneAuthenticationToken token = new PhoneAuthenticationToken(userDetails);
token.setDetails(authentication.getDetails());
return token;
}
@Override
protected UserDetails retrieveUser(String phone, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(PhoneAuthenticationToken.class);
}
}

View File

@ -0,0 +1,40 @@
package com.pig4cloud.pig.common.security.grant;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
/**
* @author hzq
* @since 2021-09-14
*/
public class PhoneAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
// 验证码/密码
private String code;
public PhoneAuthenticationToken(String phone, String code) {
super(AuthorityUtils.NO_AUTHORITIES);
this.principal = phone;
this.code = code;
}
public PhoneAuthenticationToken(UserDetails sysUser) {
super(sysUser.getAuthorities());
this.principal = sysUser;
super.setAuthenticated(true); // 设置认证成功 必须
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return this.code;
}
}

View File

@ -0,0 +1,78 @@
package com.pig4cloud.pig.common.security.grant;
import cn.hutool.core.util.StrUtil;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 资源所有者电话令牌授予者
*
* @author hzq
* @since 2021-09-14
*/
public class ResourceOwnerPhoneTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "phone";
private final AuthenticationManager authenticationManager;
public ResourceOwnerPhoneTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}
protected ResourceOwnerPhoneTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
// 手机号
String phone = parameters.get("phone");
// 验证码/密码
String code = parameters.get("code");
if (StrUtil.isBlank(phone) || StrUtil.isBlank(code)) {
throw new InvalidGrantException("Bad credentials [ params must be has phone with code ]");
}
// Protect from downstream leaks of code
parameters.remove("code");
Authentication userAuth = new PhoneAuthenticationToken(phone, code);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
}
catch (AccountStatusException | BadCredentialsException ase) {
// covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
}
// If the phone/code are wrong the spec says we should send 400/invalid grant
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + phone);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}

View File

@ -16,6 +16,7 @@
package com.pig4cloud.pig.common.security.service;
import com.pig4cloud.pig.common.security.datascope.UserDataScope;
import lombok.Getter;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.GrantedAuthority;
@ -33,13 +34,19 @@ public class PigUser extends User {
* 用户ID
*/
@Getter
private Integer id;
private final Integer id;
/**
* 部门ID
*/
@Getter
private Integer deptId;
private final Integer deptId;
/**
* 用户数据权限信息
*/
@Getter
private final UserDataScope userDataScope;
/**
* Construct the <code>User</code> with the details required by
@ -62,10 +69,11 @@ public class PigUser extends User {
*/
public PigUser(Integer id, Integer deptId, String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Collection<? extends GrantedAuthority> authorities, UserDataScope userDataScope) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.id = id;
this.deptId = deptId;
this.userDataScope = userDataScope;
}
}

View File

@ -25,6 +25,8 @@ 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.security.datascope.DataScopeProcessor;
import com.pig4cloud.pig.common.security.datascope.UserDataScope;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@ -45,7 +47,7 @@ import java.util.Set;
/**
* 用户详细信息
*
* @author lengleng
* @author lengleng hccake
*/
@Slf4j
@Service
@ -56,6 +58,8 @@ public class PigUserDetailsServiceImpl implements UserDetailsService {
private final CacheManager cacheManager;
private final DataScopeProcessor dataScopeProcessor;
/**
* 用户密码登录
* @param username 用户名
@ -77,10 +81,21 @@ public class PigUserDetailsServiceImpl implements UserDetailsService {
return userDetails;
}
/**
* 手机号码登录
* @param phone 手机号码
* @return 用户信息
*/
public UserDetails loadUserByPhone(String phone) {
R<UserInfo> result = remoteUserService.infoByPhone(phone, SecurityConstants.FROM_IN);
UserDetails userDetails = getUserDetails(result);
return userDetails;
}
/**
* 构建userdetails
* @param result 用户信息
* @return
* @return UserDetails
*/
private UserDetails getUserDetails(R<UserInfo> result) {
if (result == null || result.getData() == null) {
@ -100,10 +115,14 @@ public class PigUserDetailsServiceImpl implements UserDetailsService {
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
SysUser user = info.getSysUser();
// 数据权限填充
UserDataScope userDataScope = dataScopeProcessor.mergeScopeType(user, info.getRoleList());
// 构造security用户
return new PigUser(user.getUserId(), user.getDeptId(), user.getUsername(),
SecurityConstants.BCRYPT + user.getPassword(),
StrUtil.equals(user.getLockFlag(), CommonConstants.STATUS_NORMAL), true, true, true, authorities);
StrUtil.equals(user.getLockFlag(), CommonConstants.STATUS_NORMAL), true, true, true, authorities,
userDataScope);
}
}

View File

@ -1,4 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pig4cloud.pig.common.security.service.PigUserDetailsServiceImpl,\
com.pig4cloud.pig.common.security.component.PigSecurityInnerAspect
com.pig4cloud.pig.common.security.component.PigSecurityInnerAspect,\
com.pig4cloud.pig.common.security.datascope.PigDataScopeProcessor

View File

@ -40,5 +40,6 @@
<module>pig-common-feign</module>
<module>pig-common-swagger</module>
<module>pig-common-test</module>
</modules>
<module>pig-common-datascope</module>
</modules>
</project>

View File

@ -1,19 +1,15 @@
FROM moxm/java:1.8-full as builder
WORKDIR /build
ARG JAR_FILE=target/pig-gateway.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract && rm app.jar
FROM moxm/java:1.8-full
LABEL maintainer="jclazz@outlook.com"
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
WORKDIR pig-gateway
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/snapshot-dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/application/ ./
RUN mkdir -p /pig-gateway
WORKDIR /pig-gateway
ARG JAR_FILE=target/pig-gateway.jar
COPY ${JAR_FILE} app.jar
EXPOSE 9999
CMD sleep 60; java $JAVA_OPTS org.springframework.boot.loader.JarLauncher
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java -jar app.jar $JAVA_OPTS

View File

@ -68,9 +68,10 @@ public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory<Obje
return chain.filter(exchange);
}
// 刷新token直接向下执行
// 刷新token手机号登录也可以这里进行校验 直接向下执行
String grantType = request.getQueryParams().getFirst("grant_type");
if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)) {
if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)
|| StrUtil.equals(SecurityConstants.PHONE, grantType)) {
return chain.filter(exchange);
}

View File

@ -1,20 +1,15 @@
FROM moxm/java:1.8-full as builder
WORKDIR /build
ARG JAR_FILE=target/pig-register.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract && rm app.jar
FROM moxm/java:1.8-full
LABEL maintainer="jclazz@outlook.com"
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk --no-cache add libstdc++
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
WORKDIR pig-register
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/snapshot-dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/application/ ./
RUN mkdir -p /pig-register
WORKDIR /pig-register
ARG JAR_FILE=target/pig-register.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8848
CMD sleep 30; java $JAVA_OPTS org.springframework.boot.loader.JarLauncher
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 30; java -jar app.jar $JAVA_OPTS

View File

@ -16,10 +16,14 @@
package com.pig4cloud.pig.admin.api.dto;
import com.pig4cloud.pig.admin.api.entity.SysMenu;
import com.pig4cloud.pig.admin.api.entity.SysRole;
import com.pig4cloud.pig.admin.api.entity.SysUser;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
/**
* @author lengleng
@ -46,4 +50,9 @@ public class UserInfo implements Serializable {
*/
private Integer[] roles;
/**
* 角色集合
*/
private List<SysRole> roleList;
}

View File

@ -25,6 +25,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* <p>
@ -56,6 +57,13 @@ public class SysRole extends BaseEntity {
@ApiModelProperty(value = "角色描述")
private String roleDesc;
@NotNull(message = "数据范围类型 不能为null")
@ApiModelProperty(value = "数据范围类型")
private Integer scopeType;
@ApiModelProperty(value = "数据范围资源")
private String scopeResources;
/**
* 删除标识0-正常,1-删除
*/

View File

@ -0,0 +1,45 @@
/*
* 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.admin.api.feign;
import com.pig4cloud.pig.admin.api.feign.factory.RemoteDeptServiceFallbackFactory;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.constant.ServiceNameConstants;
import com.pig4cloud.pig.common.core.util.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import java.util.List;
/**
* @author hccake
*/
@FeignClient(contextId = "remoteDeptService", value = ServiceNameConstants.UMPS_SERVICE,
fallbackFactory = RemoteDeptServiceFallbackFactory.class)
public interface RemoteDeptService {
/**
* 查收子级id列表
* @return 返回子级id列表
*/
@GetMapping("/dept/child-id/{deptId}")
R<List<Integer>> listChildDeptId(@PathVariable("deptId") Integer deptId,
@RequestHeader(SecurityConstants.FROM) String from);
}

View File

@ -25,6 +25,10 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
import java.util.Set;
/**
* @author lengleng
@ -44,11 +48,22 @@ public interface RemoteUserService {
R<UserInfo> info(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM) String from);
/**
* 通过社交账号查询用户角色信息
* @param inStr appid@code
* @return
* 通过手机号码查询用户角色信息
* @param phone 手机号码
* @param from 调用标志
* @return R
*/
@GetMapping("/social/info/{inStr}")
R<UserInfo> social(@PathVariable("inStr") String inStr);
@GetMapping("/mobile/{phone}")
R<UserInfo> infoByPhone(@PathVariable("phone") String phone, @RequestHeader(SecurityConstants.FROM) String from);
/**
* 根据部门id查询对应的用户 id 集合
* @param deptIds 部门id 集合
* @param from 调用标志
* @return 用户 id 集合
*/
@GetMapping("/user/ids")
R<List<Integer>> listUserIdByDeptIds(@RequestParam("deptIds") Set<Integer> deptIds,
@RequestHeader(SecurityConstants.FROM) String from);
}

View File

@ -0,0 +1,37 @@
/*
* 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.admin.api.feign.factory;
import com.pig4cloud.pig.admin.api.feign.RemoteDeptService;
import com.pig4cloud.pig.admin.api.feign.fallback.RemoteDeptServiceFallbackImpl;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
* @author hccake
*/
@Component
public class RemoteDeptServiceFallbackFactory implements FallbackFactory<RemoteDeptService> {
@Override
public RemoteDeptService create(Throwable throwable) {
RemoteDeptServiceFallbackImpl remoteDeptServiceFallback = new RemoteDeptServiceFallbackImpl();
remoteDeptServiceFallback.setCause(throwable);
return remoteDeptServiceFallback;
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.admin.api.feign.fallback;
import com.pig4cloud.pig.admin.api.dto.UserInfo;
import com.pig4cloud.pig.admin.api.feign.RemoteDeptService;
import com.pig4cloud.pig.admin.api.feign.RemoteUserService;
import com.pig4cloud.pig.common.core.util.R;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author hccake
*/
@Slf4j
@Component
public class RemoteDeptServiceFallbackImpl implements RemoteDeptService {
@Setter
private Throwable cause;
@Override
public R<List<Integer>> listChildDeptId(Integer deptId, String from) {
log.error("[listChildDeptId] feign 查询子级部门id列表失败", cause);
return null;
}
}

View File

@ -23,6 +23,9 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
/**
* @author lengleng
* @date 2019/2/1
@ -47,13 +50,20 @@ public class RemoteUserServiceFallbackImpl implements RemoteUserService {
}
/**
* 通过社交账号查询用户角色信息
* @param inStr appid@code
* @return
* 通过手机号码查询用户角色信息
* @param phone 手机号码
* @param from 调用标志
* @return R
*/
@Override
public R<UserInfo> social(String inStr) {
log.error("feign 查询用户信息失败:{}", inStr, cause);
public R<UserInfo> infoByPhone(String phone, String from) {
log.error("feign 查询用户信息失败手机号码:{}", phone, cause);
return null;
}
@Override
public R<List<Integer>> listUserIdByDeptIds(Set<Integer> deptIds, String from) {
log.error("feign 根据部门ids查询用户Id集合失败:{}", deptIds, cause);
return null;
}

View File

@ -0,0 +1,49 @@
/*
* 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.admin.api.vo;
import com.pig4cloud.pig.admin.api.entity.SysUser;
import lombok.Data;
import java.io.Serializable;
/**
* @author lengleng
* @date 2019/2/1
* <p>
* commit('SET_ROLES', data) commit('SET_NAME', data) commit('SET_AVATAR', data)
* commit('SET_INTRODUCTION', data) commit('SET_PERMISSIONS', data)
*/
@Data
public class UserInfoVO implements Serializable {
/**
* 用户基本信息
*/
private SysUser sysUser;
/**
* 权限标识集合
*/
private String[] permissions;
/**
* 角色集合
*/
private Integer[] roles;
}

View File

@ -2,6 +2,8 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pig4cloud.pig.admin.api.feign.fallback.RemoteUserServiceFallbackImpl,\
com.pig4cloud.pig.admin.api.feign.fallback.RemoteLogServiceFallbackImpl,\
com.pig4cloud.pig.admin.api.feign.fallback.RemoteTokenServiceFallbackImpl,\
com.pig4cloud.pig.admin.api.feign.fallback.RemoteDeptServiceFallbackImpl,\
com.pig4cloud.pig.admin.api.feign.factory.RemoteUserServiceFallbackFactory,\
com.pig4cloud.pig.admin.api.feign.factory.RemoteLogServiceFallbackFactory,\
com.pig4cloud.pig.admin.api.feign.factory.RemoteTokenServiceFallbackFactory
com.pig4cloud.pig.admin.api.feign.factory.RemoteTokenServiceFallbackFactory,\
com.pig4cloud.pig.admin.api.feign.factory.RemoteDeptServiceFallbackFactory

View File

@ -1,19 +1,15 @@
FROM moxm/java:1.8-full as builder
WORKDIR /build
ARG JAR_FILE=target/pig-upms-biz.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract && rm app.jar
FROM moxm/java:1.8-full
LABEL maintainer="jclazz@outlook.com"
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
WORKDIR pig-upms
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/snapshot-dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/application/ ./
RUN mkdir -p /pig-upms-biz
WORKDIR /pig-upms-biz
ARG JAR_FILE=target/pig-upms-biz.jar
COPY ${JAR_FILE} app.jar
EXPOSE 4000
CMD sleep 60; java $JAVA_OPTS org.springframework.boot.loader.JarLauncher
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java -jar app.jar $JAVA_OPTS

View File

@ -60,6 +60,11 @@
<groupId>com.pig4cloud</groupId>
<artifactId>pig-common-mybatis</artifactId>
</dependency>
<!--数据权限 模块-->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pig-common-datascope</artifactId>
</dependency>
<!--注册中心客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>

View File

@ -20,6 +20,7 @@ import com.pig4cloud.pig.admin.api.entity.SysDept;
import com.pig4cloud.pig.admin.service.SysDeptService;
import com.pig4cloud.pig.common.core.util.R;
import com.pig4cloud.pig.common.log.annotation.SysLog;
import com.pig4cloud.pig.common.security.annotation.Inner;
import io.swagger.annotations.Api;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
@ -27,6 +28,7 @@ import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.List;
/**
* <p>
@ -121,4 +123,14 @@ public class DeptController {
return R.ok(sysDeptService.getOne(new QueryWrapper<>(condition)));
}
/**
* 查收子级id列表
* @return 返回子级id列表
*/
@Inner
@GetMapping(value = "/child-id/{deptId}")
public R<List<Integer>> listChildDeptId(@PathVariable Integer deptId) {
return R.ok(sysDeptService.listChildDeptId(deptId));
}
}

View File

@ -0,0 +1,51 @@
package com.pig4cloud.pig.admin.controller;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pig.admin.api.entity.SysUser;
import com.pig4cloud.pig.admin.service.MobileService;
import com.pig4cloud.pig.admin.service.SysUserService;
import com.pig4cloud.pig.common.core.util.R;
import com.pig4cloud.pig.common.security.annotation.Inner;
import io.swagger.annotations.Api;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lengleng
* @date 2021/9/16 移动端登录
*/
@RestController
@AllArgsConstructor
@RequestMapping("/mobile")
@Api(value = "mobile", tags = "手机管理模块")
public class MobileController {
private final MobileService mobileService;
private final SysUserService userService;
@Inner(value = false)
@GetMapping("/{mobile}")
public R sendSmsCode(@PathVariable String mobile) {
return mobileService.sendSmsCode(mobile);
}
/**
* 获取指定用户全部信息
* @param phone 手机号
* @return 用户信息
*/
@Inner
@GetMapping("/{phone}")
public R infoByPhone(@PathVariable String phone) {
SysUser user = userService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getPhone, phone));
if (user == null) {
return R.failed(String.format("用户信息为空 %s", phone));
}
return R.ok(userService.getUserInfo(user));
}
}

View File

@ -20,8 +20,10 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pig.admin.api.dto.UserDTO;
import com.pig4cloud.pig.admin.api.dto.UserInfo;
import com.pig4cloud.pig.admin.api.entity.SysUser;
import com.pig4cloud.pig.admin.api.vo.UserExcelVO;
import com.pig4cloud.pig.admin.api.vo.UserInfoVO;
import com.pig4cloud.pig.admin.service.SysUserService;
import com.pig4cloud.pig.common.core.util.R;
import com.pig4cloud.pig.common.log.annotation.SysLog;
@ -33,10 +35,19 @@ import io.swagger.annotations.Api;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
import java.util.Set;
/**
* @author lengleng
@ -61,7 +72,12 @@ public class UserController {
if (user == null) {
return R.failed("获取当前用户信息失败");
}
return R.ok(userService.getUserInfo(user));
UserInfo userInfo = userService.getUserInfo(user);
UserInfoVO vo = new UserInfoVO();
vo.setSysUser(userInfo.getSysUser());
vo.setRoles(userInfo.getRoles());
vo.setPermissions(userInfo.getPermissions());
return R.ok(vo);
}
/**
@ -78,6 +94,17 @@ public class UserController {
return R.ok(userService.getUserInfo(user));
}
/**
* 根据部门id查询对应的用户 id 集合
* @param deptIds 部门id 集合
* @return 用户 id 集合
*/
@Inner
@GetMapping("/ids")
public R<List<Integer>> listUserIdByDeptIds(@RequestParam("deptIds") Set<Integer> deptIds) {
return R.ok(userService.listUserIdByDeptIds(deptIds));
}
/**
* 通过ID查询用户信息
* @param id ID

View File

@ -0,0 +1,115 @@
package com.pig4cloud.pig.admin.datascope;
import cn.hutool.core.collection.CollectionUtil;
import com.pig4cloud.pig.common.security.datascope.UserDataScope;
import com.pig4cloud.pig.common.security.service.PigUser;
import com.pig4cloud.pig.common.security.util.SecurityUtils;
import com.pigcloud.pig.common.datascope.DataScope;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
/**
* 数据权限控制要求表至少有个 user_id 字段
*
* @author hccake
*/
@Component
public class PigDataScope implements DataScope {
private static final String USER_ID = "user_id";
private static final String DEPT_ID = "dept_id";
/**
* 拥有 dept_id 字段的表名集合
*/
private static final Set<String> DEPT_ID_TABLE_NAMES = CollectionUtil.newHashSet("sys_user");
/**
* 仅拥有 user_id 字段的表名集合
*/
private static final Set<String> USER_ID_TABLE_NAMES = CollectionUtil.newHashSet();
private final Set<String> tableNames;
public PigDataScope() {
Set<String> set = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
set.addAll(DEPT_ID_TABLE_NAMES);
set.addAll(USER_ID_TABLE_NAMES);
this.tableNames = Collections.unmodifiableSet(set);
}
@Override
public String getResource() {
return "userData";
}
@Override
public Collection<String> getTableNames() {
return this.tableNames;
}
@Override
public Expression getExpression(String tableName, Alias tableAlias) {
// 获取当前登录用户
PigUser user = SecurityUtils.getUser();
if (user == null) {
return null;
}
UserDataScope userDataScope = user.getUserDataScope();
// 如果数据权限是全部直接放行
if (userDataScope.isAllScope()) {
return null;
}
// 如果数据权限是仅自己
if (userDataScope.isOnlySelf()) {
// 数据权限规则where user_id = xx
return userIdEqualsToExpression(tableAlias, user.getId());
}
// 如果当前表有部门字段则优先使用部门字段控制范围
if (DEPT_ID_TABLE_NAMES.contains(tableName)) {
// 数据权限规则where (user_id =xx or dept_id in ("x""y"))
EqualsTo equalsTo = userIdEqualsToExpression(tableAlias, user.getId());
Expression inExpression = getInExpression(tableAlias, DEPT_ID, userDataScope.getScopeDeptIds());
// 这里一定要加括号否则如果有其他查询条件or 会出问题
return new Parenthesis(new OrExpression(equalsTo, inExpression));
}
else {
// 数据权限规则where user_id in ("x""y")
return getInExpression(tableAlias, USER_ID, userDataScope.getScopeUserIds());
}
}
private EqualsTo userIdEqualsToExpression(Alias tableAlias, Integer userId) {
Column column = new Column(tableAlias == null ? USER_ID : tableAlias.getName() + "." + USER_ID);
return new EqualsTo(column, new LongValue(userId));
}
private Expression getInExpression(Alias tableAlias, String columnName, Set<Integer> scopeUserIds) {
Column column = new Column(tableAlias == null ? columnName : tableAlias.getName() + "." + columnName);
ExpressionList expressionList = new ExpressionList();
List<Expression> list = scopeUserIds.stream().map(LongValue::new).collect(Collectors.toList());
expressionList.setExpressions(list);
return new InExpression(column, expressionList);
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.admin.service;
import com.pig4cloud.pig.common.core.util.R;
/**
* @author lengleng
* @date 2018/11/14
*/
public interface MobileService {
/**
* 发送手机验证码
* @param mobile mobile
* @return code
*/
R<Boolean> sendSmsCode(String mobile);
}

View File

@ -0,0 +1,84 @@
/*
* 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.admin.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pig.admin.api.entity.SysUser;
import com.pig4cloud.pig.admin.mapper.SysUserMapper;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.constant.enums.LoginTypeEnum;
import com.pig4cloud.pig.common.core.util.R;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author lengleng
* @date 2018/11/14
* <p>
* 手机登录相关业务实现
*/
@Slf4j
@Service
@AllArgsConstructor
public class MobileServiceImpl implements MobileService {
private final RedisTemplate redisTemplate;
private final SysUserMapper userMapper;
/**
* 发送手机验证码 TODO: 调用短信网关发送验证码,测试返回前端
* @param mobile mobile
* @return code
*/
@Override
public R<Boolean> sendSmsCode(String mobile) {
List<SysUser> userList = userMapper
.selectList(Wrappers.<SysUser>query().lambda().eq(SysUser::getPhone, mobile));
if (CollUtil.isEmpty(userList)) {
log.info("手机号未注册:{}", mobile);
return R.ok(Boolean.FALSE, "手机号未注册");
}
Object codeObj = redisTemplate.opsForValue()
.get(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.SMS.getType() + StringPool.AT + mobile);
if (codeObj != null) {
log.info("手机号验证码未过期:{}{}", mobile, codeObj);
return R.ok(Boolean.FALSE, "验证码发送过频繁");
}
String code = RandomUtil.randomNumbers(Integer.parseInt(SecurityConstants.CODE_SIZE));
log.debug("手机号生成验证码成功:{},{}", mobile, code);
redisTemplate.opsForValue().set(
CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.SMS.getType() + StringPool.AT + mobile, code,
SecurityConstants.CODE_TIME, TimeUnit.SECONDS);
return R.ok(Boolean.TRUE, code);
}
}

View File

@ -65,4 +65,11 @@ public interface SysDeptService extends IService<SysDept> {
*/
Boolean updateDeptById(SysDept sysDept);
/**
* 查找指定部门的子部门id列表
* @param deptId 部门id
* @return List<Integer>
*/
List<Integer> listChildDeptId(Integer deptId);
}

View File

@ -28,6 +28,7 @@ import com.pig4cloud.pig.common.core.util.R;
import org.springframework.validation.BindingResult;
import java.util.List;
import java.util.Set;
/**
* @author lengleng
@ -107,4 +108,11 @@ public interface SysUserService extends IService<SysUser> {
*/
R importUser(List<UserExcelVO> excelVOList, BindingResult bindingResult);
/**
* 根据部门 id 列表查询对应的用户 id 集合
* @param deptIds 部门 id 列表
* @return userIdList
*/
List<Integer> listUserIdByDeptIds(Set<Integer> deptIds);
}

View File

@ -33,6 +33,7 @@ import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@ -108,6 +109,16 @@ public class SysDeptServiceImpl extends ServiceImpl<SysDeptMapper, SysDept> impl
return Boolean.TRUE;
}
@Override
public List<Integer> listChildDeptId(Integer deptId) {
List<SysDeptRelation> deptRelations = sysDeptRelationService.list(Wrappers.<SysDeptRelation>lambdaQuery()
.eq(SysDeptRelation::getAncestor, deptId).ne(SysDeptRelation::getDescendant, deptId));
if (CollUtil.isNotEmpty(deptRelations)) {
return deptRelations.stream().map(SysDeptRelation::getDescendant).collect(Collectors.toList());
}
return new ArrayList<>();
}
/**
* 查询全部部门树
* @return

View File

@ -25,7 +25,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pig.admin.api.dto.UserDTO;
import com.pig4cloud.pig.admin.api.dto.UserInfo;
import com.pig4cloud.pig.admin.api.entity.*;
import com.pig4cloud.pig.admin.api.entity.SysDept;
import com.pig4cloud.pig.admin.api.entity.SysMenu;
import com.pig4cloud.pig.admin.api.entity.SysRole;
import com.pig4cloud.pig.admin.api.entity.SysUser;
import com.pig4cloud.pig.admin.api.entity.SysUserRole;
import com.pig4cloud.pig.admin.api.vo.UserExcelVO;
import com.pig4cloud.pig.admin.api.vo.UserVO;
import com.pig4cloud.pig.admin.mapper.SysDeptMapper;
@ -107,9 +111,11 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
public UserInfo getUserInfo(SysUser sysUser) {
UserInfo userInfo = new UserInfo();
userInfo.setSysUser(sysUser);
// 设置角色列表
List<SysRole> roleList = sysRoleMapper.listRolesByUserId(sysUser.getUserId());
userInfo.setRoleList(roleList);
// 设置角色列表 ID
List<Integer> roleIds = sysRoleMapper.listRolesByUserId(sysUser.getUserId()).stream().map(SysRole::getRoleId)
.collect(Collectors.toList());
List<Integer> roleIds = roleList.stream().map(SysRole::getRoleId).collect(Collectors.toList());
userInfo.setRoles(ArrayUtil.toArray(roleIds, Integer.class));
// 设置权限列表menu.permission
Set<String> permissions = sysMenuService.findMenuByRoleId(CollUtil.join(roleIds, StrUtil.COMMA)).stream()
@ -117,6 +123,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
.filter(m -> StrUtil.isNotBlank(m.getPermission())).map(SysMenu::getPermission)
.collect(Collectors.toSet());
userInfo.setPermissions(ArrayUtil.toArray(permissions, String.class));
return userInfo;
}
@ -295,6 +302,13 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
return R.ok();
}
@Override
public List<Integer> listUserIdByDeptIds(Set<Integer> deptIds) {
return this.listObjs(
Wrappers.lambdaQuery(SysUser.class).select(SysUser::getUserId).in(SysUser::getDeptId, deptIds),
Integer.class::cast);
}
/**
* 插入excel User
*/

View File

@ -1,19 +1,15 @@
FROM moxm/java:1.8-full as builder
WORKDIR /build
ARG JAR_FILE=target/pig-codegen.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract && rm app.jar
FROM moxm/java:1.8-full
LABEL maintainer="jclazz@outlook.com"
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
WORKDIR pig-codegen
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/snapshot-dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/application/ ./
RUN mkdir -p /pig-codegen
WORKDIR /pig-codegen
ARG JAR_FILE=target/pig-codegen.jar
COPY ${JAR_FILE} app.jar
EXPOSE 5002
CMD sleep 60; java $JAVA_OPTS org.springframework.boot.loader.JarLauncher
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java -jar app.jar $JAVA_OPTS

View File

@ -1,19 +1,15 @@
FROM moxm/java:1.8-full as builder
WORKDIR /build
ARG JAR_FILE=target/pig-monitor.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract && rm app.jar
FROM moxm/java:1.8-full
LABEL maintainer="jclazz@outlook.com"
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
WORKDIR pig-monitor
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/snapshot-dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/application/ ./
RUN mkdir -p /pig-monitor
WORKDIR /pig-monitor
ARG JAR_FILE=target/pig-monitor.jar
COPY ${JAR_FILE} app.jar
EXPOSE 5001
CMD sleep 60; java $JAVA_OPTS org.springframework.boot.loader.JarLauncher
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java -jar app.jar $JAVA_OPTS

View File

@ -1,19 +1,15 @@
FROM moxm/java:1.8-full as builder
WORKDIR /build
ARG JAR_FILE=target/pig-sentinel-dashboard.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract && rm app.jar
FROM moxm/java:1.8-full
LABEL maintainer="jclazz@outlook.com"
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
WORKDIR pig-sentinel-dashboard
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/snapshot-dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/application/ ./
RUN mkdir -p /pig-sentinel-dashboard
WORKDIR /pig-sentinel-dashboard
ARG JAR_FILE=target/pig-sentinel-dashboard.jar
COPY ${JAR_FILE} app.jar
EXPOSE 5003
CMD sleep 60; java $JAVA_OPTS org.springframework.boot.loader.JarLauncher
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java -jar app.jar $JAVA_OPTS

View File

@ -1,19 +1,15 @@
FROM moxm/java:1.8-full as builder
WORKDIR /build
ARG JAR_FILE=target/pig-xxl-job-admin.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract && rm app.jar
FROM moxm/java:1.8-full
LABEL maintainer="jclazz@outlook.com"
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
WORKDIR pig-xxl-job-admin
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/snapshot-dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/application/ ./
RUN mkdir -p /pig-xxl-job-admin
WORKDIR /pig-xxl-job-admin
ARG JAR_FILE=target/pig-xxl-job-admin.jar
COPY ${JAR_FILE} app.jar
EXPOSE 5004
CMD sleep 60; java $JAVA_OPTS org.springframework.boot.loader.JarLauncher
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
CMD sleep 60; java -jar app.jar $JAVA_OPTS

View File

@ -47,12 +47,6 @@
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- mybatis-startermybatis + mybatis-spring + hikaridefault -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>