目录
1.获取用户真实IP
2.统一跨域配置
3.redis令牌桶算法限流
1.获取用户真实IP
在我们的日常业务中,我们时常需要获取用户的IP地址,作登录日志、访问限制等相关操作。
而在我们的开发架构中,一般我们将服务分为多个微服务,然后使用一个统一的网关对他们进行路由控制管理:
如上图,我们可以看到,一般来说网关(一般使用ngnix或者springcloud gateway)会放在独立的一台服务器上,他的ip是不一样的。当用户请求发过来时,网关收到用户请求,然后根据路由匹配对应的微服务,使用feign调用对应的微服务,所以在微服务中获取的ip其实是网关的IP,而不是用户访问的真实IP。
所以,我们想要获取用户的真实IP有以下两个方法:
(1)在gateway中进行配置:
我们可以在springcloud gateway中的过滤器中,拦截用户请求,获取用户的真实ip后存入HTTP header中,再转发至微服务中。
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class CommonFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest().mutate()
//将获取的真实ip存入header微服务方便获取
.header("X-Real-IP",exchange.getRequest().getRemoteAddress().getHostString())
.build();
return chain.filter(exchange.mutate().request(request).build());
}
}
上述代码中奖用户的请求IP作为key:X-Real-IP的值存储到header中,然后微服务中通过获取该header的方法即可获取到用户的真实IP。
String ip = request.getHeader("X-Real-IP");
注:X-Real-IP,一般只记录真实发出请求的客户端IP。该字段不是header中自带的,需要自行在网关中进行添加配置(如上述代码)。
(2)通过转发IP列表获取:
public class IpUtil {
public static String getIpAddress(HttpServletRequest request) {
//目前则是网关ip
String ip = request.getHeader("X-Forwarded-For");
if (ip != null && !"".equals(ip) && !"unknown".equalsIgnoreCase(ip)) {
int index = ip.indexOf(',');
if (index != -1) {
//只获取第一个值
return ip.substring(0, index);
} else {
return ip;
}
} else {
//取不到真实ip则返回空,不能返回内网地址。
return null;
}
}
}
X-Forwarded-For
是用于记录代理信息的,每经过一级代理,该字段就会记录来源地址,经过多级代理,服务端就会记录每级代理的X-Forwarded-For信息,IP之间以“,”分隔开。
所以,我们只要获取X-Forwarded-For中的第一个IP,就是用户的真实IP。
(3)测试
最后,我们可以在微服务的controller中编写代码测试一下,看看获取到的ip是怎样的:
@GetMapping("/test")
public Map<String,String> test(HttpServletRequest request){
Map<String,String> map = new HashMap<>();
map.put("真实ip",request.getHeader("X-Real-IP"));
map.put("ip列表",request.getHeader("X-Forwarded-For"));
map.put("转发ip",request.getRemoteAddr());
return map;
}
可以看到,在微服务直接使用 request.getRemoteAddr()获取到的只是网关所在的地址(此处是一个内网地址),而不是真实IP。而我们通过网关配置,再使用request.getHeader("X-Real-IP")获取到的才是真实IP。request.getHeader("X-Forwarded-For")中获取到的IP列表中,由于只进行了springcloud gateway一次代理,只记录了第一次代理前的IP地址。其与真实IP是一致的,所以X-Forwarded-For中的第一个IP地址也是用户的真实IP地址。
2.统一跨域配置
跨域问题就是由于前端服务器和后端服务器的IP地址、域名、端口、子域名不同,所进行的访问行动都是跨域的,而浏览器为了安全问题一般都限制了跨域访问,也就是不允许跨域请求资源。
注意:跨域限制访问,其实是浏览器的限制。
而在springcloud gateway中我们可以通过统一配置,对其访问的所有路由进行跨域统一处理:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
// 默认可不设置这个暴露的头。这个为了安全问题,不能使用*。
// 设置成*,后面会报错:throw new IllegalArgumentException("'*' is not a valid exposed header value");
corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
3.redis令牌桶算法限流
由于网关会外界访问系统的统一入口,所以我们一般需要在网关对请求进行引流或者直接拒绝等操作,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。
令牌桶算法:
令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中(redis),令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;
当网关收到一个请求时,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。
springcloud gateway中为我们集成了基于redis的令牌桶算法,其实现方式十分简单:
server:
port: 9527
max-http-header-size: 102400
spring:
application:
name: cloud-gateway
gateway: # 配置 Spring Cloud Gateway 相关属性
discovery: # 配置网关发现机制
locator: # 配置处理机制
enabled: true # 开启网关自动映射处理逻辑
lower-case-service-id: true # 开启服务名称小写转换。
routes: # 配置网关中的一个完整路由,包括命名,地址,谓词集合(规则),过滤器集合
- id: user_student_routh # 路由定义的命名,唯一即可。
uri: lb://cloud-student-manage # 当前路由定义对应的微服务转发地址,lb - 代表loadbalance
predicates:
- Path=/student/** # 断言,路径相匹配的进行路由
filters: # 配置过滤器集合
- name: RequestRateLimiter
args:
keyResolver: '#{@myKeyResolver}' # 使用SpringEL表达式,从Spring容器中找对象,并赋值。 '#{@beanName}',服务降级
redis-rate-limiter.replenishRate: 100 # 生产令牌速度,每秒多少个令牌
redis-rate-limiter.burstCapacity: 200 # 令牌桶容量
redis:
database: 0
host: 127.0.0.1
#redis默认端口
port: 6379
password:
jedis:
pool:
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)
timeout: 5000ms
只需要在application中配置对应的过滤器即可。
上述代码中还配置了当请求被拒绝时的服务降级相关配置,需要进行相关代码的编写:
(1)服务降级hystrix相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
(2)服务降级配置:
@Component
public class MyKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
String remoteAddr = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
return Mono.just(remoteAddr);
}
}
(3)服务降级接口:
@RestController
@Slf4j
public class AuthController {
@RequestMapping(value="/downgrade")
public CommonResult<Object> downgrade(Throwable e){
return new CommonResult<>(444,"对不起,服务器繁忙,请稍后重试",e.getMessage());
}
}