自学内容网 自学内容网

实现一个自己的OpenFeign 远程调用验证协议--Springboot 自定义拦截器验证请求合法性--xunznux

Springboot 如何实现一个自定义的拦截器处理系统发出的请求以及接收到的请求(实现一个用于feign远程调用验证的简单协议)

实现Feign拦截器的意义

通过 Spring Boot中的过滤器,可以处理HTTP请求并执行一些预处理逻辑,比如验证请求的合法性等。

  • 统一处理请求:
    拦截器允许你在所有Feign调用之前和之后执行代码,这使得你可以统一处理如添加请求头、日志记录、性能监控、错误处理等操作。
  • 权限校验:
    在微服务架构中,服务间通信的安全性非常重要。通过Feign拦截器,可以在调用远程服务前检查权限,确保只有授权的服务才能访问特定资源。
  • 认证和授权:
    当使用OAuth2或其他认证机制时,Feign拦截器可以自动添加必要的认证信息(如JWT Token)到请求头中,确保远程服务能够识别调用者的身份。
  • 请求/响应修改:
    拦截器可以修改请求参数或响应数据,例如,对敏感信息进行加密或解密,或者根据业务需求转换数据格式。
  • 性能优化:
    可以在拦截器中实现缓存逻辑,避免不必要的远程调用,提高应用性能。

实现细节&代码

具体可以看代码仓库的demo:https://gitee.com/zhou-xiujun/feign-xun-interceptor

首先添加 application.yaml 的配置

spring:
  application:
    name: FeignXunInterceptor
app:
  id: 999
  key: 666

xun:
  protocol:
    context-path: /xun

其中,app.id 和 app.key 是需要保密的,用于服务端验证客户端的请求是否合法。
context-path 是用于判断请求路径是否符合该远程调用的规定,如果URI 以该路径开头,则判定是远程调用,需要进行请求的处理。

添加配置类

XunConfig
@Component
@ConfigurationProperties(prefix = "app")
public class XunConfig {
    private String id;
    private String key;

    // 添加getters和setters
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }
}

该配置类用于获取 id 和 key。

XunProperties
@Component
@ConfigurationProperties(prefix = "xun.protocol")
public class XunProperties {
    private String contextPath;

    public String getContextPath() {
        return contextPath;
    }

    public void setContextPath(String contextPath) {
        this.contextPath = contextPath;
    }
}

该配置类用于获取 contextPath。

XunAutoConfiguration
@Configuration
public class XunAutoConfiguration {
    @Bean
    public RequestInterceptor requestInterceptor() {
        return new ClientInterceptor();
    }

    @Bean
    public OncePerRequestFilter serverInterceptor() {
        return new ServerInterceptor();
    }
}

其中的 ClientInterceptor 和 ServerInterceptor 都是后面定义的客户端拦截器和服务端拦截器。这里使用 bean 注入的方式应用拦截器。

ClientInterceptor 客户端拦截器的实现

public class ClientInterceptor implements RequestInterceptor {
    @Resource
    private XunConfig xunConfig;
    @Resource
    private XunProperties xunProperties;

    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";

    @Override
    public void apply(RequestTemplate template) {
        if (template.url().startsWith(xunProperties.getContextPath())) {
            String appId = xunConfig.getId(); // 从配置或服务中获取
            String appKey = xunConfig.getKey(); // 从配置或服务中获取
            byte[] body = template.body(); // 需要从template或其他地方获取请求体内容
            Boolean signUpperCase = true; // 根据需要设置

            String sign = xunSign(appId, appKey, Optional.ofNullable(body)
                    .map(Arrays::toString)
                    .orElse(""), signUpperCase);
            template.header("Content-Type", "application/json");
            template.header("Authorization", "Bearer " + sign);
            if (body != null) {
                template.header("data", Base64.getEncoder().encodeToString(body));

            }

            process(template, StandardCharsets.UTF_8, sign, body);
        }
    }

