重命名 orange-demo-uaa 为 orange-demo-multi-uaa

This commit is contained in:
orange-form
2021-12-21 03:03:53 +00:00
committed by Gitee
parent 30ea113bf3
commit 39a7514cc4
1254 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
package com.orangeforms.gateway;
import com.orangeforms.common.core.util.ApplicationContextHolder;
import com.orangeforms.gateway.filter.AuthenticationPostFilter;
import com.orangeforms.gateway.filter.AuthenticationPreFilter;
import com.orangeforms.gateway.filter.RequestLogFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 网关服务启动类。
*
* @author Jerry
* @date 2020-08-08
*/
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@SpringCloudApplication
public class GatewayApplication {
@RestController
@RequestMapping("/fallback")
static class FallbackController {
@GetMapping("")
public String fallback() {
return "GATEWAY FALLBACK!!!";
}
}
@Bean
public AuthenticationPreFilter authenticationPreFilter() {
return new AuthenticationPreFilter();
}
@Bean
public AuthenticationPostFilter authenticationPostFilter() {
return new AuthenticationPostFilter();
}
@Bean
public RequestLogFilter requestLogPreFilter() {
return new RequestLogFilter();
}
@Bean
ApplicationContextHolder applicationContextHolder() {
return new ApplicationContextHolder();
}
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

View File

@@ -0,0 +1,55 @@
package com.orangeforms.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
import java.util.Set;
/**
* 网关业务配置类。
*
* @author Jerry
* @date 2020-08-08
*/
@Data
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "application")
public class ApplicationConfig {
/**
* token加密用的密钥该值的长度最少10个字符(过短会报错)。
*/
private String tokenSigningKey;
/**
* 客户端或者浏览器在提交http请求时携带token的header name如 Authorization
*/
private String tokenHeaderKey;
/**
* 令牌Token在被刷新之后服务器Http应答的header name客户端或浏览器需要保存并替换原有的token用于下次发送时携带
*/
private String refreshedTokenHeaderKey;
/**
* 令牌的过期时间,单位毫秒
*/
private Long expiration;
/**
* 授信ip列表没有填写表示全部信任。多个ip之间逗号分隔如: http://10.10.10.1:8080,http://10.10.10.2:8080
*/
private String credentialIpList;
/**
* Session会话和用户权限在Redis中的过期时间(秒)。
* 缺省值是 one day
*/
private int sessionExpiredSeconds = 86400;
/**
* 基于完全等于(equals)判定规则的白名单地址集合过滤效率高于whitelistUrlPattern。
*/
private Set<String> whitelistUrl;
/**
* 基于Ant Pattern模式判定规则的白名单地址集合。如/aa/**。
*/
private Set<String> whitelistUrlPattern;
}

View File

@@ -0,0 +1,39 @@
package com.orangeforms.gateway.config;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* 跨域信任配置类。
*
* @author Jerry
* @date 2020-08-08
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter(ApplicationConfig appConfig) {
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(new PathPatternParser());
CorsConfiguration config = new CorsConfiguration();
if (StringUtils.isNotBlank(appConfig.getCredentialIpList())) {
String[] credentialIpList = StringUtils.split(appConfig.getCredentialIpList(), ",");
if (credentialIpList.length > 0) {
for (String ip : credentialIpList) {
config.addAllowedOrigin(ip);
}
}
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addExposedHeader(appConfig.getRefreshedTokenHeaderKey());
config.setAllowCredentials(true);
configSource.registerCorsConfiguration("/**", config);
}
return new CorsWebFilter(configSource);
}
}

View File

@@ -0,0 +1,30 @@
package com.orangeforms.gateway.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.nio.charset.StandardCharsets;
/**
* Web通用过滤器配置类。
*
* @author Jerry
* @date 2020-08-08
*/
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<Filter> characterEncodingFilterRegistration() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(
new org.springframework.web.filter.CharacterEncodingFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("encoding", StandardCharsets.UTF_8.name());
// forceEncoding强制response也被编码另外即使request中已经设置encodingforceEncoding也会重新设置
filterRegistrationBean.addInitParameter("forceEncoding", "true");
filterRegistrationBean.setAsyncSupported(true);
return filterRegistrationBean;
}
}

