Skip to content

Instantly share code, notes, and snippets.

@notsobad
Last active August 13, 2016 03:53
Show Gist options
  • Save notsobad/5771665 to your computer and use it in GitHub Desktop.
Save notsobad/5771665 to your computer and use it in GitHub Desktop.

如何设计一个API服务

By @notsobad

认证

api key

url中附加一个key

/api/posts/?key=aasd2323423asdfasdf

应用场景:

  • google map api
  • 简单的s -> s认证

缺点:

  • key容易泄漏
  • 不可靠,不适合做强认证

api签名

用双方都知道的密钥对api参数进行签名,可以防止参数伪造,如 http://shot.scanv.com/api/thumb/?blur=0&client=scanv&sign=0881307c9f1e440f05e4cc58075c9485&size=L&url=http%3A%2F%2Fwww.douban.com

应用场景:

  • 网银交易的第三方接口常用
  • 资源地址加密,如图片、视频,可以防止直接抓取
  • b -> s, s -> s均可

Basic auth

api利用basic auth来做认证,可以用basic auth用户名来取代api key

curl https://api.stripe.com/v1/charges \
	-u sk_test_mkGsLqEW6SLnZa487HYfJVLf:
# or
curl https://sk_test_mkGsLqEW6SLnZa487HYfJVLf:@api.stripe.com/v1/charges

优点:

  • 方便调试,浏览器中访问认证一次,后续不需要再次输入
  • 调用方便,程序访问可以直接把认证信息写入url中
  • 用于s -> s

在tornado中实现basic auth:

# -*- coding= utf-8 -*-
import datetime
import os
import json
import tornado.ioloop
import tornado.web
import tornado
import tornado.httpclient
import traceback
import urllib2
import base64
import functools
import hashlib,base64,random

API_KEYS = {
	'rjtzWc674hDxTSWulgETRqHrVVQoI3T8f9RoMlO6zsQ': 'test'
}


def api_auth(username, password):
	if username in API_KEYS:
		return True
	return False

def basic_auth(auth):
	def decore(f):
		def _request_auth(handler):
			handler.set_header('WWW-Authenticate', 'Basic realm=JSL')
			handler.set_status(401)
			handler.finish()
			return False
		
		@functools.wraps(f)
		def new_f(*args):
			handler = args[0]
 
			auth_header = handler.request.headers.get('Authorization')
			if auth_header is None: 
				return _request_auth(handler)
			if not auth_header.startswith('Basic '): 
				return _request_auth(handler)
 
			auth_decoded = base64.decodestring(auth_header[6:])
			username, password = auth_decoded.split(':', 2)
 
			if (auth(username, password)):
				f(*args)
			else:
				_request_auth(handler)
					
		return new_f
	return decore


class ResHandler(tornado.web.RequestHandler):
	@basic_auth(api_auth)
	def get(self):
		self.write("hello")
app = tornado.web.Application([
	(r'/api/res/', ResHandler),
], **settings)

if __name__ == '__main__':
	import tornado.options
	tornado.options.parse_command_line()

	app.listen(9527)
	tornado.ioloop.IOLoop.instance().start()

代码在:https://gist.github.com/notsobad/5771635

## 数据结构 返回json数据结构,所有信息要在同一个wraper下。

  • 要返回dict而不是list (为什么?)
  • 要有操作时间字段 (为什么?)
  • 异常情况要给出正确的错误提示,也要返回正确的json结构

版本

如果你的api是给其他用户使用的,非内部api,最好在api中带上版本号,如果数据结构由大的变动,升级版本号即可,如可以这样设计

/api/v1/list_user/

参考 stripe api

框架选择

推荐轻量级框架

  • web.py
  • tornado
  • flask
  • ....

推荐使用tornado

优点:

  • 高效, 部署时可以无需uwsgi
  • 异步,将一些db操作,第三方api访问放入异步,可以实现高并发

下面的代码实现了一个异步的api proxy

def getJSON(url, callback):
    http_client = tornado.httpclient.HTTPClient()
    response = http_client.fetch(url)
    cont = response.body
    http_client.close()

    ret = None
    try:
        ret = json.loads(cont)
    except:
        traceback.print_exc()
    return callback(ret)
  
class ProxyHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        url = 'http://www.xxx.com/abc.json'
        getJSON(url, callback=self.on_response)

    def on_response(self, ret):
        if not ret:
            raise tornado.web.httperror(500)
        self.write(ret)
        self.finish()

部署、缓存

api要有缓存,要根据api的具体情况定制缓存策略,下面给一种简单的缓存架构

在api server端,对于不同的接口,声明合适的http头,如Cache-control, expires等,缓存本身由中间层来做,如nginx、varnish等来负责。

last_modified = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(time.time()))
web.header('Last-Modified',last_modified)
web.header('Cache-Control', 'max-age=86400')

如果你的应用使用nginx + uwsgi形式,可以使用uwsgi_cache

uwsgi_cache_path /home/data/cache levels=1:2 keys_zone=api:10m inactive=7d  max_size=600m;
server {
	listen 80;
	server_name xx-api;

	location / {
		uwsgi_cache api;
		uwsgi_cache_use_stale   error timeout invalid_header http_500;
		uwsgi_cache_key api$request_uri;

		include uwsgi_params;
		uwsgi_param UWSGI_CHDIR /var/www/api/;
		uwsgi_param UWSGI_SCRIPT wsgi_app;
		uwsgi_pass unix:////var/run/uwsgi_api/api.sock;
	}

}

类似的,如果你用nginx+proxy_pass的形式,也可以用proxy_cache模块

防滥用

nginx HttpLimitReqModule

参考链接:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment