微服务如何保证对外接口的安全?可以这样做!

如果你的微服务需要向第三方开放接口,如何确保你提供的接口是安全的呢?

1. 什么是安全接口

通常来说,要将暴露在外网的 API 接口视为安全接口,需要实现防篡改防重放的功能。

1.1 什么是篡改问题?

图片

1.1.1 如何解决篡改问题?

虽然使用 HTTPS 协议能对传输的明文进行加密,但黑客仍可截获数据包进行重放攻击。两种通用解决方案是:

  1. 使用 HTTPS 加密接口数据传输,即使被黑客破解,也需要耗费大量时间和精力。

  2. 在接口后台对请求参数进行签名验证,以防止黑客篡改。

签名的实现过程如下图所示:

图片

图片

1.2. 什么是重放问题?

图片

1.2.1 如何解决重放问题?

防重放,业界通常基于 nonce + timestamp 方案实现。

每次请求接口时生成 timestamp 和 nonce 两个额外参数,其中 timestamp 代表当前请求时间,nonce 代表仅一次有效的随机字符串。

生成这两个字段后,与其他参数一起进行签名,并发送至服务端。

服务端接收请求后,先比较 timestamp 是否超过规定时间(如60秒),再查看 Redis 中是否存在 nonce,最后校验签名是否一致,是否有篡改。

图片

2. 身份认证方案

图片

2.1 AppId + AppSecret

图片

现在,让我们再来梳理一下完整的签名方案。

图片

3. 代码实现

"Talk is cheap. Show me the code." 

说了这么久,现在让我们从代码的角度来看看如何安全地对外提供接口。

3.1 AppId 和 AppSecret的生成

图片

 private static String getAppKey() {
 long num = IdUtils.nextId();
 StringBuilder sb = new StringBuilder();
 do {
  int remainder = (int) (num % 62);
  sb.insert(0, BASE62_CHARACTERS.charAt(remainder));
  num /= 62;
 } while (num != 0);
 return sb.toString();
}

通过这个算法生成的 AppId 和 AppSecret 形如:

appKey=6iYWoL2hBk9, appSecret=5de8bc4d8278ed4f14a3490c0bdd5cbe369e8ec9

3.2 API校验器

图片

//认证接口
public interface ApiAuthenticator {
  AuthenticatorResult auth(ServerWebExchange request); 
}

//具体实现
@Slf4j
public class ProtectedApiAuthenticator implements ApiAuthenticator {
  ...
}

3.2 网关过滤器

接口的安全校验很适合放在网关层实现,因此我们需要在网关服务中创建一个过滤器 ApiAuthenticatorFilter

@Component
@Slf4j
public class ApiAuthenticatorFilter implements GlobalFilter, Ordered {
    ...
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
       
        // 获取认证逻辑
        ApiAuthenticator apiAuthenticator = getApiAuthenticator(rawPath);
        AuthenticatorResult authenticatorResult = apiAuthenticator.auth(exchange);
      
        if (!authenticatorResult.isResult()) {
            return Mono.error(new HttpServerErrorException(
                    HttpStatus.METHOD_NOT_ALLOWED, authenticatorResult.getMessage()));
        }
        
        return chain.filter(exchange);
        
    }
    
   
   /**
     * 确定认证策略
     * @param rawPath 请求路径
     */
    private ApiAuthenticator getApiAuthenticator(String rawPath) {
        String[] parts = rawPath.split("/");
        if (parts.length >= 4) {
            String parameter = parts[3];
              return switch (parameter) {
                case PROTECT_PATH ->   new ProtectedApiAuthenticator();
                case PRIVATE_PATH ->   new PrivateApiAuthenticator();
                case PUBLIC_PATH ->    new PublicApiAuthenticator();
                case DEFAULT_PATH ->   new DefaultApiAuthenticator();
                default -> throw new IllegalStateException("Unexpected value: " + parameter);
              };
        }
        return new DefaultApiAuthenticator();
    }
    
}

图片

3.3 接口安全认证

正如上文所说,服务端获取到请求参数以后需要检查请求时间是否过期,nonce是否已经被使用,签名是否正确。

图片

 

按照这个逻辑我们很容易在ProtectedApiAuthenticator认证器中写出这样的代码。

@Slf4j
public class ProtectedApiAuthenticator implements ApiAuthenticator {

