一. 网关+白名单
此方案需要在缓存中维护一套接口白名单,请求到达网关处,先判断白名单缓存中是否存在,存在则放行,反之则拦截。
该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;
不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;
另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。
二. 网关+AOP
相比于方案一对接口进行白名单判断而言,方案二是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。
我们可以在所有内部的调用请求头中增加一个header标志这是一个内部请求,比如加个请求头:from=Y
只要在业务接口处通过AOP的方式判断一下请求头中是否含有from=Y,如果有,则是内部请求,反之则是外部请求
1. 定义注解
这里AOP在码猿慢病云管理系统中采用的是注解的方式,注解如下:
1 2 3 4 5 6 7 8 9
| @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Inner {
boolean value() default true; }
|
2. 网关处理
在网关处需要对请求头中的from进行清洗,避免有意之人伪装内部请求,这里需要做的就是对每个请求直接移除from这个请求头,直接使用全局过滤器即可完成,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
public class CodeapeRequestGlobalFilter implements GlobalFilter, Ordered {
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest().mutate().headers(httpHeaders -> { httpHeaders.remove(SecurityConstants.FROM); httpHeaders.put(CommonConstants.REQUEST_START_TIME, Collections.singletonList(String.valueOf(System.currentTimeMillis()))); }).build(); ....... ....... }
|
3. feign接口处理
既然是内部调用,按照之前的约定是要在请求头中添加一个from=Y,因此在feign接口中需要新增这个请求头,方式很简单,比如设备feign接口,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
@FeignClient(contextId = "remoteDeviceService", value = ServiceNameConstants.DEVICE_SERVICE) public interface RemoteDeviceService {
@GetMapping(value = "/device/sn/{sn}",headers = "from=Y") R<DeviceInfoVO> getBySn(@PathVariable("sn" ) String sn); }
|
@GetMapping中的headers属性即可完成新增请求头,同样的比如@RequestMapping、@PostMapping等也是支持的。
这样的话在feign接口发出请求时则会自动在请求头中新增from=Y了。
4. AOP处理
在第1步中定义了@Inner这个注解,标注在controller方法上表示这个接口只允许内部调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Slf4j @Aspect @RequiredArgsConstructor public class CodeapeSecurityInnerAspect implements Ordered {
private final HttpServletRequest request;
@SneakyThrows @Around("@within(inner) || @annotation(inner)") public Object around(ProceedingJoinPoint point, Inner inner) { String header = request.getHeader("from"); if (inner.value() && !"Y".equals(header)) { log.warn("访问接口 {} 没有权限", point.getSignature().getName()); throw new AccessDeniedException("Access is denied"); } return point.proceed(); } ....... }
|
如果请求头中的from属性不匹配,则抛出AccessDeniedException异常,会被全局异常捕获,返回403的状态码