commit:修改微服务工程目录名称

This commit is contained in:
Jerry
2020-09-26 20:14:58 +08:00
parent d05ad53a17
commit 322b3bc19d
463 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.orange.demo</groupId>
<artifactId>application</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gateway</artifactId>
<version>1.0.0</version>
<name>gateway</name>
<packaging>jar</packaging>
<dependencies>
<!-- 网关服务 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.orange.demo</groupId>
<artifactId>common-redis</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 通用组件依赖 -->
<dependency>
<groupId>com.orange.demo</groupId>
<artifactId>common-core</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<artifactId>mysql-connector-java</artifactId>
<groupId>mysql</groupId>
</exclusion>
<exclusion>
<artifactId>druid-spring-boot-starter</artifactId>
<groupId>com.alibaba</groupId>
</exclusion>
<exclusion>
<artifactId>spring-boot-starter-web</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,64 @@
package com.orange.demo.gateway;
import com.orange.demo.common.core.util.ApplicationContextHolder;
import com.orange.demo.gateway.filter.AuthenticationPostFilter;
import com.orange.demo.gateway.filter.AuthenticationPreFilter;
import com.orange.demo.gateway.filter.RequestLogFilter;
import com.orange.demo.gateway.filter.ResponseLogFilter;
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 Orange Team
* @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
public ResponseLogFilter responseLogPostFilter() {
return new ResponseLogFilter();
}
@Bean
ApplicationContextHolder applicationContextHolder() {
return new ApplicationContextHolder();
}
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

View File

@@ -0,0 +1,50 @@
package com.orange.demo.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;
/**
* 网关业务配置类。
*
* @author Orange Team
* @date 2020-08-08
*/
@Data
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "application")
public class ApplicationConfig {
/**
* token加密时的盐
*/
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 + 60s
*/
private int sessionIdRedisExpiredSeconds = 86460;
/**
* Session的用户权限在Redis中的过期时间(秒)。
* 缺省值是 one day
*/
private int permRedisExpiredSeconds = 86400;
}

View File

@@ -0,0 +1,39 @@
package com.orange.demo.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 Orange Team
* @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.orange.demo.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 Orange Team
* @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.orange.demo.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 Orange Team
* @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,36 @@
package com.orange.demo.gateway.constant;
/**
* 网关业务相关的常量对象。
*
* @author Orange Team
* @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/login/doLogin";
/**
* 登出URL。
*/
public static final String ADMIN_LOGOUT_URL = "/admin/login/doLogout";
/**
* sessionId的键名称。
*/
public static final String SESSION_ID_KEY_NAME = "sessionId";
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private GatewayConstant() {
}
}

View File

