侧边栏壁纸
博主头像
qingtian博主等级

喜欢是一件细水流长的事,是永不疲惫的双向奔赴~!

  • 累计撰写 104 篇文章
  • 累计创建 48 个标签
  • 累计收到 1 条评论

使用Spring Gateway完成网关的登录、注册和鉴权

qingtian
2021-12-27 / 0 评论 / 0 点赞 / 979 阅读 / 8,040 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2021-12-27,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

使用Spring Gateway完成网关的登录、注册和鉴权

三大核心概念

Route、Predicate、Filter

  • Route(路由):是构建网关的基本模块,由ID、URI、一系列的断言和过滤器组成

  • Predicate(断言):可以匹配Http请求中所有的内容(请求头、参数等等),请求和断言相匹配则通过当前断言

  • Filter(过滤器):包括全局过滤器和局部过滤器,可以在请求被路由的前后对请求进行修改

Spring Cloud Gateway过滤器

全局过滤器和局部过滤器

  • 全局过滤器作用于所有的路由,不需要单独配置,通常用来实现统一化才处理的业务需求

  • 局部过滤器实现并生效的三步骤

    • 需要实现GatewayFliter,Ordered,实现相关的方法

    • 加入过滤器工厂,并且将工厂注册到Spring容器中

    • 在配置文件中进行配置,如果不配置则不启用此过滤器规则(路由规则)

Spring Cloud Gateway路由的配置

常见的三种配置方式

  1. 在代码中注入 RouteLocator Bean,并手工编写配置路由定义

  2. application.ymlbootstrap.yml等配置文件中配置spring.cloud.gateway

  3. 通过配置中心(Nacos)实现动态的路由配置

创建局部过滤器

/**
 * @description: 请求头部携带token的验证过滤器,局部过滤器
 * @author qingtian
 * @date 2021/12/13 18:04
 * @version 1.0
 */
public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //从http header中寻找key为token value为imooc的键值对
        String name = exchange.getRequest().getHeaders().getFirst("token");
        if("imooc".equals(name)) {
            return chain.filter(exchange);
        }

        //标记此次请求无权限
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 2;
    }
}

使用局部过滤器时我们必须要加入过滤器工厂,并且将它放入Spring容器中

/**
 * @author qingtian
 * @version 1.0
 * @description: 使用局部过滤器必须要配置,否则不生效
 * @date 2021/12/13 18:21
 */
@Component
public class HeaderTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
    @Override
    public GatewayFilter apply(Object config) {
        return new HeaderTokenGatewayFilter();
    }
}

创建全局过滤器

/**
 * @author qingtian
 * @version 1.0
 * @description: 缓存RequestBody的全局过滤器
 * @date 2021/12/13 18:30
 */
@Slf4j
@Component
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //判断请求的路径中是否含有login或者是register
        boolean isLoginOrRegister = exchange.getRequest().getURI().getPath().contains(GatewayConstant.LOGIN_URI)
                || exchange.getRequest().getURI().getPath().contains(GatewayConstant.REGISTER_URI);

        if (null == exchange.getRequest().getHeaders().getContentType()
        || !isLoginOrRegister) {
            return chain.filter(exchange);
        }

        //DataBufferUtils.join()拿到请求中的数据
        return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {

            //确保数据缓冲区的数据不被释放DataBufferUtils.retain
            DataBufferUtils.retain(dataBuffer);
            //defer just创建数据源,得到当前数据的副本
            Flux<DataBuffer> cachedFlux = Flux.defer(() ->
                    Flux.just(dataBuffer.slice(0,dataBuffer.readableByteCount())));
            //重新包装ServerHttp请求,重写getBody方法,能狗返回请求数据
            ServerHttpRequest mutateRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                @Override
                public Flux<DataBuffer> getBody() {
                    return cachedFlux;
                }
            };
            //将包装后的serverhttpRequest向下传递
            return chain.filter(exchange.mutate().request(mutateRequest).build());
        });
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 1;
    }
}
/**
 * @author qingtian
 * @version 1.0
 * @description: 全局鉴权过滤器
 * @date 2021/12/21 23:22
 */
