提交 83947478a4281bb902e0b62acbb09fde15ddc990

作者 qianyingz
1 个父辈 8fe47fba

oidc用户认证接口

... ... @@ -28,4 +28,5 @@ kservice/tmp
28 28 dist/
29 29 build/
30 30 *.egg-info/
31   -.git/
\ No newline at end of file
  31 +.git/
  32 +.vscode/
\ No newline at end of file
... ...
... ... @@ -7,8 +7,8 @@ import time
7 7 import configure
8 8 from app.util import BlueprintApi
9 9 from app.util import find_class
10   -from app.models import db,Table,InsertingLayerName,Database,DES,Task
11   -
  10 +from app.models import db, Table, InsertingLayerName, Database, DES, Task
  11 +from app.modules.auth.oauth2 import config_oauth
12 12 from flasgger import Swagger
13 13 # from rtree import index
14 14 import logging
... ... @@ -31,12 +31,15 @@ import os
31 31 """
32 32 因为decimal不能序列化,增加Flask对decimal类的解析
33 33 """
  34 +
  35 +
34 36 class JSONEncoder(_JSONEncoder):
35 37 def default(self, o):
36 38 if isinstance(o, decimal.Decimal):
37 39 return float(o)
38 40 super(JSONEncoder, self).default(o)
39 41
  42 +
40 43 class Flask(_Flask):
41 44 json_encoder = JSONEncoder
42 45
... ... @@ -52,12 +55,18 @@ def create_app():
52 55 app.config['echo'] = True
53 56 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
54 57 app.config['JSON_AS_ASCII'] = False
  58 + app.config['SECRET_KEY'] = configure.SECRET_KEY
  59 + app.config['OAUTH2_JWT_ENABLED'] = True
  60 +
  61 + app.config['OAUTH2_JWT_ISS'] = 'http://localhost:5000'
  62 + app.config['OAUTH2_JWT_KEY'] = 'secret-key'
  63 + app.config['OAUTH2_JWT_ALG'] = 'HS256'
55 64 # app.config['SQLALCHEMY_ECHO'] = True
56 65
57 66 # 跨域设置
58 67 CORS(app)
59 68
60   - #swagger设置
  69 + # swagger设置
61 70 swagger_config = Swagger.DEFAULT_CONFIG
62 71 swagger_config.update(configure.swagger_configure)
63 72 Swagger(app, config=swagger_config)
... ... @@ -66,41 +75,47 @@ def create_app():
66 75 db.init_app(app)
67 76 db.create_all(app=app)
68 77
69   -
70 78 # 日志
71 79 logging.basicConfig(level=logging.INFO)
72   - log_file = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))),"logs","log.txt")
73   - handler = logging.FileHandler(log_file, encoding='UTF-8') # 设置日志字符集和存储路径名字
74   - logging_format = logging.Formatter('[%(levelname)s] %(asctime)s %(message)s')
  80 + log_file = os.path.join(os.path.dirname(os.path.dirname(
  81 + os.path.realpath(__file__))), "logs", "log.txt")
  82 + handler = logging.FileHandler(
  83 + log_file, encoding='UTF-8') # 设置日志字符集和存储路径名字
  84 + logging_format = logging.Formatter(
  85 + '[%(levelname)s] %(asctime)s %(message)s')
75 86 handler.setFormatter(logging_format)
76 87 app.logger.addHandler(handler)
77   -
78   -
  88 +
  89 + # 配置使用鉴权组件,不写无法认证授权
  90 + config_oauth(app)
  91 +
79 92 # 注册blueprint,查找BlueprintApi的子类
80 93 for scan in configure.scan_module:
81 94 for api in find_class(scan, BlueprintApi):
82 95 app.register_blueprint(api.bp)
83 96
84   -
85 97 # 入库监测线程
  98 +
86 99 @app.before_first_request
87 100 def data_entry_process():
88 101 StructurePrint.print("start listen")
89 102 process = threading.Thread(target=data_entry_center)
90 103 process.start()
  104 +
  105 + # 不检测https
  106 + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
  107 +
91 108
92 109 return app
93 110
94 111
95   -
96   -
97 112 def data_entry_center():
98 113 running_dict = {}
99   - sys_session: Session = PGUtil.get_db_session(configure.SQLALCHEMY_DATABASE_URI)
  114 + sys_session: Session = PGUtil.get_db_session(
  115 + configure.SQLALCHEMY_DATABASE_URI)