@@ -0,0 +1,255 @@
package com.orange.demo.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.orange.demo.common.core.constant.ErrorCodeEnum;
import com.orange.demo.common.core.object.ResponseResult;
import com.orange.demo.common.core.object.TokenData;
import com.orange.demo.common.core.util.JwtUtil;
import com.orange.demo.common.core.util.MyCommonUtil;
import com.orange.demo.common.core.util.RedisKeyUtil;
import com.orange.demo.gateway.config.ApplicationConfig;
import com.orange.demo.gateway.constant.GatewayConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
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 redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Transaction;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* 全局后处理过滤器。主要用于将用户的会话信息存到缓存服务器,以及在登出时清除缓存中的会话数据。
*
* @author Orange Team
* @date 2020-08-08
*/
@Slf4j
public class AuthenticationPostFilter implements GlobalFilter, Ordered {
@Autowired
private ApplicationConfig appConfig;
@Autowired
private JedisPool jedisPool;
@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_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) {
List<String> list = new LinkedList<>();
int dataCount = 0;
for (DataBuffer dataBuffer : dataBuffers) {
dataCount += dataBuffer.readableByteCount();
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
list.add(new String(content, StandardCharsets.UTF_8));
}
StringBuilder responseBuilder = new StringBuilder(dataCount + 1);
for (String data : list) {
responseBuilder.append(data);
}
return responseBuilder.toString();
}
@SuppressWarnings("unchecked")
private String doProcess(ServerWebExchange exchange, String responseBody) {
// 这个解析出来的就是upms登录或登出接口返回的ResponseResult对象。
ServerHttpRequest originalRequest = exchange.getRequest();
if (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);
try (Jedis jedis = jedisPool.getResource()) {
Pipeline pipeline = jedis.pipelined();
pipeline.del(RedisKeyUtil.makeSessionIdKeyForRedis(sessionId));
pipeline.del(RedisKeyUtil.makeSessionPermIdKeyForRedis(sessionId));
pipeline.sync();
}
}
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, "内部错误,用户登录令牌对象没有正确返回!");
}
Integer userId = tokenData.getInteger("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 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. 更新缓存
// 3.1 sessionId -> userId 是hash结构的缓存
String sessionIdKey = RedisKeyUtil.makeSessionIdKeyForRedis(sessionId);
String sessionPermKey = null;
JSONArray permSet = null;
if (Boolean.FALSE.equals(isAdmin)) {
// 3.2 sessionId -> permList 是set结构的缓存
sessionPermKey = RedisKeyUtil.makeSessionPermIdKeyForRedis(sessionId);
permSet = loginData.getJSONArray("permSet");
}
try (Jedis jedis = jedisPool.getResource()) {
Transaction t = jedis.multi();
for (String tokenKey : tokenData.keySet()) {
t.hset(sessionIdKey, tokenKey, tokenData.getString(tokenKey));
}
t.expire(sessionIdKey, appConfig.getSessionIdRedisExpiredSeconds());
if (permSet != null) {
for (int i = 0; i < permSet.size(); ++i) {
String perm = permSet.getString(i);
t.sadd(sessionPermKey, perm);
}
t.expire(sessionPermKey, appConfig.getPermRedisExpiredSeconds());
}
t.exec();
}
// 4. 构造返回给用户的应答
JSONObject resultJsonData = new JSONObject();
resultJsonData.put(TokenData.REQUEST_ATTRIBUTE_NAME, token);
resultJsonData.put("isAdmin", isAdmin);
resultJsonData.put("showName", showName);
JSONArray menuList = loginData.getJSONArray("menuList");
if (CollectionUtils.isNotEmpty(menuList)) {
resultJsonData.put("menuList", menuList);
}
if (Boolean.FALSE.equals(isAdmin)) {
JSONArray permCodeList = loginData.getJSONArray("permCodeList");
if (CollectionUtils.isNotEmpty(permCodeList)) {
resultJsonData.put("permCodeList", permCodeList);
}
}
return ResponseResult.success(resultJsonData);
}
}

View File

