前言
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)
服务端
def test(request): key='sssdkjrjefjewfakfhkj' clent_key=request.META.get('HTTP_AUTH_API') if clent_key == key: return HttpResponse('你得到我了') else: return HttpResponse('休想')
漏洞
虽然双方约定好了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)
服务端
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('休想')
漏洞
折腾了半天虽然可以动态加密,但依然可以获取到,且客户端会生成很多加密字符串,黑客获取任意一个都可以访问到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)
服务端
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('休想')
漏洞
虽然加密字符串有了时间限制,但时间就是漏洞
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')
服务端
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")
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) }
服务端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
服务端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) }
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)
缺陷
if判断分支过多,需要使用责任链模式进行代码优化。