View File

@@ -0,0 +1,47 @@
package com.orangeforms.gateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
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.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
import java.util.*;
/**
* Spring Cloud Gateway的Sentinel流控配置类。
*
* @author Jerry
* @date 2020-08-08
*/
@Configuration
public class SentinelConfig {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public SentinelConfig(
ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
}

View File

@@ -0,0 +1,52 @@
package com.orangeforms.gateway.config;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
/***
* 返回Swagger UI需要读取的资源数据这里是微服务的路由数据。
*
* @author Knife4j Team。
* @date 2020-08-08
*/
@Slf4j
@Component
@Primary
@AllArgsConstructor
public class SwaggerResourceConfig implements SwaggerResourcesProvider {
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId()))
.forEach(route -> route.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
.replace("**", "v2/api-docs")))));
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}

View File

@@ -0,0 +1,46 @@
package com.orangeforms.gateway.constant;
/**
* 网关业务相关的常量对象。
*
* @author Jerry
* @date 2020-08-08
*/
public final class GatewayConstant {
/**
* 请求进入网关的开始时间。
*/
public static final String START_TIME_ATTRIBUTE = "startTime";
/**
* 登录URL。
*/
public static final String ADMIN_LOGIN_URL = "/admin/upms/login/doLogin";
/**
* UAA登录URL。
*/
public static final String ADMIN_LOGIN_BY_UAA_URL = "/admin/upms/login/doLoginByUaa";
/**
* 获取UAA登录验证的重定向URL。
*/
public static final String GET_UAA_LOGIN_URL = "/admin/upms/login/getUaaLoginUrl";
/**
* 登出URL。
*/
public static final String ADMIN_LOGOUT_URL = "/admin/upms/login/doLogout";
/**
* sessionId的键名称。
*/
public static final String SESSION_ID_KEY_NAME = "sessionId";
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private GatewayConstant() {
}
}

View File