100 116
101 117 while True:
102 118
103   -
104 119 try:
105 120 time.sleep(3)
106 121
... ... @@ -109,10 +124,11 @@ def data_entry_center():
109 124
110 125 # structured_print(running_dict.__len__().__str__())
111 126
112   - for process,layer_names in running_dict.items():
  127 + for process, layer_names in running_dict.items():
113 128 if not process.is_alive():
114 129 for l in layer_names:
115   - inserted = sys_session.query(InsertingLayerName).filter_by(name=l).one_or_none()
  130 + inserted = sys_session.query(
  131 + InsertingLayerName).filter_by(name=l).one_or_none()
116 132 if inserted:
117 133 sys_session.delete(inserted)
118 134 sys_session.commit()
... ... @@ -124,38 +140,40 @@ def data_entry_center():
124 140
125 141 # 入库进程少于阈值,开启入库进程
126 142
127   - inter_size = sys_session.query(distinct(InsertingLayerName.task_guid)).count()
  143 + inter_size = sys_session.query(
  144 + distinct(InsertingLayerName.task_guid)).count()
128 145
129 146 if inter_size < configure.entry_data_thread:
130 147 # 锁表啊
131   - ready_task:Task = sys_session.query(Task).filter_by(state=0).order_by(Task.create_time).with_lockmode("update").limit(1).one_or_none()
  148 + ready_task: Task = sys_session.query(Task).filter_by(state=0).order_by(
  149 + Task.create_time).with_lockmode("update").limit(1).one_or_none()
132 150 if ready_task:
133 151
134 152 try:
135 153 parameter = json.loads(ready_task.parameter)
136 154 StructurePrint.print("检测到入库任务")
137   - ready_task.state=2
138   - ready_task.process="入库中"
  155 + ready_task.state = 2
  156 + ready_task.process = "入库中"
139 157 sys_session.commit()
140 158
141   - metas: list = json.loads(parameter.get("meta").__str__())
  159 + metas: list = json.loads(
  160 + parameter.get("meta").__str__())
142 161 parameter["meta"] = metas
143 162
144   -
145   - database = sys_session.query(Database).filter_by(guid=ready_task.database_guid).one_or_none()
146   - pg_ds: DataSource = PGUtil.open_pg_data_source(1,DES.decode(database.sqlalchemy_uri))
  163 + database = sys_session.query(Database).filter_by(
  164 + guid=ready_task.database_guid).one_or_none()
  165 + pg_ds: DataSource = PGUtil.open_pg_data_source(
  166 + 1, DES.decode(database.sqlalchemy_uri))
147 167
148 168 this_task_layer = []
149 169 for meta in metas:
150 170 overwrite = parameter.get("overwrite", "no")
151 171
152   -
153   -
154 172 for layer_name_origin, layer_name in meta.get("layer").items():
155 173 origin_name = layer_name
156 174 no = 1
157 175
158   - while (overwrite.__eq__("no") and pg_ds.GetLayerByName(layer_name)) or sys_session.query(InsertingLayerName).filter_by(name=layer_name).one_or_none() :
  176 + while (overwrite.__eq__("no") and pg_ds.GetLayerByName(layer_name)) or sys_session.query(InsertingLayerName).filter_by(name=layer_name).one_or_none():
159 177 layer_name = origin_name + "_{}".format(no)
160 178 no += 1
161 179
... ... @@ -171,7 +189,8 @@ def data_entry_center():
171 189 meta["layer"][layer_name_origin] = layer_name
172 190
173 191 pg_ds.Destroy()
174   - entry_data_process = multiprocessing.Process(target=EntryDataVacuate().entry,args=(parameter,))
  192 + entry_data_process = multiprocessing.Process(
  193 + target=EntryDataVacuate().entry, args=(parameter,))
175 194 entry_data_process.start()
176 195 running_dict[entry_data_process] = this_task_layer
177 196 except Exception as e:
... ... @@ -184,4 +203,4 @@ def data_entry_center():
184 203 sys_session.commit()
185 204 except Exception as e:
186 205 sys_session.commit()
187   - StructurePrint.print(e.__str__(),"error")
\ No newline at end of file
  206 + StructurePrint.print(e.__str__(), "error")
