再谈接口安全

直接上代码。

#! /usr/bin/env python
# -*- coding: utf-8 -*-
# 结合 redis 实现一人一密的接口安全认证方案。加密方案使用jwt,但是实际落地不仅限于 jwt(md5等均可),jwt 方便之处是可以自带并自检 exp。
# 可以防止撞库,撞对一个的概率是1/(52**32)。如果有撞库现象,可以对 ip 进行频次监控。
# 不怕密钥泄漏,密钥不会传输,不怕被抓取。如果用户主动伪造攻击,可以对 uid 进行频次监控。
# 不怕加密算法泄漏,因为一人一密。
# 可以防止重放攻击。
import functools
import random
import time

import jwt
from tornado.log import gen_log

from const import RESP_AUTH_CHECK_ERROR, RESP_TOP_MONITOR_ERROR
from database import redis_cli

ALGORITHM_SIGN = 'HS256'
UNIQUE_EXP_KEY = 'unique:exp:{exp}'
SECRET_UID_KEY = 'secret:uid:{user_id}'

# 下面是限流方案。滑动窗口算法实现,更精准,但也更复杂,姑且不考虑。如有意,可以加多个限流标准:比如现在建立的是分钟级别的,可以增加十分钟级别的,半小时级别的...。
# nginx ngx_http_limit_req_module 进行频率限制,也可以应用程序结合 redis 进行频率监控。
# 后者对嫌疑请求的处理更灵活(比如出滑块,返回假数据,返回错误提示等),前者对恶意 ip 或关键 url 进行保护(直接拒绝响应)。
# 一般应用程序内部滑块处理:
# 0,什么都不可信:不会有人知道这个接口?不会有人知道加密算法?不会有人无聊到压测这个接口?不会有人想获取这个接口到返回数据?no。所以,尽可能多的接口使用 sign 验证。
# 1,无 sign 接口,对 ip 进行频次监控并处理。
# 2,有 sign 接口,只需要 uid 进行频次监控并处理。
# 3,对关键接口保护,比如读写关键数据的接口,比如资源消耗严重的接口,在1,2的基础上,增加对于指定 url 的监控和处理。
# 4,一般 1/2 对触发频次阈值非常宽松,3 可以适当严格。
# 5, 滑块验证通过后,可以对请求放行,并对频次清零重新计数监控,做到对正常用户的最小影响。
# 6,尽可能实时的自动化的维护一个 ip 黑名单,以及一个 uid 黑名单,并在应用中对这些 ip/uid 发出的请求进行处理。
# 7,如果 6 中黑名单请求量特别大,已经影响到系统正常提供服务,就直接在 ng 进行 503 拒绝响应。
FAILED_CNT_ONE_MINUTE = 'failed_cnt:one_minute:{remote_ip}'
SUCCESS_CNT_ONE_MINUTE = 'success_cnt:one_minute:{user_id}'
SUCCESS_CNT_LIMIT_ONE_MINUTE = 300
FAILED_CNT_LIMIT_ONE_MINUTE = 10