@@ -0,0 +1,177 @@
package com.orange.demo.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.orange.demo.common.core.constant.ErrorCodeEnum;
import com.orange.demo.common.core.object.ResponseResult;
import com.orange.demo.common.core.object.TokenData;
import com.orange.demo.common.core.util.JwtUtil;
import com.orange.demo.common.core.util.RedisKeyUtil;
import com.orange.demo.common.core.util.IpUtil;
import com.orange.demo.gateway.config.ApplicationConfig;
import com.orange.demo.gateway.constant.GatewayConstant;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* 全局前处理过滤器。主要用于用户操作权限验证。
*
* @author Orange Team
* @date 2020-08-08
*/
@Slf4j
public class AuthenticationPreFilter implements GlobalFilter, Ordered {
@Autowired
private ApplicationConfig appConfig;
@Autowired
private JedisPool jedisPool;
private static List<String> whitelistUrlPattern = new LinkedList<>();
static {
// 这里可以添加URL部分匹配的白名单列表
// 另外解释一下数据库中配置的白名单列表在doLogin中直接合并到当前用户的权限列表中了。
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String url = request.getURI().getPath();
// 登录请求直接转发给login验证服务器。
// NOTE: 所有不需要登录验证的url都可以添加在下面。
if (url.equals(GatewayConstant.ADMIN_LOGIN_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()));
}
try (Jedis jedis = jedisPool.getResource()) {
// 先基于sessionId获取userInfo
String sessionId = (String) c.get(GatewayConstant.SESSION_ID_KEY_NAME);
Map<String, String> userMap = jedis.hgetAll(RedisKeyUtil.makeSessionIdKeyForRedis(sessionId));
if (userMap == 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 = userMap.get("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)));
}
boolean isAdmin = false;
String isAdminString = userMap.get("isAdmin");
if (Boolean.parseBoolean(isAdminString)) {
isAdmin = true;
}
String showName = userMap.get("showName");
// 因为http header中不支持中文传输所以需要编码。
try {
showName = URLEncoder.encode(showName, StandardCharsets.UTF_8.name());
userMap.put("showName", showName);
} catch (UnsupportedEncodingException e) {
log.error("Failed to call AuthenticationPreFilter.filter.", e);
}
// 对于isAdmin == false的用户继续查找权限资源信息是否存在
if (Boolean.FALSE.equals(isAdmin)
&& !this.hasPermission(jedis, 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中。转发后业务服务可以根据需要自定读取。
JSONObject tokenData = new JSONObject();
tokenData.putAll(userMap);
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(Jedis jedis, String sessionId, String url) {
// 对于退出登录操作,不需要进行权限验证,仅仅确认是已经登录的合法用户即可。
if (url.equals(GatewayConstant.ADMIN_LOGOUT_URL)
|| Boolean.TRUE.equals(jedis.sismember(RedisKeyUtil.makeSessionPermIdKeyForRedis(sessionId), url))) {
return true;
}
for (String urlPattern : whitelistUrlPattern) {
if (url.startsWith(urlPattern)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,48 @@
package com.orange.demo.gateway.filter;
import com.orange.demo.common.core.constant.ApplicationConstant;
import com.orange.demo.common.core.util.MyCommonUtil;
import com.orange.demo.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.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 链路日志前置过虑器。
* 为整个链路生成唯一的traceId并存储在Request Head中。
*
* @author Orange Team
* @date 2020-08-08
*/
@Slf4j
public class RequestLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String traceId = MyCommonUtil.generateUuid();
// 分别记录traceId和执行开始时间。
exchange.getAttributes().put(ApplicationConstant.HTTP_HEADER_TRACE_ID, 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();
MDC.put(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId);
log.info("开始请求app={gateway}, url={}", exchange.getRequest().getURI().getPath());
return chain.filter(mutableExchange);
}
/**
* 返回过滤器在在调用链上的优先级。
*
* @return 数值越低,优先级越高。
*/
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 9900;
}
}

View File

@@ -0,0 +1,51 @@
package com.orange.demo.gateway.filter;
import com.orange.demo.common.core.constant.ApplicationConstant;
import com.orange.demo.gateway.constant.GatewayConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 链路日志后置过虑器。
* 将整个链路的traceId存储在Response Head中并返回给前端便于问题定位。
*
* @author Orange Team
* @date 2020-08-08
*/
@Slf4j
public class ResponseLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 下面两个属性都是在RequestLogFilter过滤器中设置的。
String traceId = exchange.getAttribute(ApplicationConstant.HTTP_HEADER_TRACE_ID);
Long startTime = exchange.getAttribute(GatewayConstant.START_TIME_ATTRIBUTE);
if (StringUtils.isNotBlank(traceId)) {
MDC.put(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId);
exchange.getResponse().getHeaders().add(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 chain.filter(exchange);
}
/**
* 返回过滤器在在调用链上的优先级。
*
* @return 数值越低,优先级越高。
*/
@Override
public int getOrder() {
// -1 is response write filter, must be called before that
return -10;
}
}

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,70 @@
<?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">
<!-- 下面注释中 %X{PtxId}, SpanId: %X{PspanId} 为PinPoint 中的traceid -->
[%-5p] [%d{YYYY-MM-dd HH:mm:ss}] 请求Id[%X{traceId}] [%t] ==> [TxId: %X{PtxId}, SpanId: %X{PspanId}] %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>
<!-- AsyncLogger 是基于Disruptor的全量异步队列性能极高队列默认大小4096。-->
<!-- 队列默认值可通过JVM参数设置参考博客https://www.jianshu.com/p/82469047acbf -->
<AsyncLogger name="com.orange.demo" additivity="false" level="info">
<AppenderRef ref="console"/>
<AppenderRef ref="kafka_log"/>
<AppenderRef ref="file_log"/>
</AsyncLogger>
</Loggers>
</configuration>