---
layout: post
title: 如何让Python脚本接收OAuth2.0的Code？
tags: [Python, 服务器, OAuth]
---

  越简单，就越复杂。<!--more-->    

# 起因
  最近我在写一个在Windows上运行的Python脚本，需要做一个类似于第三方登录的功能。一般来说像这种东西都是使用的OAuth2.0来做认证，我接入的平台没有悬念的也是使用的这个东西，其中的重点就是获取到第三方网站的授权码Code，然后用它换取用户信息。于是我就在想怎么做比较好呢？

# 解决方法
## 1. 使用URI Scheme
  我看VSCode在实现类似功能的时候似乎用到了URI Scheme，就是在系统中注册一个伪协议，然后通过这个伪协议来调用程序实现获取Code，方法很简单，首先先在注册表里导入：   
```ini
Windows Registry Editor Version 5.00  
  
[HKEY_CLASSES_ROOT\mayx]  
"URL Protocol"="D:\\mayx.exe"  
@="MayxProtocol"  
  
[HKEY_CLASSES_ROOT\mayx\DefaultIcon]  
@="D:\\mayx.exe,1"  
  
[HKEY_CLASSES_ROOT\mayx\shell]  
  
[HKEY_CLASSES_ROOT\mayx\shell\open]  
  
[HKEY_CLASSES_ROOT\mayx\shell\open\command]  
@="\"D:\\mayx.exe\" \"%1\""  
```
  然后编写一个Python脚本并用Pyinstaller打包：   
```python
import sys
print(sys.argv[1])
```
  最后将回调地址填为“mayx://get”，这样认证完成之后就会像这样调用程序：“D:\mayx.exe mayx://get?code=something”，再用urllib简单做个解析就完成了。   
  这样看起来是不是非常的简单？看起来确实是挺简单的，可惜坑比较大，第一个是像上述这样的注册表有些杀毒软件会拒绝导入，因为像“shell\open\command”这种东西被病毒啥的滥用的地方太多了，除非软件通过了数字签名或者某些认证，否则正常情况下根本没法导入。二是不是所有的第三方平台都支持伪协议的方式调用，不过我试了一下我用的那个平台倒是支持😂，除了杀毒软件比较讨厌之外其他的倒还好。   
## 2. 使用http服务监听
  很多跨平台的程序，像有些主要在Linux上运行的某些程序就喜欢在获取Code的时候启动一个简单的web服务。其实用这个方法的话可能还更方便一点，不容易出问题。
### 使用Flask实现
  最开始，我想着要不然就用Flask来实现这个功能吧，实现起来也简单，写起来就几行：   
```python
from flask import Flask, request
code = ""
app = Flask(__name__)
@app.route('/getcode')
def get():
    global code
    code = request.args.get("code")
    shutdown = request.environ["werkzeug.server.shutdown"]
    shutdown()
    return "OK"

app.run(host="127.0.0.1",port=8000)
print(code)
```
  看起来也确实很简单，功能实现的也很完美，但是有个问题是总感觉用Flask做这么简单的事情实在是大材小用，打包成程序也要占不少空间，另外就是这个方法已经被弃用了，感觉就是很不爽。那要说不爽的话怎么才爽呢？   
### 使用socket实现
  我想起来之前期末时写的期末作业[socket-bbs](https://github.com/Mabbs/socket-bbs)，这就是用socket实现的一个简单的论坛，那我这么简单的功能想来用socket实现不是很好吗？于是就写了一个：（这段代码没有测试，因为这个方法不能用所以把代码删了，这是重新做的）   
```python
import socket
import urllib.parse
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #打开一个网络连接
server.bind(('127.0.0.1',8000)) #绑定要监听的端口
server.listen(5)  # 设置最大的连接数量为5
code = ""
while True:
    sock, addr = server.accept()  # 建立客户端连接
    data = sock.recv(8192).decode('utf-8').split('\r\n')#接收TCP数据，数据以字符串的形式返还
    if not data[0]:
        sock.close()  # 关闭连接
        continue
    url = urllib.parse.urlparse(data[0].split()[1])
    if url.path == '/getcode':
        query = urllib.parse.parse_qs(self.data)
        code = query["code"][0]
        sock.send(("HTTP/1.0 200 OK" + '\r\n').encode('utf-8'))
        sock.send(("Content-Type: text/html; charset=utf-8" + '\r\n').encode('utf-8'))
        sock.send('\r\n'.encode('utf-8'))
        sock.send("OK".encode('utf-8')) #发送TCP数据
        sock.close()  # 关闭连接
        break
    else:
        sock.send(("HTTP/1.0 404 Not Found" + '\r\n').encode('utf-8'))
        sock.send(("Content-Type: text/html; charset=utf-8" + '\r\n').encode('utf-8'))
        sock.send('\r\n'.encode('utf-8'))
        sock.send("Not Found".encode('utf-8')) #发送TCP数据
        sock.close()  # 关闭连接
print(code)
```
  看起来是不是复杂多了？其实大多数时候工作的好像挺正常的，但是不知道为什么莫名其妙第一次访问的时候会卡住，改了半天头都大了，于是就只好放弃这种方法了……
### 使用http.server实现
  这时候我突然想到我平时用来传文件用的python自带的模块http.server，既然这个模块是自带的，大小应该大不到哪去吧？随后我就写了一个：
```python
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import urllib.parse
code = ""
class Resquest(BaseHTTPRequestHandler):
    timeout = 5
    def do_GET(self):
        url = urllib.parse.urlparse(self.path)
        if url.path == "/getuser":
            self.send_response(200)
            self.send_header("Content-type", "text/html")  # 设置服务器响应头
            code = urllib.parse.parse_qs(url.query)["code"][0]
            buf = '''OK'''
            self.wfile.write(buf.encode())
            self.server.shutdown()
        else:
            self.send_response(404)
            self.send_header("Content-type", "text/html")  # 设置服务器响应头
            self.end_headers()
            buf = '''Not Found'''
            self.wfile.write(buf.encode())
host = ("127.0.0.1", 8000)
server = ThreadingHTTPServer(host, Resquest)
print("Starting server, listen at: %s:%s" % host)
server.serve_forever()
server.socket.close()
print(code)
```
  最后打包试了一下，功能正常，虽然还是大了点，不过比Flask小😁。其实我在写这个代码的时候，最开始不知道要在最后加上`server.socket.close()`，一开始写的时候总是遇到程序执行完之前端口必定不会释放，令人很烦，直接搜也没什么好的结果，不过问了下ChatGPT立马就解决了🤣，它说：   
> 当你调用`server.shutdown()`来停止服务器时，它会停止接收新的连接，并关闭已有的连接。但是，由于Python的socket库的设计，socket对象并不会立即释放端口。相反，它会在一段时间内处于TIME_WAIT状态，以确保在网络中所有挂起的数据都被正确传递或丢弃。这是一种网络协议的要求，称为"TCP TIME_WAIT"状态。

  AI还真是方便啊……

# 感想
  解决这么一个小问题却一时半会拿不下最合适的方案，这难道就是了解太多的副作用吗😝，不过最终来看还是AI厉害，一下子就解决了我的问题。