【第23章】Spring Cloud之微服务通用拦截器


前言

前面我们讲述了通过网关层(Gateway)增加全局过滤器,完成了对服务的校验,确保服务的安全。
但是我们把请求地址从localhost:8888/provider/hello,修改为localhost:9000/provider/hello,直接请求到提供者服务,还是可以拿到数据。

什么阿猫阿狗都来访问我的接口,数据都被搞走了,那我饭碗岂不是要不保!
上科技,不对!上狠活,还不对!上代码,做猛男!

先说方案:

  • 第一种是从网络层面,我们的服务一般会部署到Linux服务器,通过服务器防火墙或者网络管理员只对外开通网关的端口,这种方法的实现是最简单的。但是,服务器不只你一个系统在用,很多服务的话,就不太可控。
  • 第二种是增加通用的微服务拦截器,微服务验证之后增加标志位,后台服务根据标志位判断请求来源,有标志位则继续执行,无标志位给出错误提示并返回。

一、公共模块

1. 引入依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2. 响应实体

package org.example.common.model;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

//统一响应结果
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code;//业务状态码  0-成功  1-失败
    private String message;//提示信息
    private T data;//响应数据

    //快速返回操作成功响应结果(带响应数据)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
    }

    public static <E> Result<E> success(String message,E data) {
        return new Result<>(0, message, data);
    }

    public static Result success(String message) {
        return new Result<>(0, message, null);
    }

    //快速返回操作成功响应结果
    public static Result success() {
        return new Result(0, "操作成功", null);
    }

    public static Result error(String message) {
        return new Result(1, message, null);
    }
    public static Result error(int code,String message) {
        return new Result(code, message, null);
    }
}

3. 启用注解

package org.example.common.annotation;

import org.example.common.config.LoginInterceptorConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 启用拦截器
 * Create by zjg on 2024/7/24
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(LoginInterceptorConfiguration.class)
@Documented
public @interface EnableLoginInterceptor {
}

4. 登录拦截器配置类

package org.example.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 登录拦截器配置类
 * Create by zjg on 2024/7/24
 */
@Configuration(proxyBeanMethods = false)
public class LoginInterceptorConfiguration {
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new CommonWebMvcConfigurer();
    }
}

5. 添加拦截器

package org.example.common.config;

import org.example.common.interceptor.LoginInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;

/**
 * 添加拦截器
 * Create by zjg on 2024/7/24
 */
public class CommonWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        List<String> patterns=new ArrayList<>();
        patterns.add("/user/login");
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(patterns);
    }
}

6. 登录标识加密

package org.example.common;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * Create by zjg on 2024/7/27
 */
public class CommonTest {
    public static void main(String[] args) {
        System.out.println(new String(Base64.getEncoder().encode("gateway".getBytes(StandardCharsets.UTF_8))));
        System.out.println(new String(Base64.getEncoder().encode("feign".getBytes(StandardCharsets.UTF_8))));
    }
}

7. 登录拦截器

package org.example.common.interceptor;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.common.model.Result;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;

/**
 * 登录拦截器
 * Create by zjg on 2024/7/24
 */
public class LoginInterceptor implements HandlerInterceptor {
    private final List<String> marks=List.of("Z2F0ZXdheQ==","ZmVpZ24=");
    private final List<String> whiteList=List.of("/v3/api-docs");
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    	//白名单
        for (String white:whiteList) {
            if(request.getRequestURI().endsWith(white))
                return true;
        }
        //这里分为两种mark:  gateway、feign
        // 1.从网关过来的(外部来源),肯定携带了token值且网关层已经验证过了
        // 2.从微服务内部(内部来源)注册中心访问
        String mark = request.getHeader("Source-Mark");
        if(Boolean.FALSE.equals(StringUtils.hasText(mark))||Boolean.FALSE.equals(marks.contains(mark))){
            response.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
            response.setContentType("application/json;charset=UTF-8");
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.writerFor(Result.class);
            String message = objectMapper.writeValueAsString(Result.error(HttpStatus.NOT_ACCEPTABLE.value(),"未知的来源,禁止访问!"));
            response.getWriter().println(message);
            return false;
        }
        return true;
    }
}