    @Override
    public AuthenticatorResult auth(ServerWebExchange exchange)  {
        
        // 1. 校验参数
        boolean checked = preAuthenticationCheck(requestHeader);
        if (!checked) {
            return new AuthenticatorResult(false, "请携带正确参数访问");
        }

        // 2 . 重放校验
        // 判断timestamp时间戳与当前时间是否操过60s(过期时间根据业务情况设置),如果超过了就提示签名过期。
        long now = System.currentTimeMillis() ;      
         if (now - Long.parseLong(requestHeader.getTimestamp()) > 60000) {
            return new AuthenticatorResult(false, "请求超时,请重新访问");
         }

        // 3. 判断nonce
        boolean nonceExists = distributedCache.hasKey(NONCE_KEY + requestHeader.getNonce());
        if (nonceExists) {
            return new AuthenticatorResult(false, "请勿重复提交请求");
        } else {
            distributedCache.put(NONCE_KEY + requestHeader.getNonce(), requestHeader.getNonce(), 60000);
        }
      
        // 4. 签名校验
       SortedMap<String, Object> requestBody = CachedRequestUtil.resolveFromBody(exchange);
       String sign = buildSign(requestHeader,requestBody);
      if(!sign.equals(requestHeader.getSign())){
        return new AuthenticatorResult(false, "签名错误");
      }
      
      return new AuthenticatorResult(true, "");
}

图片

@Slf4j
public class ProtectedApiAuthenticator implements ApiAuthenticator {

    @Override
    public AuthenticatorResult auth(ServerWebExchange exchange)  {
        ...
        //构建校验对象
        ProtectedRequest protectedRequest = ProtectedRequest.builder()
                .requestHeader(requestHeader)
                .requestBody(requestBody)
                .build();

    //责任链上下文
        SecurityVerificationChain securityVerificationChain = SpringBeanUtils.getInstance().getBean(SecurityVerificationChain.class);

        return securityVerificationChain.handler(protectedRequest);

    }

}

3.4 基于责任链的认证实现

3.4.1 创建责任链的认证接口
public interface SecurityVerificationHandler extends Ordered {
    /**
     * 请求校验
     */
    AuthenticatorResult handler(ProtectedRequest protectedRequest);
}
3.4.2 实现参数校验逻辑
@Component
public class RequestParamVerificationHandler implements SecurityVerificationHandler {

    @Override
    public AuthenticatorResult handler(ProtectedRequest protectedRequest) {

        boolean checked = checkedHeader(protectedRequest.getRequestHeader());

        if(!checked){
            return new AuthenticatorResult(false,"请携带正确的请求参数");
        }
        return new AuthenticatorResult(true,"");
    }

    private boolean checkedHeader(RequestHeader requestHeader) {
        return Objects.nonNull(requestHeader.getAppId()) &&
                Objects.nonNull(requestHeader.getSign()) &&
                Objects.nonNull(requestHeader.getNonce()) &&
                Objects.nonNull(requestHeader.getTimestamp());
    }

    @Override
    public int getOrder() {
        return 1;
    }
}
3.4.3 实现nonce的校验
@Component
public class NonceVerificationHandler implements SecurityVerificationHandler {
    private static final String NONCE_KEY = "x-nonce-";

    @Value("${dailymart.sign.timeout:60000}")
    private long expireTime ;
  
    @Resource
    private DistributedCache distributedCache;

    @Override
    public AuthenticatorResult handler(ProtectedRequest protectedRequest) {
        String nonce = protectedRequest.getRequestHeader().getNonce();
        boolean nonceExists = distributedCache.hasKey(NONCE_KEY + nonce);

        if (nonceExists) {
            return new AuthenticatorResult(false, "请勿重复提交请求");
        } else {
            distributedCache.put(NONCE_KEY + nonce, nonce, expireTime);
            return new AuthenticatorResult(true, "");
        }
    }

    @Override
    public int getOrder() {
        return 3;
    }
}
3.4.4 实现签名认证
@Component
@Slf4j
public class SignatureVerificationHandler implements SecurityVerificationHandler {
    @Override
    public AuthenticatorResult handler(ProtectedRequest protectedRequest) {

        //1. 服务端按照规则重新签名
        String serverSign = sign(protectedRequest);
        log.info("服务端签名结果: {}", serverSign);

        String clientSign = protectedRequest.getRequestHeader().getSign();
        // 2、获取客户端传递的签名
        log.info("客户端签名: {}", clientSign);

        if (!Objects.equals(serverSign,clientSign)) {
            return new AuthenticatorResult(false, "请求签名无效");
        }
        return new AuthenticatorResult(true, "");
    }

