spring cloud项目微服务间互相调用使用自定义标注进行鉴权方案

来吧,贴代码。

一、背景

我们有一个项目使用了spring cloud,有的微服务需要调用别的微服务,但这些调用没有鉴权;当初项目时间非常紧,同时这部分微服务有的对外也没有鉴权,在代码中设置了无须鉴权,可直接访问。近期客户进行安全性测评,查出了一堆安全性漏洞。你睇下:

@Override
public void configure(HttpSecurity http) throws Exception {
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .and()
            .authorizeRequests()
            //添加放行接口,不进行OAuth2授权认证
            .antMatchers(
                    "/camera/**",
                    "/jcDeviceManufacturer/file/preview/**",
                    "/jcStationFile/**",
                    "/jcTimedTask/**",
                    "/jcDevice/**",
                    "/jcStation/**",
                    "/user/query/warning/**",
                    "/jcSenorDataCurrent/**",
                    "/jcSensorData003/**",
                    "/jcSensorData007/**",
                    "/jcSensorData008/**",
                    "/jcSensorData009/**",
                    "/jcSensorData012/**",
                    "/jcSensorData014/**",
                    "/jcSensorData015/**",
                    "/jcSensorData024/**",
                    "/jcSensorData025/**",
                    "/jcSensorData027/**",
                    "/jcSensorData034/**",
                    "/jcSensorGnssResolvedata/**",
                    "/jcSensorDataDxs/**",
                    "/jcSensorDataGnss/**",
                    "/jcStationMap/**",
                    "/jcWarnConfigDevice/**",
                    "/jcStationDeviceMap/**"
                    ).permitAll()
            // 指定监控访问权限
            .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
            .anyRequest().authenticated()
            .and()
            //认证鉴权错误处理
            .exceptionHandling()
            .accessDeniedHandler(new OpenAccessDeniedHandler())
            .authenticationEntryPoint(new OpenAuthenticationEntryPoint())
            .and()
            .csrf().disable();
}

如之奈何,计将安出?

二、思路

项目早就验收了,维护期也过期了。本着为客户着想,并幻想他们能再续期,丢个几万元让我们维护,所以我奋不顾身地维护一下。

我的指导原则是代码不要进行大的调整,尽量简单处理,毕竟量体裁衣,看菜吃饭。而且当时项目开发的人很多,我只负责其中几个模块,好多都不是我弄的。现在人员已经走得差不多了,维护任务就落到我头上。我只好硬着猪头皮,献上思路如下:

1)微服务间调用,检查请求头有无带上特定信息,有则通过,无则抛出异常
2)外部访问,设置白名单,检查发出请求的IP,符合则通过,否则抛出异常。这样第三方系统就不用更改了
3)但这些服务中,有一些前端也会请求。由于前端有登录,那么前端的请求应该不受上面的限制措施影响。
4)搞一个标注来完成这些鉴权动作,并且应用AOP,尽量将现有代码改动减到最小。

三、具体实现

1、标注@Inner,用于标记类

import java.lang.annotation.*;

/**
 * 微服务内部访问方法
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inner {
    /**
     * 是否AOP统一处理
     */
    boolean value() default true;
}

2、标注@InnerMethod,用于标记方法

import java.lang.annotation.*;

/**
 * 微服务内部访问方法
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InnerMethod {
    /**
     * 是否AOP统一处理
     */
    boolean value() default true;
}

3、AOP

1)InnerAspect.java

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Aspect
@Component
public class InnerAspect implements Ordered {
/**
配置文件内容:
inner.head.name=X-From
inner.head.value=internal
inner.white-ip=127.0.0.1,192.168.10.8,192.168.10.9
*/
    @Value(value = "${inner.head.name:X-From}")
    private String from;
    @Value(value = "${inner.head.value:internal}")
    private String fromIn;
    @Value(value = "${inner.white-ip:127.0.0.1}")
    private String whiteIps;

    private static List<String> whiteList = null;

    @Around("@within(inner)")  // Modified pointcut expression
    public Object around(ProceedingJoinPoint point, Inner inner) throws Throwable {
        if(!isValid(point,inner.value())){
            throw new AccessDeniedException("Access is denied");
        }
        return point.proceed();  // Proceed with the original method call
    }

    /**
     * 注意 @Around("@annotation(innerMethod)")中的"innerMethod",
     * 名称要与aroundMethod(ProceedingJoinPoint point, InnerMethod innerMethod) 中的参数名称一致
     * @param point
     * @param innerMethod
     * @return
     * @throws Throwable
     */
    @Around("@annotation(innerMethod)")
    public Object aroundMethod(ProceedingJoinPoint point, InnerMethod innerMethod) throws Throwable {
        if(!isValid(point,innerMethod.value())){
            throw new AccessDeniedException("Access is denied");
        }
        return point.proceed();
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }

