本节内容是通过 OAuth2 实现 AcApp 端的 AcWing 一键登录功能。
AcApp 端使用 AcWing 一键授权登录的流程与之前网页端的流程一样,只有申请授权码这一步有一点细微的差别。
我们在打开 AcApp 应用之后会自动向 AcWing 请求账号登录,客户端会向后端服务器请求一些参数,然后后端服务器向 AcWing 请求授权码,然后 AcWing 在接到请求之后会询问用户是否要授权登录,如果用户同意了那么 AcWing 会给客户端发送一个授权码,客户端可以通过授权码加上自己的身份信息向 AcWing 服务器请求自己的授权令牌 access_token
和用户的 openid
,最后客户端在拿到令牌和 ID 后即可向 AcWing 服务器请求用户的用户名和头像等信息。
在网页端授权登录时我们使用的方法是通过 URL 的方式重定向到某一个链接里申请授权码,而这次的 AcApp 不是通过链接,而是通过 AcWing 的一个 API 申请,请求授权码的 API:
1 AcWingOS .api .oauth2 .authorize (appid, redirect_uri, scope, state, callback);
参数说明:
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 中,设置两小时有效期;
callback
:redirect_uri
返回后的回调函数,即接受 receive_code
函数向前端返回的信息。
用户同意授权后,会将 code
和 state
传递给 redirect_uri
。
如果用户拒绝授权,则将会收到如下错误码:
1 2 3 4 { errcode : "40010" errmsg : "user reject" }
我们在 game/views/settings/acwing/acapp
目录中将之前网页端的 apply_code.py
与 receive_code.py
复制过来,然后对 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 23 24 25 26 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/acapp/receive_code/' ) scope = 'userinfo' state = get_state() cache.set (state, True , 7200 ) return JsonResponse({ 'result' : 'success' , 'appid' : appid, 'redirect_uri' : redirect_uri, 'scope' : scope, 'state' : state, })
进入 game/urls/settings/acwing
修改一下路由:
1 2 3 4 5 6 7 8 9 10 11 12 from django.urls import pathfrom game.views.settings.acwing.web.apply_code import apply_code as web_apply_codefrom game.views.settings.acwing.web.receive_code import receive_code as web_receive_codefrom game.views.settings.acwing.acapp.apply_code import apply_code as acapp_apply_codefrom game.views.settings.acwing.acapp.receive_code import receive_code as acapp_receive_codeurlpatterns = [ path('web/apply_code/' , web_apply_code, name='settings_acwing_web_apply_code' ), path('web/receive_code/' , web_receive_code, name='settings_acwing_web_receive_code' ), path('acapp/apply_code/' , acapp_apply_code, name='settings_acwing_acapp_apply_code' ), path('acapp/receive_code/' , acapp_receive_code, name='settings_acwing_acapp_receive_code' ), ]
现在访问 https://app4007.acapp.acwing.com.cn/settings/acwing/acapp/apply_code/
即可看到返回内容。
然后我们修改一下 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 from django.http import JsonResponsefrom django.core.cache import cachefrom django.contrib.auth.models import Userfrom game.models.player.player import Playerfrom random import randintimport requestsdef receive_code (request ): data = request.GET if 'errcode' in data: return JsonResponse({ 'result' : 'apply failed' , 'errcode' : data['errcode' ], 'errmsg' : data['errmsg' ], }) code = data.get('code' ) state = data.get('state' ) if not cache.has_key(state): return JsonResponse({ 'result' : 'state not exist' , }) 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' ] players = Player.objects.filter (openid=openid) if players.exists(): player = players[0 ] return JsonResponse({ 'result' : 'success' , 'username' : player.user.username, 'avatar' : player.avatar, }) 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) return JsonResponse({ 'result' : 'success' , 'username' : player.user.username, 'avatar' : player.avatar, })
接着我们修改前端文件,也就是 game/static/js/src/settings
目录中的 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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 class Settings { constructor (root ) { this .root = root; this .platform = 'WEB' ; if (this .root .acwingos ) this .platform = 'ACAPP' ; this .username = '' ; this .avatar = '' ; this .$settings = $(` ... ` ); ... this .start (); } start ( ) { if (this .platform === 'WEB' ) { this .getinfo_web (); this .add_listening_events (); } else { this .getinfo_acapp (); } } add_listening_events ( ) { ... } add_listening_events_login ( ) { ... } add_listening_events_register ( ) { ... } login_on_remote ( ) { ... } register_on_remote ( ) { ... } acwing_login ( ) { ... } register ( ) { ... } login ( ) { ... } getinfo_web ( ) { let outer = this ; $.ajax ({ url : 'https://app4007.acapp.acwing.com.cn/settings/getinfo/' , type : 'GET' , data : { platform : outer.platform , }, success : function (resp ) { console .log (resp); if (resp.result === 'success' ) { outer.username = resp.username ; outer.avatar = resp.avatar ; outer.hide (); outer.root .menu .show (); } else { outer.login (); } } }); } acapp_login (appid, redirect_uri, scope, state ) { let outer = this ; this .root .acwingos .api .oauth2 .authorize (appid, redirect_uri, scope, state, function (resp ) { console .log (resp); if (resp.result === 'success' ) { outer.username = resp.username ; outer.avatar = resp.avatar ; outer.hide (); outer.root .menu .show (); } }); } getinfo_acapp ( ) { let outer = this ; $.ajax ({ url : 'https://app4007.acapp.acwing.com.cn/settings/acwing/acapp/apply_code/' , type : 'GET' , success : function (resp ) { if (resp.result === 'success' ) { outer.acapp_login (resp.appid , resp.redirect_uri , resp.scope , resp.state ); } } }); } hide ( ) { this .$settings .hide (); } show ( ) { this .$settings .show (); } }
注意,如果遇到跨域问题:Access to XMLHttpRequest at 'XXX'
,大概率是某个文件的内容写错了,可以检查 uWSGI 启动后的报错内容修改代码。
上一章:Django学习笔记-VS Code本地运行项目 。
下一章:Django学习笔记-实现联机对战(上) 。