最近写的后端项目需要云盘,由于各种原因,最终选择了 Onedrive。Onedrive 的一个优点是文档是中文的,但缺点是中文也看不懂。。。。

于是借助文档、博客、示例代码等等,慢慢摸索过来,并把摸索的这个过程形成一篇博文。

注册应用、用户登录授权

参考博客:zhangguanzhang’s Blog
参考文档:Microsoft Graph 中的 OneDrive 授权和登录

Onedrive 的认证(以及 MS 家的其他功能)都统一使用 Microsoft Graph 进行认证,而 Microsoft Graph 似乎只支持 OAuth2(必须让用户在浏览器完成登录,不能拿到密码然后在后台登录),不支持 OAuth1(可以在后台利用用户密码发起请求完成登录),所以会麻烦一些。

在 Azure 创建应用

第一件事,是按照上面的博客所说,注册应用。这个应用其实就是一个 API,以下引用并修改了 zhangguanzhang’s Blog

我们首先要创建一个应用程序。

https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade

到上面链接里去注册一个应用程序,属性为:

  • 受支持的帐户类型记得选 任何组织目录(任何 Azure AD 目录 - 多租户)中的帐户和个人 Microsoft 帐户(例如,Skype、Xbox)
  • 重定向 URI (可选) 我用的是 http://localhost:8000/getAToken,在摸索的时候这个随便写就行。另外,这个值要存在代码里,命名为 redirect_uri

然后设置权限。点击左边侧边栏的 “API 权限”,点击中间的“添加权限”,然后在右边点“Microsoft Graph” - “委托的权限”,搜索并找到以下三个权限,勾选上。

1
2
3
Files.ReadWrite
Files.ReadWrite.All
offline_access
勾选权限
勾选权限

然后在侧边栏的“概述”里面,把应用程序(客户端) ID 复制下来(在代码里存为 client_id)。

然后在侧边栏“证书和密码”里面,点击“新客户端密码”,生成一个 ID 和值,把值存到代码里,命名为 client_secret

用户在浏览器登录,获取 auth_token

在 Azure 创建应用以后,就是授权直至拿到 refresh_token 的过程。这个过程就是参考 Microsoft Graph 中的 OneDrive 授权和登录 了,这里我用 Python requests 实现。

现在文档迁移到了 Microsoft Graph 身份验证概述而且更加详细了。而上述链接不再提供中文文档。

首先设置上述变量:

1
2
3
4
5
6
7
import requests
headers = {'Content-Type':'application/x-www-form-urlencoded'}
scope = "Files.ReadWrite offline_access"

client_id = "3aea792a-f5f5-49a6-b64d-e1a45d375323"
redirect_uri = "http://localhost:8000/getAToken"
client_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

然后是生成登录链接。这个链接需要在浏览器里访问然后输入账号密码,于是就不用 requests.get 而是把链接 print 出来,我们复制到浏览器里面。

1
2
login_link = f"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={client_id}&scope={scope}&response_type=code&redirect_uri={redirect_uri}"
print(login_link)

打开 print 的链接,成功登录并授权后,跳转到 localhost,其中的参数的 code 字段就是我们需要的 auth_token,729个字符,有点长。

1
http://localhost:8000/getAToken?code=0.AAAApOutbSzcpEiz8KuV1mGJ2ip56jr19aZJtk3hpF03UyM-AI4.AQABAAIAAAD--DLA3VO7QrddgJg7Wevry9C8xxU0YmDl1t3kRL1Rt8c4ZYINbxBW_X7KGMwL80bFg2I99rHKlQuC_bwGWIMjn1C4GjZg8fR2v2r-9J6fffSYUVMmrA5tGJ-7AUyulg076ViCOLWvtDzUh1T09f3Tt2Q8TpgCgO8P-0PqGMPqNOivzAxQz8WH5ZKSTMaI7WeOSBGe9yrpdjskO4pJrZv62E-jl2udaTBSXJAG4hKc18feCvuhJk27gT4H1W7ZiONqwMMkpzK6nlMhBgRP7-hTBNIU82Y0ASNOsOu2aAzrCQJmmbDdPHvsEYIq5jDnlOqeoNNFh-0v_AEbf-YfUfvIxN79eGpgrXvH19sLstDqFagJaOayNm6sf4HHuo2ikAot5kLZoBwYus57BaWeLI2IA_jDKd9T899Pv_Gfc_fwkgI9PynaHfoDRHb9A-6fXaJYPE5IYkTEnunNDaBf6jKtoTPub5LFIZv3OP38c3zJrTBZL5Wr5dpo3-pa0FFkbYPgHGC5APlWFNiBx-OECd4OJnbdzM7hrA2YzCLa6Bwl7SG4KTVXv2fwW1gFLgCZdI_xYBEDHfYuKUnlC5eqcebWwtkfJ8roFj9p3hzJ_GQSgEKjTgrdSUMaxrYwhSnAzS06H4BqnLKL-FrKsDBWJXuAIAA&session_state=697ec6eb-a4b9-4567-9873-6065f156164b#

