来吧,贴代码。
一、背景
我们有一个项目使用了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 expressionpublic 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();}@Overridepublic 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 classHttpServletRequest request = ServletUtils.getRequest();String header = request.getHeader(from);String ipAddress = getOriginalIp(request);// Authorization check based on request header or IP addressif (!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 classHttpServletRequest request = ServletUtils.getRequest();String header = request.getHeader(from);String ipAddress = getOriginalIp(request);// Authorization check based on request header or IP addressif (!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")@InnerMethodpublic 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;@Overridepublic void apply(RequestTemplate template) {template.header(from, fromIn);}
}
五、小结
上述代码中,IP白名单在本地是没有问题的。但请求的转发是vue开发环境下实现的。部署到生产服务器nginx上,就拿不到最原始的请求IP,拿到的都是nginx服务器的IP。这个问题下周有时间再看看。
但不一定有时间。公司没啥活,员工却还是那么忙,搞不懂。
参考文章:
服务之间调用还需要鉴权?
Feign的拦截器RequestInterceptor