class Checker(object):
    def __init__(self, redis_cli, remote_ip, user_id, sign):
        self.redis_cli = redis_cli
        self.remote_ip = remote_ip
        self.user_id = user_id
        self.sign = sign

    def check(self):
        """对请求进行检查,拦截无效/非法/恶意的请求。
        攻击是不能完全防控的,还需要监控日志识别恶意ip和虚假user,并进行管控和清理。"""
        self.check_failed_cnt()  # 有爬取用户信息攻击行为时放开,1次redis查询操作
        self.check_success_cnt()  # 有消耗服务资源攻击行为时放开,1~3次redis操作
        secret = self.check_secret()  # 1次redis查询操作
        exp = self.check_sign(secret)  # 1次解密操作
        self.check_unique(exp)  # 1~2次redis写入操作

    def fail(self):
        """校验失败处理"""
        failed_key = FAILED_CNT_ONE_MINUTE.format(remote_ip=self.remote_ip)
        failed_cnt = self.redis_cli.incr(failed_key)
        if failed_cnt == 1:
            self.redis_cli.expire(60)

    def success(self):
        """校验成功处理"""
        success_cnt_key = SUCCESS_CNT_ONE_MINUTE.format(user_id=self.user_id)
        success_cnt = self.redis_cli.incr(success_cnt_key)
        if success_cnt == 1:
            self.redis_cli.expire(60)
        return success_cnt

    def check_failed_cnt(self):
        """防止撞库"""
        failed_key = FAILED_CNT_ONE_MINUTE.format(remote_ip=self.remote_ip)
        failed_cnt = self.redis_cli.get(failed_key) or 0
        assert failed_cnt < FAILED_CNT_LIMIT_ONE_MINUTE, 'failed too frequent'

    def check_success_cnt(self):
        """防止伪造"""
        success_cnt_key = SUCCESS_CNT_ONE_MINUTE.format(user_id=self.user_id)
        success_cnt = self.redis_cli.get(success_cnt_key) or 0
        if success_cnt > SUCCESS_CNT_LIMIT_ONE_MINUTE:
            self.redis_cli.delete(success_cnt_key)
            secret_key = SECRET_UID_KEY.format(user_id=self.user_id)
            self.redis_cli.delete(secret_key)
            raise Exception('success too frequent')

    def check_secret(self):
        """身份认证"""
        key = SECRET_UID_KEY.format(user_id=self.user_id)
        secret = self.redis_cli.get(key)
        assert secret, 'get secret error'
        return secret

    def check_sign(self, secret):
        """签名认证"""
        data = jwt.decode(self.sign, secret, algorithm=ALGORITHM_SIGN)  # 完成有效期校验和 secret 校验,具体加密方式多种多样不一定jwt
        return data['exp']  # 过期时间戳,精确到微秒,假设不会有两个请求的 exp 是一样的

    def check_unique(self, exp):
        """防止重放"""
        key = UNIQUE_EXP_KEY.format(exp=exp)
        assert exp < time.time() + 60, 'exp is too big'
        ret = self.redis_cli.setnx(key, 1)
        assert int(ret), 'set unique error'
        self.redis_cli.expireat(key)


def request_checker(func):
    """
    校验,装饰器放到 view 方法上即可。
    :param func:
    :return:
    """
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        sign = self.get_argument('sign')
        user_id = self.get_argument('user_id')
        remote_ip = self.request.remote_ip
        request_msg = 'remote_ip: %s, params:%s, body:%s' % \
                      (remote_ip, self.request.query_arguments, self.request.body)

        response = None
        checker = Checker(redis_cli, remote_ip, user_id, sign)
        try:
            checker.check()
        except Exception as e:
            checker.fail()
            response = RESP_AUTH_CHECK_ERROR
            gen_log.error('request_msg[%s], err[%s]', request_msg, e)
        else:
            checker.success()

        if not response:
            try:
                response = func(self, *args, **kwargs)
            except Exception as e:
                response = RESP_TOP_MONITOR_ERROR
                gen_log.exception('request_msg[%s] err[%s]', request_msg, e)
        self.write(response)

        log_msg = 'request_msg[%s] ret[%s]' % (request_msg, response)
        gen_log.info(log_msg)
        return response

    return wrapper


if __name__ == '__main__':
    expire_time = time.time() + 60
    # 假设客户端已经和服务端通过认证握手,获得了一人一密的 user_id 和 secret, 服务端也存储了这个 user_id/secret
    user_id, secret = 23331342, ''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 32))
    sign = jwt.encode({'exp': expire_time}, secret, algorithm=ALGORITHM_SIGN)
    print(sign)  # 产生一个 sign,拿去请求 restful.

    s = time.time()
    # 本地执行一万次对称解密,耗时小于1s,每次平均耗时不到0.1ms。
    for i in range(10000):
        try:
            jwt.decode(sign, secret, algorithm=ALGORITHM_SIGN)
        except Exception as e:
            pass
    e = time.time()
    print(e - s)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值