... ...
  1 +from app.util import BlueprintApi
  2 +from app.util import BlueprintApi
  3 +from flask import Blueprint, render_template, redirect, url_for, request, session, jsonify
  4 +from .models import *
  5 +from werkzeug.security import gen_salt
  6 +import time
  7 +from .oauth2 import authorization, require_oauth, generate_user_info
  8 +from authlib.oauth2 import OAuth2Error
  9 +from authlib.integrations.flask_oauth2 import current_token
  10 +
  11 +def current_user():
  12 + if 'id' in session:
  13 + uid = session['id']
  14 + return User.query.get(uid)
  15 + return None
  16 +
  17 +
  18 +def split_by_crlf(s):
  19 + return [v for v in s.splitlines() if v]
  20 +
  21 +
  22 +class DataManager(BlueprintApi):
  23 + bp = Blueprint("Auth", __name__, url_prefix="/auth")
  24 +
  25 + @staticmethod
  26 + @bp.route('/test', methods=('GET', 'POST'))
  27 + def Test():
  28 + res = {}
  29 + try:
  30 + res['user'] = User.query.all()
  31 + except Exception as e:
  32 + raise e
  33 + return res
  34 +
  35 + @staticmethod
  36 + @bp.route('/', methods=('GET', 'POST'))
  37 + def test():
  38 + if request.method == 'POST':
  39 + username = request.form['username']
  40 + password = request.form['password']
  41 + user = User.query.filter_by(username=username).first()
  42 + if not user:
  43 + user = User(username=username,
  44 + password=password, role='admin')
  45 + db.session.add(user)
  46 + db.session.commit()
  47 + session['id'] = user.id
  48 + return redirect('/auth')
  49 + user = current_user()
  50 + if user:
  51 + clients = OAuth2Client.query.filter_by(user_id=user.id).all()
  52 + else:
  53 + clients = []
  54 + return render_template('auth/login.html', user=user, clients=clients)
  55 +
  56 +
  57 +
  58 + @staticmethod
  59 + @bp.route('/create_client', methods=('GET', 'POST'))
  60 + def create_client():
  61 + user = current_user()
  62 + if not user:
  63 + return redirect('/auth')
  64 + if request.method == 'GET':
  65 + return render_template('auth/create_client.html')
  66 + form = request.form
  67 + client_id = gen_salt(24)
  68 + client = OAuth2Client(client_id=client_id, user_id=user.id)
  69 + # Mixin doesn't set the issue_at date
  70 + client.client_id_issued_at = int(time.time())
  71 + if client.token_endpoint_auth_method == 'none':
  72 + client.client_secret = ''
  73 + else:
  74 + client.client_secret = gen_salt(48)
  75 + client_metadata = {
  76 + "client_name": form["client_name"],
  77 + "client_uri": form["client_uri"],
  78 + "grant_types": split_by_crlf(form["grant_type"]),
  79 + "redirect_uris": split_by_crlf(form["redirect_uri"]),
  80 + "response_types": split_by_crlf(form["response_type"]),
  81 + "scope": form["scope"],
  82 + "token_endpoint_auth_method": form["token_endpoint_auth_method"]
  83 + }
  84 + client.set_client_metadata(client_metadata)
  85 + db.session.add(client)
  86 + db.session.commit()
  87 + return redirect('/auth')
  88 +
  89 + @staticmethod
  90 + @bp.route('/authorize', methods=('GET', 'POST'))
  91 + def authorize():
  92 + user = current_user()
  93 + if request.method == 'GET':
  94 + try:
  95 + grant = authorization.validate_consent_request(end_user=user)
  96 + except OAuth2Error as error:
  97 + return jsonify(dict(error.get_body()))
  98 + return render_template('auth/authorize.html', user=user, grant=grant)
  99 + if not user and 'username' in request.form:
  100 + username = request.form.get('username')
  101 + user = User.query.filter_by(username=username).first()
  102 + if request.form['confirm']:
  103 + grant_user = user
  104 + else:
  105 + grant_user = None
  106 + return authorization.create_authorization_response(grant_user=grant_user)
  107 +
  108 +
  109 + @staticmethod
  110 + @bp.route('/token', methods=['POST'])
  111 + def issue_token():
  112 + return authorization.create_token_response()
  113 +
  114 + @staticmethod
  115 + @bp.route('/userinfo')
  116 + @require_oauth('profile')
  117 + def api_me():
  118 + return jsonify(generate_user_info(current_token.user, current_token.scope))