在之后开发 WebApp 时,我们需要把 redirect_url 设置为我们能路由到的链接,在路由到的 View 中获取 uri 参数,就可以获得 auth_token 了。不过现在还是手动赋值吧。

1
2
# 赋值
auth_token = "0.AAAApOutbSzcpEiz8KuV1mGJ2ip56jr19aZJtk3hpF03UyM-AI4.AQABAAIAAAD--DLA3VO7QrddgJg7WevrnDiEeOedZvv94_iCOZdHtunnh-HD4-yA7CnKCnlskkTcXuD_nVujxrzDErLOPO5Kdba-gLFaUgOCxScnphpTmaR-bJbb6KCOJ6WEsecTZhdoBVvhgG8SzPAmolHemGaOOymSx1L3opaGXSo6YolsgwSCYcokocf9jl9Jq8pOAfg4tuZTYh1uX6f-IvQgLoIhUhE8i7GoaLIFhel9ICjiWgO9imtgNiLIjMX-MMRxqEKbcvbjVi9vnat1LE29v7uX_0Ogs6feGFzKEZOvT8pNlbm1t0frSwSIktQeIELEU3kzbDX7TEp1b_Cguj2u7wqF-kLv3P9w2_Gzhi-lzdRph1h9SW-RFYyWtdoCfhQgFf5m8K7aUGUEKgDK0RomoPvuZ1jfoNxe5JWQdcHqNo-LcHblYGRI1azy-p1fp28aBB6qKZ_0pcRTb4BrU5qLi9ze4RJ5kPajodTSSloUSVkf64B9OEdPeBvT8XU8_to0aVeHXOUTlsgVSlliatLPx9pFhsOZ4ec2-7fQMr8_CBhPlY4rjMfl8plefsfwcIsDBc0PRPITcsxA7OYSxZXvSchHA6XmCSzdl1413mTh7d0cvLYnBmzJm1tc6z6PEW270mtL_enHuzbtZpK9D1Epu0HIIAA"

用 auth_token 换 access_token 和 refresh_token

赋值以后,就可以请求拿 access_tokenrefresh_token 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
headers={'Content-Type':'application/x-www-form-urlencoded'}
response = requests.post(
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
data={
'client_id': client_id,
'redirect_uri': redirect_uri,
'client_secret': client_secret,
'code': auth_token,
'grant_type': 'authorization_code'
},
headers=headers)

