微服務要求實現身份認證, 不可能在每個服務中做認證
既然所有的服務都走網關, 那麼把認證放在網關中, 將用戶信息放在請求中, 後面的服務可以從中獲取用戶信息, 以便進行數據權限操作
具體看代碼:
https://gitee.com/gongwei_3/spring-cloud-gateway-token
思路:
1, 過濾所有的請求, 如果是login, 那麼從請求中獲取username, 然後放行, 驗證微服務驗證用戶名和密碼後, 攔截響應體, 如果驗證通過, 那麼通過username生成token, 修改響應體, 把token放到響應體中, 這裏要求驗證接口是post,並且字段是username和password, 響應體也要統一
2, 如果是白名單,直接放行, 如果是其他接口, 那麼獲取token, 並驗證(包括刷新token), 獲取token的用戶名, 將用戶名放在請求頭中, 刷新後的token放在響應體中
實現
經過查spring文檔 https://www.springcloud.cc/
參考: https://blog.51cto.com/thinklili/2329184?cid=725051
發現了一個預言類,ReadBodyPredicateFactory ,發現裏面緩存了request body的信息,於是在自定義router中配置了ReadBodyPredicateFactory,然後在filter中通過cachedRequestBodyObject緩存字段獲取request body信息,這種解決,一不會帶來重複讀取問題,二不會帶來requestbody取不全問題。三在低版本的Spring Cloud Finchley.SR2也可以運行。
ReadRequestBodyRouterConfig配置路由表,配置了ReadBodyPredicateFactory
package com.gw.nacosgatewaydemo1.config;
import com.gw.nacosgatewaydemo1.filter.AuthFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
/**
* 從登錄請求中獲取請求body的登錄數據
*/
@EnableAutoConfiguration
@Configuration
public class ReadRequestBodyRouterConfig {
private static final Logger log = LoggerFactory.getLogger(ReadRequestBodyRouterConfig.class);
@Autowired
private AuthFilter authFilter;
private static final String SERVICE = AuthFilter.LOGIN_URL_PATTERN;
private static final String URI = "lb://nacos-demo1";
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
/*
route2 是post請求,Content-Type是application/x-www-form-urlencoded,readbody爲String.class
route3 是post請求,Content-Type是application/json,readbody爲Object.class
*/
RouteLocatorBuilder.Builder routes = builder.routes();
RouteLocatorBuilder.Builder serviceProvider = routes
.route("route2",
r -> r
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.and()
.method(HttpMethod.POST)
.and()
.readBody(String.class, readBody -> {
log.info("request method POST, Content-Type is application/x-www-form-urlencoded, body is:{}", readBody);
return true;
})
.and()
.path(SERVICE)
.filters(f -> {
f.filter(authFilter);
return f;
})
.uri(URI))
.route("route3",
r -> r
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.and()
.method(HttpMethod.POST)
.and()
.readBody(Object.class, readBody -> {
log.info("request method POST, Content-Type is application/json, body is:{}", readBody);
return true;
})
.and()
.path(SERVICE)
.filters(f -> {
f.filter(authFilter);
return f;
})
.uri(URI));
RouteLocator routeLocator = serviceProvider.build();
log.info("custom RouteLocator is loading ... {}", routeLocator);
return routeLocator;
}
}
全局認證過濾器,
package com.gw.nacosgatewaydemo1.filter;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.gw.nacosgatewaydemo1.util.JwtUtil;
import com.gw.nacosgatewaydemo1.util.SerializeUtil;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
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.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* 身份認證
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered, GatewayFilter {
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
/**
* 緩存的請求body
*/
private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
public static final String LOGIN_USERNAME_KEY = "username";
public static final String LOGIN_URL_PATTERN = ".*/login";
private static final Set<String> ignore = new HashSet<>();
static {
ignore.add(".*/swagger-resources.*");
ignore.add(".*/configuration.*");
ignore.add(".*/v2/api-docs.*");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();
log.debug(url);
//登錄
if (isLogin(url)) {
return doLogin(exchange, chain);
}
//跳過不需要驗證的路徑
if (isIgnore(url)) {
return chain.filter(exchange);
}
//需要驗證token
Map claims = JwtUtil.validToken(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
if (claims == null) {
//token驗證不通過
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
} else {
//刷新token
String username = (String) claims.get(LOGIN_USERNAME_KEY);
String token = (String) claims.get(HttpHeaders.AUTHORIZATION);
//將登錄用戶的信息存在請求頭中,提供給後面其他服務
ServerHttpRequest exchangeRequest = exchange.getRequest().mutate().header(LOGIN_USERNAME_KEY, username).build();
ServerWebExchange build = exchange.mutate().request(exchangeRequest).build();
exchange.getResponse().getHeaders().add(HttpHeaders.AUTHORIZATION, token);
return chain.filter(build);
}
}
private Mono<Void> doLogin(ServerWebExchange exchange, GatewayFilterChain chain) {
Object loginData;
ServerHttpRequest exchangeRequest = exchange.getRequest();
String method = exchangeRequest.getMethodValue();
if ("POST".equals(method)) {
//從login請求中獲取用戶信息,要求login接口是post,入參是body,字段名字段名是username和password, 返回統一的json數據,
loginData = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
//暫時不支持get請求的登錄
// } else if ("GET".equals(method)) {
// loginData = exchangeRequest.getQueryParams();
} else {
exchange.getResponse().setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
return exchange.getResponse().setComplete();
}
//從登錄請求頭獲取用戶名
Map map = SerializeUtil.toObject(loginData.toString(), Map.class);
String username = (String) map.get(LOGIN_USERNAME_KEY);
//根據用戶名生成token
String token = JwtUtil.genToken(username);
//去用戶服務驗證用戶名和密碼正確後,修改返回體,並將token寫進返回體中
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (HttpStatus.OK.equals(getStatusCode()) && body instanceof Flux) {
// 獲取ContentType,判斷是否返回JSON格式數據
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (StringUtils.isNotBlank(originalResponseContentType) && originalResponseContentType.contains("application/json")) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {//解決返回體分段傳輸
List<String> list = Lists.newArrayList();
dataBuffers.forEach(dataBuffer -> {
try {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
list.add(new String(content, StandardCharsets.UTF_8));
} catch (Exception e) {
log.info("動態加載API加密規則失敗,失敗原因:{}", Throwables.getStackTraceAsString(e));
}
});
Joiner joiner = Joiner.on("");
String responseData = joiner.join(list);
Map map = SerializeUtil.toObject(responseData, Map.class);
//todo 這裏只是判斷了http的狀態, 還要根據用戶驗證接口去判斷是否登錄成功, 具體和登陸接口保持一致
//可能密碼不對, 但是返回的是200
map.put("data", token);
String res = SerializeUtil.toJson(map);
originalResponse.getHeaders().setContentLength(res.length());
return bufferFactory.wrap(res.getBytes(StandardCharsets.UTF_8));
}));
}
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return -100;
}
private boolean isIgnore(String url) {
for (String urlPatter : ignore) {
return Pattern.matches(urlPatter, url);
}
return false;
}
private boolean isLogin(String url) {
return Pattern.matches(LOGIN_URL_PATTERN, url);
}
}