@@ -0,0 +1,233 @@
package com.orangeforms.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.orangeforms.common.core.constant.ErrorCodeEnum;
import com.orangeforms.common.core.object.ResponseResult;
import com.orangeforms.common.core.object.TokenData;
import com.orangeforms.common.core.util.JwtUtil;
import com.orangeforms.common.core.util.MyCommonUtil;
import com.orangeforms.common.core.util.RedisKeyUtil;
import com.orangeforms.gateway.config.ApplicationConfig;
import com.orangeforms.gateway.constant.GatewayConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.redisson.api.RBucket;
import org.redisson.api.RSet;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.lang.NonNull;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 全局后处理过滤器。主要用于将用户的会话信息存到缓存服务器,以及在登出时清除缓存中的会话数据。
*
* @author Jerry
* @date 2020-08-08
*/
@Slf4j
public class AuthenticationPostFilter implements GlobalFilter, Ordered {
@Autowired
private ApplicationConfig appConfig;
@Autowired
private RedissonClient redissonClient;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest originalRequest = exchange.getRequest();
ServerHttpResponse originalResponse = exchange.getResponse();
String refreshedToken =
(String) exchange.getAttributes().get(appConfig.getRefreshedTokenHeaderKey());
if (refreshedToken != null) {
originalResponse.getHeaders().add(appConfig.getRefreshedTokenHeaderKey(), refreshedToken);
}
if (!originalRequest.getURI().getPath().equals(GatewayConstant.ADMIN_LOGIN_URL)
&& !originalRequest.getURI().getPath().equals(GatewayConstant.ADMIN_LOGIN_BY_UAA_URL)
&& !originalRequest.getURI().getPath().equals(GatewayConstant.ADMIN_LOGOUT_URL)) {
return chain.filter(exchange);
}
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@SuppressWarnings("unchecked")
@Override
public Mono<Void> writeWith(@NonNull Publisher<? extends DataBuffer> bodyData) {
StringBuilder sb = new StringBuilder(128);
sb.append("url: ")
.append(originalRequest.getURI().getPath())
.append(" -- status: ")
.append(getStatusCode());
if (getStatusCode() != HttpStatus.OK) {
log.error(sb.toString());
return super.writeWith(bodyData);
}
if (!(bodyData instanceof Flux)) {
return super.writeWith(bodyData);
}
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) bodyData;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 读取完整的服务应答消息体。
String responseBody = readResponseBody(dataBuffers);
originalResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);
// 先判断body中是否包含数据。
if (StringUtils.isBlank(responseBody)) {
sb.append(" -- Internal Error, no RESPONSE DATA returns !!");
log.error(sb.toString());
String errorMessage = "后台服务没有任何数据返回!";
responseBody = JSON.toJSONString(
ResponseResult.error(ErrorCodeEnum.SERVER_INTERNAL_ERROR, errorMessage));
byte[] uppedContent = new String(responseBody.getBytes(), StandardCharsets.UTF_8).getBytes();
originalResponse.getHeaders().setContentLength(uppedContent.length);
return bufferFactory.wrap(uppedContent);
}
// 处理登录和登出请求。
String result;
try {
result = doProcess(exchange, responseBody);
} catch (Exception e) {
setStatusCode(HttpStatus.BAD_REQUEST);
String errorMsg = "Server Internal Error";
sb.append(errorMsg);
log.error(sb.toString(), e);
result = JSON.toJSONString(
ResponseResult.error(ErrorCodeEnum.SERVER_INTERNAL_ERROR, errorMsg));
}
byte[] uppedContent = new String(result.getBytes(), StandardCharsets.UTF_8).getBytes();
originalResponse.getHeaders().setContentLength(uppedContent.length);
return bufferFactory.wrap(uppedContent);
}));
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
/**
* 返回过滤器在在调用链上的优先级。
*
* @return 数值越低,优先级越高。
*/
@Override
public int getOrder() {
// -1 is response write filter, must be called before that
return -2;
}
private String readResponseBody(List<? extends DataBuffer> dataBuffers) {
int dataCount = 0;
for (DataBuffer dataBuffer : dataBuffers) {
dataCount += dataBuffer.readableByteCount();
}
byte[] allBytes = new byte[dataCount];
int offset = 0;
for (DataBuffer dataBuffer : dataBuffers) {
int length = dataBuffer.readableByteCount();
dataBuffer.read(allBytes, offset, length);
DataBufferUtils.release(dataBuffer);
offset += length;
}
return new String(allBytes, StandardCharsets.UTF_8);
}
@SuppressWarnings("unchecked")
private String doProcess(ServerWebExchange exchange, String responseBody) {
// 这个解析出来的就是upms登录或登出接口返回的ResponseResult对象。
ServerHttpRequest originalRequest = exchange.getRequest();
if (originalRequest.getURI().getPath().equals(GatewayConstant.ADMIN_LOGIN_BY_UAA_URL)
|| originalRequest.getURI().getPath().equals(GatewayConstant.ADMIN_LOGIN_URL)) {
// 处理登录服务的消息体,同时重构该消息体,并最终返回前端。
ResponseResult<JSONObject> result = processLoginResponse(responseBody);
return JSON.toJSONString(result);
}
if (originalRequest.getURI().getPath().equals(GatewayConstant.ADMIN_LOGOUT_URL)) {
ResponseResult<Void> result = JSON.parseObject(responseBody, ResponseResult.class);
if (result.isSuccess()) {
String sessionId = (String) exchange.getAttributes().get(GatewayConstant.SESSION_ID_KEY_NAME);
redissonClient.getBucket(RedisKeyUtil.makeSessionIdKey(sessionId)).deleteAsync();
redissonClient.getSet(RedisKeyUtil.makeSessionPermIdKey(sessionId)).deleteAsync();
}
return responseBody;
}
return null;
}
@SuppressWarnings("unchecked")
private ResponseResult<JSONObject> processLoginResponse(String responseBody) {
ResponseResult<JSONObject> responseResult = JSON.parseObject(responseBody, ResponseResult.class);
if (!responseResult.isSuccess()) {
return responseResult;
}
JSONObject loginData = responseResult.getData();
// 1. 先验证登陆服务器返回的应答数据是否正确
JSONObject tokenData = loginData.getJSONObject(TokenData.REQUEST_ATTRIBUTE_NAME);
ErrorCodeEnum errorCode = ErrorCodeEnum.SERVER_INTERNAL_ERROR;
if (tokenData == null) {
return ResponseResult.error(errorCode, "内部错误,用户登录令牌对象没有正确返回!");
}
Long userId = tokenData.getLong("userId");
if (MyCommonUtil.isBlankOrNull(userId)) {
return ResponseResult.error(errorCode, "内部错误用户Id没有正确返回");
}
Boolean isAdmin = tokenData.getBoolean("isAdmin");
if (isAdmin == null) {
return ResponseResult.error(errorCode, "内部错误,是否为管理员标记没有正确返回!");
}
String showName = tokenData.getString("showName");
if (StringUtils.isBlank(showName)) {
return ResponseResult.error(errorCode, "内部错误,用户显示名没有正确返回!");
}
String loginName = tokenData.getString("loginName");
if (StringUtils.isBlank(loginName)) {
return ResponseResult.error(errorCode, "内部错误,用户登录名没有正确返回!");
}
String sessionId = tokenData.getString("sessionId");
if (StringUtils.isBlank(sessionId)) {
return ResponseResult.error(errorCode, "内部错误SESSION_ID没有正确返回");
}
// 2. 生成sessionId并存放到token中
Map<String, Object> claims = new HashMap<>(1);
claims.put(GatewayConstant.SESSION_ID_KEY_NAME, sessionId);
String token = JwtUtil.generateToken(claims, appConfig.getExpiration(), appConfig.getTokenSigningKey());
// 3. 更新缓存
String sessionIdKey = RedisKeyUtil.makeSessionIdKey(sessionId);
String sessionData = JSON.toJSONString(tokenData, SerializerFeature.WriteNonStringValueAsString);
RBucket<String> bucket = redissonClient.getBucket(sessionIdKey);
bucket.set(sessionData);
bucket.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS);
// 3.2 sessionId -> permList 是set结构的缓存
JSONArray permSet = loginData.getJSONArray("permSet");
if (permSet != null) {
String sessionPermKey = RedisKeyUtil.makeSessionPermIdKey(sessionId);
RSet<String> redisPermSet = redissonClient.getSet(sessionPermKey);
redisPermSet.addAll(permSet.stream().map(Object::toString).collect(Collectors.toSet()));
redisPermSet.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS);
}
// 4. 构造返回给用户的应答,将加密后的令牌返回给前端。
loginData.put(TokenData.REQUEST_ATTRIBUTE_NAME, token);
// 5. 这里需要移除权限资源集合的数据,验证在后端进行,无需返回给前端。
loginData.remove("permSet");
return ResponseResult.success(loginData);
}
}