    private boolean isValid(ProceedingJoinPoint point, boolean innerHasValue){
        boolean yes = true;

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || "anonymousUser".equals(authentication.getPrincipal())){//尚未登录
            initWhiteList();
            Signature signature = point.getSignature();
            if (innerHasValue) {  // Check if AOP is enabled for the class
                HttpServletRequest request = ServletUtils.getRequest();
                String header = request.getHeader(from);
                String ipAddress = getOriginalIp(request);

                // Authorization check based on request header or IP address
                if (!fromIn.equals(header) && !whiteList.contains(ipAddress)) {
                    System.err.println(String.format("没有权限访问接口 %s", signature.getName()));
                    yes = false;
                }
            }
        }

        return yes;
    }
    private void initWhiteList(){
        if(whiteList == null || whiteList.size() == 0){
            whiteList = new ArrayList<>(Arrays.asList(whiteIps.split(",")));
        }
    }
    /**
     * 获取最原始的请求IP
     * 因为请求有可能经过nginx等转发
     * @param request
     * @return
     */
    private String getOriginalIp(HttpServletRequest request) {
        String originalIp = request.getHeader("X-Forwarded-For");
        if (originalIp == null || originalIp.isEmpty()) {
            originalIp = request.getRemoteAddr();
        } else {
            // 可能会有多个IP,获取第一个IP地址
            originalIp = originalIp.split(",")[0].trim();
        }
        return originalIp;
    }
}

其中,主要逻辑部分:

private boolean isValid(ProceedingJoinPoint point, boolean innerHasValue){
    boolean yes = true;

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication == null || "anonymousUser".equals(authentication.getPrincipal())){//尚未登录
        initWhiteList();
        Signature signature = point.getSignature();
        if (innerHasValue) {  // Check if AOP is enabled for the class
            HttpServletRequest request = ServletUtils.getRequest();
            String header = request.getHeader(from);
            String ipAddress = getOriginalIp(request);

            // Authorization check based on request header or IP address
            if (!fromIn.equals(header) && !whiteList.contains(ipAddress)) {
                System.err.println(String.format("没有权限访问接口 %s", signature.getName()));
                yes = false;
            }
        }
    }

    return yes;
}

首先看是否已经登录,未登录的话才进行考察。如果既无请求头,又不在白名单内,才抛出异常;否则都通过,宽松得很。

值得一提得是,@Around的写法。里面的参数要跟函数的参数保持一致:
在这里插入图片描述

2)自定义的HttpServletRequest.java

上面代码中用到这个自定义类。

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

public class ServletUtils {
    public static HttpServletRequest getRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }
}

四、使用

1、被调用的服务

1)类

@Api(value = "监测设备信息", tags = "监测设备信息")
@RestController
@RequestMapping("jcDevice")
@Inner
public class JcDeviceController implements IJcDeviceServiceClient {
。。。
}

2)方法

  @GetMapping("/file/preview")
  @InnerMethod
  public void previewDemo(HttpServletRequest request, HttpServletResponse response, @RequestParam("code") String code) {
。。。
  }

2、主动发起调用的服务

服务之间是通过Feign来调用的,只要在主动发起调用的微服务中实现Feign的拦截器即可:

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class InnerAuthInterceptor implements RequestInterceptor {
    @Value(value = "${inner.head.name:X-From}")
    private String from;
    @Value(value = "${inner.head.value:internal}")
    private String fromIn;

    @Override
    public void apply(RequestTemplate template) {
        template.header(from, fromIn);
    }
}

五、小结

上述代码中,IP白名单在本地是没有问题的。但请求的转发是vue开发环境下实现的。部署到生产服务器nginx上,就拿不到最原始的请求IP,拿到的都是nginx服务器的IP。这个问题下周有时间再看看。

但不一定有时间。公司没啥活,员工却还是那么忙,搞不懂。
在这里插入图片描述
在这里插入图片描述

参考文章:
服务之间调用还需要鉴权?

Feign的拦截器RequestInterceptor


2024.03.25
有关经过nginx转发后拿不到原始请求IP问题已经解决了。nginx需要配置一下,在转发设置中加上:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

如:

nginx.conf:

