Django学习笔记-AcApp端授权AcWing一键登录


本节内容是通过 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 中,设置两小时有效期;
  • callbackredirect_uri 返回后的回调函数,即接受 receive_code 函数向前端返回的信息。

用户同意授权后,会将 codestate 传递给 redirect_uri

如果用户拒绝授权,则将会收到如下错误码:

1
2
3
4
{
errcode: "40010"
errmsg: "user reject"
}

我们在 game/views/settings/acwing/acapp 目录中将之前网页端的 apply_code.pyreceive_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 JsonResponse
from django.core.cache import cache
from urllib.parse import quote
from random import randint

def get_state(): # 获得8位长度的随机数
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) # 有效期2小时

# 需要返回四个参数
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 path
from game.views.settings.acwing.web.apply_code import apply_code as web_apply_code
from game.views.settings.acwing.web.receive_code import receive_code as web_receive_code
from game.views.settings.acwing.acapp.apply_code import apply_code as acapp_apply_code
from game.views.settings.acwing.acapp.receive_code import receive_code as acapp_receive_code

urlpatterns = [
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 JsonResponse
from django.core.cache import cache
from django.contrib.auth.models import User
from game.models.player.player import Player
from random import randint
import requests

def 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) # filter不管存不存在都会返回一个列表,get如果不存在会报异常
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'; // 默认为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() { // 此处将之前的getinfo函数名进行了修改用来区分
let outer = this;
$.ajax({
url: 'https://app4007.acapp.acwing.com.cn/settings/getinfo/', // 用AcWing部署
// url: 'http://8.130.54.44:8000/settings/getinfo/', // 用云服务器部署
type: 'GET',
data: {
platform: outer.platform,
},
success: function(resp) { // 调用成功的回调函数,返回的Json字典会传给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;
// resp是redirect_uri的返回值,此处为用户名和头像
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学习笔记-实现联机对战(上)