View File

@@ -0,0 +1,190 @@
package com.orangeforms.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.orangeforms.common.core.constant.ErrorCodeEnum;
import com.orangeforms.common.core.object.ResponseResult;
import com.orangeforms.common.core.object.TokenData;
import com.orangeforms.common.core.util.JwtUtil;
import com.orangeforms.common.core.util.RedisKeyUtil;
import com.orangeforms.common.core.util.IpUtil;
import com.orangeforms.gateway.config.ApplicationConfig;
import com.orangeforms.gateway.constant.GatewayConstant;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 全局前处理过滤器。主要用于用户操作权限验证。
*
* @author Jerry
* @date 2020-08-08
*/
@Slf4j
public class AuthenticationPreFilter implements GlobalFilter, Ordered {
@Autowired
private ApplicationConfig appConfig;
@Autowired
private RedissonClient redissonClient;
/**
* Ant Pattern模式的白名单地址匹配器。
*/
private final AntPathMatcher antMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String url = request.getURI().getPath();
// 判断是否为白名单请求,以及一些内置不需要验证的请求。(登录请求也包含其中)。
if (this.shouldNotFilter(url)) {
return chain.filter(exchange);
}
String token = this.getTokenFromRequest(request);
Claims c = JwtUtil.parseToken(token, appConfig.getTokenSigningKey());
if (JwtUtil.isNullOrExpired(c)) {
log.warn("EXPIRED request [{}] from REMOTE-IP [{}].", url, IpUtil.getRemoteIpAddress(request));
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] responseBody = JSON.toJSONString(ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN,
"用户登录已过期或尚未登录,请重新登录!")).getBytes(StandardCharsets.UTF_8);
return response.writeWith(Flux.just(response.bufferFactory().wrap(responseBody)));
}
// 这里判断是否需要定时刷新token
if (JwtUtil.needToRefresh(c)) {
exchange.getAttributes().put(appConfig.getRefreshedTokenHeaderKey(),
JwtUtil.generateToken(c, appConfig.getExpiration(), appConfig.getTokenSigningKey()));
}
// 先基于sessionId获取userInfo
String sessionId = (String) c.get(GatewayConstant.SESSION_ID_KEY_NAME);
String sessionIdKey = RedisKeyUtil.makeSessionIdKey(sessionId);
RBucket<String> sessionData = redissonClient.getBucket(sessionIdKey);
JSONObject tokenData = null;
if (sessionData.isExists()) {
tokenData = JSON.parseObject(sessionData.get());
}
if (tokenData == null) {
log.warn("UNAUTHORIZED request [{}] from REMOTE-IP [{}] because no sessionId exists in redis.",
url, IpUtil.getRemoteIpAddress(request));
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] responseBody = JSON.toJSONString(ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN,
"用户会话已失效,请重新登录!")).getBytes(StandardCharsets.UTF_8);
return response.writeWith(Flux.just(response.bufferFactory().wrap(responseBody)));
}
String userId = tokenData.getString("userId");
if (StringUtils.isBlank(userId)) {
log.warn("UNAUTHORIZED request [{}] from REMOTE-IP [{}] because userId is empty in redis.",
url, IpUtil.getRemoteIpAddress(request));
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] responseBody = JSON.toJSONString(ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN,
"用户登录验证信息已过期,请重新登录!")).getBytes(StandardCharsets.UTF_8);
return response.writeWith(Flux.just(response.bufferFactory().wrap(responseBody)));
}
String showName = tokenData.getString("showName");
// 因为http header中不支持中文传输所以需要编码。
try {
showName = URLEncoder.encode(showName, StandardCharsets.UTF_8.name());
tokenData.put("showName", showName);
} catch (UnsupportedEncodingException e) {
log.error("Failed to call AuthenticationPreFilter.filter.", e);
}
boolean isAdmin = tokenData.getBoolean("isAdmin");
if (Boolean.FALSE.equals(isAdmin) && !this.hasPermission(redissonClient, sessionId, url)) {
log.warn("FORBIDDEN request [{}] from REMOTE-IP [{}] for USER [{} -- {}] no perm!",
url, IpUtil.getRemoteIpAddress(request), userId, showName);
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] responseBody = JSON.toJSONString(ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION,
"用户对该URL没有访问权限请核对")).getBytes(StandardCharsets.UTF_8);
return response.writeWith(Flux.just(response.bufferFactory().wrap(responseBody)));
}
// 将session中关联的用户信息添加到当前的Request中。转发后业务服务可以根据需要自定读取。
tokenData.put("sessionId", sessionId);
exchange.getAttributes().put(GatewayConstant.SESSION_ID_KEY_NAME, sessionId);
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header(
TokenData.REQUEST_ATTRIBUTE_NAME, tokenData.toJSONString()).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
return chain.filter(mutableExchange);
}
/**
* 返回过滤器在在调用链上的优先级。
*
* @return 数值越低,优先级越高。
*/
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 10000;
}
private String getTokenFromRequest(ServerHttpRequest request) {
String token = request.getHeaders().getFirst(appConfig.getTokenHeaderKey());
if (StringUtils.isBlank(token)) {
token = request.getQueryParams().getFirst(appConfig.getTokenHeaderKey());
}
return token;
}
private boolean hasPermission(RedissonClient redissonClient, String sessionId, String url) {
// 对于退出登录操作,不需要进行权限验证,仅仅确认是已经登录的合法用户即可。
if (url.equals(GatewayConstant.ADMIN_LOGOUT_URL)) {
return true;
}
String permKey = RedisKeyUtil.makeSessionPermIdKey(sessionId);
return redissonClient.getSet(permKey).contains(url);
}
/**
* 判断当前请求的url是否为配置中的白名单地址。以及一些内置的不需要登录即可访问的url。
* @param url 请求的url。
* @return 是返回true否则false。
*/
private boolean shouldNotFilter(String url) {
// 这里过滤和swagger相关的url
if (url.endsWith("/v2/api-docs") || url.endsWith("/v2/api-docs-ext")) {
return true;
}
if (url.equals(GatewayConstant.ADMIN_LOGIN_BY_UAA_URL)
|| url.equals(GatewayConstant.GET_UAA_LOGIN_URL)
|| url.equals(GatewayConstant.ADMIN_LOGIN_URL)) {
return true;
}
// 先过滤直接匹配的白名单url。
if (CollectionUtils.isNotEmpty(appConfig.getWhitelistUrl())) {
if (appConfig.getWhitelistUrl().contains(url)) {
return true;
}
}
// 过滤ant pattern模式的白名单url。
if (CollectionUtils.isNotEmpty(appConfig.getWhitelistUrlPattern())) {
for (String urlPattern : appConfig.getWhitelistUrlPattern()) {
if (antMatcher.match(urlPattern, url)) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,61 @@
package com.orangeforms.gateway.filter;
import com.orangeforms.common.core.constant.ApplicationConstant;
import com.orangeforms.common.core.util.MyCommonUtil;
import com.orangeforms.gateway.constant.GatewayConstant;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
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.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 链路日志前置过虑器。
* 为整个链路生成唯一的traceId并存储在Request Head中。
*
* @author Jerry
* @date 2020-08-08
*/
@Slf4j
public class RequestLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
final String traceId = MyCommonUtil.generateUuid();
MDC.put(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId);
log.info("开始请求app={gateway}, url={}", exchange.getRequest().getURI().getPath());
// 分别记录traceId和执行开始时间。
exchange.getAttributes().put(GatewayConstant.START_TIME_ATTRIBUTE, System.currentTimeMillis());
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header(
ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
ServerHttpResponse response = mutableExchange.getResponse();
response.beforeCommit(() -> {
response.getHeaders().set(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId);
return Mono.empty();
});
return chain.filter(mutableExchange).then(Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(GatewayConstant.START_TIME_ATTRIBUTE);
MDC.put(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId);
long elapse = 0;
if (startTime != null) {
elapse = System.currentTimeMillis() - startTime;
}
log.info("请求完成, app={gateway}, url={}elapse={}", exchange.getRequest().getURI().getPath(), elapse);
}));
}
/**
* 返回过滤器在在调用链上的优先级。
*
* @return 数值越低,优先级越高。
*/
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 9900;
}
}

