网关这边主要两个运用:
- 外部服务入口(ios/android/mweb/小程序/管理后台等等),即对外只提供网关接口,其他所有服务都必须通过网关
- 鉴权服务
关键配置流程:
1、pom.xml(spring boot 2.0.2 RELEASE/spring cloud Finchley.RELEASE)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2、url白名单配置
/**
* url匹配工具
*
* @author tums
*/
public class UrlResolver {
private final static PathMatcher MATCHER = new AntPathMatcher();
/**
* 验证url是否匹配,支持精确匹配和模糊匹配
*
* @param patternPaths
* @param requestPath
* @return
*/
public static boolean check(List<String> patternPaths, String requestPath) {
for (String i : patternPaths) {
if (i.endsWith("*")) {
i = i.substring(0, i.length() - 1);
if (MATCHER.matchStart(requestPath, i)) {
return true;
}
}
if (MATCHER.match(i, requestPath)) {
return true;
}
}
return false;
}
}
/**
* 请求地址白名单,无需校验token
*
* @author tums
*/
@Configuration
public class UrlWhileList implements InitializingBean {
private final static List<String> URL_LIST = new ArrayList<String>();
@Override
public void afterPropertiesSet() throws Exception {
//后台-获取图形验证码
URL_LIST.add("/xxx1-service/v1/validateCode");
//APP登录注册
URL_LIST.add("/xxx2-service/v1/token/app/login/*");
URL_LIST.add("/xxx3-service/v1/token/app/register/*");
//网页登录注册
URL_LIST.add("/xxx4-service/v1/token/mweb/login/*");
URL_LIST.add("/xxx5-service/v1/token/mweb/register/*");
//获取短信验证码
URL_LIST.add("/xxx6-service/v1/message/login");
......
}
public static List<String> getUrlList() {
return URL_LIST;
}
}
3、自定义异常配置,为了让进出网关的服务返回统一的状态码和异常信息
/**
* 自定义异常配置
*
* @author tums
* @date 2018/12/3 21:06
*/
@Configuration
public class ExceptionConfig {
/**
* 自定义异常处理[@@]注册Bean时依赖的Bean,会从容器中直接获取,所以直接注入即可
*/
@Primary
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ErrorWebExceptionHandler errorWebExceptionHandler(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
JsonExceptionHandler jsonExceptionHandler = new JsonExceptionHandler();
jsonExceptionHandler.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList));
jsonExceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
jsonExceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
return jsonExceptionHandler;
}
}
/**
* 自定义异常处理
*
* @author tums
* @date 2018/12/3 21:04
*/
public class JsonExceptionHandler implements ErrorWebExceptionHandler {
/**
* MessageReader
*/
private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
/**
* MessageWriter
*/
private List<HttpMessageWriter<?>> messageWriters = Collections.emptyList();
/**
* ViewResolvers
*/
private List<ViewResolver> viewResolvers = Collections.emptyList();
/**
* 存储处理异常后的信息
*/
private ThreadLocal<Map<String, Object>> exceptionHandlerResult = new ThreadLocal<>();
/**
* 参考AbstractErrorWebExceptionHandler
*/
public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
Assert.notNull(messageReaders, "'messageReaders' must not be null");
this.messageReaders = messageReaders;
}
/**
* 参考AbstractErrorWebExceptionHandler
*/
public void setViewResolvers(List<ViewResolver> viewResolvers) {
this.viewResolvers = viewResolvers;
}
/**
* 参考AbstractErrorWebExceptionHandler
*/
public void setMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
Assert.notNull(messageWriters, "'messageWriters' must not be null");
this.messageWriters = messageWriters;
}
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
// 按照异常类型进行处理
HttpStatus httpStatus;
String message;
if (ex instanceof NotFoundException) {
httpStatus = HttpStatus.NOT_FOUND;
message = "Service Not Found";
} else if (ex instanceof TokenExpiredException) {
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
httpStatus = responseStatusException.getStatus();
message = responseStatusException.getReason();
} else if (ex instanceof ResponseStatusException) {
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
httpStatus = responseStatusException.getStatus();
message = responseStatusException.getMessage();
} else {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
message = StringUtils.isEmpty(ex.getMessage()) ? "Internal Server Error" : ex.getMessage();
}
//封装响应体,此body可修改为自己的jsonBody
Map<String, Object> result = new HashMap<>(2, 1);
result.put("httpStatus", httpStatus);
String msg = "{\"status\":" + httpStatus + ",\"message\": \"" + message + "\"}";
result.put("body", msg);
if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
exceptionHandlerResult.set(result);
ServerRequest newRequest = ServerRequest.create(exchange, this.messageReaders);
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse).route(newRequest)
.switchIfEmpty(Mono.error(ex))
.flatMap((handler) -> handler.handle(newRequest))
.flatMap((response) -> write(exchange, response));
}
/**
* 参考DefaultErrorWebExceptionHandler
*/
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Map<String, Object> result = exceptionHandlerResult.get();
return ServerResponse.status((HttpStatus) result.get("httpStatus"))
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(result.get("body")));
}
/**
* 参考AbstractErrorWebExceptionHandler
*/
private Mono<? extends Void> write(ServerWebExchange exchange,
ServerResponse response) {
exchange.getResponse().getHeaders()
.setContentType(response.headers().getContentType());
return response.writeTo(exchange, new ResponseContext());
}
/**
* 参考AbstractErrorWebExceptionHandler
*/
private class ResponseContext implements ServerResponse.Context {
@Override
public List<HttpMessageWriter<?>> messageWriters() {
return JsonExceptionHandler.this.messageWriters;
}
@Override
public List<ViewResolver> viewResolvers() {
return JsonExceptionHandler.this.viewResolvers;
}
}
}
/**
* 令牌已经过期, 固定状态码 UNAUTHORIZED(401, "Unauthorized"),
*
* @author tums
*/
public class TokenExpiredException extends ResponseStatusException {
public TokenExpiredException(@Nullable String reason) {
super(HttpStatus.UNAUTHORIZED, reason);
}
}
4、鉴权配置(jwt-token)
/**
* JWT 鉴权机制
* 注:每次请求头和响应头都会存储当前有效的token,白名单接口除外
* 1、JWT-Filter对登录/注册不鉴权,成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)
* 2、当该用户这次请求JWTToken值还在生命周期内,且该token对应cache中的k存在,则会通过重新PUT的方式k、v都为Token值,缓存中的token值生命周期时间重新计算(这时候k、v值一样)
* 3、当该用户这次请求JWTToken值还在生命周期内,但该token对应cache中的k不存在,返回用户信息已失效,请重新登录。
* 4、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
* 5、当该用户这次请求jwt生成的token值已经超时,且该token对应cache中的k不存在,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
*
* @author tums
* @date 2018/11/10 20:10
*/
@Component
public class TokenFilter implements GlobalFilter, Ordered {
/**
* header中token的key
*/
private static final String LOGIN_TOKEN = "LOGIN_TOKEN";
private final static int TEST_TOKEN_EXPIRES_MILLISECONDS = 1000 * 24 * 3600;
/**
* 当前登录用户ID
*/
private static final String LOGIN_ID = "LOGIN_ID";
@Resource
private RedisService redisService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
//白名单
if (UrlResolver.check(UrlWhileList.getUrlList(), url)) {
return chain.filter(exchange);
}
HttpHeaders httpHeaders = request.getHeaders();
List<String> tokens = httpHeaders.get(LOGIN_TOKEN);
Assert.isTrue(!CollectionUtils.isEmpty(tokens), LOGIN_TOKEN + " 不能为空");
String token = tokens.get(0);
Claims claims = null;
try {
claims = JwtTokenHelper.parseJWT(token);
} catch (RuntimeException e) {
throw new TokenExpiredException("token过期,请重新登录");
}
Assert.isTrue(!(claims == null || claims.isEmpty()), LOGIN_TOKEN + " 无效");
String id = claims.getId();
Assert.isTrue(!StringUtils.isEmpty(id), LOGIN_TOKEN + " 无效");
Date expiration = claims.getExpiration();
Date today = new Date();
long expiresMilliseconds = expiration.getTime() - claims.getNotBefore().getTime();
//token 未过期,判断redis 缓存是否过期
if (today.getTime() <= expiration.getTime()) {
String cacheToken = redisService.getToken(token);
if (StringUtils.isEmpty(cacheToken)) {
throw new TokenExpiredException("token过期,请重新登录");
}
//重新刷新有效期k=y
redisService.setToken(token, expiresMilliseconds);
//token 已经过期,判断redis 缓存是否过期
} else {
String cacheToken = redisService.getToken(token);
if (StringUtils.isEmpty(cacheToken)) {
throw new TokenExpiredException("token过期,请重新登录");
}
//redis 未过期
token = JwtTokenHelper.createJWT(id, expiresMilliseconds);
redisService.setToken(token, expiresMilliseconds);
}
//响应Header 中增加可用的token
exchange.getResponse().getHeaders().add(LOGIN_TOKEN, token);
//请求链路中增加token 主键
ServerHttpRequest serverHttpRequest = request.mutate().header(LOGIN_ID, id).build();
ServerWebExchange serverWebExchange = exchange.mutate().request(serverHttpRequest).build();
return chain.filter(serverWebExchange);
}
@Override
public int getOrder() {
return 0;
}
}