commit:1.6版本发布

This commit is contained in:
Jerry
2021-05-02 20:19:06 +08:00
parent 1a713fa34f
commit 5f9a18a8eb
78 changed files with 1183 additions and 826 deletions

View File

@@ -72,8 +72,6 @@
<orderEntry type="library" name="Maven: com.alibaba.csp:sentinel-core:1.7.1" level="project" />
<orderEntry type="library" name="Maven: com.alibaba.csp:sentinel-reactor-adapter:1.7.1" level="project" />
<orderEntry type="module" module-name="common-redis" />
<orderEntry type="library" name="Maven: redis.clients:jedis:3.1.0" level="project" />
<orderEntry type="library" name="Maven: org.apache.commons:commons-pool2:2.7.0" level="project" />
<orderEntry type="library" name="Maven: org.redisson:redisson:3.12.3" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-common:4.1.45.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-codec:4.1.45.Final" level="project" />

View File

@@ -4,7 +4,6 @@ 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;
@@ -48,11 +47,6 @@ public class GatewayApplication {
return new RequestLogFilter();
}
@Bean
public ResponseLogFilter responseLogPostFilter() {
return new ResponseLogFilter();
}
@Bean
ApplicationContextHolder applicationContextHolder() {
return new ApplicationContextHolder();

View File

@@ -40,15 +40,10 @@ public class ApplicationConfig {
*/
private String credentialIpList;
/**
* Session在Redis中的过期时间(秒)。
* 缺省值是 one day + 60s
*/
private int sessionIdRedisExpiredSeconds = 86460;
/**
* Session的用户权限在Redis中的过期时间(秒)。
* Session会话和用户权限在Redis中的过期时间(秒)。
* 缺省值是 one day
*/
private int permRedisExpiredSeconds = 86400;
private int sessionExpiredSeconds = 86400;
/**
* 基于完全等于(equals)判定规则的白名单地址集合过滤效率高于whitelistUrlPattern。
*/

View File

@@ -3,6 +3,7 @@ package com.orange.demo.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.orange.demo.common.core.constant.ErrorCodeEnum;
import com.orange.demo.common.core.object.ResponseResult;
import com.orange.demo.common.core.object.TokenData;
@@ -14,6 +15,9 @@ import com.orange.demo.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;
@@ -30,16 +34,13 @@ 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;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 全局后处理过滤器。主要用于将用户的会话信息存到缓存服务器,以及在登出时清除缓存中的会话数据。
@@ -53,7 +54,7 @@ public class AuthenticationPostFilter implements GlobalFilter, Ordered {
@Autowired
private ApplicationConfig appConfig;
@Autowired
private JedisPool jedisPool;
private RedissonClient redissonClient;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
@@ -134,20 +135,19 @@ public class AuthenticationPostFilter implements GlobalFilter, Ordered {
}
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);
}
byte[] allBytes = new byte[dataCount];
int offset = 0;
for (DataBuffer dataBuffer : dataBuffers) {
int length = dataBuffer.readableByteCount();
dataBuffer.read(allBytes, offset, length);
DataBufferUtils.release(dataBuffer);
list.add(new String(content, StandardCharsets.UTF_8));
offset += length;
}
StringBuilder responseBuilder = new StringBuilder(dataCount + 1);
for (String data : list) {
responseBuilder.append(data);
}
return responseBuilder.toString();
return new String(allBytes, StandardCharsets.UTF_8);
}
@SuppressWarnings("unchecked")
@@ -162,14 +162,9 @@ public class AuthenticationPostFilter implements GlobalFilter, Ordered {
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();
}
String sessionId = (String) exchange.getAttributes().get(GatewayConstant.SESSION_ID_KEY_NAME);
redissonClient.getBucket(RedisKeyUtil.makeSessionIdKeyForRedis(sessionId)).deleteAsync();
redissonClient.getSet(RedisKeyUtil.makeSessionPermIdKeyForRedis(sessionId)).deleteAsync();
}
return responseBody;
}
@@ -201,6 +196,10 @@ public class AuthenticationPostFilter implements GlobalFilter, Ordered {
if (StringUtils.isBlank(showName)) {
return ResponseResult.error(errorCode, "内部错误,用户显示名没有正确返回!");
}
String loginName = tokenData.getString("loginName");
if (StringUtils.isBlank(showName)) {
return ResponseResult.error(errorCode, "内部错误,用户登录名没有正确返回!");
}
String sessionId = tokenData.getString("sessionId");
if (StringUtils.isBlank(sessionId)) {
return ResponseResult.error(errorCode, "内部错误SESSION_ID没有正确返回");
@@ -209,26 +208,19 @@ public class AuthenticationPostFilter implements GlobalFilter, Ordered {
Map<String, Object> claims = new HashMap<>(1);
claims.put(GatewayConstant.SESSION_ID_KEY_NAME, sessionId);
String token = JwtUtil.generateToken(claims, appConfig.getExpiration(), appConfig.getTokenSigningKey());
try (Jedis jedis = jedisPool.getResource()) {
// 3. 更新缓存
// 3.1 sessionId -> userId 是hash结构的缓存
String sessionIdKey = RedisKeyUtil.makeSessionIdKeyForRedis(sessionId);
Transaction t = jedis.multi();
for (String tokenKey : tokenData.keySet()) {
t.hset(sessionIdKey, tokenKey, tokenData.getString(tokenKey));
}
t.expire(sessionIdKey, appConfig.getSessionIdRedisExpiredSeconds());
// 3.2 sessionId -> permList 是set结构的缓存
JSONArray permSet = loginData.getJSONArray("permSet");
if (permSet != null) {
String sessionPermKey = RedisKeyUtil.makeSessionPermIdKeyForRedis(sessionId);
for (int i = 0; i < permSet.size(); ++i) {
String perm = permSet.getString(i);
t.sadd(sessionPermKey, perm);
}
t.expire(sessionPermKey, appConfig.getPermRedisExpiredSeconds());
}
t.exec();
// 3. 更新缓存
String sessionIdKey = RedisKeyUtil.makeSessionIdKeyForRedis(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.makeSessionPermIdKeyForRedis(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);

View File

@@ -14,6 +14,8 @@ 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;
@@ -26,13 +28,10 @@ import org.springframework.util.AntPathMatcher;
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.Map;
/**
* 全局前处理过滤器。主要用于用户操作权限验证。
@@ -46,7 +45,7 @@ public class AuthenticationPreFilter implements GlobalFilter, Ordered {
@Autowired
private ApplicationConfig appConfig;
@Autowired
private JedisPool jedisPool;
private RedissonClient redissonClient;
/**
* Ant Pattern模式的白名单地址匹配器。
*/
@@ -76,61 +75,58 @@ public class AuthenticationPreFilter implements GlobalFilter, Ordered {
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);
}
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);
// 先基于sessionId获取userInfo
String sessionId = (String) c.get(GatewayConstant.SESSION_ID_KEY_NAME);
String sessionIdKey = RedisKeyUtil.makeSessionIdKeyForRedis(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);
}
/**
@@ -151,10 +147,13 @@ public class AuthenticationPreFilter implements GlobalFilter, Ordered {
return token;
}
private boolean hasPermission(Jedis jedis, String sessionId, String url) {
private boolean hasPermission(RedissonClient redissonClient, String sessionId, String url) {
// 对于退出登录操作,不需要进行权限验证,仅仅确认是已经登录的合法用户即可。
return url.equals(GatewayConstant.ADMIN_LOGOUT_URL)
|| Boolean.TRUE.equals(jedis.sismember(RedisKeyUtil.makeSessionPermIdKeyForRedis(sessionId), url));
if (url.equals(GatewayConstant.ADMIN_LOGOUT_URL)) {
return true;
}
String permKey = RedisKeyUtil.makeSessionPermIdKeyForRedis(sessionId);
return redissonClient.getSet(permKey).contains(url);
}
/**

View File

@@ -9,6 +9,7 @@ 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;
@@ -24,16 +25,27 @@ public class RequestLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String traceId = MyCommonUtil.generateUuid();
log.info("开始请求app={gateway}, url={}", exchange.getRequest().getURI().getPath());
final 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);
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);
}));
}
/**

View File

@@ -1,51 +0,0 @@
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 Jerry
* @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;
}
}