zoukankan      html  css  js  c++  java
  • 网关安全(五)-引入网关,在网关上实现流控,认证,审计,授权

    1、当前项目存在的问题

      在前面我们已经完成了一个基于Oauth2认证和授权的流程(如上图)。但是到现在还没有进入到微服务的环境下,如果资源服务器(订单服务),不仅仅是一个单一服务。而是几十个微服务,并且每个微服务都是一个集群,在这样一个流程中存在如下问题:

      1.1、安全处理和业务逻辑耦合,增加了复杂性和变更成本。

      1.2、随着业务节点增加,认证服务器压力增大。

      1.3、多个微服务同时暴漏,增加了外部访问的复杂性。

    2、引入网关解决问题

      针对上面的问题,我们引入网关来解决,将安全处理放到网关中,微服务只处理自己的业务;有网关来验证令牌,微服务不与认证服务器直接交互了。对于外部访问来说,只需要网关的地址即可,内部微服务由网关进行转发。

    3、搭建zuul网关,转发路由

      3.1、pom.xml

        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>2.2.0.RELEASE</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>Greenwich.SR2</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <properties>
            <java.version>1.8</java.version>
            <maven.compiler.source>${java.version}</maven.compiler.source>
            <maven.compiler.target>${java.version}</maven.compiler.target>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
            </dependency>
        </dependencies>

      3.2、启动类GatewayServerApplication

    /**
     * 网关
     *
     * @author caofanqi
     * @date 2020/2/2 15:36
     */
    @EnableZuulProxy
    @SpringBootApplication
    public class GatewayServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(GatewayServerApplication.class,args);
        }
    
    }

      3.3、application.yml

    server:
      port: 9010
    
    spring:
      application:
        name: gateway-server
    
    zuul:
      routes:
        token:
          url: http://127.0.0.1:9020
          path: /token/**
        order:
          url: http://127.0.0.1:9080
          path: /order/**
      #敏感头设置为空,因为默认的包含 Authorization,我们需要通过Authorization传递信息
      sensitive-headers:

      3.4、启动项目,通过网关获取令牌及创建订单

      

    4、将在网关上实现认证,审计,授权

      4.1、删除order微服务上的安全配置,只留下业务代码

      4.2、在网关上添加OAuth2认证过滤器

    /**
     * OAuth2认证过滤器
     *
     * @author caofanqi
     * @date 2020/2/2 22:54
     */
    @Slf4j
    @Component
    public class OAuth2Filter extends ZuulFilter {
    
    
        private RestTemplate restTemplate = new RestTemplate();
    
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 2;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
    
            log.info("++++++认证++++++");
    
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
    
            if (StringUtils.startsWith(request.getRequestURI(), "/token")) {
                //发往认证服务器的请求直接放行
                return null;
            }
    
            String authorization = request.getHeader("Authorization");
    
            if (StringUtils.isBlank(authorization)) {
                //没有Authorization头的直接放行
                return null;
            }
    
            if (!StringUtils.startsWithIgnoreCase(authorization, "bearer ")) {
                //不是OAuth认证的直接放行
                return null;
            }
    
            try {
                request.setAttribute("tokenInfo", getTokenInfo(authorization));
            } catch (Exception e) {
                log.info("check token fail :", e);
            }
    
            return null;
        }
    
        /**
         *  向认证服务器校验token的有效性
         */
        private TokenInfoDTO getTokenInfo(String authorization) {
    
            String token = StringUtils.substringAfter(authorization, "bearer ");
            String checkTokenEndpointUrl = "http://127.0.0.1:9020/oauth/check_token";
    
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            headers.setBasicAuth("gateway", "123456");
    
            MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
            params.set("token", token);
    
            HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
    
            ResponseEntity<TokenInfoDTO> response = restTemplate.exchange(checkTokenEndpointUrl, HttpMethod.POST, httpEntity, TokenInfoDTO.class);
            TokenInfoDTO tokenInfo = response.getBody();
    
            log.info("tokenInfo : {}", tokenInfo);
    
            return tokenInfo;
    
        }
    
    
    }

      4.3、添加审计过滤器

    /**
     * 审计日志过滤器
     *
     * @author caofanqi
     * @date 2020/2/2 23:59
     */
    @Slf4j
    @Component
    public class AuditLogPreFilter extends ZuulFilter {
    
        @Resource
        private AuditLogRepository auditLogRepository;
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 3;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
    
            log.info("++++++pre审计++++++");
    
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
            TokenInfoDTO tokenInfo = (TokenInfoDTO) request.getAttribute("tokenInfo");
            String username = "anonymous";
            if (tokenInfo != null) {
                username = tokenInfo.getUser_name();
            }
    
            AuditLogDO auditLogDO = new AuditLogDO();
            auditLogDO.setPath(request.getRequestURI());
            auditLogDO.setHttpMethod(request.getMethod());
            auditLogDO.setUsername(username);
            auditLogRepository.saveAndFlush(auditLogDO);
    
            request.setAttribute("auditLogId",auditLogDO.getId());
    
            return null;
        }
    
    }
    /**
     * 审计日志过滤器
     *
     * @author caofanqi
     * @date 2020/2/2 23:59
     */
    @Slf4j
    @Component
    public class AuditLogPostFilter extends ZuulFilter {
    
        @Resource
        private AuditLogRepository auditLogRepository;
    
        @Override
        public String filterType() {
            return "post";
        }
    
        @Override
        public int filterOrder() {
            return 5;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
    
            log.info("++++++post审计++++++");
    
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
    
            Long auditLogId = (Long) request.getAttribute("auditLogId");
            Optional<AuditLogDO> auditLogOp = auditLogRepository.findById(auditLogId);
            AuditLogDO auditLogDO = auditLogOp.orElse(new AuditLogDO());
            auditLogDO.setHttpStatus(requestContext.getResponseStatusCode());
            if (requestContext.getThrowable()!= null){
                auditLogDO.setErrorMessage(requestContext.getThrowable().getMessage());
            }
            auditLogRepository.saveAndFlush(auditLogDO);
    
            return null;
        }
    
    }

      4.4、添加授权过滤器,将/token/** 设置为忽略校验

    /**
     * 授权过滤器
     *
     * @author caofanqi
     * @date 2020/2/3 0:15
     */
    @Slf4j
    @Component
    public class AuthorizationFilter extends ZuulFilter implements InitializingBean {
    
        @Value("${permit.urls}")
        private String permitUrls;
    
        private Set<String> permitUrlSet = new HashSet<>();
    
        private AntPathMatcher pathMatcher = new AntPathMatcher();
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 4;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
    
            log.info("++++++授权++++++");
    
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
    
            if (isPermitUrl(request)) {
                return null;
            }
    
            /*
             * 需要认证
             */
            TokenInfoDTO tokenInfo = (TokenInfoDTO) request.getAttribute("tokenInfo");
    
            if (tokenInfo != null && tokenInfo.getActive()) {
                if (!hasPermission(tokenInfo, request)) {
                    //没权限
                    handlerError(HttpStatus.FORBIDDEN.value(), requestContext);
                }
                //认证通过,向请求头中放入用户名,供微服务获取
                requestContext.addZuulRequestHeader("username", tokenInfo.getUser_name());
            } else {
                //没认证,或认证信息有误
                handlerError(HttpStatus.UNAUTHORIZED.value(), requestContext);
            }
    
            return null;
        }
    
        private void handlerError(int httpStatus, RequestContext requestContext) {
            requestContext.setResponseStatusCode(httpStatus);
            requestContext.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE);
            requestContext.setResponseBody("{"message":"auth fail"}");
            //不继续往下走了,返回
            requestContext.setSendZuulResponse(false);
    
        }
    
    
        private boolean isPermitUrl(HttpServletRequest request) {
            String uri = request.getRequestURI();
            for (String url : permitUrlSet) {
                if (pathMatcher.match(url, uri)) {
                    // 不需要认证和权限,直接访问
                    return true;
                }
            }
    
            return false;
        }
    
    
        private boolean hasPermission(TokenInfoDTO tokenInfo, HttpServletRequest request) {
    
            String[] scope = tokenInfo.getScope();
            if (StringUtils.equalsIgnoreCase(request.getMethod(), HttpMethod.GET.name())) {
                return ArrayUtils.contains(scope, "read");
            }
    
            if (StringUtils.equalsIgnoreCase(request.getMethod(), HttpMethod.POST.name())) {
                return ArrayUtils.contains(scope, "write");
            }
    
            return true;
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            Collections.addAll(permitUrlSet, StringUtils.splitByWholeSeparatorPreserveAllTokens(permitUrls, ","));
        }
    
    }

      4.5、order中获取用户名

        @PostMapping
        public OrderDTO create(@RequestBody OrderDTO orderDTO, @RequestHeader String username) {
            log.info("username is :{}", username);
            PriceDTO price = restTemplate.getForObject("http://127.0.0.1:9070/prices/" + orderDTO.getProductId(), PriceDTO.class);
            log.info("price is : {}", price.getPrice());
            return orderDTO;
        }

      4.6、启动各个项目

        4.6.1、将网关配置到oauth_client_details中

        4.6.2、通过网关获取scope为read和write的令牌,并通过网关携带正确的令牌访问获取订单请求,创建订单请求都成功,并且创建订单成功拿到了用户名

        4.6.3、通过网关获取scope为read的令牌,并通过网关携带正确的令牌访问获取订单请求,可以正常访问,但访问创建订单响应403。带错误的令牌或不带令牌访问服务响应401,说明我们的配置都生效了。

    5、使用spring-cloud-zuul-ratelimit进行限流

      5.1、pom中添加spring-cloud-zuul-ratelimit依赖

            <dependency>
                <groupId>com.marcosbarbero.cloud</groupId>
                <artifactId>spring-cloud-zuul-ratelimit</artifactId>
                <version>2.3.0.RELEASE</version>
            </dependency>

      5.2、在限流时需要存放一些信息,需要有相应的存储,支持的如下,推荐使用REDIS,我们引入spring-boot-starter-data-redis依赖

    public enum RateLimitRepository {
    
        REDIS,
    
        CONSUL,
    
        JPA,
    
        BUCKET4J_JCACHE,
    
        BUCKET4J_HAZELCAST,
    
        BUCKET4J_IGNITE,
    
        BUCKET4J_INFINISPAN,
    }
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>

      5.3、application.yml添加限流配置,我们只配置默认策略,更多配置请看 https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit 

    zuul:
      routes:
        token:
          url: http://127.0.0.1:9020
          path: /token/**
        order:
          url: http://127.0.0.1:9080
          path: /order/**
      #敏感头设置为空,因为默认的包含 Authorization,我们需要通过Authorization传递信息
      sensitive-headers:
      #限流相关配置
      ratelimit:
        key-prefix: zuul-ratelimit  #key的前缀,默认为应用名
        enabled: true #是否启用限流
        repository: REDIS #使用的存储
        behind-proxy: false #是否是代理之后,默认false
        default-policy-list: #默认策略列表:可选,针对所有的路由配置的策略,除非有具体的policy,否者使用默认该默认策略
          - limit: 2 # 可选,每个 refresh-interval 窗口的请求数限制
            quota: 1 # 可选,每个refresh-interval窗口的请求时间限制,单位秒
            refresh-interval: 10 # 默认值,单位秒
            type: #可选,限流方式,组合使用
              - url #根据请求路径
              - http_method #根据请求方法

      我们这段配置的意思是,1、相同的url和http method在10秒内,只可以有两个请求(limit)。2、相同的url和http method在10秒内的请求响应时间不能超过1秒(quota)。这两个条件满足任何一个都会被限流。

      5.4、启动各项目,启动redis,我们在10秒内,请求三次获取订单服务,被限流,如下

      5.5、我们可以将refresh-interval设置的大一点,可以发现redis中会根据我们的配置生成key,key的名称就是 配置的前缀:路由:url:httpmethod,并且类型是string,有过期时间。针对limit和quota会生成两个key。

       5.6、可以自定义key的生成规则,自定义错误处理,和自定义超速事件监听

    /**
     * 限流自定义配置 https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit
     *
     * @author caofanqi
     * @date 2020/2/3 15:34
     */
    @Slf4j
    @Configuration
    public class RateLimitConfig {
    
    
        /**
         * 自定义限流key生成规则
         */
        @Bean
        public RateLimitKeyGenerator ratelimitKeyGenerator(RateLimitProperties properties, RateLimitUtils rateLimitUtils) {
            return new DefaultRateLimitKeyGenerator(properties, rateLimitUtils) {
                @Override
                public String key(HttpServletRequest request, Route route, RateLimitProperties.Policy policy) {
                    /*
                     * 可以根据自己的需求自定义
                     */
                    return super.key(request, route, policy) + ":custom";
                }
            };
        }
    
        /**
         * 自定义错误处理
         */
        @Bean
        public RateLimiterErrorHandler rateLimitErrorHandler() {
            return new DefaultRateLimiterErrorHandler() {
                @Override
                public void handleSaveError(String key, Exception e) {
                    // 自定义代码
                    super.handleSaveError(key, e);
                }
    
                @Override
                public void handleFetchError(String key, Exception e) {
                    // 自定义代码
                    super.handleFetchError(key, e);
                }
    
                @Override
                public void handleError(String msg, Exception e) {
                    // 自定义代码
                    super.handleError(msg, e);
                }
            };
        }
    
    
        /**
         * 超速事件监听
         */
        @EventListener
        public void observe(RateLimitExceededEvent event) {
            log.info("监听到超速了...");
        }
    
    }

      注意:网关上不要做细粒度的限流,主要为服务器硬件设备的并发处理能力做限流。

     项目源码:https://github.com/caofanqi/study-security/tree/dev-gateway

  • 相关阅读:
    思维方法
    设计模式之创建者模式
    舍本逐末 事倍功半
    凡事预则立,不预则废
    股票投资,你一定要知道的运气三定律
    具体问题具体分析
    实事求是
    炒股与运气
    判大势定策略 寻找最适合的类型股
    炒股的本质-规避风险,增大收益-按客观规律办事
  • 原文地址:https://www.cnblogs.com/caofanqi/p/12254503.html
Copyright © 2011-2022 走看看