response # <Response [200]>
eval(response.content)
# {'token_type': 'Bearer',
# 'scope': 'Files.ReadWrite profile openid email',
# 'expires_in': 3599,
# 'ext_expires_in': 3599,
# 'access_token': 'eyJ0eXAiOiJKV1QiLCJub25jZSI6ImZCREdZc3JfSzVYLXBkU2o5ZWotUUJMc2JGRGFDVGFBQ0lvSk90T3I2UlEiLCJhbGciOiJSUzI1NiIsIng1dCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyIsImtpZCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC82ZGFkZWJhNC1kYzJjLTQ4YTQtYjNmMC1hYjk1ZDY2MTg5ZGEvIiwiaWF0IjoxNjE0MjYyMDgzLCJuYmYiOjE2MTQyNjIwODMsImV4cCI6MTYxNDI2NTk4MywiYWNjdCI6MCwiYWNyIjoiMSIsImFjcnMiOlsidXJuOnVzZXI6cmVnaXN0ZXJzZWN1cml0eWluZm8iLCJ1cm46bWljcm9zb2Z0OnJlcTEiLCJ1cm46bWljcm9zb2Z0OnJlcTIiLCJ1cm46bWljcm9zb2Z0OnJlcTMiLCJjMSIsImMyIiwiYzMiLCJjNCIsImM1IiwiYzYiLCJjNyIsImM4IiwiYzkiLCJjMTAiLCJjMTEiLCJjMTIiLCJjMTMiLCJjMTQiLCJjMTUiLCJjMTYiLCJjMTciLCJjMTgiLCJjMTkiLCJjMjAiLCJjMjEiLCJjMjIiLCJjMjMiLCJjMjQiLCJjMjUiXSwiYWlvIjoiRTJaZ1lORGY2OWJFVUxnd3BkR0FMM2J5aHZRZCsxNkg2THppdUwvNGxPejYrR25mbE1VQSIsImFtciI6WyJwd2QiXSwiYXBwX2Rpc3BsYXluYW1lIjoi6Ziu6JaH6JaH54K55ZCN5ZWmIiwiYXBwaWQiOiIzYWVhNzkyYS1mNWY1LTQ5YTYtYjY0ZC1lMWE0NWQzNzUzMjMiLCJhcHBpZGFjciI6IjEiLCJpZHR5cCI6InVzZXIiLCJpcGFkZHIiOiIxNTQuMTcuNy44NCIsIm5hbWUiOiLnlLXlrZDnp5HmioDlpKflraZNU0MiLCJvaWQiOiIyZDU1MTQyNS01Mzk2LTQxNTktOTQ1MC0wNzU2MzdiMDE2N2IiLCJwbGF0ZiI6IjMiLCJwdWlkIjoiMTAwMzIwMDA1N0RCODYzRSIsInJoIjoiMC5BQUFBcE91dGJTemNwRWl6OEt1VjFtR0oyaXA1NmpyMTlhWkp0azNocEYwM1V5TS1BSTQuIiwic2NwIjoiRmlsZXMuUmVhZFdyaXRlIHByb2ZpbGUgb3BlbmlkIGVtYWlsIiwic3ViIjoid2R3RDhmLXM0ajJDWHlmQ2phOWd1LXpVdk91V0ZUWFg2bXk1d0ZGODROcyIsInRlbmFudF9yZWdpb25fc2NvcGUiOiJBUyIsInRpZCI6IjZkYWRlYmE0LWRjMmMtNDhhNC1iM2YwLWFiOTVkNjYxODlkYSIsInVuaXF1ZV9uYW1lIjoidWVzdGNtc2NAZGVtbzRjLm9ubWljcm9zb2Z0LmNvbSIsInVwbiI6InVlc3RjbXNjQGRlbW80Yy5vbm1pY3Jvc29mdC5jb20iLCJ1dGkiOiJFQ0Vaei1LeEprZVhOMDRTYlVNa0FBIiwidmVyIjoiMS4wIiwid2lkcyI6WyJiNzlmYmY0ZC0zZWY5LTQ2ODktODE0My03NmIxOTRlODU1MDkiXSwieG1zX3N0Ijp7InN1YiI6Inl0WGtlTWM0dFVnV3dpMmJLbS1xc3JMazJuY3Zjc3lrYVFYcm1Zdm1lRE0ifSwieG1zX3RjZHQiOjE0OTcyNDg0OTN9.EjpbLnqwdSsjeQwy5oVWjqHfSjFyHwQHcwNNvVhq8w9J96PQEjx6xpyIc-VgrQc0h1DdfikzyOVkMEHSVifM_KZoaaUIcW3T4xgXjgZVeEtznAKEqSDB4PN5qR45hsZbJLoVFBxaRZsQrEiPK7m2F4-4_VCK-I6Dfhvhm2_XTiBXnbqSsQ87VhF01BqSVXBIlJwSS1mQKEYHJGCjKnTB5yToDzJvoh4p7GGY3SCmfWY-pDMsAvMlJbErfhXh9Hxc41eBESSEmoajZAnxFOJU5LRtVHqPQW6PwlSTHL2nlGfdXFpspTc6hy4pSyVDNq6I3HJRFXl1cW0L6DjD2FoWMw',
# 'refresh_token': '0.AAAApOutbSzcpEiz8KuV1mGJ2ip56jr19aZJtk3hpF03UyM-AI4.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P_bTRjPRZzMorqH901Y19Ppp0sTRQdBBKFPHBjjxCNhD6fQlBevSxmidflskapxgyxbPlJysKnmnXMvEw6oy9tTRGn5QAB6kuYsP9BVFNiEpqeyqyyABuuiZZXHfMVSiqcIuUYdC5Cbt-meckpgVSC9Dfhvr50ilZ99m5iUHDkFVJLdPQLlrgdHqV8Mhe1V6Eh7F5Mfqrg1xb-K-QLZ3EGrcBUsDarXTkKPWP_9rKgukN_PTFerEBaM90u--8iytv0RJAui31HTa4yIInU0g7vZ16fhaEvJSXQjMSc5TYeepCHkK2fi3UpJe8TOoQ-qpORU_CJl83YMF59_enWxi0f_5ouV_9CPpeZ0xReU1Nb2y4g7uBYVPPNyroVlSmU0zV9c_rgs-dkSXK3b6r-AN-c5msS_ZoGvjiL3qBevNKo3Ws4w4IBWO-wQV8arNtNSuCAv5u9z41gITSDDg1Z8EYizTjQHjVaP095n5CWcBghqboVL4H2AAzW1SIgDJIs7ybwhg0oVsjUW4s9FQguPe9lj3HrAHzk5_NMRJsuGIZpDJ3ENz2pldXaRBqk42X07_y549Aw3qmgd8UY2KqsbA0EMNharkf-3q0_4AyeyxtqSqJETF5F3wy0bQ0RViS5W5mVobaRU90ia-zHzOjvXtxvenaY-r2ANjh_yg3qiV2D8Z7ODW50YzLL9SgiFqMm7zg7RYd4jjpdTYOik2yCX7cn_sSaA29KAqkbWKZNqX2Ds_ZRyby8RYeHBOC_I0XbrngLHnTszik-eKI3rHBv7UdTXCM9PHJwl2X4pocIREzfFPOTnLauiyfAS_zpb2Lsw8exVEqIpesVZt1S8uMvaEPctk7P22myx-vdxrk937c9z368vGpN6dBS--LPq5ZaxX9TuxCRbfCesT6KpkgLMq6Ywho59Tc6cAeE18sw'}