... ...
  1 +from flask_sqlalchemy import sqlalchemy
  2 +from sqlalchemy import Column, Integer, Text, Time, ForeignKey
  3 +from app.models import db
  4 +from authlib.integrations.sqla_oauth2 import (
  5 + OAuth2ClientMixin,
  6 + OAuth2TokenMixin,
  7 + OAuth2AuthorizationCodeMixin
  8 +)
  9 +from sqlalchemy.orm import relationship
  10 +
  11 +
  12 +class User (db.Model):
  13 + '''
  14 + 用户信息表
  15 + '''
  16 + __tablename__ = "dmdms_user"
  17 + id = Column(Integer, primary_key=True)
  18 + username = Column(Text)
  19 + password = Column(Text)
  20 + company = Column(Text)
  21 + position = Column(Text)
  22 + phone = Column(Text)
  23 + email = Column(Text)
  24 + create_time = Column(Time)
  25 + update_time = Column(Time)
  26 + role = Column(Text)
  27 +
  28 + def __str__(self):
  29 + return self.username
  30 +
  31 + def get_user_id(self):
  32 + return self.id
  33 +
  34 +
  35 +class OAuth2Client(db.Model, OAuth2ClientMixin):
  36 + __tablename__ = 'oauth2_client'
  37 +
  38 + id = Column(Integer, primary_key=True)
  39 + user_id = Column(
  40 + Integer, ForeignKey('dmdms_user.id', ondelete='CASCADE'))
  41 + user = relationship('User')
  42 +
  43 +
  44 +class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin):
  45 + __tablename__ = 'oauth2_code'
  46 +
  47 + id = Column(Integer, primary_key=True)
  48 + user_id = Column(
  49 + Integer, ForeignKey('dmdms_user.id', ondelete='CASCADE'))
  50 + user = relationship('User')
  51 +
  52 +
  53 +class OAuth2Token(db.Model, OAuth2TokenMixin):
  54 + __tablename__ = 'oauth2_token'
  55 +
  56 + id = Column(Integer, primary_key=True)
  57 + user_id = Column(
  58 + Integer, ForeignKey('dmdms_user.id', ondelete='CASCADE'))
  59 + user = relationship('User')
