网关这边主要两个运用:

  • 外部服务入口(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;
    }
}