好耶!

MS 还提供了网址对 access_token 进行解密:https://jwt.ms/

用 refresh_token 换新的 access_token 和新的 refresh_token

然后,我们把上面 response 里的 refresh_token 取出来,然后请求刷新 access_tokenrefresh_token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
refresh_token = eval(response.content)['refresh_token']

response_refresh = requests.post(
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
data = {
'client_id': client_id,
'redirect_uri': redirect_uri,
'client_secret': client_secret,
'refresh_token': refresh_token,
'grant_type': 'refresh_token'
},
headers = headers)

response # <Response [200]>
eval(response.content)
# {'token_type': 'Bearer',
# 'scope': 'Files.ReadWrite profile openid email',
# 'expires_in': 3599,
# 'ext_expires_in': 3599,
# 'access_token': 'eyJ0eXAiOiJKV1QiLCJub25jZSI6ImhFSy1UT0xyZC1WZVNQOGVCU3plTURzeW1EODk5RWdlMEJzVDM5ak5uZEkiLCJhbGciOiJSUzI1NiIsIng1dCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyIsImtpZCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC82ZGFkZWJhNC1kYzJjLTQ4YTQtYjNmMC1hYjk1ZDY2MTg5ZGEvIiwiaWF0IjoxNjE0MjYyNjg4LCJuYmYiOjE2MTQyNjI2ODgsImV4cCI6MTYxNDI2NjU4OCwiYWNjdCI6MCwiYWNyIjoiMSIsImFjcnMiOlsidXJuOnVzZXI6cmVnaXN0ZXJzZWN1cml0eWluZm8iLCJ1cm46bWljcm9zb2Z0OnJlcTEiLCJ1cm46bWljcm9zb2Z0OnJlcTIiLCJ1cm46bWljcm9zb2Z0OnJlcTMiLCJjMSIsImMyIiwiYzMiLCJjNCIsImM1IiwiYzYiLCJjNyIsImM4IiwiYzkiLCJjMTAiLCJjMTEiLCJjMTIiLCJjMTMiLCJjMTQiLCJjMTUiLCJjMTYiLCJjMTciLCJjMTgiLCJjMTkiLCJjMjAiLCJjMjEiLCJjMjIiLCJjMjMiLCJjMjQiLCJjMjUiXSwiYWlvIjoiQVNRQTIvOFRBQUFBcFB5WUtCamlUK2kxWnVaY3BLNitudHh2ektkMGovNGtvSVhGVnBwOE5vQT0iLCJhbXIiOlsicHdkIl0sImFwcF9kaXNwbGF5bmFtZSI6IumYruiWh-iWh-eCueWQjeWVpiIsImFwcGlkIjoiM2FlYTc5MmEtZjVmNS00OWE2LWI2NGQtZTFhNDVkMzc1MzIzIiwiYXBwaWRhY3IiOiIxIiwiaWR0eXAiOiJ1c2VyIiwiaXBhZGRyIjoiMTU0LjE3LjcuODQiLCJuYW1lIjoi55S15a2Q56eR5oqA5aSn5a2mTVNDIiwib2lkIjoiMmQ1NTE0MjUtNTM5Ni00MTU5LTk0NTAtMDc1NjM3YjAxNjdiIiwicGxhdGYiOiIzIiwicHVpZCI6IjEwMDMyMDAwNTdEQjg2M0UiLCJyaCI6IjAuQUFBQXBPdXRiU3pjcEVpejhLdVYxbUdKMmlwNTZqcjE5YVpKdGszaHBGMDNVeU0tQUk0LiIsInNjcCI6IkZpbGVzLlJlYWRXcml0ZSBwcm9maWxlIG9wZW5pZCBlbWFpbCIsInN1YiI6Indkd0Q4Zi1zNGoyQ1h5ZkNqYTlndS16VXZPdVdGVFhYNm15NXdGRjg0TnMiLCJ0ZW5hbnRfcmVnaW9uX3Njb3BlIjoiQVMiLCJ0aWQiOiI2ZGFkZWJhNC1kYzJjLTQ4YTQtYjNmMC1hYjk1ZDY2MTg5ZGEiLCJ1bmlxdWVfbmFtZSI6InVlc3RjbXNjQGRlbW80Yy5vbm1pY3Jvc29mdC5jb20iLCJ1cG4iOiJ1ZXN0Y21zY0BkZW1vNGMub25taWNyb3NvZnQuY29tIiwidXRpIjoiUjZERXd0TWlBazJsaTVXaWRIOGpBQSIsInZlciI6IjEuMCIsIndpZHMiOlsiYjc5ZmJmNGQtM2VmOS00Njg5LTgxNDMtNzZiMTk0ZTg1NTA5Il0sInhtc19zdCI6eyJzdWIiOiJ5dFhrZU1jNHRVZ1d3aTJiS20tcXNyTGsybmN2Y3N5a2FRWHJtWXZtZURNIn0sInhtc190Y2R0IjoxNDk3MjQ4NDkzfQ.GdgiFzmdXNFuBqaDSsLzalQU0QULm1fAYZw-mKw5D1KdR4j9WSl6sIa9vPcuogg1x7oLMujcgfdyY0Cf4KNLk3fQ3vzLo395R5GDbg-djBREWCBBgFvcgbu8AyH7yj6MxdXsY59U9nFqVdeIfqEZxxxFCT2F2Nc-CfrDsI8PZQL3kn0VFcvSiUwuOMlH06ydKwyyBNhCRh5yc9x32XWwlRde-GPZCd1SdyvcPvo_mUcEJxh97tRfehHNTDltXGFsAZqcYaz18iJk9MqZ4tlyOIIWPX_lXGRtNe2dUDQE6g6znWY0ClGvU3X9mdScUUtfblmnfmzMj-ECBkAjmjKBnA',
# 'refresh_token': '0.AAAApOutbSzcpEiz8KuV1mGJ2ip56jr19aZJtk3hpF03UyM-AI4.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P89XPzxfnsn8nWMuIFittfpaEqpEzvy5sSPvkBHSIgu_y7Ppazm1F3l2nJ6hEWraRomalXRdwDi1Y78yIC_yA1xAspMEEuoxejDjoBhcWqOWLUTfXkfKk7i6pugNA-a_GOH9-EvKc_g9NvuCK4QgK6FQadMUf62wpoCkB53kgnH8GtOYKW34AutKaFi-N3v8RstrJRtsT3ZXanWmlA_q2WbDSou3-L_H6MS62u3Fr_mxnaKx4lKyy4cXaWbb_29dbx_yVO4zd_x5WHbcnrkfk3qoxD_b5kxHOMlKpVHtX_NthQvg8ZekDZ0pLawRsCDhJ8NFGwJvIXKxaMAyCbhHsyklnw97sU3M9jg08oAbaoQ0JA7eDZ4gd4juSzvkdGVK77unbROzOC1GjR12MWd9n_lYz4KMv_ErKx8wN2MHMAVK-QBBkJUsdB4JbBzSVgneD3jaDvWQRyz4BJMMx5e8qjR-bRwCBjm4Y1aK7vYNBGl2DVR1sVbf8eRZje6AxRQYy3rjdDod_30IqjNhDzX-eILlXR570RmoOGZgaWNPf-3-_3AaDGtsBE8tlCGux8lIR7jIxIKeBjfp1S5ymiofVirYYJiAg17KHy_W9FHUHS9CIs6CrKEvPesjDuFmerE14hzmJ4LCJarZSy2AsV6XiTMmamzaKX-WwFSzGXpipOwPbcO4tjcq5tFWz7-1TzDKsaAyvvh-K3fQiD2GUAv4v9zgRVczUQXN0Z37RULpnSGhj2eXaOenNXl9EoIgBLKXpj-lOshEyRbL2P3WzgvS1MUaXH31IHt-NiQuMpmeBfGg-gxrc9inQ0n7ATfD0T5vZ8YodjccP_rJvzI1Ntfl1yQs0g2h2JyZp694MOqc7Clv8W0P8F-uJIVWK2yv3iPRz-_nBM3im0A9OrCaugArHGPJ9gRt03nHzUHeJIAMc2Xqg'}