... ...
  1 +from authlib.integrations.flask_oauth2 import (
  2 + AuthorizationServer, ResourceProtector)
  3 +from authlib.integrations.sqla_oauth2 import (
  4 + create_query_client_func,
  5 + create_save_token_func,
  6 + create_bearer_token_validator,
  7 +)
  8 +from authlib.oauth2.rfc6749.grants import (
  9 + AuthorizationCodeGrant as _AuthorizationCodeGrant,
  10 +)
  11 +from authlib.oidc.core.grants import (
  12 + OpenIDCode as _OpenIDCode,
  13 + OpenIDImplicitGrant as _OpenIDImplicitGrant,
  14 + OpenIDHybridGrant as _OpenIDHybridGrant,
  15 +)
  16 +from authlib.oidc.core import UserInfo
  17 +from werkzeug.security import gen_salt
  18 +from .models import db, User
  19 +from .models import OAuth2Client, OAuth2AuthorizationCode, OAuth2Token
  20 +
  21 +
  22 +DUMMY_JWT_CONFIG = {
  23 + 'key': 'secret-key',
  24 + 'alg': 'HS256',
  25 + 'iss': 'https://authlib.org',
  26 + 'exp': 7200,
  27 +}
  28 +
  29 +def exists_nonce(nonce, req):
  30 + exists = OAuth2AuthorizationCode.query.filter_by(
  31 + client_id=req.client_id, nonce=nonce
  32 + ).first()
  33 + return bool(exists)
  34 +
  35 +
  36 +def generate_user_info(user, scope):
  37 + return UserInfo(sub=str(user.id), name=user.username)
  38 +
  39 +
  40 +def create_authorization_code(client, grant_user, request):
  41 + code = gen_salt(48)
  42 + nonce = request.data.get('nonce')
  43 + item = OAuth2AuthorizationCode(
  44 + code=code,
  45 + client_id=client.client_id,
  46 + redirect_uri=request.redirect_uri,
  47 + scope=request.scope,
  48 + user_id=grant_user.id,
  49 + nonce=nonce,
  50 + )
  51 + db.session.add(item)
  52 + db.session.commit()
  53 + return code
  54 +
  55 +
  56 +class AuthorizationCodeGrant(_AuthorizationCodeGrant):
  57 + def create_authorization_code(self, client, grant_user, request):
  58 + return create_authorization_code(client, grant_user, request)
  59 +
  60 + def parse_authorization_code(self, code, client):
  61 + item = OAuth2AuthorizationCode.query.filter_by(
  62 + code=code, client_id=client.client_id).first()
  63 + if item and not item.is_expired():
  64 + return item
  65 +
  66 + def delete_authorization_code(self, authorization_code):
  67 + db.session.delete(authorization_code)
  68 + db.session.commit()
  69 +
  70 + def authenticate_user(self, authorization_code):
  71 + return User.query.get(authorization_code.user_id)
  72 +
  73 +
  74 +class OpenIDCode(_OpenIDCode):
  75 + def exists_nonce(self, nonce, request):
  76 + return exists_nonce(nonce, request)
  77 +
  78 + def get_jwt_config(self, grant):
  79 + return DUMMY_JWT_CONFIG
  80 +
  81 + def generate_user_info(self, user, scope):
  82 + return generate_user_info(user, scope)
  83 +
  84 +
  85 +class ImplicitGrant(_OpenIDImplicitGrant):
  86 + def exists_nonce(self, nonce, request):
  87 + return exists_nonce(nonce, request)
  88 +
  89 + def get_jwt_config(self, grant):
  90 + return DUMMY_JWT_CONFIG
  91 +
  92 + def generate_user_info(self, user, scope):
  93 + return generate_user_info(user, scope)
  94 +
  95 +
  96 +class HybridGrant(_OpenIDHybridGrant):
  97 + def create_authorization_code(self, client, grant_user, request):
  98 + return create_authorization_code(client, grant_user, request)
  99 +
  100 + def exists_nonce(self, nonce, request):
  101 + return exists_nonce(nonce, request)
  102 +
  103 + def get_jwt_config(self):
  104 + return DUMMY_JWT_CONFIG
  105 +
  106 + def generate_user_info(self, user, scope):
  107 + return generate_user_info(user, scope)
  108 +
  109 +
  110 +authorization = AuthorizationServer()
  111 +require_oauth = ResourceProtector()
  112 +
  113 +
  114 +def config_oauth(app):
  115 + query_client = create_query_client_func(db.session, OAuth2Client)
  116 + save_token = create_save_token_func(db.session, OAuth2Token)
  117 + authorization.init_app(
  118 + app,
  119 + query_client=query_client,
  120 + save_token=save_token
  121 + )
  122 +
  123 + # support all openid grants
  124 + authorization.register_grant(AuthorizationCodeGrant, [
  125 + OpenIDCode(require_nonce=True),
  126 + ])
  127 + authorization.register_grant(ImplicitGrant)
  128 + authorization.register_grant(HybridGrant)
  129 +
  130 + # protect resource
  131 + bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
  132 + require_oauth.register_token_validator(bearer_cls())
... ...
  1 +<p>{{grant.client.client_name}} is requesting:
  2 +<strong>{{ grant.request.scope }}</strong>
  3 +</p>
  4 +
  5 +<form action="" method="post">
  6 + <label>
  7 + <input type="checkbox" name="confirm">
  8 + <span>Consent?</span>
  9 + </label>
  10 + {% if not user %}
  11 + <p>You haven't logged in. Log in with:</p>
  12 + <div>
  13 + <input type="text" name="username">
  14 + </div>
  15 + {% endif %}
  16 + <br>
  17 + <button>Submit</button>
  18 +</form>