    private void process(RequestTemplate template, Charset charset, String key, byte[] data) {
        template.removeHeader("Content-Type");
        template.header("Content-Type", "application/json");
        template.body(data, charset);
    }

    private String encode(String string, Charset charset) {
        try {
            return URLEncoder.encode(string, charset.name());
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Failed to encode URL", e);
        }
    }

    /**
     * 生成签名
     * @param appId 应用ID
     * @param appKey 应用密钥
     * @param bodyStr 请求体字符串
     * @param signUpperCase 是否大写签名
     * @return 生成的签名
     */
    private String xunSign(String appId, String appKey, String bodyStr, Boolean signUpperCase) {
        try {
            String data = appId + (bodyStr != null ? bodyStr : "");
            Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
            SecretKeySpec secretKey = new SecretKeySpec(appKey.getBytes(StandardCharsets.UTF_8), HMAC_SHA256_ALGORITHM);
            mac.init(secretKey);
            byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            String signature = Base64.getEncoder().encodeToString(hash);
            return signUpperCase ? signature.toUpperCase() : signature;
        } catch (Exception e) {
            throw new RuntimeException("Failed to generate signature", e);
        }
    }

}

apply方法,用于对RequestTemplate对象进行处理。具体功能如下:

  1. 首先判断template的URL是否以xunProperties.getContextPath()开头,如果是则执行以下步骤,否则直接返回。
  2. 从配置或服务中获取appId和appKey。
  3. 从template中获取请求体内容body。
  4. 根据需要设置signUpperCase为true。
  5. 调用xunSign方法生成签名sign。
  6. 设置请求头Content-Type为application/json,Authorization为Bearer + sign。
  7. 如果请求体body不为空,则将body进行Base64编码,并设置请求头data为编码后的字符串。
  8. 最后调用process方法对template进行进一步处理,传入UTF-8编码、sign和body作为参数。

该函数主要用于对请求进行签名认证和设置请求头,以便进行安全的API调用。这里的实现较为简单,可以个人对此进行优化改进,保证更好的安全性和合理性。主要展示是是这个处理流程。

ServerInterceptor 服务端拦截器

public class ServerInterceptor extends OncePerRequestFilter {
    @Resource
    private XunConfig xunConfig;
    @Resource
    private XunProperties xunProperties;

    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 认证放行与协议验证鉴权逻辑
        String authorization = request.getHeader("Authorization");
        String requestURI = request.getRequestURI();
        if (requestURI.startsWith(xunProperties.getContextPath()) && authorization != null && authorization.startsWith("Bearer ")) {
            String appId = xunConfig.getId(); // 从配置或服务中获取
            String appKey = xunConfig.getKey(); // 从配置或服务中获取
            Boolean signUpperCase = true; // 根据需要设置
            String generatedSign = xunSign(appId, appKey, getRequestBody(request), signUpperCase);
            String token = authorization.substring(7);
            if (generatedSign.equals(token)) {
                filterChain.doFilter(request, response);
            } else {
                // 验证不通过
                throw new RuntimeException("验证不通过");
            }
        }
        // 放行
        filterChain.doFilter(request, response);
    }

    private String xunSign(String appId, String appKey, String bodyStr, Boolean signUpperCase) {
        try {
            String data = appId + (bodyStr != null ? bodyStr : "");
            Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
            SecretKeySpec secretKey = new SecretKeySpec(appKey.getBytes(StandardCharsets.UTF_8), HMAC_SHA256_ALGORITHM);
            mac.init(secretKey);
            byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            String signature = Base64.getEncoder().encodeToString(hash);
            return signUpperCase ? signature.toUpperCase() : signature;
        } catch (Exception e) {
            throw new RuntimeException("Failed to generate signature", e);
        }
    }

    private String getRequestBody(HttpServletRequest request) throws IOException {
        StringBuilder requestBody = new StringBuilder();
        try (BufferedReader reader = request.getReader()) {
            String line;
            while ((line = reader.readLine()) != null) {
                requestBody.append(line);
            }
        }
        return requestBody.toString();
    }
}