server {
    listen 80; # 或者 listen 443 ssl; 如果使用 HTTPS
    server_name your_domain.com; # 替换为实际的域名

    # 其他 SSL 或 TLS 相关配置(如果适用)

    location / {
        proxy_pass http://backend_server:port; # 替换为后端服务器的实际 IP 和端口
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

关键配置解释:

proxy_pass: 设置后端服务器的 URL,即请求被转发到的目标地址。 proxy_set_header: Host: 保持原请求中的 Host 头部,这对于许多后端应用识别请求的主机名至关重要。

X-Real-IP: 设置为 $remote_addr,这是 Nginx 记录的直接与之连接的客户端(即您的浏览器)的 IP 地址。

X-Forwarded-For: 设置为 $proxy_add_x_forwarded_for。如果该头部已存在(可能因为之前已有代理),Nginx 会将其值追加到现有值的末尾,用逗号分隔;如果不存在,则设置为 $remote_addr。这样,后端服务器就可以从 X-Forwarded-For 中获取完整的客户端 IP 路径。 X-Forwarded-Proto: 传递请求的原始协议(http 或 https),以便后端服务器了解客户端是通过哪种协议发起请求的。

现在是周一上午,客户看上去暂时还没有活过来。正常情况下,周一上午,他们的问题和要求会如潮水般劈头盖脸地涌过来,让人分身乏术,恨不得三头六臂。一个上午下来,身心俱疲。

但是我又发现,之前原本非常清闲的,处于事业单位的客户,今年好像卷的厉害,常常下班很久了还给我发信息。而我下班就准点走了。这世界变化好快啊。

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spring Cloud的核心组件包括: 1. Eureka:服务注册与发现组件,用于管理微服务的注册和发现。 2. Ribbon:负载均衡组件,用于在微服务之间分配负载。 3. Feign:声明式REST客户端,用于简化微服务之间的通信。 4. Hystrix:容错组件,用于处理微服务之间的故障和延迟。 5. Zuul:API网关组件,用于管理微服务的访问和安全。 6. Config:配置中心组件,用于集中管理微服务的配置信息。 7. Bus:消息总线组件,用于在微服务之间传递消息和事件。 8. Sleuth:分布式跟踪组件,用于跟踪微服务之间调用和请求。 ### 回答2: Spring Cloud是一个用于构建分布式系统的开发工具集合,其核心组件有: 1. 服务注册与发现:Spring Cloud使用Netflix Eureka或Consul等服务注册与发现组件来管理微服务应用的注册与发现。它允许应用程序在注册中心注册自己的服务,并通过查询注册中心来发现其他服务。 2. 负载均衡:Spring Cloud使用Netflix Ribbon实现负载均衡,可以自动将请求分发到多个微服务实例中,以提高系统的可靠性和性能。 3. 服务调用Spring Cloud使用Netflix Feign实现服务的通信。开发人员可以使用标注接口的方式定义微服务之间调用,从而简化了远程调用的开发流程。 4. 断路器:Spring Cloud使用Netflix Hystrix实现断路器模式,帮助开发人员实现容错和故障转移。断路器可以监控微服务之间调用情况,并且在出现故障或延迟时提供备用方案,避免整个系统的崩溃。 5. 分布式配置中心:Spring Cloud使用Spring Cloud Config实现分布式配置中心,将应用程序的配置集中管理,可以动态刷新配置,避免了重新部署微服务的麻烦。 6. API 网关:Spring Cloud使用Netflix Zuul或Spring Cloud Gateway实现API网关,统一处理进入微服务架构的所有请求,提供认证鉴权、限流等功能。 7. 分布式追踪:Spring Cloud使用Zipkin或SkyWalking等作为分布式追踪系统,可以跟踪微服务调用链路、性能指标等,帮助开发人员快速定位问题。 8. 消息总线:Spring Cloud使用Spring Cloud Bus实现消息总线功能,可以在微服务之间广播配置的变更,实现配置的动态刷新。 除了以上核心组件,Spring Cloud还提供了许多其他的工具和扩展,以满足分布式系统开发的各种需求。 ### 回答3: Spring Cloud 是一个分布式系统的基础设施框架,它由多个核心组件组成。 1. 服务注册与发现:Spring Cloud通过Eureka或Consul提供了服务注册与发现的能力。服务提供者在启动时会将自己的信息注册到注册中心,消费者则通过注册中心找到可用的服务。 2. 负载均衡:Spring Cloud支持多种负载均衡算法,例如基于Ribbon的客户端负载均衡和基于Zuul的API网关,用于将请求动态路由到多个实例中。 3. 服务熔断与容错:通过Hystrix,Spring Cloud实现了服务熔断与容错的功能。当服务出现故障或超时时,Hystrix会通过断路器的机制控制服务的访问,保证系统的稳定性。 4. 服务配置管理:Spring Cloud的配置中心可以集中管理系统的配置信息,支持动态刷新配置。常用的配置中心有Spring Cloud Config Server和Consul。 5. 消息总线:Spring Cloud Bus提供了一种消息总线,在微服务架构中可以用于发送广播消息或刷新配置操作,以便实现整个系统的状态同步。 6. 分布式链路追踪:Spring Cloud Sleuth集成了Zipkin,用于实现分布式链路追踪,可以跟踪请求在不同微服务之间调用流程和耗时情况。 7. API网关:Spring Cloud Gateway提供了一个统一的入口,用于将请求路由到对应的微服务,实现对外暴露API接口的功能。 8. 分布式事务:Spring Cloud中基于Seata的分布式事务解决方案用于保证跨多个微服务的事务一致性。 这些核心组件共同构建了Spring Cloud微服务架构,帮助开发者构建分布式系统的基础设施。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值