来吧,贴代码。
一、背景
我们有一个项目使用了spring cloud,有的微服务需要调用别的微服务,但这些调用没有鉴权;当初项目时间非常紧,同时这部分微服务有的对外也没有鉴权,在代码中设置了无须鉴权,可直接访问。近期客户进行安全性测评,查出了一堆安全性漏洞。你睇下:
java
@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,用于标记类
java
import java.lang.annotation.*;
/**
* 微服务内部访问方法
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inner {
/**
* 是否AOP统一处理
*/
boolean value() default true;
}
2、标注@InnerMethod,用于标记方法
java
import java.lang.annotation.*;
/**
* 微服务内部访问方法
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InnerMethod {
/**
* 是否AOP统一处理
*/
boolean value() default true;
}
3、AOP
1)InnerAspect.java
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;
}
}
其中,主要逻辑部分:
java
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
上面代码中用到这个自定义类。
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)类
java
@Api(value = "监测设备信息", tags = "监测设备信息")
@RestController
@RequestMapping("jcDevice")
@Inner
public class JcDeviceController implements IJcDeviceServiceClient {
。。。
}
2)方法
java
@GetMapping("/file/preview")
@InnerMethod
public void previewDemo(HttpServletRequest request, HttpServletResponse response, @RequestParam("code") String code) {
。。。
}
2、主动发起调用的服务
服务之间是通过Feign来调用的,只要在主动发起调用的微服务中实现Feign的拦截器即可:
java
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。这个问题下周有时间再看看。
但不一定有时间。公司没啥活,员工却还是那么忙,搞不懂。
参考文章:
服务之间调用还需要鉴权?
2024.03.25
有关经过nginx转发后拿不到原始请求IP问题已经解决了。nginx需要配置一下,在转发设置中加上:
java
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
如:
nginx.conf:
bash
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),以便后端服务器了解客户端是通过哪种协议发起请求的。
现在是周一上午,客户看上去暂时还没有活过来。正常情况下,周一上午,他们的问题和要求会如潮水般劈头盖脸地涌过来,让人分身乏术,恨不得三头六臂。一个上午下来,身心俱疲。
但是我又发现,之前原本非常清闲的,处于事业单位的客户,今年好像卷的厉害,常常下班很久了还给我发信息。而我下班就准点走了。这世界变化好快啊。