前言

1个API接口都需被合法的客户端安全访问,所以C/S之间需要1个交互认证策略;

以下Token认证策略均源自于时间戳的动态递增性启发。

Token策略1

客户端发送HTTP请求访问API时,在请求头里设置一个双方约定好的key;

知识点:

1、如果给Django程序发送请求头,headers携带内容包含下滑杠 _,Django会不认识;

2、客户端 auth-api ----->服务端 转换成 'HTTP_AUTH_API'格式

3、服务端获取clent_key=request.META.get('HTTP_AUTH_API')

客户端

import  requests
key='sssdkjrjefjewfakfhkj'
respose=requests.get(url='http://127.0.0.1:8000/test.html/',headers={'auth-api':key}).text
#如果给Django程序发送请求头,如果headers里面的内容使用下滑杠 _,Django会不认识;
#auth-api  -----> 转换成 'HTTP_AUTH_API'格式
#服务端获取clent_key=request.META.get('HTTP_AUTH_API')
print(respose)
View Code

服务端

def test(request):
    key='sssdkjrjefjewfakfhkj'
    clent_key=request.META.get('HTTP_AUTH_API')
    if clent_key == key:
        return HttpResponse('你得到我了')
    else:
        return HttpResponse('休想')
View Code

 

漏洞

虽然双方约定好了key,但是请求头依然会被截获到;

Token策略2

1.key+当前客户端时间戳 组成1个MD5加密字符串

2.MD5加密字符串|当前时间戳 组成1串密码,hearder携带

3.服务端接收到客户端发送的那1串密码,split 出客户端时间

4.来着客户端时间+服务端key做MD5加密还原,对比客户端和服务端

客户端

import  requests
import time
import hashlib
key='sssdkjrjefjewfakfhkj'
ctime=str(time.time())
def MD5(arg):
    hs=hashlib.md5()
    hs.update(arg.encode('utf-8'))  #python3加密使用字节类型
    return hs.hexdigest()


new_key='%s|%s' % (key,ctime)  # sssdkjrjefjewfakfhkj | 时间戳
md5_str=MD5(new_key)
auth_api_val='%s|%s'%(md5_str,ctime)  #d0e0ca7d1f8f72d60715696d4baac3b2(key和时间戳加密后的结果)| 时间戳
print(md5_str)
respose=requests.get(url='http://127.0.0.1:8000/test.html/',headers={'auth-api':auth_api_val}).text
print(respose)
View Code

服务端

import hashlib
import time

def MD5(arg):
    hs = hashlib.md5()
    hs.update(arg.encode('utf-8'))  # python3加密使用字节类型
    return hs.hexdigest()

def test(request):
    key='sssdkjrjefjewfakfhkj'
    auth_api_val=request.META.get('HTTP_AUTH_API')  #052dd27c130f4b9b5a8a4ec4b243962d | 1507374976.4620001
    client_md5_str,client_ctime =auth_api_val.split('|',maxsplit=1)
    server_md5_str=MD5('%s|%s'%(key,client_ctime))

    if client_md5_str== server_md5_str:
        return HttpResponse('你得到我了')
    else:
        return HttpResponse('休想')
View Code

漏洞

折腾了半天虽然可以动态加密,但依然可以获取到,且客户端会生成很多加密字符串,黑客获取任意一个都可以访问到API

Token策略3

1.key+当前客户端时间戳 组成1个MD5加密字符串

2.MD5加密字符串|当前时间戳 组成1串密码,hearder携带

3.服务端接收到客户端发送的那1串密码,split 出客户端时间

4.来着客户端时间+服务端key做MD5加密还原,对比客户端和服务端是否相等

5.动态密码有时间限制,超过5秒失效

客户端

import  requests
import time
import hashlib
key='sssdkjrjefjewfakfhkj'
ctime=str(time.time())
def MD5(arg):
    hs=hashlib.md5()
    hs.update(arg.encode('utf-8'))  #python3加密使用字节类型
    return hs.hexdigest()


new_key='%s|%s' % (key,ctime)  # sssdkjrjefjewfakfhkj | 时间戳
md5_str=MD5(new_key)
auth_api_val='%s|%s'%(md5_str,ctime)  #d0e0ca7d1f8f72d60715696d4baac3b2(key和时间戳加密后的结果)| 时间戳
print(md5_str)
respose=requests.get(url='http://127.0.0.1:8000/test.html/',headers={'auth-api':auth_api_val}).text
print(respose)
View Code

