fix(*) 首次提交General项目

This commit is contained in:
2023-08-23 16:37:12 +08:00
parent 31fb0398bb
commit 3e05e32d4b
20 changed files with 1183 additions and 0 deletions

114
Gateway/pom.xml Normal file
View File

@ -0,0 +1,114 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.crtech.cloud.gateway</groupId>
<artifactId>Gateway</artifactId>
<version>1.0.1</version>
<!-- 父工程 -->
<parent>
<groupId>cn.crtech.cloud.dependencies</groupId>
<artifactId>Dependencies</artifactId>
<version>1.0.1</version>
<relativePath/>
</parent>
<!-- 依赖的版本锁定 -->
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<common.version>1.0.1</common.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.crtech.cloud.common</groupId>
<artifactId>Common</artifactId>
<version>${common.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- nacos 客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<!-- 自定义的元数据依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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<AuthorizationContext> {
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> 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<String> 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<String> 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<MisAppDto> 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<String> getFreeAuthority(MisAppDto thisApp, String clientId, String requestURI, List<String> 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<String> getAppFeign(String realPath, List<String> authorities) {
List<String> 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<String> getAuthorityData(MisAppDto thisApp, String redisName, String realPath, List<String> authorities) {
List<String> 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] : "";
}
}

View File

@ -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<Void> 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));
}
}

View File

@ -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<Void> 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));
}
}

View File

@ -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);
}
}

View File

@ -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<String> urls;
}

View File

@ -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<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> 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;
}
}

View File

@ -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<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> 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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<MisPopedomAuthorityDto> authorityList;
@Override
public Boolean isRoot() {
return ObjectUtils.isEmpty(this.parentId);
}
}

View File

@ -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<String> roles;
@DataExportAnnotation("用户所有公司数据集合")
private List<MisComUserDto> companys;
@DataExportAnnotation("当前选中产品")
private MisAppDto application;
@DataExportAnnotation("用户所有授权产品数据集合")
private List<MisAppDto> applications;
@DataExportAnnotation("用户应用授权路由菜单")
private List<MisPopedomDto> routes;
}

View File

@ -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<Void> 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;
}
}

View File

@ -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<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
PathMatcher pathMatcher = new AntPathMatcher();
//白名单路径移除JWT请求头
List<String> 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);
}
}

View File

@ -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/**"

View File

@ -0,0 +1,6 @@
spring:
profiles:
active: dev

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/crtech-cloud-gateway.%d{yyyy-MM-dd}.log
</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd_HH:mm:ss} %logger{18} -%msg%n
</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>1000</queueSize>
<appender-ref ref="FILE" />
</appender>
<logger name="org" level="info" additivity="false">
<appender-ref ref="FILE"></appender-ref>
<appender-ref ref="STDOUT"></appender-ref>
</logger>
<logger name="com" level="info" additivity="false">
<appender-ref ref="FILE"></appender-ref>
<appender-ref ref="STDOUT"></appender-ref>
</logger>
<logger name="net" level="info" additivity="false">
<appender-ref ref="FILE"></appender-ref>
<appender-ref ref="STDOUT"></appender-ref>
</logger>
<logger name="com.netflix" level="debug" additivity="false">
<appender-ref ref="STDOUT"></appender-ref>
<appender-ref ref="FILE"></appender-ref>
</logger>
<logger name="cn.crtech.cloud.gateway" level="debug" additivity="false">
<appender-ref ref="STDOUT"></appender-ref>
<appender-ref ref="FILE"></appender-ref>
</logger>
<root level="INFO">
<appender-ref ref="ASYNC" />
</root>
</configuration>