View File

@@ -0,0 +1,53 @@
package com.orangeforms.gateway.handler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;
import java.util.Optional;
/**
* Swagger的资源请求处理器。
*
* @author Knife4j Team。
* @date 2020-08-08
*/
@RestController
public class SwaggerHandler {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
@GetMapping("/swagger-resources/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration)
.orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("/swagger-resources/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration)
.orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("/swagger-resources")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}

View File

@@ -0,0 +1,19 @@
spring:
application:
name: gateway
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yaml
# 共享配置文件,排序越高后,优先级越高。
shared-configs:
- data-id: application-dev.yaml
group: DEFAULT_GROUP
refresh: true
main:
allow-bean-definition-overriding: true

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 本项目全部使用log4j2性能上有很大提升 -->
<!--monitorInterval="60" 自动检测配置文件更改时间 单位为秒 最小值为5 -->
<!--Configuration后面的status这个用于设置log4j2自身内部的信息输出可以不设置当设置成trace时你会看到log4j2内部各种详细输出。 -->
<configuration monitorInterval="20" status="OFF">
<!--日志变量 -->
<properties>
<!-- 日志主目录 ,需要保存到文件时请自己配置-->
<property name="LOG_HOME">./zzlogs/gateway</property>
<!-- 日志备份目录 -->
<property name="BACKUP_HOME">./zzlogs/gateway/backup</property>
<!-- 日志输出级别 -->
<property name="OUTPUT_LOG_LEVEL">info</property>
<!-- 日志输出格式 -->
<property name="LOG_PATTERN">
<!-- 输出格式%d{HH:mm:ss}时间24小时制 -->
<!-- %-5p日志级别 5位左对齐 [%t]线程名 [%c]类名 -->
<!--%l输出日志事件的发生位置相当于%c.%M(%F:%L)的组合包括类全名、方法、文件名以及在代码中的行数。例如test.TestLog4j.main(TestLog4j.java:10)。 -->
<!-- 另一种输出风格<PatternLayout pattern="级别%-5p [%d{YYYY-MM-dd HH:mm:ss}] [%t] 位置[%l] - 信息:%msg%n" /> -->
<!-- [%-5p][%d{yy-MM-dd HH:mm:ss}][%t]==>%m==>%c==>%L%n -->
[%-5p] [%d{YYYY-MM-dd HH:mm:ss}] [%t] ==> %msg%n
</property>
<property name="LOG_PATTERN_EX">
<!-- 下面注释中 %traceid 为SkyWalking 中的traceid -->
[%-5p] [%d{YYYY-MM-dd HH:mm:ss}] T:[%X{traceId}] [%t] ==> [%traceId] %msg%n
</property>
<!-- 日志保留天数 -->
<property name="EVERY_FILE_COUNT">31</property>
<!-- 日志切割的最小单位 -->
<property name="EVERY_FILE_SIZE">20M</property>
</properties>
<appenders>
<!--Kafka输出 -->
<Kafka name="kafka_log" topic="zz-log-topic" syncSend="false" ignoreExceptions="false">
<PatternLayout pattern="${LOG_PATTERN_EX}"/>
<Property name="bootstrap.servers">localhost:9092</Property>
<Property name="max.block.ms">10000</Property>
</Kafka>
<!--控制台输出 -->
<console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
</console>
<!--每次大小超过size则这size大小的日志会自动进行压缩作为存档 -->
<rollingFile name="file_log" fileName="${LOG_HOME}/gateway.log"
filePattern="${LOG_HOME}/gateway-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout charset="UTF-8" pattern="${LOG_PATTERN_EX}"/>
<!-- 日志切割的最小单位 -->
<SizeBasedTriggeringPolicy size="${EVERY_FILE_SIZE}"/>
<!-- 默认的日志文件数量 -->
<DefaultRolloverStrategy max="${EVERY_FILE_COUNT}"/>
</rollingFile>
</appenders>
<!-- 然后定义logger只有定义了logger并引入的appenderappender才会生效 -->
<!-- 这里我们把输出到控制台appender的日志级别设置为DEBUG便于调试。但是输出文件我们缺省为INFO两者均可随时修改。-->
<Loggers>
<Root level="${OUTPUT_LOG_LEVEL}">
<AppenderRef ref="console"/>
</Root>
<Logger name="springfox.documentation" additivity="false" level="error">
<AppenderRef ref="console"/>
</Logger>
<!-- AsyncLogger 是基于Disruptor的全量异步队列性能极高队列默认大小4096。-->
<!-- 队列默认值可通过JVM参数设置参考博客https://www.jianshu.com/p/82469047acbf -->
<AsyncLogger name="com.orangeforms" additivity="false" level="info">
<AppenderRef ref="console"/>
<AppenderRef ref="kafka_log"/>
<AppenderRef ref="file_log"/>
</AsyncLogger>
</Loggers>
</configuration>