OAuth2.0技术比较实用,像我这种反感注册的用户一般都是申请一个没用的QQ,然后用这个QQ登录那些不想注册,有点小用的系统。技术不多说,拿QQ来说,简单列几点:
1.QQ给第三方平台分配一个appkey和appsecret
2.用户选择使用QQ账号登录第三方平台,此时,第三方平台使用自己的appkey获取code,code是在用户输入QQ账号密码后生成的,是与QQ那边交互的,并且生命周期很短,是安全的
3.第三方平台获取到code后,再使用code+appkey+appsecret获取access_token
4.第三方平台获取到access_token后,便可使用该access_token获取openid,进而使用openid+access_token+appkey获取用户信息
5.此时用户已认证成功,第三方平台可使用该用户的唯一标示openid来创建账号
tornado框架封装的OAuth2Mixin:
class OAuth2Mixin(object):
"""Abstract implementation of OAuth 2.0.
See `FacebookGraphMixin` below for an example implementation.
Class attributes:
* ``_OAUTH_AUTHORIZE_URL``: The service's authorization url.
* ``_OAUTH_ACCESS_TOKEN_URL``: The service's access token url.
"""
@return_future
def authorize_redirect(self, redirect_uri=None, client_id=None,
client_secret=None, extra_params=None,
callback=None):
"""Redirects the user to obtain OAuth authorization for this service.
Some providers require that you register a redirect URL with
your application instead of passing one via this method. You
should call this method to log the user in, and then call
``get_authenticated_user`` in the handler for your
redirect URL to complete the authorization process.
.. versionchanged:: 3.1
Returns a `.Future` and takes an optional callback. These are
not strictly necessary as this method is synchronous,
but they are supplied for consistency with
`OAuthMixin.authorize_redirect`.
"""
args = {
"redirect_uri": redirect_uri,
"client_id": client_id
}
if extra_params:
args.update(extra_params)
self.redirect(
url_concat(self._OAUTH_AUTHORIZE_URL, args))
callback()
def _oauth_request_token_url(self, redirect_uri=None, client_id=None,
client_secret=None, code=None,
extra_params=None):
url = self._OAUTH_ACCESS_TOKEN_URL
args = dict(
redirect_uri=redirect_uri,
code=code,
client_id=client_id,
client_secret=client_secret,
)
if extra_params:
args.update(extra_params)
return url_concat(url, args)
QQ Oauth2.0封装:
class QQGraphOAuth2Mixin(OAuth2Mixin):
_OAUTH_AUTHORIZE_URL = "https://graph.qq.com/oauth2.0/authorize?"
_OAUTH_ACCESS_TOKEN_URL = "https://graph.qq.com/oauth2.0/token?"
_OAUTH_OPENID_URL = "https://graph.qq.com/oauth2.0/me?"
_OAUTH_NO_CALLBACKS = False
_QQ_BASE_URL = "https://graph.qq.com"
@_auth_return_future
def get_authenticated_user(self, redirect_uri, client_id, client_secret,
code, callback, extra_fields=None, extra_params=None):
http = self.get_auth_http_client()
args = {
"redirect_uri": redirect_uri,
"code": code,
"client_id": client_id,
"client_secret": client_secret,
"extra_params": extra_params,
}
http.fetch(self._oauth_request_token_url(**args),
self.async_callback(self._on_access_token, redirect_uri, client_id,
client_secret, callback, extra_fields))
def _on_access_token(self, redirect_uri, client_id, client_secret,
future, extra_fields, response):
if response.error:
future.set_exception(AuthError('QQ auth error: %s' % str(response)))
return
args = escape.parse_qs_bytes(escape.native_str(response.body))
session = {
"access_token": args["access_token"][-1],
"expires_in": args["expires_in"][-1],
"refresh_token": args["refresh_token"][-1],
"client_id": client_id,
}
http = self.get_auth_http_client()
http.fetch(self._oauth_request_openid(session["access_token"]),
self.async_callback(self._on_open_id, future, session, extra_fields))
def _on_open_id(self, future, session, extra_fields, response):
if response.error:
future.set_exception(AuthError('QQ auth error: %s' % str(response)))
return
response = response.body.replace("callback( ", "").replace(" );", "")
args = escape.json_decode(response)
session["openid"] = str(args["openid"])
fields = set(['ret', 'msg', 'nickname'])
if extra_fields:
fields.update(extra_fields)
self.qq_request(
path="/user/get_user_info",
callback=self.async_callback(
self._on_get_user_info, future, session, fields),
access_token=session["access_token"],
openid=session["openid"],
oauth_consumer_key=session["client_id"],
fields=",".join(fields)
)
def _on_get_user_info(self, future, session, fields, user):
if user is None:
future.set_result(None)
return
fieldmap = {}
for field in fields:
fieldmap[field] = user.get(field)
fieldmap.update(session)
future.set_result(fieldmap)
@_auth_return_future
def qq_request(self, path, callback, access_token=None,
post_args=None, **args):
url = self._QQ_BASE_URL + path
all_args = {}
if access_token:
all_args["access_token"] = access_token
all_args.update(args)
if all_args:
url += "?" + urllib_parse.urlencode(all_args)
callback = self.async_callback(self._on_qq_request, callback)
http = self.get_auth_http_client()
if post_args is not None:
http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
callback=callback)
else:
http.fetch(url, callback=callback)
def _on_qq_request(self, future, response):
if response.error:
future.set_exception(AuthError("Error response %s fetching %s" %
(response.error, response.request.url)))
return
future.set_result(escape.json_decode(response.body))
def get_auth_http_client(self):
return httpclient.AsyncHTTPClient()
def _oauth_request_openid(self, access_token):
return self._OAUTH_OPENID_URL + "access_token=" + access_token
class SinaGraphOAuth2Mixin(OAuth2Mixin):
_OAUTH_AUTHORIZE_URL = "https://api.weibo.com/oauth2/authorize?"
_OAUTH_ACCESS_TOKEN_URL = "https://api.weibo.com/oauth2/access_token?"
_OAUTH_NO_CALLBACKS = False
_OAUTH_SINA_BASE_URL = "https://api.weibo.com/2"
@_auth_return_future
def get_authenticated_user(self, redirect_uri, client_id, client_secret,
code, callback, extra_fields=None, extra_params=None):
post_args = {
"client_id" : client_id,
"client_secret" : client_secret,
"grant_type" : "authorization_code",
"redirect_uri" : redirect_uri,
"code" : code
}
self.sina_request(
path="https://api.weibo.com/oauth2/access_token",
callback=self.async_callback(self._on_access_token, callback, extra_fields),
post_args = post_args,
)
def _on_access_token(self, future, extra_fields, response):
if response is None:
future.set_result(None)
return
fields = set(['error_code', 'error', 'id', 'screen_name'])
if extra_fields:
fields.update(extra_fields)
self.sina_request(
path=self._OAUTH_SINA_BASE_URL+"/users/show.json",
callback=self.async_callback(self._on_get_user_info, future, fields),
access_token=response["access_token"],
uid=response["uid"]
)
def _on_get_user_info(self, future, fields, user):
if user is None:
future.set_result(None)
return
fieldmap = {}
for field in fields:
fieldmap[field] = user.get(field, "")
future.set_result(fieldmap)
@_auth_return_future
def sina_request(self, path, callback, access_token=None,
post_args=None, **args):
url = path
all_args = {}
if access_token:
all_args["access_token"] = access_token
all_args.update(args)
if all_args:
url += "?" + urllib_parse.urlencode(all_args)
callback = self.async_callback(self._on_sina_request, callback)
http = self.get_auth_http_client()
if post_args is not None:
http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
callback=callback)
else:
http.fetch(url, callback=callback)
def _on_sina_request(self, future, response):
if response.error:
future.set_exception(AuthError("Error response %s fetching %s" %
(response.error, response.request.url)))
return
future.set_result(escape.json_decode(response.body))
def get_auth_http_client(self):
return httpclient.AsyncHTTPClient()
class QQGraphLoginHandler(BaseHandler, QQGraphOAuth2Mixin):
@tornado.web.asynchronous
def get(self):
my_url = (self.request.host.replace("localhost", "127.0.0.1") +
"/qqlogin?next=" +
tornado.escape.url_escape(self.get_argument("next", "/")))
if self.get_argument("code", False):
self.get_authenticated_user(
redirect_uri=my_url,
client_id=self.settings["qq_api_key"],
client_secret=self.settings["qq_api_secret"],
code=self.get_argument("code"),
extra_params={"grant_type": "authorization_code"},
callback=self._on_auth)
return
self.authorize_redirect(redirect_uri=my_url,
client_id=self.settings["qq_api_key"],
extra_params={"response_type": "code"})
def _on_auth(self, user):
if not user:
raise tornado.web.HTTPError(500, "qq auth failed")
#user: openid, nickname
uid = user.get("openid", 0)
nick = user.get("nickname", uid)#default uid
if user.get("ret", 0) or not uid:
self.render('error.html', msg = user.get('msg', 'error'))
else:
ZQ_Account().login(uid, nick, "QQ", self._on_login)
def _on_login(self, result, ex):
if not ex:
self.set_secure_cookie(settings.MGR_USER_COOKIE_KEY, json_encode(result) , expires_days = 1)
self.redirect(self.get_argument("next", "/"))
else:
self.writeError(result)
class SinaGraphLoginHandler(BaseHandler, SinaGraphOAuth2Mixin):
@tornado.web.asynchronous
def get(self):
my_url = ("http://" + self.request.host.replace("localhost", "127.0.0.1") +#必须要http://
"/sinalogin")
if self.get_argument("code", False):
self.get_authenticated_user(
redirect_uri=my_url,
client_id=self.settings["sina_api_key"],
client_secret=self.settings["sina_api_secret"],
code=self.get_argument("code"),
extra_params={"grant_type": "authorization_code"},
callback=self._on_auth)
return
self.authorize_redirect(redirect_uri=my_url,
client_id=self.settings["sina_api_key"],
extra_params={"response_type": "code"})
def _on_auth(self, user):
if not user:
raise tornado.web.HTTPError(500, "sina auth failed")
#user: id, screen_name
uid = user.get("id", 0)
nick = user.get("screen_name", uid)#default uid
if user.get("error_code", 0):
self.render('error.html', msg = user.get('error'))
else:
ZQ_Account().login(uid, nick, "SINA", self._on_login)
def _on_login(self, result, ex):
if not ex:
self.set_secure_cookie(settings.MGR_USER_COOKIE_KEY, json_encode(result) , expires_days = 1)
self.redirect(self.get_argument("next", "/"))
else:
self.writeError(result)
QQ OAuth直接参考官方文档即可,很详细:
http://wiki.connect.qq.com/
Sina OAuth官方文档有点乱,可参考:
http://jingyan.baidu.com/article/455a99508c91c8a166277893.html
http://rsj217.diandian.com/post/2013-04-17/40050093587