服务端

def test(request):
    server_float_ctime=time.time()
    key='sssdkjrjefjewfakfhkj'
    auth_api_val=request.META.get('HTTP_AUTH_API')  #052dd27c130f4b9b5a8a4ec4b243962d | 1507374976.4620001
    client_md5_str,client_ctime =auth_api_val.split('|',maxsplit=1)
    client_float_ctime=float(client_ctime)

    if client_float_ctime+5 < server_float_ctime:
        return HttpResponse('想要破解密码最在5秒之内')

    server_md5_str = MD5('%s|%s' % (key, client_ctime))
    if client_md5_str== server_md5_str:
        return HttpResponse('你得到我了')
    else:
        return HttpResponse('休想')
View Code

漏洞

虽然加密字符串有了时间限制,但时间就是漏洞

Token策略4

1.key+当前客户端时间戳 组成1个MD5加密字符串

2.MD5加密字符串|当前时间戳 组成1串密码,hearder携带

3.服务端接收到客户端发送的那1串密码,split 出客户端时间

4.来着客户端时间+服务端key做MD5加密还原,对比客户端和服务端是否相等

5.动态+加密字符串+时间限制,超过5秒失效

6.记录最近5秒访问客户端的加密字符串,如果当前客户端使用的字符串存在记录中,说明是窃取(因为正常用户每次,访问API会携带不同的加密字符串)

客户端

import  requests
import time
import hashlib
key='sssdkjrjefjewfakfhkj'
ctime=str(time.time())
def MD5(arg):
    hs=hashlib.md5()
    hs.update(arg.encode('utf-8'))  #python3加密使用字节类型
    return hs.hexdigest()


new_key='%s|%s' % (key,ctime)  # sssdkjrjefjewfakfhkj | 时间戳
md5_str=MD5(new_key)
auth_api_val='%s|%s'%(md5_str,ctime)  #d0e0ca7d1f8f72d60715696d4baac3b2(key和时间戳加密后的结果)| 时间戳
print(md5_str)
respose=requests.get(url='http://127.0.0.1:8000/test.html/',headers={'auth-api':auth_api_val}).text
print(respose)




#如果给Django程序发送请求头,如果headers里面的内容使用下滑杠 _,Django会不认识;
#auth-api  -----> 转换成 'HTTP_AUTH_API'格式
#服务端获取clent_key=request.META.get('HTTP_AUTH_API')
View Code

服务端

import time
import hashlib
from django.shortcuts import render, HttpResponse
from django.views.decorators.csrf import csrf_exempt

# 有效Token使用记录
visited_keys = {}
# Salt密钥
key = 'sssdkjrjefjewfakfhkj'


# md5加密算法
def MD5(arg):
    hs = hashlib.md5()
    hs.update(arg.encode('utf-8'))  # python3加密使用字节类型
    return hs.hexdigest()


# api验证装饰器
def api_auth(func):
    def inner(request, *args, **kwargs):
        server_float_ctime = time.time()
        key = 'sssdkjrjefjewfakfhkj'
        auth_api_val = request.META.get('HTTP_AUTH_API')  # 052dd27c130f4b9b5a8a4ec4b243962d | 1507374976.4620001
        client_md5_str, client_ctime = auth_api_val.split('|', maxsplit=1)
        client_float_ctime = float(client_ctime)
        # 第1关:时间限制,检查客户端Token是否过期?
        if client_float_ctime + 5 < server_float_ctime:
            return HttpResponse('想要破解密码最在5秒之内')
        # 第2关:Token值相等判断,检查Token是否伪造?
        server_md5_str = MD5('%s|%s' % (key, client_ctime))
        if client_md5_str != server_md5_str:
            return HttpResponse('休想')
        # 有效期限内,有效的Token使用记录
        for k in list(visited_keys.keys()):
            v = visited_keys[k]
            if server_float_ctime > v:
                del visited_keys[k]
        # 第3关:使用记录检查,确认Token是否在有效期内被2次使用?
        if visited_keys.get(client_md5_str):
            return HttpResponse('你放弃吧')
        # 只需维护有效期内、合法的Token访问记录即可,因为Token超过有效期,第1关都过不去了
        visited_keys[client_md5_str] = client_float_ctime + 5
        return func(request, *args, **kwargs)

    return inner


# 视图函数
@api_auth
@csrf_exempt
def index(request):
    return HttpResponse("index")
View Code

Token策略5

0.检查客户端时间戳和服务端时间戳,是否存在误差

1.key+当前客户端时间戳 组成1个MD5加密字符串

2.MD5加密字符串|当前时间戳 组成1串密码,hearder携带

3.服务端接收到客户端发送的那1串密码,split 出客户端时间

4.来着客户端时间+服务端key做MD5加密还原,对比客户端和服务端是否相等

5.动态+加密字符串+时间限制,超过5秒失效

6.记录最近5秒访问客户端的加密字符串,如果当前客户端使用的字符串存在记录中,说明是窃取(因为正常用户每次,访问API会携带不同的加密字符串)

Golang服务端

服务端1

package main

import (
    "crypto/md5"
    "encoding/hex"
    "fmt"
    "io"
    "time"
)

type TokenAuth struct {
    //Token有效期
    TimeSpan int64
    //使用该验证方式的客户端
    CallerName string
    salt       string
}

func (self TokenAuth) GetToken(timeStamp int64) string {
    //当前时间戳不断递增,当前时间戳 % 60 =60以内的动态数字
    numberWithinSpan := timeStamp % self.TimeSpan
    //当前时间戳-60以内的动态数字=过去60分钟之内的递增时间戳
    timeWithinSpan := timeStamp - numberWithinSpan
    //组成Token字符串的Token元素
    tokenElement := fmt.Sprintf("%s.%s.%d", self.salt, self.CallerName, timeWithinSpan)
    //对Token元素进行Ma5加密生成动态Token
    h := md5.New()
    _, _ = io.WriteString(h, tokenElement)
    b := h.Sum(nil)
    tokenStr := hex.EncodeToString(b[:])
    return tokenStr
}

func (self TokenAuth) CheckToken(tokenC string) bool {
    //2.后端生成Token的时间戳
    serverTs := time.Now().Unix()
    //2.1.后端支持前后端的时间戳前后相差1——6秒
    tsList := []int64{serverTs}
    for i := int64(1); i <= int64(6); i++ {
        tsList = append(tsList, serverTs+i)
        tsList = append(tsList, serverTs-i)
    }
    //3.检查客户端传来的Token和后端生成的Token是否一致?
    isValid := false
    for _, ts := range tsList {
        tokenS := self.GetToken(ts)
        if tokenC == tokenS {
            isValid = true
            break
        }
    }
    return isValid

}

func main() {
    //1.客户端传来的Token:clientTs 和serverTs时间戳保持一致
    clientTs := time.Now().Unix()
    auth := TokenAuth{60, "App01", "123.com"}
    clientToken := auth.GetToken(clientTs)
    //2.服务端Token和客户端Token对比
    isValid := auth.CheckToken(clientToken)
    fmt.Println(isValid)

}
动态Token

服务端1存在漏洞

使用客户端和服务端的时间戳 % 过期时间,想法虽好但存在漏洞。

import time

"""
时间戳 % X = 0-X之间的数字
时间戳不断递增:所以每间隔X秒会进行新1轮变化,每1轮取余的值都是0-X之间的数字
"""
stime = int(time.time())
stimeP = int(time.time()) + 60
stimeM = int(time.time()) - 60
# 假设:x=60
# print(int(time.time()) % 60)  # 客户端当前时间戳取余=59
# time.sleep(1)                 # 客户端与服务端之间传输网络耗时=1秒
# print(stime%60)               # Token到达服务端,服务端的当前时间戳再取余=0
print(stimeP % 60)              #服务端当前时间+60,再取余也=0
print(stimeM % 60)              #服务端当前时间-60,再取余也=0
#产生客户端与服务端生成Token不一致的Bug
# print((stime- 59) == (stime - 0)) #False
Bug在这儿

服务端2

package main

import (
    "crypto/md5"
    "encoding/hex"
    "fmt"
    "io"
    "time"
)

