起因
这两天在用tornado实现一个http代理,属于正向代理,同步的方式调用。 对于我这的应用场景是爬虫的代理,这代理服务会发布在各个公网云主机上。 但是放到公网就会遇到安全的问题,我想大家的web服务,经常会受到别人的嗅探包,主要是http proxy的方式,他这是撞代理。 对于安全,我这边不仅用了黑白名单的方式,而且用了会加入基本认证。 简单看了下基本认证的资料,他其实就是在request请求体里面加入header,名叫Authorization。 通过抓包分析得出,他的格式是这样的, “Basic Zm9vOmJhcg==” 空格左面是他的加密方式,右面是base64加密的用户名和密码。
BASIC认证的过程
1. 客户端向服务器请求数据,请求的内容可能是一个网页或者是一个其它的MIME类型,此时,假设客户端尚未被验证,则客户端提供如下请求至服务器:
Get /index.html HTTP/1.0
Host:www.google.com
2. 服务器向客户端发送验证请求代码401,服务器返回的数据大抵如下:
HTTP/1.0 401 Unauthorised
Server: SokEvo/1.0
WWW-Authenticate: Basic realm=”google.com”
Content-Type: text/html
Content-Length: xxx
3. 当符合http1.0或1.1规范的客户端(如IE,FIREFOX)收到401返回值时,将自动弹出一个登录窗口,要求用户输入用户名和密码。
4. 用户输入用户名和密码后,将用户名及密码以BASE64加密方式加密,并将密文放入前一条请求信息中,则客户端发送的第一条请求信息则变成如下内容:
Get /index.html HTTP/1.0
Host:www.google.com
Authorization: Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxx
注:xxxx….表示加密后的用户名及密码。
5. 服务器收到上述请求信息后,将Authorization字段后的用户信息取出、解密,将解密后的用户名及密码与用户数据库进行比较验证,如用户名及密码正确,服务器则根据请求,将所请求资源发送给客户端:
长话短说,直接仍代码。
标注下原文链接,最近不少缺德网站转载后,也不留个链接。 http://xiaorui.cc/?p=1904
这是服务端的实现,很简单就是先用self.request.headers.get取出Authorization,然后decode(‘base64’)来解析用户名和密码。
from tornado.escape import utf8 from hashlib import md5 class BasicAuthHandler(RequestHandler): def get(self): # Authorization: Basic base64("user:passwd") auth_header = self.request.headers.get('Authorization', None) if auth_header is not None: # Basic Zm9vOmJhcg== auth_mode, auth_base64 = auth_header.split(' ', 1) assert auth_mode == 'Basic' auth_username, auth_password = auth_base64.decode('base64').split(':', 1) if auth_username == username or auth_password == password: self.write('ok') else: self.write('fail') else: ''' HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="renjie" ''' self.set_status(401) self.set_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
可以直接扔到一个函数里判断….
你自己定义下 base_auth_user , base_auth_passwd def base_auth_valid(auth_header): from tornado.escape import utf8 from hashlib import md5 auth_mode, auth_base64 = auth_header.split(' ', 1) assert auth_mode == 'Basic' auth_username, auth_password = auth_base64.decode('base64').split(':', 1) if auth_username == base_auth_user and auth_password == base_auth_passwd: return True else: return False
客户端测试:
curl -u xiaorui:123 -x 127.0.0.1:8888 http://www.163.com
[ruifengyun@devops toproxy (master)]$ curl -vv -u 1xiaorui:123 -x 127.0.0.1:8888 http://www.xiaorui.cc
* Rebuilt URL to: http://www.xiaorui.cc/
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1…
* Connected to 127.0.0.1 (127.0.0.1) port 8888 (#0)
* Server auth using Basic with user ‘1xiaorui’
> GET http://www.xiaorui.cc/ HTTP/1.1
> Authorization: Basic MXhpYW9ydWk6MTIz
> User-Agent: curl/7.37.1
> Host: www.xiaorui.cc
> Accept: */*
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 403 Forbidden
< Date: Tue, 25 Aug 2015 15:36:56 GMT
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
* Server TornadoServer/4.0.2 is not blacklisted
< Server: TornadoServer/4.0.2
<
* Connection #0 to host 127.0.0.1 left intact
下面是我服务段的返回结果,直接给他一个403认证失败。
一般来说,客户端和服务器的认证方式,除了basic auth外,还有digest , 这里我就不详细说明,大家瞅瞅就懂了。
if auth_header is not None: auth_mode, params = auth_header.split(' ', 1) assert auth_mode == 'Digest' param_dict = {} for pair in params.split(','): k, v = pair.strip().split('=', 1) if v[0] == '"' and v[-1] == '"': v = v[1:-1] param_dict[k] = v assert param_dict['realm'] == realm assert param_dict['opaque'] == opaque assert param_dict['nonce'] == nonce assert param_dict['username'] == username assert param_dict['uri'] == self.request.path h1 = md5(utf8('%s:%s:%s' % (username, realm, password))).hexdigest() h2 = md5(utf8('%s:%s' % (self.request.method, self.request.path))).hexdigest() digest = md5(utf8('%s:%s:%s' % (h1, nonce, h2))).hexdigest()
在nginx、apache http服务上,还记得有个htpasswd么? 对的,他是生成基本认证的本地库,其实就一个文件。 我们也可以把账号密码都放入htpasswd里面,正好python也有针对htpassswd的操作接口。 我打算在自己的开源项目toproxy里面用htpasswd,验证http代理认证。
import htpasswd with htpasswd.Basic("/path/xiaorui/user.db") as userdb: userdb.add("bob", "password") userdb.change_password("alice", "newpassword") with htpasswd.Group("/path/xiaorui/group.db") as groupdb: groupdb.add_user("bob", "admins") groupdb.delete_user("alice", "managers")
我的用token的方式,更好控制