本节内容是通过 OAuth2 实现网页端的 AcWing 一键登录功能。
1. 在Django中集成Redis
Redis 为内存数据库,目前我们使用的是 Django 自带的数据库 SQLite,且能够很容易地迁移到 MySQL,这些数据库的效率不如 Redis,其特点为:
Redis 存的内容为 key, value
对,而其它数据库存的是若干张表,每张表为若干条目;
Redis 为单线程的,不会出现读写冲突。
首先我们需要先安装 Redis:
1 2 3 4 sudo apt-get update sudo apt-get install redis-server sudo service redis-server status # 查看redis-server状态 pip install django_redis
接着配置一下 Django 的缓存机制,将下面这段代码复制到 settings.py
中:
1 2 3 4 5 6 7 8 9 10 CACHES = { 'default' : { 'BACKEND' : 'django_redis.cache.RedisCache' , 'LOCATION' : 'redis://127.0.0.1:6379/1' , "OPTIONS" : { "CLIENT_CLASS" : "django_redis.client.DefaultClient" , }, }, } USER_AGENTS_CACHE = 'default'
然后启动 redis-server
,启动后可以使用 top
命令查看 redis-server
是否运行:
1 sudo redis-server /etc/redis/redis.conf
此时在项目根目录执行 python3 manage.py shell
进入 IPython 交互界面,输入以下代码测试一下 Redis:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 In [1 ]: from django.core.cache import cache In [2 ]: cache.keys('*' ) Out[2 ]: [] In [3 ]: cache.set ('yyj' , 1 , 5 ) Out[3 ]: True In [4 ]: cache.set ('abc' , 2 , None ) Out[4 ]: True In [5 ]: cache.has_key('abc' ) Out[5 ]: True In [6 ]: cache.has_key('yyj' ) Out[6 ]: False In [7 ]: cache.get('abc' ) Out[7 ]: 2 In [8 ]: cache.delete('abc' ) Out[8 ]: True
注意如果出现报错:ConnectionError: Error 111 connecting to 127.0.0.1:6379. Connection refused.
说明 redis-server
没有启动。
2. 申请授权码
一键授权登录的流程是用户点击一键登录后弹出确认授权的页面,用户确认后就自动创建一个新的账号,且登录到网站里。
具体交互流程为:用户点击按钮后向网站服务器端(Web)发起申请,请求用 AcWing 账号登录,然后 Web 将自己的 AppID
报给 AcWing,AcWing 给用户返回一个页面询问用户是否要授权给刚刚的网站,如果用户同意,那么 AcWing 会将一个授权码 code
(两小时有效期)发给 Web,Web 接到授权码后再加上自己的身份信息 AppSecret
以及 AppID
向 AcWing 申请一个授权令牌 access-token
(两小时有效期)和用户的 openid
(唯一辨别用户),Web 拿到令牌和用户 ID 后即可向 AcWing 申请用户的用户名和头像。
由于我们需要记录每个用户的 openid
,因此我们需要在数据库(game/models/player/
)的 Player
类中添加一个信息:
1 2 3 4 5 6 7 8 9 10 11 from django.db import modelsfrom django.contrib.auth.models import Userclass Player (models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) avatar = models.URLField(max_length=256 , blank=True ) openid = models.CharField(default='' , max_length=50 , blank=True , null=True ) def __str__ (self ): return str (self.user)
在根目录下更新一下数据库:
1 2 python3 manage.py makemigrations python3 manage.py migrate
申请授权码的请求地址:https://www.acwing.com/third_party/api/oauth2/web/authorize/
。
请求方法为 GET
,参考示例:
1 https://www.acwing.com/third_party/api/oauth2/web/authorize/?appid=APPID&redirect_uri=REDIRECT_URI&scope=SCOPE&state=STATE
参数说明:
appid
:应用的唯一 ID,可以在 AcWing 编辑 AcApp 的界面里看到;
redirect_uri
:接收授权码的地址,表示 AcWing 端要将授权码返回到哪个链接,需要对链接进行编码:Python3 中使用 urllib.parse.quote
;Java 中使用 URLEncoder.encode
;
scope
:申请授权的范围,目前只需填 userinfo
;
state
:用于判断请求和回调的一致性,授权成功后原样返回该参数值,即接收授权码的地址需要判断是否是 AcWing 发来的请求(判断收到的 state
与发送出去的 state
是否相同),如果不是直接 Pass。该参数可用于防止 CSRF 攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数(如果是将第三方授权登录绑定到现有账号上,那么推荐用 随机数 + user_id
作为 state
的值,可以有效防止 CSRF 攻击)。此处 state
可以存到 Redis 中,设置两小时有效期。
用户同意授权后会重定向到 redirect_uri
,返回参数为 code
和 state
。链接格式如下:
1 redirect_uri?code=CODE&state=STATE
如果用户拒绝授权,则不会发生重定向。
我们在 game/views/settings
目录中创建一个 acwing
目录表示 AcWing 授权登录,然后在该目录中先创建一个 __init__.py
,再创建两个子目录 web
和 acapp
分别表示 Web 端的 AcWing 一键登录以及 AcApp 端的 AcWing 一键登录,最后同样在这两个子目录中创建 __init__.py
文件。
在 web
目录下创建申请授权码的 API apply_code.py
:
1 2 3 4 5 6 7 from django.http import JsonResponsedef apply_code (request ): appid = '4007' return JsonResponse({ 'result' : 'success' , })
接着创建接收授权码的 API receive_code.py
:
1 2 3 4 from django.shortcuts import redirectdef receive_code (request ): return redirect('index' )
然后在 game/urls/settings
目录中也创建一个 acwing
目录,在该目录中创建 __init__.py
和 index.py
,index.py
内容如下:
1 2 3 4 5 6 7 8 from django.urls import pathfrom game.views.settings.acwing.web.apply_code import apply_codefrom game.views.settings.acwing.web.receive_code import receive_codeurlpatterns = [ path('web/apply_code/' , apply_code, name='settings_acwing_web_apply_code' ), path('web/receive_code/' , receive_code, name='settings_acwing_web_receive_code' ), ]
然后需要将该目录 include
进 settings
目录的 index.py
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 from django.urls import path, includefrom game.views.settings.getinfo import getinfofrom game.views.settings.login import myloginfrom game.views.settings.logout import mylogoutfrom game.views.settings.register import registerurlpatterns = [ path('getinfo/' , getinfo, name='settings_getinfo' ), path('login/' , mylogin, name='settings_login' ), path('logout/' , mylogout, name='settings_logout' ), path('register/' , register, name='settings_register' ), path('acwing/' , include('game.urls.settings.acwing.index' )), ]
重启项目后即可访问 https://<公网IP>/settings/acwing/web/apply_code/
以及 https://<公网IP>/settings/acwing/web/receive_code/
测试效果。
现在来看看 urllib.parse.quote
的作用,它能够将链接重新编码,替换掉原本的部分特殊字符防止出现 BUG:
1 2 3 4 5 6 In [1 ]: from urllib.parse import quote In [2 ]: url = 'https://app4007.acapp.acwing.com.cn/settings/acwing/web/receive_code/?args=yyj' In [3 ]: quote(url) Out[3 ]: 'https%3A//app4007.acapp.acwing.com.cn/settings/acwing/web/receive_code/%3Fargs%3Dyyj'
现在我们完善一下 apply_code.py
的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from django.http import JsonResponsefrom django.core.cache import cachefrom urllib.parse import quotefrom random import randintdef get_state (): res = '' for i in range (8 ): res += str (randint(0 , 9 )) return res def apply_code (request ): appid = '4007' redirect_uri = quote('https://app4007.acapp.acwing.com.cn/settings/acwing/web/receive_code/' ) scope = 'userinfo' state = get_state() cache.set (state, True , 7200 ) apply_code_url = 'https://www.acwing.com/third_party/api/oauth2/web/authorize/' return JsonResponse({ 'result' : 'success' , 'apply_code_url' : apply_code_url + '?appid=%s&redirect_uri=%s&scope=%s&state=%s' % (appid, redirect_uri, scope, state) })
然后修改一下前端代码(Settings
类):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 class Settings { constructor (root ) { ... this .$acwingoption = this .$login .find ('.ac_game_settings_acwingoption img' ); ... } start ( ) { ... } add_listening_events ( ) { this .add_listening_events_login (); this .add_listening_events_register (); } add_listening_events_login ( ) { let outer = this ; this .$login_register .click (function ( ) { outer.register (); }); this .$login_submit .click (function ( ) { outer.login_on_remote (); }); this .$acwingoption .click (function ( ) { outer.acwing_login (); }); } add_listening_events_register ( ) { ... } login_on_remote ( ) { ... } register_on_remote ( ) { ... } logout_on_remote ( ) { ... } acwing_login ( ) { $.ajax ({ url : 'https://app4007.acapp.acwing.com.cn/settings/acwing/web/apply_code/' , type : 'GET' , success : function (resp ) { console .log (resp); if (resp.result === 'success' ) { window .location .replace (resp.apply_code_url ); } } }) } register ( ) { ... } login ( ) { ... } getinfo ( ) { ... } hide ( ) { ... } show ( ) { ... } }
3. 申请授权令牌和用户ID
请求地址:https://www.acwing.com/third_party/api/oauth2/access_token/
。
请求方法为 GET
,参考示例:
1 https://www.acwing.com/third_party/api/oauth2/access_token/?appid=APPID&secret=APPSECRET&code=CODE
参数说明:
appid
:应用的唯一 ID,可以在 AcWing 编辑 AcApp 的界面里看到;
secret
:应用的秘钥,可以在 AcWing 编辑 AcApp 的界面里看到;
code
:上一步中获取的授权码。
申请成功的返回内容示例:
1 2 3 4 5 6 7 { "access_token" : "ACCESS_TOKEN" , "expires_in" : 7200 , "refresh_token" : "REFRESH_TOKEN" , "openid" : "OPENID" , "scope" : "SCOPE" , }
申请失败的返回内容示例:
1 2 3 4 { "errcode" : 40001 , "errmsg" : "code expired" , }
返回参数说明:
access_token
:授权令牌,有效期2小时;
expires_in
:授权令牌还有多久过期,单位(秒);
refresh_token
:用于刷新 access_token
的令牌,有效期30天;
openid
:用户的 ID。每个 AcWing 用户在每个 AcApp 中授权的 openid
是唯一的,可用于识别用户;
scope
:用户授权的范围。目前范围为 userinfo
,包括用户名、头像。
现在我们点击 AcWing 一键登录按钮即可跳出请求授权的页面,AcWing 会向 receive_code
函数发送 code
以及 state
,现在我们完善 receive_code.py
接到授权后的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from django.shortcuts import redirectfrom django.core.cache import cacheimport requestsdef receive_code (request ): data = request.GET code = data.get('code' ) state = data.get('state' ) if not cache.has_key(state): return redirect('index' ) cache.delete(state) apply_access_token_url = 'https://www.acwing.com/third_party/api/oauth2/access_token/' params = { 'appid' : '4007' , 'secret' : '0edf233ee876407ea3542220e2a8d83e' , 'code' : code } access_token_res = requests.get(apply_access_token_url, params=params).json() print (access_token_res) return redirect('index' )
现在我们点击一键登录按钮即可在后台看到接收到的授权令牌内容。
4. 申请用户信息
请求地址:https://www.acwing.com/third_party/api/meta/identity/getinfo/
。
请求方法为 GET
,参考示例:
1 https://www.acwing.com/third_party/api/meta/identity/getinfo/?access_token=ACCESS_TOKEN&openid=OPENID
参数说明:
access_token
:上一步中获取的授权令牌;
openid
:上一步中获取的用户 openid
。
申请成功的返回内容示例:
1 2 3 4 { 'username' : "USERNAME" , 'photo' : "https:cdn.acwing.com/xxxxx" }
申请失败的返回内容示例:
1 2 3 4 { 'errcode' : "40004" , 'errmsg' : "access_token expired" }
现在我们用授权令牌向 AcWing 申请用户的用户名和头像,进一步完善 receive_code.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 from django.shortcuts import redirectfrom django.core.cache import cachefrom django.contrib.auth.models import Userfrom django.contrib.auth import loginfrom game.models.player.player import Playerfrom random import randintimport requestsdef receive_code (request ): data = request.GET code = data.get('code' ) state = data.get('state' ) if not cache.has_key(state): return redirect('index' ) cache.delete(state) apply_access_token_url = 'https://www.acwing.com/third_party/api/oauth2/access_token/' params = { 'appid' : '4007' , 'secret' : '0edf233ee876407ea3542220e2a8d83e' , 'code' : code } access_token_res = requests.get(apply_access_token_url, params=params).json() access_token = access_token_res['access_token' ] openid = access_token_res['openid' ] player = Player.objects.filter (openid=openid) if player.exists(): login(request, player[0 ].user) return redirect('index' ) get_userinfo_url = 'https://www.acwing.com/third_party/api/meta/identity/getinfo/' params = { 'access_token' : access_token, 'openid' : openid } get_userinfo_res = requests.get(get_userinfo_url, params=params).json() username = get_userinfo_res['username' ] avatar = get_userinfo_res['photo' ] while User.objects.filter (username=username).exists(): username += str (randint(0 , 9 )) user = User.objects.create(username=username) player = Player.objects.create(user=user, avatar=avatar, openid=openid) login(request, user) return redirect('index' )
至此 Web 端的 AcWing 一键登录已经实现。
上一章:Django学习笔记-用户名密码登录 。
下一章:Django学习笔记-VS Code本地运行项目 。