至此,关于 token 的申请方法就讲完了。

Django 实现

我把上面的过程改成了一个 OnedriveAuthentication 类(代码见 GitHub,views 部分在 views.py 中)。

另外,MS 也给了一个利用 requests_oauthlib.OAuth2Session 封装的示例,也可以参考。

Onedrive API 使用

完成登录授权,拿到 Access Token 以后,就可以用 MS Graph 的 API 进行操作了。可以在 Graph Explorer 进行测试。

而具体使用方法,就直接看文档啦。这文档蛮详细的,边写边看就行。

由于 REST API 的寻址部分比较麻烦,我使用 Python 对 API 进行了简单封装,代码在 GitHub

这里只写了自己在开发过程中遇到的一些场景,以及解决方案。

项目的场景是使用一个账户的云盘作为整个项目的共有云盘,文件上传、删除等操作全部交给后端完成。后端判断前端登录的用户权限后,使用该账户的 access_token 执行操作。

上传

为了省去文件经过后端服务器的流量,后端可以使用 Onedrive 的通过上传会话上传大文件 方案上传文件。前后端和 Onedrive 的交互过程如下:

  1. 在登陆状态下,前端向后端 POST /api/cloud/file 发起请求,后端请求 Onedrive 生成一个临时上传对话,并将 Onedrive 的应答(格式见上面的链接)转发给前端;
  2. 前端按照上面链接所述方法,直接向 Onedrive 上传文件。上传完成后,Onedrive 返回文件的 id 等信息,文件将位于 /(应用文件夹)/temp/{userid}/ 文件夹;
  3. 前端根据需求(如上传沙龙相关文件、沙龙照片)向对应接口发起请求(请求需包含文件 id),后端将文件移动至每个功能对应的文件夹,并完成后续操作(录入数据库等)。

