浅谈如何保证API接口安全性

在实际的业务中,需要通过定义各种接口规范来保证传输的安全性。

token

访问令牌access token,用于标识接口调用者的身份、凭证,减少用户名和密码的传输次数。一般情况下客户端(接口调用方)需要先向服务器端申请一个接口调用的账号,服务器会给出一个appId和一个key, key用于参数签名使用,注意key保存到客户端,需要做一些安全处理,防止泄露。

服务端生成Token后需要将token作为key,将一些和token关联的信息作为value保存到缓存服务器中(redis),当一个请求过来后,服务器就去缓存服务器中查询这个Token是否存在,存在则调用接口,不存在返回接口错误,一般通过拦截器或者过滤器来实现。

Token分为两种:

  • API Token(接口令牌):用于访问不需要用户登录的接口,如登录、注册、一些基本数据的获取等。 获取接口令牌需要拿appId、timestamp和sign来换,sign=加密(timestamp+key)
  • USER Token(用户令牌):用于访问需要用户登录之后的接口,如:获取我的基本信息、保存、修改、删除等操作。获取用户令牌需要拿用户名和密码来换

timestamp

客户端调用接口时对应的当前时间戳,用于防止DoS攻击。每次调用接口时接口都会判断服务器当前系统时间和接口中传的的timestamp的差值,如果这个差值超过某个设置的时间,那么这个请求将被拦截掉。

缺点

  • 如果在设置的超时时间范围内,是不能阻止DoS攻击的。
  • 攻击方可以修改时间戳,需要通过sign签名机制来处理。 

sign签名机制

对参数进行签名,防止参数被非法篡改。sign一般是将所有非空参数按照升序排序然后+token+key+timestamp拼接在一起,然后使用加密算法进行加密,作为接口中的一个参数sign来传递,也可以将sign放到请求头中。

接口在网络传输过程中,参数值可以被修改,但是因为不了解sign计算方式,一般没法修改sign的值。当服务器调用接口前会按照sign的规则重新计算出sign的值然后和接口传递的sign参数的值做比较,如果相等表示参数值没有被篡改,如果不等,表示参数被非法篡改了。

防止重复提交

对于一些重要的操作需要防止客户端重复提交的(如非幂等性重要操作),具体办法是当请求第一次提交时将sign作为key保存到redis,并设置超时时间,超时时间和Timestamp中设置的差值相同。

当同一个请求第二次访问时会先检测redis是否存在该sign,如果存在则证明重复提交了,接口就不再继续调用了,直接返回报错或者之前处理的结果。

如果sign在缓存服务器中因过期时间到了,而被删除了,此时当这个url再次请求服务器时,因token的过期时间和sign的过期时间一致,sign过期也意味着token过期,那样同样的url再访问服务器会因token错误会被拦截掉,这就是为什么sign和token的过期时间要保持一致的原因。

伪代码

生成token

@PostMapping("/getToken")
    public ApiResponse getToken(String appId, @RequestHeader("timestamp") String timestamp, @RequestHeader("sign") String sign) {
        if (!StringUtils.isEmpty(appId) && !StringUtils.isEmpty(timestamp) && !StringUtils.isEmpty(sign)) {
            return ApiResponse.error(ApiException.PRAMS_ERROR);
        }

        long reqeustInterval = System.currentTimeMillis() - Long.valueOf(timestamp);

        if (reqeustInterval > 60) {
           return ApiResponse.error(ApiException.OVER_TIME);
        }

        // 1. 根据appId查询数据库获取appSecret
        AppInfo appInfo = appSerivce.getAppInfo(appId);

        // 2. 校验签名
        String signString = timestamp + appId + appInfo.getKey();
        String signature = MD5Util.encode(signString);
        if (!signature.equals(sign)) {
            return ApiResponse.error(ApiException.SIGN_ERROR);
        }

        // 3. 如果正确生成一个token保存到redis中,如果错误返回错误信息
        AccessToken accessToken = createToken(appInfo);

        return ApiResponse.success(accessToken);
    }

private AccessToken createToken(AppInfo appInfo) {
        String token = UUID.randomUUID().toString();

        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.SECOND, timeout);
        Date expireTime = calendar.getTime();

        // 保存token
        redisTemplate.opsForValue().set(prefix + token, appInfo, timeout, TimeUnit.SECONDS);

        AccessToken accessToken = new AccessToken(token, expireTime);

        return accessToken;
    }

拦截器实现token验证

@Aspect
@Component
public class TestAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    @Pointcut("execution(public * com.test.controller..*.*(..))")
    public void auth() {
    }

    @Around("checkAuth()")
    public Object authHttpServletRequest(final ProceedingJoinPoint joinPoint) throws Throwable {
        String token = request.getHeader("token");
        String timestamp = request.getHeader("timestamp");

        String sign = request.getHeader("sign");
        if (!StringUtils.isEmpty(token) && !StringUtils.isEmpty(timestamp) && !StringUtils.isEmpty(sign)) {
         throw new ApiException(ApiException.PRAMS_ERROR);
        }

        long reqeustInterval = System.currentTimeMillis() - Long.valueOf(timestamp);

        if (reqeustInterval > 60) {
          throw new ApiException(ApiException.OVER_TIME);
        }

        Boolean checkTokern = redisTemplate.stringRedisTemplate.hasKey(prefix + key);
        if (!checkTokern) {
          throw new ApiException(ApiException.TOKEN);
        }

        Boolean checkSign = redisTemplate.stringRedisTemplate.hasKey(prefix + key);
        if (!checkSign) {
          throw new ApiException(ApiException.REPEAT);
        }
        
        String signString = ApiUtil.concatSignString(request) + tokenInfo.getAppInfo().getKey() + token + timestamp + nonce;
        String signature = MD5Util.encode(signString);
        boolean flag = signature.equals(sign);
        if (!flag) {
         throw new ApiException(ApiException.SIGN);
        }
        
        return joinPoint.proceed();
    }

}

 

 

你可能感兴趣的:(java,程序人生,经验分享)