type TokenAuth struct {
    //Token有效期
    timeSpan   int64
    callerName string
    salt       string
    visitedMap map[string]int64
    //平衡客户端、服务端的时间误差
    timeError int64
}

// 客户端时间戳+服务端的密钥=服务端Token
func (self TokenAuth) GetToken(ctime int64) (tokenStr string) {
    //加密Token字符串的密钥元素
    tokenElement := fmt.Sprintf("%s.%s.%d", self.salt, self.callerName, ctime)
    //对Token元素进行Ma5加密生成动态Token
    tokenStr = self.Cipher(tokenElement)
    return

}

// 加密算法
func (self TokenAuth) Cipher(element string) (encryptedStr string) {
    h := md5.New()
    _, _ = io.WriteString(h, element)
    b := h.Sum(nil)
    encryptedStr = hex.EncodeToString(b[:])
    return
}

// 检查客户端Token是否合法?
func (self TokenAuth) CheckToken(ctime int64, ctoken string) (isValid bool, reason string) {
    currentTime := time.Now().Unix()
    //客户端与服务端的时间误差:时间误差控制在前、后3秒范围内
    timeErr := currentTime - ctime
    //第1关:检查客户端时间?防止客户端和服务端的时间不一致 or 客户端上传Token至服务端的传输耗时大,
    if !((timeErr > -self.timeError) && (timeErr < self.timeError)) {
        isValid = false
        reason = "请检查客户端和服务端的时间是否一致?"
        return
    }
    //第2关:检查Token是否过期?
    if ctime+self.timeSpan <= currentTime {
        isValid = false
        reason = "Token过期!"
        return
    }
    //第3关:检查客户端Token和服务端Token进行等值判断
    stoken := self.GetToken(ctime)
    if stoken != ctoken {
        isValid = false
        reason = "Token不合法,请检查客户端制作过程!"
        return
    }
    //第1-3关都通过了说明Token是未过期、值合法,需要清理使用字典,继续往下
    self.deleExpiredToken()
    //第4关:检查Token是否被2次使用?
    int64Val := self.visitedMap[ctoken]
    if int64Val != 0 {
        isValid = false
        reason = "动态Token只能使用1次!"
        return
    }
    //1-4都通过了说明Token是可以使用的,记录使用记录
    self.visitedMap[ctoken] = ctime + self.timeSpan
    //最后通关放行
    isValid = true
    reason = "OK"
    return

}

// 删除Token使用记录中已过期Token
func (self TokenAuth) deleExpiredToken() {
    cuurentTs := time.Now().Unix()
    for k, v := range self.visitedMap {
        if v < cuurentTs {
            delete(self.visitedMap, k)
        }

    }
}

func main() {
    //1.客户端传来Token
    ctime := time.Now().Unix() + 3
    auth := TokenAuth{60, "App01", "123.com", map[string]int64{}, 3}
    ctoken := auth.GetToken(ctime)
    //2.服务端校验Token,待使用责任链模式进行优化.......
    isValid, tipmsg := auth.CheckToken(ctime, ctoken)
    fmt.Println(isValid, tipmsg)

}
View Code

Python客户端

import requests
import time
import hashlib


def get_token(password, req_random, req_time):
    token_str_list = [password, req_random, req_time]
    token_str_list.sort()
    token_str = "".join(token_str_list)
    m1 = hashlib.md5()
    m1.update(token_str.encode("utf-8"))
    token = m1.hexdigest()
    return token


def send_request(url):
    req_random = "abcd"
    req_time = str(int(time.time()))
    username = "独立的后端用户"  # 找管理员开通
    password = "独立的后端密码"
    req_token = get_token(password, req_random, req_time)
    req_url = "%s?reqTime=%s&reqRandom=%s&reqToken=%s" % (url, req_time, req_random, req_token)
    data = {"username": username}
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    return requests.post(req_url, data=data, headers=headers)


if __name__ == "__main__":
    url = "http://online.letv.cn/interface/xxx/"
    send_request(url)
View Code

缺陷

if判断分支过多,需要使用责任链模式进行代码优化。

 

 

参考

posted on 2017-10-07 17:35  Martin8866  阅读(268)  评论(0编辑  收藏  举报