前端交互

前端交互原理很简单好像也不简单,对于错误处理就更麻烦了,虽然 MS Graph 最后给了对各种错误处理的详细策略,但是没有示例代码还是很麻烦。

因此,这里放一个我的 JavaScript 实现,其中 createUploadSession 是创建上传会话的接口。

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
// 调用后端接口,将文件上传至 onedrive。
// 参考文档:https://docs.microsoft.com/zh-cn/graph/api/driveitem-createuploadsession
export async function uploadFile(file) {
let res = await createUploadSession({"filename": file.name});
const uploadUrl = JSON.parse(res.data).uploadUrl;
let size = file.size;

const totalRetryTimes = 5; // 4xx 导致的上传失败的重试次数
const firstDelay = 1000; // 5xx 导致的上传失败的第一次等待时间(之后采用 * 2 的指数退避策略)

// MS 直接给了上传时错误处理的策略,nb!
// https://docs.microsoft.com/zh-cn/graph/api/driveitem-createuploadsession#best-practices
async function uploadPart(start, end, retryTimes = 0, nextDelay = firstDelay) {
// console.log(`uploadPart: start=${start}, end=${end}, retryTimes=${retryTimes}, nextDelay=${nextDelay}`);
let headers = {};
let status = 0;
headers['Content-Range'] = `bytes ${start}-${end - 1}/${size}`;
headers['Content-Type'] = file.type;
let config = {
withCredentials: false,
timeout: 0,
headers
};
try {
res = await axios.put(uploadUrl, file.slice(start, end), config);
status = res.status;
} catch (err) {
status = 500;
}

if (status >= 500) {
// 继续或重试由于连接中断或任意 5xx 错误而失败的上载
// 请使用指数退避战略
console.warn(`上传失败,等待 ${nextDelay / 1000}s 后上传...`);
await sleep(nextDelay);
return await uploadPart(start, end, retryTimes, nextDelay * 2);
} else if (status >= 200 && status < 300) {
// 成功上传该段,返回
return res;
} else {
// 对于其他错误,不应使用指数退避战略,而应限制尝试重试的次数
if (retryTimes > 0) {
if (retryTimes > totalRetryTimes) {
console.warn(`上传失败 ${retryTimes} 次,取消上传`);
throw res;
} else {
console.warn(`上传失败 ${retryTimes} 次,重试中...`);
return await uploadPart(start, end, retryTimes+1, nextDelay);
}
}
}
}

let status = 0;
let start = 0;
// 200 为上传成功(覆盖),201 为上传成功(新建)
while (status !== 200 && status !== 201) {
let end = Math.min(start + maxFileContentLength, file.size);
res = await uploadPart(start, end);
start = end;
status = res.status;
}
return res.data.id;
}

这段代码还没有加上传进度等功能,不过上传的大体思路就是这样的。

下载

为了省去文件经过后端服务器的流量,本后端只提供下载链接。由于 Onedrive API 限制,提供两种下载链接:

  • 永久链接,但需在浏览器中访问
  • 临时链接(Onedrive 链接有效期 15min,但本后端提供即时获取链接并重定向的 REST API)

永久链接

有网友发现,手动或利用 Onedrive API 生成分享链接后,在分享链接后追加 ?download=1 参数,在浏览器访问该链接即可自动下载文件。

但该链接对应的 html 需要运行 JavaScript,因此不能通过 Python requests 或 JavaScript XMLHTTPRequest 直接下载。推荐使用后面的 API,或者在 JavaScript 使用 window.open() 在新窗口打开这个链接。

临时链接

利用 Onedrive API 下载文件 时,响应报文为 302 FoundLocation 为一个下载 URL。该 URL 仅在较短的一段时间 (几分钟后)内有效,不需要认证即可下载。

本后端提供 /cloud/file/{id}/download/ API,该 API 调用上述 API 后将报文返回给前端。