来吧,贴代码。

一、背景

我们有一个项目使用了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),以便后端服务器了解客户端是通过哪种协议发起请求的。

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

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

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