该函数是一个过滤器,用于对请求进行认证和授权验证。首先,它从请求头中获取Authorization信息和请求的URI。然后,判断请求的URI是否以某个特定的路径开头,并且Authorization信息是否以Bearer开头。如果是,则从配置或服务中获取appId和appKey,并根据需要生成一个签名。接着,从Authorization信息中获取token,并将其与生成的签名进行比较。如果两者相等,则放行请求,否则抛出一个运行时异常,表示验证不通过。最后,无论认证和授权验证是否通过,都会放行请求。
同样的,这里还可以再进行优化改进。

以上这两部分就是实现拦截器的核心部分。
接下来是使用部分。

服务端提供的接口方法

@RestController
@RequestMapping("/xun")
public class ServerController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

客户端的Feign调用方法

@RestController
@RequestMapping("/client")
public class ClientController {
    @Resource
    private SampleClient sampleClient;

    @GetMapping("/hello")
    public String hello() {
        // 调用远程服务
        return sampleClient.hello();
    }
}
@FeignClient(name = "server", url = "http://localhost:8080", configuration = XunAutoConfiguration.class)
public interface SampleClient {
// 这定义了一个公共接口SampleClient,该接口将被Feign客户端实现,从而允许调用远程服务的方法。
    @GetMapping("/xun/hello")
    String hello();
}

@FeignClient 注解是Spring Cloud Feign提供的,用于声明一个Feign客户端。

  • name = “server” 指定了这个Feign客户端的名字,通常这个名字会被用来识别一个服务实例,特别是在使用服务注册与发现机制如Eureka或Consul时。但是在这个例子中,因为指定了URL,name可能不会用于服务发现。
  • url = “http://localhost:8080” 直接指定了目标服务的URL,这意味着这个Feign客户端将直接调用位于http://localhost:8080的微服务,而不是通过服务发现机制寻找服务位置。
  • configuration = XunAutoConfiguration.class 指定了一个配置类,该配置类包含了Feign客户端的额外配置,比如编码器、解码器、日志级别等。这使得可以为特定的Feign客户端定制行为。
  • @GetMapping 是Spring MVC的注解,用于映射HTTP GET请求到特定的方法上。在这里,它将/xun/hello的GET请求映射到了hello()方法。
  • String hello() 是一个无参数的方法,当远程服务的/xun/hello端点被调用时,它期望返回一个字符串类型的结果。

综上所述,SampleClient是一个Feign客户端接口,它可以调用位于http://localhost:8080/xun/hello的远程服务端点,并期待返回一个字符串类型的响应。在Spring Boot应用中,你可以像调用本地方法一样调用SampleClient的hello()方法,Feign框架会自动处理HTTP请求和响应的序列化与反序列化。

Springboot 启动类增加客户端

@SpringBootApplication
@EnableFeignClients(clients = {SampleClient.class})
public class FeignXunInterceptorApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignXunInterceptorApplication.class, args);
    }
}

总结

至此,一个简单的用于 OpenFeign 远程调用的验证协议就完成了,可以将其作为一个项目打包为jar包,然后引入其他项目中使用,这里将不做介绍。当然,拦截器的使用也不仅仅是可以只用于 OpenFeign 的远程调用验证,可以用于任何 HTTP 请求的拦截与验证。

之前的文章有对Springboot 启动时Bean的创建与注入这个过程的讲解以及对应的源码解读,感兴趣的可以去看看:
Springboot 启动时Bean的创建与注入(一)-源码解读-xunznux
Springboot 启动时Bean的创建与注入(二)-源码解读-xunznux
Springboot 的Bean生命周期五步、七步、十步详解以及框架源码解读


原文地址:https://blog.csdn.net/weixin_44615954/article/details/140620540

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!