@Component
@Slf4j
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {

    /**
     * @description: 从注册中心中获取对应服务的实例信息
     * @date 2021/12/21 23:42
     */
    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    /**
     * @description: 登录,注册,鉴权
     * 1. 如果是登录或注册,则去注册中心拿到token并返回给客户端
     * 2. 如果是访问其他的服务,则鉴权,没有权限返回401
     * @param: exchange
     * @param: chain
     * @return: reactor.core.publisher.Mono<java.lang.Void>
     * @date: 2021/12/22 0:17
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //如果是登录
        if (request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)) {
            //去授权中心拿token
            String token = getTokenFromAuthorityCenter(
                    request,GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT
            );
            //header 中不能设置null
            response.getHeaders().add(
                    CommonConstant.JWT_USER_INFO_KEY,
                    null == token ? "null" : token
            );
            response.setStatusCode(HttpStatus.OK);
            return response.setComplete();
        }

        //如果是注册
        if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)) {
            //去注册中心拿token: 会先创建用户,在返回token
            String token = getTokenFromAuthorityCenter(
                    request,GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT
            );
            //header中不允许为null对象
            response.getHeaders().add(
                    CommonConstant.JWT_USER_INFO_KEY,
                    null == token ? "null" : token
            );
            response.setStatusCode(HttpStatus.OK);
            response.setComplete();
        }

        //带着token去访问其他的服务,则鉴权
        HttpHeaders headers = request.getHeaders();
        String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
        LoginUserInfo loginUserInfo = null;

        try {
            loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
        } catch (Exception ex) {
            log.error("parse user info from token has error : [{}]",ex.getMessage(),ex);
        }

        //获取不到用户信息
        if (null == loginUserInfo) {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //解析通过放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        //因为在之前缓存body数据的过滤器中是 + 1 ,这个过滤器必须在它之后执行,所以这里是 + 2.
        return HIGHEST_PRECEDENCE + 2;
    }

    /**
     * @description:  从post请求中获取到数据
     * @param: request
     * @return: java.lang.String
     * @date: 2021/12/21 23:35
     */
    private String parseBodyFromRequest(ServerHttpRequest request) {

        //获取请求体
        Flux<DataBuffer> body = request.getBody();
        //获取原子引用
        AtomicReference<String> atomRef = new AtomicReference<>();

        //订阅缓冲去去消费请求体中的数据
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            //由于在之前的GlobalCacheRequestBodyFilter中将Body进行了retain所以在这里必须要释放掉
            //否则会造成内存泄露
            DataBufferUtils.release(buffer);
            atomRef.set(charBuffer.toString());
        });

        //获取到request body
        return atomRef.get();
    }

    /**
     * @description: 从授权中心获取 token
     * @param: request
     * @param: urlFormat
     * @return: java.lang.String
     * @date: 2021/12/21 23:38
     */
    private String getTokenFromAuthorityCenter(ServerHttpRequest request,String urlFormat) {

        //负载均衡
        ServiceInstance serviceInstance = loadBalancerClient.choose(CommonConstant.AUTHORITY_CENTER_SERVICE_ID);
        log.info("Nacos Client Info : [{}],[{}],[{}]",
                serviceInstance.getServiceId(),serviceInstance.getInstanceId(),
                JSON.toJSONString(serviceInstance.getMetadata()));
        //拼接请求的url
        String requestUrl = String.format(
                urlFormat,serviceInstance.getHost(),serviceInstance.getPort()
        );
        UsernameAndPassword requestBody = JSON.parseObject(
                parseBodyFromRequest(request),UsernameAndPassword.class
        );
        log.info("login request url and body : [{}],[{}]",requestUrl,JSON.toJSONString(requestBody));

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        JwtToken token = restTemplate.postForObject(
                requestUrl,
                new HttpEntity<>(JSON.toJSONString(requestBody),headers),
                JwtToken.class
        );

        if (null != token) {
            return token.getToken();
        }
        return null;
    }
}

这里两个全局过滤器是协同的,我们首先在request的请求中缓存了requestBody,然后在之后的过滤器中判断请求是登录,注册还是鉴权,去授权中心拿到token并且返回。

一些常量

/**
 * @author qingtian
 * @version 1.0
 * @description: 定义网关常量
 * @date 2021/12/14 15:15
 */
public class GatewayConstant {

    /**
     * @description: 登录uri
     * @date 2021/12/14 15:20
     */
    public static final String LOGIN_URI = "/e-commerce/login";

    /**
     * @description: 注册uri
     * @date 2021/12/14 18:52
     */
    public static final String REGISTER_URI = "/e-commerce/register";

    /**
     * @description: 去授权中心拿到token的格式化接口
     * @date 2021/12/14 18:58
     */
    public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT =
            "http://%s:%s/ecommerce-authority-center/authority/token";

    /**
     * @description: 去授权中心注册并拿到token的rurl格式化接口
     * @date 2021/12/14 21:57
     */
    public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT =
            "http://%s:%s/ecommerce-authority-center/authority/register";
}
/**
 * @author qingtian
 * @version 1.0
 * @description: Gateway注册到容器中的bean
 * @date 2021/12/21 23:39
 */
@Configuration
public class GatewayBeanConf {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

我们发现在GatewayConstant中的LOGIN_URIREGISTER_URI我们并没有配置相应的配置让它们起作用,我们选择使用
在代码中注入 RouteLocator Bean,并手工编写配置路由定义

/**
 * @author qingtian
 * @version 1.0
 * @description: 配置登录请求转发规则
 * @date 2021/12/25 23:58
 */
@Configuration
public class RouteLocatorConfig {

    /**
     * @description:  使用代码定义路由规则,在网关层面上拦截登录和注册接口
     * @param: builder
     * @return: org.springframework.cloud.gateway.route.RouteLocator
     * @date: 2021/12/26 0:00
     */
    @Bean
    public RouteLocator loginRouteLocator(RouteLocatorBuilder builder) {
        //手动定义Gateway路由规则需要指定id,path 和url
        return builder.routes()
                .route(
                        "e_commerce_authority",
                        r -> r.path(
                                "/imooc/e-commerce/login",
                                "/imooc/e-commerce/register"
                        ).uri("http://localhost:9001/")
                ).build();
    }
}
至此

至此我们已经完成了使用Spring Gateway对请求的登录,注册和鉴权功能的编写。

0

评论区