diff --git a/.gitignore b/.gitignore index ae31e7f..da05752 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ /Auth/src/main/resources/bootstrap-test.yml /Feign/src/main/resources/application-test.yml /Feign/src/main/resources/bootstrap-test.yml +/Gateway/src/main/resources/application-test.yml diff --git a/Gateway/pom.xml b/Gateway/pom.xml new file mode 100644 index 0000000..2ceb7a4 --- /dev/null +++ b/Gateway/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + cn.crtech.cloud.gateway + Gateway + 1.0.1 + + + + cn.crtech.cloud.dependencies + Dependencies + 1.0.1 + + + + + + 8 + 8 + 1.0.1 + + + + + cn.crtech.cloud.common + Common + ${common.version} + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + org.apache.commons + commons-lang3 + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + org.springframework.security + spring-security-config + + + + org.springframework.security + spring-security-oauth2-resource-server + + + + org.springframework.security + spring-security-oauth2-client + + + + org.springframework.security + spring-security-oauth2-jose + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + org.springframework.boot + spring-boot-starter-data-redis + + + io.lettuce + lettuce-core + + + + + + redis.clients + jedis + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + -Dfile.encoding=UTF-8 + + + + + diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/GatewayApplication.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/GatewayApplication.java new file mode 100644 index 0000000..d00fc31 --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/GatewayApplication.java @@ -0,0 +1,20 @@ +package cn.crtech.cloud.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * Author : yj + * Date : 2020-12-31 + * Description: + */ + +@EnableDiscoveryClient +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +public class GatewayApplication { + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class); + } +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/authorization/AuthorizationManager.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/authorization/AuthorizationManager.java new file mode 100644 index 0000000..b24728f --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/authorization/AuthorizationManager.java @@ -0,0 +1,282 @@ +package cn.crtech.cloud.gateway.authorization; + +import cn.crtech.cloud.common.constant.AuthConstant; +import cn.crtech.cloud.common.constant.RedisConstant; +import cn.crtech.cloud.gateway.dto.MisAppDto; +import cn.crtech.cloud.gateway.dto.MisUserInfoDto; +import cn.crtech.cloud.gateway.filter.AuthGlobalFilter; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONObject; +import com.nimbusds.jose.JWSObject; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.server.authorization.AuthorizationContext; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 自定义的鉴权管理器,用于判断是否有资源的访问权限 + */ + +@Component +public class AuthorizationManager implements ReactiveAuthorizationManager { + private RedisTemplate redisTemplate; + + @Autowired + private void setRedisTemplate(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class); + + @Override + public Mono check(Mono authentication, AuthorizationContext authorizationContext) { + //获取请求URI + URI uri = authorizationContext.getExchange().getRequest().getURI(); + String realPath = uri.getPath(); + //获取当前登录的用户信息 + ServerHttpRequest request = authorizationContext.getExchange().getRequest(); + String token = request.getHeaders().getFirst("Authorization"); + List userAuthorities = new ArrayList<>(); + if (token != null) { + try { + String realToken = token.replace("Bearer ", ""); + JWSObject jwsObject = JWSObject.parse(realToken); + String userStr = jwsObject.getPayload().toString(); + JSONObject userData = JSONObject.parseObject(userStr); + + Boolean isCompanyAdmin = userData.getBoolean("company_admin"); + List authorities = JSONObject.parseArray(userData.getJSONArray("authorities").toString(), String.class); + String clientId = getAppClientId(realPath); + String requestURI = getRealPath(realPath, clientId); + Integer userId = userData.getInteger("id"); + MisAppDto thisApp = getThisApp(userId, clientId); + + // 获取免费授权权限数据 + userAuthorities = getFreeAuthority(thisApp, clientId, requestURI, authorities); + + // 获取服务间调用免费授权权限 + userAuthorities.addAll(getAppFeign(requestURI, authorities)); + + // 其他相关权限校验处理逻辑 + if (isCompanyAdmin) { + // 获取企业管理员权限 + userAuthorities.addAll(getAuthorityData(thisApp, RedisConstant.RESOURCE_ADMIN_MAP, requestURI, authorities)); + } else { + // 获取企业角色权限 + userAuthorities.addAll(getAuthorityData(thisApp, RedisConstant.RESOURCE_ADMIN_MAP, requestURI, authorities)); + // 获取产品默认角色权限 + userAuthorities.addAll(getAuthorityData(thisApp, RedisConstant.DEFAULT_ROLES_MAP, requestURI, authorities)); + } + + //所有的角色前面增加 “ROLE_” + userAuthorities = userAuthorities.stream() + .map(i -> i = AuthConstant.AUTHORITY_PREFIX + i) + .collect(Collectors.toList()); + //认证通过且角色匹配的用户可访问当前路径 + LOGGER.info("AuthorizationManager.check() authorities:{}", userAuthorities); + } catch (ParseException e) { + e.printStackTrace(); + } + } + + //双冒号方法引用 + return authentication + .filter(Authentication::isAuthenticated) + .flatMapIterable(Authentication::getAuthorities) + .map(GrantedAuthority::getAuthority) + .any(userAuthorities::contains) + .map(AuthorizationDecision::new) + .defaultIfEmpty(new AuthorizationDecision(false)); + } + + /** + * 获取当前请求所在产品信息 + * + * @param userId 用户ID + * @param clientId 产品标识 + * @return 返回结果 + */ + private MisAppDto getThisApp(Integer userId, String clientId) { + // 获取缓存用户登录授权信息 + Object userInfoObj = redisTemplate.opsForValue().get(RedisConstant.CURRENT_USREINFO + userId); + if (ObjectUtils.isEmpty(userInfoObj)) { + return null; + } + MisUserInfoDto userInfo = JSONObject.parseObject(userInfoObj.toString(), MisUserInfoDto.class); + + // 获取当前授权产品详细数据 + if (CollectionUtils.isEmpty(userInfo.getApplications())) { + return null; + } + List freeAppList = userInfo.getApplications().stream() + .filter(item -> item.getCode().equals(clientId)) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(freeAppList)) { + return null; + } + return freeAppList.get(0); + } + + /** + * 获取登录用户授权产品免费过滤权限内容 + * + * @param thisApp 产品信息对象 + * @param requestURI 接口请求地址 + * @param authorities 已有角色列表 + * @return 返回处理结果 + */ + public List getFreeAuthority(MisAppDto thisApp, String clientId, String requestURI, List authorities) { + if (ObjectUtils.isEmpty(thisApp)) { + return new ArrayList<>(); + } + + // 缓存获取对应免费授权数据 + String freeAppAuthorityName = RedisConstant.FREE_VERSION_API + .replace("${SERIESCODE}", thisApp.getSeriesCode()) + .replace("${APPCODE}", clientId) + .replace("${VERSION}", thisApp.getVersion()); + + // 处理请求路由(去除前缀及端口) + String realPath = getRealPath(requestURI, clientId); + + return getAuthorityData(thisApp, freeAppAuthorityName, realPath, authorities); + } + + /** + * 获取服务间调用免费授权权限 + * + * @param realPath 实际请求路由 + * @param authorities 已有角色数据列表 + * @return 返回校验处理结果 + */ + public List getAppFeign(String realPath, List authorities) { + List userAuthorities = new ArrayList<>(); + Object authorityObj = redisTemplate.opsForHash().get(RedisConstant.APP_FEIGN_API_MAP, realPath); + + // 如果全部url没有设置权限,就看有没有带*的权限 + if (ObjectUtil.isEmpty(authorityObj)) { + String paths = realPath; + boolean first = true; + while (paths.indexOf('/') >= 0 && ObjectUtil.isEmpty(authorityObj)) { + paths = realPath.substring(0, paths.lastIndexOf("/")); + if (first) { + first = false; + authorityObj = redisTemplate.opsForHash().get(RedisConstant.APP_FEIGN_API_MAP, paths + "/*"); + if (!ObjectUtil.isEmpty(authorityObj)) break; + } + authorityObj = redisTemplate.opsForHash().get(RedisConstant.APP_FEIGN_API_MAP, paths + "/**"); + } + } + + // 处理可请求的权限 + if (!ObjectUtil.isEmpty(authorityObj)) { + assert authorityObj != null; + String[] requestAuths = authorityObj.toString().split(","); + for (String requestAuth : requestAuths) { + for (String userAuth : authorities) { + if (requestAuth.equals(userAuth)) { + userAuthorities.add(userAuth); + } + } + } + } + return userAuthorities; + } + + /** + * 获取用户权限内容并进行校验处理 + * + * @param thisApp 当前请求产品信息 + * @param redisName 缓存名称 + * @param realPath 实际请求路由 + * @param authorities 已有角色数据列表 + * @return 返回校验处理结果 + */ + public List getAuthorityData(MisAppDto thisApp, String redisName, String realPath, List authorities) { + List userAuthorities = new ArrayList<>(); + Object authorityObj = redisTemplate.opsForHash().get(redisName, realPath); + + // 查询缓存判断此请求是否为特殊权限请求 如果是则进行下一步处理 否则进行下方内容处理 + boolean isLimitApi = false; + if (ObjectUtils.isNotEmpty(thisApp)) { + String valueName = thisApp.getSeriesCode() + "_" + thisApp.getCode(); + Object limitObj = redisTemplate.opsForHash().get(RedisConstant.LIMIT_AUTHORITY_API_MAP, realPath); + isLimitApi = (ObjectUtils.isNotEmpty(limitObj) && valueName.equals(limitObj.toString())); + } + + // 如果全部url没有设置权限,就看有没有带*的权限 + if (ObjectUtil.isEmpty(authorityObj) && !isLimitApi) { + String paths = realPath; + boolean first = true; + while (paths.indexOf('/') >= 0 && ObjectUtil.isEmpty(authorityObj)) { + paths = realPath.substring(0, paths.lastIndexOf("/")); + if (first) { + first = false; + authorityObj = redisTemplate.opsForHash().get(redisName, paths + "/*"); + if (!ObjectUtil.isEmpty(authorityObj)) break; + } + authorityObj = redisTemplate.opsForHash().get(redisName, paths + "/**"); + } + } + + LOGGER.info("可请求的权限 ===> " + authorityObj); + LOGGER.info("当前用户权限 ===> " + authorities); + + // 处理可请求的权限 + if (!ObjectUtil.isEmpty(authorityObj)) { + assert authorityObj != null; + String[] requestAuths = authorityObj.toString().split(","); + for (String requestAuth : requestAuths) { + for (String userAuth : authorities) { + if (requestAuth.equals(userAuth)) { + userAuthorities.add(userAuth); + } + } + } + } + return userAuthorities; + } + + /** + * 获取请求实际有效地址 + * + * @param requestURI 请求地址 + * @param clientId 产品标识 + * @return 返回处理结果 + */ + public String getRealPath(String requestURI, String clientId) { + if (StringUtils.isAnyBlank(requestURI, clientId)) { + throw new NullPointerException("请求地址或产品标识为空"); + } + int splitIndex = requestURI.indexOf("/" + clientId); + return splitIndex > -1 ? requestURI.substring(splitIndex) : requestURI; + } + + /** + * 获取发起请求实际产品标识 + * + * @param requestURI 请求API + * @return 返回处理结果 + */ + public String getAppClientId(String requestURI) { + String[] splitList = requestURI.split("\\/"); + return splitList.length > 0 ? splitList[1] : ""; + } +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/component/RestAuthenticationEntryPoint.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/component/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..e6794d1 --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/component/RestAuthenticationEntryPoint.java @@ -0,0 +1,33 @@ +package cn.crtech.cloud.gateway.component; + +import cn.crtech.cloud.common.api.CommonResult; +import cn.hutool.json.JSONUtil; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.nio.charset.Charset; + +/** + * 自定义返回结果:没有登录或token过期时 + */ + +@Component +public class RestAuthenticationEntryPoint implements ServerAuthenticationEntryPoint { + @Override + public Mono commence(ServerWebExchange exchange, AuthenticationException e) { + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(HttpStatus.OK); + response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + String body = JSONUtil.toJsonStr(CommonResult.unauthorized(e.getMessage())); + DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8"))); + return response.writeWith(Mono.just(buffer)); + } +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/component/RestfulAccessDeniedHandler.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/component/RestfulAccessDeniedHandler.java new file mode 100644 index 0000000..63e07a1 --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/component/RestfulAccessDeniedHandler.java @@ -0,0 +1,33 @@ +package cn.crtech.cloud.gateway.component; + +import cn.crtech.cloud.common.api.CommonResult; +import cn.hutool.json.JSONUtil; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +/** + * 自定义返回结果:没有权限访问时 + */ + +@Component +public class RestfulAccessDeniedHandler implements ServerAccessDeniedHandler { + @Override + public Mono handle(ServerWebExchange exchange, AccessDeniedException denied) { + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(HttpStatus.OK); + response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + String body = JSONUtil.toJsonStr(CommonResult.forbidden(denied.getMessage())); + DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); + return response.writeWith(Mono.just(buffer)); + } +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/config/CorsConfig.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/config/CorsConfig.java new file mode 100644 index 0000000..d9b61c5 --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/config/CorsConfig.java @@ -0,0 +1,31 @@ +package cn.crtech.cloud.gateway.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.gateway.config.GlobalCorsProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * Author : yj + * Date : 2021-03-12 + * Description:配置跨域 + */ + +@Configuration +@EnableConfigurationProperties(GlobalCorsProperties.class) +public class CorsConfig { + @Bean + @RefreshScope + @Order(Ordered.HIGHEST_PRECEDENCE) + public CorsWebFilter corsWebFilter(GlobalCorsProperties globalCorsProperties) { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); + globalCorsProperties.getCorsConfigurations().forEach((k, v) -> source.registerCorsConfiguration(k, v)); + return new CorsWebFilter(source); + } +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/config/IgnoreUrlsConfig.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/config/IgnoreUrlsConfig.java new file mode 100644 index 0000000..942adbf --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/config/IgnoreUrlsConfig.java @@ -0,0 +1,19 @@ +package cn.crtech.cloud.gateway.config; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 网关白名单配置 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Component +@ConfigurationProperties(prefix="secure.ignore") +public class IgnoreUrlsConfig { + private List urls; +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/config/RedisRepositoryConfig.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/config/RedisRepositoryConfig.java new file mode 100644 index 0000000..be800b4 --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/config/RedisRepositoryConfig.java @@ -0,0 +1,31 @@ +package cn.crtech.cloud.gateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis相关配置 + */ + +@Configuration +@EnableRedisRepositories +public class RedisRepositoryConfig { + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + redisTemplate.setKeySerializer(stringRedisSerializer); + redisTemplate.setHashKeySerializer(stringRedisSerializer); + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); + redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); + redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/config/ResourceServerConfig.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/config/ResourceServerConfig.java new file mode 100644 index 0000000..97e8617 --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/config/ResourceServerConfig.java @@ -0,0 +1,66 @@ +package cn.crtech.cloud.gateway.config; + +import cn.crtech.cloud.common.constant.AuthConstant; +import cn.crtech.cloud.gateway.authorization.AuthorizationManager; +import cn.crtech.cloud.gateway.component.RestAuthenticationEntryPoint; +import cn.crtech.cloud.gateway.component.RestfulAccessDeniedHandler; +import cn.crtech.cloud.gateway.filter.IgnoreUrlsRemoveJwtFilter; +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; +import org.springframework.security.web.server.SecurityWebFilterChain; +import reactor.core.publisher.Mono; + +/** + * 资源服务器配置 + */ + +@AllArgsConstructor +@Configuration +@EnableWebFluxSecurity +public class ResourceServerConfig { + private final AuthorizationManager authorizationManager; + private final IgnoreUrlsConfig ignoreUrlsConfig; + private final RestfulAccessDeniedHandler restfulAccessDeniedHandler; + private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; + private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter; + + @Bean + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http.oauth2ResourceServer().jwt() + .jwtAuthenticationConverter(jwtAuthenticationConverter()); + //自定义处理JWT请求头过期或签名错误的结果 + http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint); + //对白名单路径,直接移除JWT请求头 + http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION); + http.authorizeExchange() + .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(), String.class)).permitAll()//白名单配置 + .anyExchange().access(authorizationManager)//配置自定义的鉴权管理器 + .and().exceptionHandling() + .accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权 + // 无访问权限handler + .authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证 + .and().csrf().disable(); + return http.build(); + } + + @Bean + public Converter> jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX); + jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME); + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); + return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); + } +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisAppDto.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisAppDto.java new file mode 100644 index 0000000..6cea0d9 --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisAppDto.java @@ -0,0 +1,51 @@ +package cn.crtech.cloud.gateway.dto; + +import cn.crtech.cloud.common.annotation.DataExportAnnotation; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.io.Serializable; +import java.util.Date; +import lombok.*; + +/** + * 授权产品实体DTO + * + * @author TYP + * @since 2023-08-08 10:43 + */ + +@Data +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class MisAppDto implements Serializable { + @DataExportAnnotation("产品名称") + private String name; + + @DataExportAnnotation("产品标识") + private String code; + + @DataExportAnnotation("产品描述信息") + private String description; + + @DataExportAnnotation("是否默认展示在产品展示列表") + private Boolean isShow; + + @DataExportAnnotation("授权过期时间") + @JsonFormat(pattern = "YYYY-MM-dd", locale = "cn", timezone = "GMT+8") + private Date expireDate; + + @DataExportAnnotation("授权产品系列ID") + private Integer seriesId; + + @DataExportAnnotation("授权产品系列标识") + private String seriesCode; + + @DataExportAnnotation("授权产品版本ID") + private Integer versionId; + + @DataExportAnnotation("授权产品版本标识") + private String version; + +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisComUserDto.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisComUserDto.java new file mode 100644 index 0000000..9538e4d --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisComUserDto.java @@ -0,0 +1,56 @@ +package cn.crtech.cloud.gateway.dto; + +import cn.crtech.cloud.common.annotation.DataExportAnnotation; +import lombok.*; + +import java.io.Serializable; + +/** + * desc + * + * @author TYP + * @since 2023-08-08 10:20 + */ + +@Data +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class MisComUserDto implements Serializable { + @DataExportAnnotation("公司标识") + private String companyCode; + + @DataExportAnnotation("公司名称") + private String companyName; + + @DataExportAnnotation("公司图标") + private String logo; + + @DataExportAnnotation("所在省地址") + private String provinceName; + + @DataExportAnnotation("所在市地址") + private String cityName; + + @DataExportAnnotation("所在区/县地址") + private String countyName; + + @DataExportAnnotation("详细地址") + private String address; + + @DataExportAnnotation("用户ID") + private Integer userId; + + @DataExportAnnotation("是否公司所有人") + private Boolean isOwner; + + @DataExportAnnotation("职位") + private String position; + + @DataExportAnnotation("是否默认公司") + private Boolean isDefault; + + @DataExportAnnotation("状态") + private Integer state; +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisPopedomAuthorityDto.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisPopedomAuthorityDto.java new file mode 100644 index 0000000..1e1df54 --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisPopedomAuthorityDto.java @@ -0,0 +1,35 @@ +package cn.crtech.cloud.gateway.dto; + +import cn.crtech.cloud.common.annotation.DataExportAnnotation; + +import java.io.Serializable; +import lombok.*; + +/** + * 菜单权限功能实体 + * + * @author TYP + * @since 2023-07-17 14:42 + */ + +@Data +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class MisPopedomAuthorityDto implements Serializable { + @DataExportAnnotation("主键id") + private Integer id; + + @DataExportAnnotation("菜单ID") + private Integer popedomId; + + @DataExportAnnotation("权限名称") + private String name; + + @DataExportAnnotation("路由权限功能标识") + private String authorityCode; + + @DataExportAnnotation("产品标识") + private String applicationCode; +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisPopedomDto.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisPopedomDto.java new file mode 100644 index 0000000..ec1714c --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisPopedomDto.java @@ -0,0 +1,62 @@ +package cn.crtech.cloud.gateway.dto; + +import cn.crtech.cloud.common.annotation.DataExportAnnotation; +import cn.crtech.cloud.common.pojo.Tree; + +import java.io.Serializable; +import java.util.List; +import lombok.*; +import org.apache.commons.lang3.ObjectUtils; + +/** + * 产品版本实体 + * + * @author TYP + * @since 2023-07-14 14:48 + */ + +@Data +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class MisPopedomDto extends Tree implements Serializable { + @DataExportAnnotation("主键id") + private Integer id; + + @DataExportAnnotation("菜单名称") + private String name; + + @DataExportAnnotation("父级菜单ID") + private Integer parentId; + + @DataExportAnnotation("菜单路由跳转地址") + private String route; + + @DataExportAnnotation("菜单图标") + private String icon; + + @DataExportAnnotation("菜单图标(bootstrap专用)") + private String bIcon; + + @DataExportAnnotation("菜单类型 0菜单目录 1跳转菜单") + private Integer menuType; + + @DataExportAnnotation("菜单排序值") + private Integer orderNo; + + @DataExportAnnotation("路由菜单类型") + private Integer isMenu; + + @DataExportAnnotation("产品标识") + private String applicationCode; + + @DataExportAnnotation("产品特殊权限") + private List authorityList; + + @Override + public Boolean isRoot() { + return ObjectUtils.isEmpty(this.parentId); + } +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisUserInfoDto.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisUserInfoDto.java new file mode 100644 index 0000000..3c987d8 --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/dto/MisUserInfoDto.java @@ -0,0 +1,57 @@ +package cn.crtech.cloud.gateway.dto; + +import cn.crtech.cloud.common.annotation.DataExportAnnotation; + +import java.io.Serializable; +import java.util.List; +import lombok.*; + +/** + * 用户信息实体DTO + * + * @author TYP + * @since 2023-08-08 9:59 + */ + +@Data +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class MisUserInfoDto implements Serializable { + @DataExportAnnotation("用户ID") + private Integer id; + + @DataExportAnnotation("用户名称") + private String name; + + @DataExportAnnotation("用户邮箱地址") + private String email; + + @DataExportAnnotation("用户手机号码") + private String mobile; + + @DataExportAnnotation("用户所属公司标识") + private String companyCode; + + @DataExportAnnotation("是否所属公司拥有人") + private Boolean isOwner; + + @DataExportAnnotation("当前登录公司信息") + private MisComUserDto company; + + @DataExportAnnotation("已授权角色名字集合") + private List roles; + + @DataExportAnnotation("用户所有公司数据集合") + private List companys; + + @DataExportAnnotation("当前选中产品") + private MisAppDto application; + + @DataExportAnnotation("用户所有授权产品数据集合") + private List applications; + + @DataExportAnnotation("用户应用授权路由菜单") + private List routes; +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/filter/AuthGlobalFilter.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/filter/AuthGlobalFilter.java new file mode 100644 index 0000000..3d2ed05 --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/filter/AuthGlobalFilter.java @@ -0,0 +1,57 @@ +package cn.crtech.cloud.gateway.filter; + +import cn.hutool.core.util.StrUtil; +import com.nimbusds.jose.JWSObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.text.ParseException; + +/** + * 将登录用户的JWT转化成用户信息的全局过滤器 + */ +@Component +public class AuthGlobalFilter implements GlobalFilter, Ordered { + private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + String token = exchange.getRequest().getHeaders().getFirst("Authorization"); + if (StrUtil.isEmpty(token)) { + return chain.filter(exchange); + } + try { + //从token中解析用户信息并设置到Header中去 + String realToken = token.replace("Bearer ", ""); + JWSObject jwsObject = JWSObject.parse(realToken); + String userStr = jwsObject.getPayload().toString(); + LOGGER.info("AuthGlobalFilter.filter() user:{}", userStr); + String userStrEncode = null; + try { + userStrEncode = URLEncoder.encode(userStr, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStrEncode).build(); + exchange = exchange.mutate().request(request).build(); + } catch (ParseException e) { + e.printStackTrace(); + } + return chain.filter(exchange); + } + + @Override + public int getOrder() { + return 0; + } +} diff --git a/Gateway/src/main/java/cn/crtech/cloud/gateway/filter/IgnoreUrlsRemoveJwtFilter.java b/Gateway/src/main/java/cn/crtech/cloud/gateway/filter/IgnoreUrlsRemoveJwtFilter.java new file mode 100644 index 0000000..2c8372c --- /dev/null +++ b/Gateway/src/main/java/cn/crtech/cloud/gateway/filter/IgnoreUrlsRemoveJwtFilter.java @@ -0,0 +1,46 @@ +package cn.crtech.cloud.gateway.filter; + +import cn.crtech.cloud.gateway.config.IgnoreUrlsConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.util.List; + +/** + * 白名单路径访问时需要移除JWT请求头 + */ + +@Component +public class IgnoreUrlsRemoveJwtFilter implements WebFilter { + private IgnoreUrlsConfig ignoreUrlsConfig; + + @Autowired + private void setIgnoreUrlsConfig(IgnoreUrlsConfig ignoreUrlsConfig) { + this.ignoreUrlsConfig = ignoreUrlsConfig; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + URI uri = request.getURI(); + PathMatcher pathMatcher = new AntPathMatcher(); + //白名单路径移除JWT请求头 + List ignoreUrls = ignoreUrlsConfig.getUrls(); + for (String ignoreUrl : ignoreUrls) { + if (pathMatcher.match(ignoreUrl, uri.getPath())) { + request = exchange.getRequest().mutate().header("Authorization", "").build(); + exchange = exchange.mutate().request(request).build(); + return chain.filter(exchange); + } + } + return chain.filter(exchange); + } +} diff --git a/Gateway/src/main/resources/application-dev.yml b/Gateway/src/main/resources/application-dev.yml new file mode 100644 index 0000000..960bc3f --- /dev/null +++ b/Gateway/src/main/resources/application-dev.yml @@ -0,0 +1,127 @@ +server: + port: 7001 + servlet: + encoding: + charset: utf-8 + enabled: true + force: true + tomcat: + uri-encoding: UTF-8 +logging: + config: classpath:logback.xml + file: + path: logs/crtech-cloud-gateway.log + level: + cn.crtech.cloud.gateway: debug +spring: + application: + name: crtech-cloud-gateway + cloud: + nacos: + discovery: + server-addr: localhost:8848 + gateway: + routes: #配置路由规则 短横线必须对齐routes + - id: auth-route + uri: lb://crtech-cloud-auth + predicates: + - Path=/auth/** + filters: + - StripPrefix=1 + - id: general-route + uri: lb://crtech-cloud-general + predicates: + - Path=/general/** + filters: + - StripPrefix=1 + - id: rm-route + uri: lb://crtech-cloud-resmanager + predicates: + - Path=/rm/** + filters: + - StripPrefix=1 + - id: mis-route + uri: lb://crtech-cloud-mis + predicates: + - Path=/mis/** + filters: + - StripPrefix=1 + - id: lms-route + uri: lb://crtech-cloud-lms + predicates: + - Path=/lms/** + filters: + - StripPrefix=1 + - id: pivas-kms-route + uri: lb://crtech-cloud-pivas-kms + predicates: + - Path=/pivaskms/** + filters: + - StripPrefix=1 + - id: crtech-cloud-pivas-tm-route + uri: lb://crtech-cloud-pivas-tm + predicates: + - Path=/pivastm/** + filters: + - StripPrefix=1 + - id: crtech-cloud-main-route + uri: lb://crtech-cloud-main + predicates: + - Path=/main/** + filters: + - StripPrefix=1 + - id: pivas-customer-route + uri: lb://crtech-cloud-customer + predicates: + - Path=/customer/** + filters: + - StripPrefix=1 + - id: lserp-invoice-route + uri: lb://lserp-invoice + predicates: + - Path=/lserp-invoice/** + filters: + - StripPrefix=1 + discovery: + locator: + enabled: true #开启从注册中心动态创建路由的功能 + lower-case-service-id: true #使用小写服务名,默认是大写 + globalcors: + add-to-simple-url-handler-mapping: true + corsConfigurations: + '[/**]': + # 支持跨域访问的来源 + allowedOrigins: "*" + # 切记 allowCredentials 配置 为true时,allowedOrigins不能为 * + #allowCredentials: true + # 浏览器跨域嗅探间隔 单位秒 + maxAge: 86400 + # 支持的方法 * 代表所有 + allowedMethods: "*" + allowedHeaders: "*" + #exposedHeaders: "setToken" + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: 'http://localhost:9401/rsa/publicKey' #配置RSA的公钥访问地址 + redis: + database: 4 + port: 6379 + host: localhost + password: +secure: + ignore: + urls: #配置白名单路径, actuator spingboot的健康检查 + - "/actuator/**" + - "/customer/wx/cp/**" + - "/customer/wx/mp/**" + - "/customer/open/**" + - "/main/home/**" + - "/auth/oauth/token" + - "/auth/oauth/initRedis" + - "/auth/oauth/logout" + - "/mis/system/uploadAddressData" + - "/mis/system/uploadIconData" + - "/mis/ls/erp/**" + - "/lserp-invoice/no/auth/**" diff --git a/Gateway/src/main/resources/bootstrap.yml b/Gateway/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..1ab53e5 --- /dev/null +++ b/Gateway/src/main/resources/bootstrap.yml @@ -0,0 +1,6 @@ +spring: + profiles: + active: dev + + + diff --git a/Gateway/src/main/resources/logback.xml b/Gateway/src/main/resources/logback.xml new file mode 100644 index 0000000..80508da --- /dev/null +++ b/Gateway/src/main/resources/logback.xml @@ -0,0 +1,56 @@ + + + + + + logs/crtech-cloud-gateway.%d{yyyy-MM-dd}.log + + + + + %d{yyyy-MM-dd_HH:mm:ss} %logger{18} -%msg%n + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + 0 + 1000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file