    /**
     * 服务端重建签名
     * @param protectedRequest 请求体
     * @return 签名结果
     */
    private String sign(ProtectedRequest protectedRequest) {
        RequestHeader requestHeader = protectedRequest.getRequestHeader();
        String appId = requestHeader.getAppId();

        String appSecret = getAppSecret(appId);
        // 1、 按照规则对数据进行签名
        SortedMap<String, Object> requestBody = protectedRequest.getRequestBody();
        requestBody.put("app_id",appId);
        requestBody.put("nonce_number",requestHeader.getNonce());
        requestBody.put("request_time",requestHeader.getTimestamp());

        StringBuilder signBuilder = new StringBuilder();
        for (Map.Entry<String, Object> entry : requestBody.entrySet()) {
            signBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        signBuilder.append("appSecret=").append(appSecret);

        return DigestUtils.md5DigestAsHex(signBuilder.toString().getBytes()).toUpperCase();
    }


    @Override
    public int getOrder() {
        return 4;
    }

}
3.4.5 责任链上下文
@Component
@Slf4j
public class SecurityVerificationChain {
    @Resource
    private List<SecurityVerificationHandler> securityVerificationHandlers;

    public AuthenticatorResult handler(ProtectedRequest protectedRequest){
        AuthenticatorResult authenticatorResult = new AuthenticatorResult(true,"");
        for (SecurityVerificationHandler securityVerificationHandler : securityVerificationHandlers) {
            AuthenticatorResult result = securityVerificationHandler.handler(protectedRequest);
            // 有一个校验不通过理解返回
            if(!result.isResult()){
                return result;
            }
        }
        return authenticatorResult;

    }

}

组合所有的校验逻辑,任意一个校验逻辑不通过则直接返回!

最后说一句(求关注!别白嫖!)

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记  就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

  • 21
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Cloud是一个基于Spring Boot的微服务框架,该框架主要用于快速搭建和管理分布式系统中的微服务架构。下面我将以简书为例,简要介绍Spring Cloud微服务框架的搭建过程。 1. 创建Spring Boot项目:首先,我们需要创建一个Spring Boot项目作为微服务的基础。可以通过Spring Initializr来快速创建一个Spring Boot项目,选择相应的依赖,如Spring Web、Eureka Discovery等。 2. 配置服务注册中心:在Spring Cloud中,Eureka被广泛用作服务注册和发现的中心。在项目的配置文件中,添加Eureka的配置,指定注册中心的地址。 3. 创建微服务:在项目中,创建需要的微服务模块,例如用户服务、文章服务、评论服务等。每个微服务都是一个独立的Spring Boot应用,可以独立运行和部署。 4. 配置服务间的通信:微服务之间需要进行通信,可以使用Spring Cloud提供的Feign或RestTemplate来实现。在服务之间的API接口中,通过注解的方式定义接口,使得服务之间的调用更加简洁。 5. 配置服务网关:为了统一管理微服务接口的访问和安全,可以使用Spring Cloud Gateway作为服务网关。通过配置路由规则和过滤器,对外暴露统一的API接口。 6. 配置服务熔断与降级:在分布式系统中,不可避免地会遇到服务不可用或故障的情况。可以使用Spring Cloud提供的Hystrix进行熔断和降级处理,保证系统的稳定性。 7. 配置服务监控与追踪:为了对微服务进行监控和追踪,可以使用Spring Cloud提供的Actuator和Sleuth。Actuator可以暴露微服务的运行指标,Sleuth可以追踪微服务之间的调用链路。 8. 部署与管理:最后,将各个微服务打包成独立的Docker镜像,并通过Docker容器技术进行部署。使用Kubernetes、Docker Swarm或者Mesos等容器管理工具,可以更好地管理和扩展微服务集群。 以上是简要的Spring Cloud微服务框架搭建过程,通过这个框架,可以实现高可用、弹性、可伸缩的分布式系统架构,帮助开发者更快速地构建和管理微服务应用。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值