... ...
  1 +<style>
  2 + label, label > span { display: block; }
  3 + label { margin: 15px 0; }
  4 +</style>
  5 +
  6 +<a href="/">Home</a>
  7 +
  8 +<form action="" method="post">
  9 + <label>
  10 + <span>Client Name</span>
  11 + <input type="text" name="client_name">
  12 + </label>
  13 + <label>
  14 + <span>Client URI</span>
  15 + <input type="url" name="client_uri">
  16 + </label>
  17 + <label>
  18 + <span>Allowed Scope</span>
  19 + <input type="text" name="scope">
  20 + </label>
  21 + <label>
  22 + <span>Redirect URIs</span>
  23 + <textarea name="redirect_uri" cols="30" rows="10"></textarea>
  24 + </label>
  25 + <label>
  26 + <span>Allowed Grant Types</span>
  27 + <textarea name="grant_type" cols="30" rows="10"></textarea>
  28 + </label>
  29 + <label>
  30 + <span>Allowed Response Types</span>
  31 + <textarea name="response_type" cols="30" rows="10"></textarea>
  32 + </label>
  33 + <label>
  34 + <span>Token Endpoint Auth Method</span>
  35 + <select name="token_endpoint_auth_method">
  36 + <option value="client_secret_basic">client_secret_basic</option>
  37 + <option value="client_secret_post">client_secret_post</option>
  38 + <option value="none">none</option>
  39 + </select>
  40 + </label>
  41 + <button>Submit</button>
  42 +</form>
... ...
  1 +{% if user %}
  2 + <style>pre{white-space:wrap}</style>
  3 +<div>Logged in as <strong>{{user}}</strong></div>
  4 +
  5 +{% for client in clients %}
  6 +<pre>
  7 +{{ client.client_info|tojson }}
  8 +{{ client.client_metadata|tojson }}
  9 +</pre>
  10 +<hr>
  11 +{% endfor %}
  12 +
  13 +<br><a href="{{ url_for('.create_client') }}">Create Client</a>
  14 +
  15 +{% else %}
  16 +<form action="" method="post">
  17 + <input type="text" name="username" placeholder="账号">
  18 + <input type="text" name="password" placeholder="密码">
  19 + <button type="submit">Login / Signup</button>
  20 +</form>
  21 +{% endif %}
... ...
... ... @@ -3,15 +3,16 @@
3 3 # 程序部署ip:host
4 4 deploy_ip_host = "172.26.99.160:8840"
5 5 # 系统数据库
6   -SQLALCHEMY_DATABASE_URI = "postgresql://postgres:chinadci@172.26.99.160:5432/dmap_dms_test"
  6 +SQLALCHEMY_DATABASE_URI = "postgresql://postgres:postgres@172.26.40.254:5433/dmap_dms_test"
7 7
8 8
9 9 # 部署模式cluster,standalone
10   -deployment_mode="cluster"
  10 +deployment_mode = "cluster"
11 11 # 部署模式味cluster时有用,master,slave
12   -application_name="master"
  12 +application_name = "master"
13 13
14 14 # 固定配置不需要修改
15   -swagger_configure={"title":"DMapManager"}
  15 +swagger_configure = {"title": "DMapManager"}
16 16 entry_data_thread = 3
17   -scan_module = ["app.modules"]# API所在的模块
  17 +scan_module = ["app.modules"] # API所在的模块
  18 +SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/'
... ...
1 1 flask==1.1.2
2 2 SQLAlchemy==1.3.17
3 3 Flask-SQLAlchemy==2.4.3
4   -gevent==20.9.0
  4 +#gevent==20.9.0
5 5 gunicorn==20.0.4
6 6 flask_cors==3.0.8
7 7 flasgger==0.9.5
8   -GDAL==3.2.1
  8 +#GDAL==3.2.1
9 9 psycopg2==2.8.5
10 10 pyDes==2.0.1
11 11 gevent-websocket==0.10.1
12 12 Pillow==8.1.2
13 13 #Rtree==0.9.7
14 14 opencv-python==4.5.1.48
15   -pstuil==5.8.0
16   -mod_wsgi==4.8.0
\ No newline at end of file
  15 +psutil==5.8.0
  16 +#mod_wsgi==4.8.0
  17 +Authlib==0.13
\ No newline at end of file
... ...
... ... @@ -3,5 +3,4 @@ from flask import Flask
3 3 from app import create_app
4 4 app:Flask = create_app()
5 5 if __name__ == '__main__':
6   - app.run(host="0.0.0.0", port="8840", threaded=True, debug=True)
7   -
  6 + app.run(host="0.0.0.0", port="8840", threaded=True, debug=True)
\ No newline at end of file
... ...
注册登录 后发表评论