二、提供者模块

提供者作为我们的核心模块,首先肯定要登录才能访问,接下来我们来演示使用公共模块的拦截器
关于公共模块的引入之前已经做过了,这里不再展示

1. 启用注解

package org.example.nacos.provider;

import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
import org.example.common.annotation.EnableLoginInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.concurrent.TimeUnit;

@EnableLoginInterceptor
@EnableDiscoveryClient
@SpringBootApplication
public class NacosDiscoveryProviderApplication {

    public static void main(String[] args) throws InterruptedException {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(NacosDiscoveryProviderApplication.class, args);
        System.out.println(applicationContext);
//        while(true) {
//            //当动态配置刷新时,会更新到 Enviroment中,因此这里每隔一秒中从Enviroment中获取配置
//            String userName = applicationContext.getEnvironment().getProperty("user.name");
//            String userAge = applicationContext.getEnvironment().getProperty("user.age");
//            System.err.println("user name :" + userName + "; age: " + userAge);
//            TimeUnit.SECONDS.sleep(1);
//        }
    }

}

2. 启动服务

在这里插入图片描述

3. 拦截器测试

在这里插入图片描述

到这里我们的拦截器已经成功引入,并生效了,是不是很简单。

三、网关模块(外部来源)

网关模块需要添加登录标识,可以使用AddRequestHeader网关过滤器,也可以通过之前的LoginGlobalFilter登录过滤器添加

1. 调整前

在这里插入图片描述

2. 增加请求头

这里我们在登录过滤器安全认证之后,请求转发之前,给所有的请求添加请求头标识

package org.example.gateway.filter;

import org.example.common.util.JwtUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
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.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * Create by zjg on 2024/7/21
 */
@Component
public class LoginGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String uri = request.getURI().getPath();
        if(uri.equals("/user/login")||uri.equals("/user/login/")){
            MultiValueMap<String, String> queryParams = request.getQueryParams();
            if(queryParams.containsKey("username")&&queryParams.containsKey("password")){
                return chain.filter(exchange);
            }else {
                response.setStatusCode(HttpStatus.BAD_REQUEST);
                return response.writeWith(Flux.just(response.bufferFactory().wrap("用户名和密码不能为空".getBytes())));
            }
        }
        HttpHeaders headers = request.getHeaders();
        String authorization = headers.getFirst("Authorization");
        if(Boolean.FALSE.equals(StringUtils.hasText(authorization))||Boolean.FALSE.equals(JwtUtils.verify(authorization.startsWith("Bearer")?authorization.substring(authorization.indexOf("Bearer")+7):authorization))){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            String message="token验证失败,请重新获取token后重试!";
            return response.writeWith(Flux.just(response.bufferFactory().wrap(message.getBytes())));
        }
        request = exchange.getRequest().mutate()
        						.headers(httpHeaders -> httpHeaders.add("Source-Mark", "Z2F0ZXdheQ==")).build();
        return chain.filter(exchange.mutate().request(request).build());
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

3. 调整后

在这里插入图片描述

四、消费者(内部来源)

1. RestTemplate

package org.example.nacos.consumer.config;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.web.client.RestTemplate;

/**
 * Create by zjg on 2024/7/16
 */
@Configuration
public class ConsumerConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder.defaultHeader("Source-Mark", "ZmVpZ24=").build();
    }
}

2. feign

package org.example.nacos.consumer.feign.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;

/**
 * Create by zjg on 2024/7/27
 */
@Component
public class LoginRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("Source-Mark", "ZmVpZ24=");
    }
}

总结

回到顶部

这种模式可以很方便地启用拦截器,极大地提升了系统的灵活性,增加了微服务的安全保障;
核心模块可进一步验证token值,以保证核心服务的安全。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值