├── .editorconfig ├── .flake8 ├── .gitignore ├── .style.yapf ├── LICENSE ├── README.md ├── app ├── __init__.py ├── auth │ └── auths.py ├── config.py ├── database.py ├── models │ ├── __init__.py │ └── user.py ├── routers │ ├── __init__.py │ ├── items.py │ └── users.py └── util │ ├── common.py │ ├── dateencoder.py │ └── sms.py ├── deploy ├── gunicorn_fast.service └── test_user.sql ├── gunicorn.py ├── local.py └── run.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | # https://raw.githubusercontent.com/django/django/master/.editorconfig 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | end_of_line = lf 12 | charset = utf-8 13 | 14 | # Docstrings and comments use max_line_length = 79 15 | [*.py] 16 | max_line_length = 119 17 | 18 | # Use 2 spaces for the HTML files 19 | [*.html] 20 | indent_size = 2 21 | 22 | [*.js] 23 | indent_size = 2 24 | 25 | # The JSON files contain newlines inconsistently 26 | [*.json] 27 | indent_size = 2 28 | insert_final_newline = ignore 29 | 30 | [**/admin/js/vendor/**] 31 | indent_style = ignore 32 | indent_size = ignore 33 | 34 | # Minified JavaScript files shouldn't be changed 35 | [**.min.js] 36 | indent_style = ignore 37 | insert_final_newline = ignore 38 | 39 | # Makefiles always use tabs for indentation 40 | [Makefile] 41 | indent_size = 8 42 | indent_style = tab 43 | 44 | # Batch files use tabs for indentation 45 | [*.bat] 46 | indent_style = tab 47 | 48 | [docs/**.txt] 49 | max_line_length = 79 50 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,W292,W391 3 | exclude = 4 | .git, 5 | __pycache__, 6 | build, 7 | dist 8 | max-complexity = 10 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__* 3 | models.py 4 | node_modules/ 5 | dist/ 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Editor directories and files 11 | .idea 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | *.log 17 | *.__pycache__ 18 | *.pyc 19 | .vscode 20 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | # https://github.com/google/yapf?utm_source=tuicool&utm_medium=referral 2 | 3 | [style] 4 | based_on_style = pep8 5 | spaces_before_comment = 4 6 | split_before_logical_operator = true 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 zhiyongma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | **Chinese Documentation**: https://www.cnblogs.com/mazhiyong/ 5 | 6 | **Source Code**: https://github.com/zhiyongma/fastproject 7 | 8 | --- 9 | 10 | 11 | ## Requirements 12 | 13 | Python 3.6+ 14 | 15 | You should install the requirments below first. 16 | 17 |
18 | 19 | ```console 20 | $ pip install fastapi 21 | $ pip install pymysql 22 | $ pip install sqlalchemy 23 | $ pip install pyjwt 24 | $ pip install bcrypt 25 | $ pip install passlib 26 | $ pip install python-multipart 27 | ``` 28 | 29 |
30 | 31 | Then you should create your mysql database. Sample sql in the deploy directory.
32 | You can configure the database connection in config.py. 33 | 34 | You will also need an ASGI server, for production such as Uvicorn or Hypercorn. 35 | 36 |
37 | 38 | ```console 39 | $ pip install uvicorn 40 | ``` 41 | 42 |
43 | 44 | For production deployment, you should also install Gunicorn. 45 |
46 | 47 | ```console 48 | $ pip install gunicorn 49 | ``` 50 | 51 |
52 | 53 | 54 | ## Example 55 | 56 | ### Local Run 57 |
58 | 59 | ```console 60 | $ uvicorn local:app --reload 61 | ``` 62 | 63 |
64 | 65 | ### Server Run 66 | 67 | Run the server with gunicorn. 68 | 69 |
70 | 71 | ```console 72 | $ gunicorn -c /data/fastest/gunicorn.py -e FASTAPI_ENV=production run:app 73 | ``` 74 | 75 | * `-c`: gunicorn config. 76 | * `-e`: environment parameter. 77 | 78 |
79 | 80 | Run the server with service.
81 | See the file gunicorn_fast.service in deploy directory. You should put it in /usr/lib/systemd/system for linux os.
82 |
83 | 84 | ```console 85 | $ systemctl start/stop/restart/enable gunicorn_fast.service 86 | ``` 87 | 88 |
89 | 90 | 91 | ### Production config 92 | * `-e`: `FASTAPI_ENV=production`. 93 | * `mysql`: configure file `config.py` . 94 | * `gunicorn`: configure file `gunicorn.py` . 95 | * `service`: configure file `gunicorn_fast.service` . 96 | 97 | remarks: the `pidfile` in `gunicorn_fast.service` and `gunicorn.py` should point to the same one file. 98 | 99 | ### API test 100 | ##### `1、user register` 101 |

102 | FastAPI 103 |

104 | 105 | ##### `2、user login` 106 |

107 | FastAPI 108 |

109 | 110 | ##### `3、user info` 111 |

112 | FastAPI 113 |

114 | 115 | 116 | ## License 117 | 118 | This project is licensed under the terms of the MIT license. 119 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright(C) 2018-2019 TEST 4 | All rights reserved 5 | 6 | File : __init__.py 7 | Time : 2020/07/27 15:04:35 8 | Author : mazhiyong 9 | Version : 1.0 10 | """ 11 | 12 | import time 13 | import logging 14 | from fastapi import Depends, FastAPI, Header, HTTPException, Request 15 | from .routers import items, users 16 | from app.auth.auths import Auth 17 | from fastapi.responses import JSONResponse 18 | 19 | 20 | def create_app(): 21 | """Create and configure an instance of the Flask application.""" 22 | app = FastAPI() 23 | 24 | # create logger 25 | logger = logging.getLogger('fastapi') 26 | 27 | async def get_token_header(x_token: str = Header(...)): 28 | if x_token != "fake-super-secret-token": 29 | raise HTTPException(status_code=400, detail="X-Token header invalid") 30 | 31 | app.include_router(users.router) 32 | app.include_router( 33 | items.router, 34 | prefix="/items", 35 | tags=["items"], 36 | dependencies=[Depends(get_token_header)], 37 | responses={404: {"description": "Not found"}}, 38 | ) 39 | 40 | @app.middleware("http") 41 | async def process_authorization(request: Request, call_next): 42 | """ 43 | 在这个函数里统一对访问做权限token校验。 44 | 1、如果是用户注册、登陆,那么不做token校验,由路径操作函数具体验证 45 | 2、如果是其他操作,则需要从header或者cookie中取出token信息,解析出内容 46 | 然后对用户身份进行验证,如果用户不存在则直接返回 47 | 如果用户存在则将用户信息附加到request中,这样在后续的路径操作函数中可以直接使用。 48 | """ 49 | start_time = time.time() 50 | 51 | # print(request.url) 52 | # print(request.url.path) 53 | 54 | if request.url.path == '/login' or request.url.path == '/register': 55 | logger.info('no jwt verify.') 56 | else: 57 | logger.info('jwt verify.') 58 | 59 | result = Auth.identifyAll(request) 60 | if result['status'] and result['data']: 61 | user = result['data']['user'] 62 | 63 | logger.info('jwt verify success. user: %s ' % user.username) 64 | 65 | # state中记录用户基本信息 66 | request.state.user = user 67 | else: 68 | return JSONResponse(content=result) 69 | 70 | response = await call_next(request) 71 | 72 | process_time = time.time() - start_time 73 | response.headers["X-Process-Time"] = str(process_time) 74 | return response 75 | 76 | return app 77 | -------------------------------------------------------------------------------- /app/auth/auths.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright (C) 2018 PPCMS 4 | All rights reserved 5 | 6 | Filename : auths.py 7 | Description : auths.py 8 | 9 | Created by mazhiyong at 2018-11-01 14:49:36 10 | 11 | 这里采用了JWT的方式进行身份鉴权。 12 | 随着前后端分离的趋势,基于Token的认证方式成为主流,而JWT是基于Token认证方式的一种机制,是实现单点登录认证的一种有效方法。 13 | 14 | 基本校验流程: 15 | 1、用户注册账号。 16 | 2、用户登陆,成功后返回JWT token,数据库中记录token中对应的登陆时间。 17 | 3、用户发起业务请求,header传值字段名为“Authorization”的校验字段,字段值以“JWT”开头,并与token空格隔开。 18 | 4、服务端解析token发起校验,如果解析正确且与数据库中保存的登陆时间一致则认为校验通过 19 | 20 | 注意: 21 | 目前并没有对token的时效性进行校验,不排除日后进行处理。 22 | """ 23 | 24 | import logging 25 | import datetime 26 | import jwt 27 | import time 28 | import os 29 | import re 30 | from app.models.user import DBUser 31 | from app import config 32 | from app.util import common 33 | from app.config import get_config 34 | from sqlalchemy.orm import Session 35 | from typing import Optional 36 | from fastapi import HTTPException, status, Response, Depends 37 | from passlib.context import CryptContext 38 | from app.database import get_db_local 39 | 40 | 41 | # create logger 42 | logger = logging.getLogger('fastapi') 43 | 44 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 45 | 46 | 47 | class Auth(): 48 | @staticmethod 49 | def encode_auth_token(origin_data, exp): 50 | """ 51 | 生成认证Token 52 | :param user_id: int 53 | :param login_time: int(timestamp) 54 | :return: string 55 | """ 56 | try: 57 | payload = { 58 | 'exp': exp if exp else datetime.datetime.now() + datetime.timedelta(days=7), 59 | 'iat': datetime.datetime.now(), 60 | 'iss': 'paperpie', 61 | 'data': origin_data 62 | } 63 | 64 | # logger.debug('jwt encode secret_key %s', secret_key) 65 | 66 | encoded_jwt = jwt.encode( 67 | payload, 68 | get_config().SECRET_KEY, 69 | get_config().ALGORITHM 70 | ) 71 | 72 | return encoded_jwt 73 | except Exception as e: 74 | return e 75 | 76 | @staticmethod 77 | def decode_auth_token(auth_token): 78 | """ 79 | 验证Token 80 | :param auth_token: 81 | :return: integer|string 82 | """ 83 | try: 84 | secret_key = get_config().SECRET_KEY 85 | algorithm = get_config().ALGORITHM 86 | 87 | payload = jwt.decode(auth_token, secret_key, algorithms=[algorithm], options={'verify_exp': True}) 88 | if ('data' in payload): 89 | return payload 90 | else: 91 | return '无效Token' 92 | # raise jwt.InvalidTokenError 93 | except jwt.ExpiredSignatureError: 94 | return 'Token过期' 95 | except jwt.InvalidTokenError: 96 | return '无效Token' 97 | 98 | # 校验密码 99 | @staticmethod 100 | def verify_password(plain_password, hashed_password): 101 | return pwd_context.verify(plain_password, hashed_password) 102 | 103 | # 密码哈希 104 | @staticmethod 105 | def get_password_hash(password): 106 | return pwd_context.hash(password) 107 | 108 | # 用户信息校验:username和password分别校验 109 | @staticmethod 110 | def authenticate_user(username: str, password: str, db: Session): 111 | user = DBUser.get_by_username(db, username) 112 | if not user: 113 | return False 114 | if not Auth.verify_password(password, user.password): 115 | return False 116 | return user 117 | 118 | @staticmethod 119 | def login_authenticate(username: str, password: str, db: Session): 120 | """ 121 | 用户登录,登录成功返回token,将登录时间写入数据库;登录失败返回失败原因 122 | """ 123 | result = {} 124 | 125 | # 首先校验用户信息 126 | user = Auth.authenticate_user(username, password, db) 127 | if not user: 128 | # close db 129 | db.close() 130 | return common.falseReturn(result, '登录失败') 131 | 132 | # 登陆时间 133 | login_time = int(time.time()) 134 | user.login_time = login_time 135 | DBUser.update_login_time(db, user.id, login_time) 136 | 137 | origin_data = { 138 | 'user_id': user.id, 139 | 'login_time': login_time 140 | } 141 | 142 | access_token = Auth.encode_auth_token(origin_data, None).decode() 143 | bearer_token = 'Bearer ' + access_token 144 | 145 | result['user_id'] = user.id 146 | result['username'] = user.username 147 | result['access_token'] = access_token 148 | result['token_type'] = "bearer" 149 | 150 | print(result) 151 | 152 | rsp = common.trueReturn(result, '登录成功') 153 | rsp.set_cookie(key="Bearer", value=bearer_token) 154 | return rsp 155 | 156 | 157 | @staticmethod 158 | def identifyAll(request): 159 | """ 160 | 用户鉴权 161 | :return: list 162 | """ 163 | auth_header = request.headers.get('Authorization') 164 | logger.info('auth_header %s', auth_header) 165 | 166 | jwt_cookie = request.cookies.get('Bearer') 167 | logger.info('jwt_cookie %s', jwt_cookie) 168 | 169 | if (auth_header or jwt_cookie): 170 | auth_tokenArr = '' 171 | 172 | if(auth_header): 173 | auth_tokenArr = auth_header.split(" ") 174 | # print('auth token from auth_header. ', auth_header) 175 | else: 176 | auth_tokenArr = jwt_cookie.split(" ") 177 | # print('auth token from jwt_cookie. ', jwt_cookie) 178 | 179 | if (not auth_tokenArr or auth_tokenArr[0] != 'Bearer' or len(auth_tokenArr) != 2): 180 | result = common.falseContent('', '请传递正确的验证头信息') 181 | else: 182 | auth_token = auth_tokenArr[1] 183 | payload = Auth.decode_auth_token(auth_token) 184 | if not isinstance(payload, str): 185 | user_id = payload['data']['user_id'] 186 | login_time = payload['data']['login_time'] 187 | 188 | # get db 189 | db = get_db_local() 190 | 191 | user = DBUser.get_by_user_id(db, user_id) 192 | if (user is None): 193 | result = common.falseContent('', '找不到该用户信息') 194 | else: 195 | if (user.login_time == login_time): 196 | returnUser = { 197 | 'user': user 198 | } 199 | result = common.trueContent(returnUser, '请求成功') 200 | else: 201 | result = common.falseContent('', 'Token已更改,请重新登录获取') 202 | 203 | # close db 204 | db.close() 205 | else: 206 | result = common.falseContent('', payload) 207 | else: 208 | result = common.falseContent('', '没有提供认证token') 209 | 210 | return result 211 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright(C) 2018 PPCMS 4 | All rights reserved 5 | 6 | Filename: config.py 7 | Description: config.py 8 | 9 | Created by mazhiyong at 2018-11-01 14:41:55 10 | """ 11 | 12 | import os 13 | 14 | 15 | class Config: 16 | SITE_NAME = u'PPCMS' 17 | 18 | # Consider SQLALCHEMY_COMMIT_ON_TEARDOWN harmful 19 | # SQLALCHEMY_COMMIT_ON_TEARDOWN = True 20 | 21 | SQLALCHEMY_POOL_RECYCLE = 10 22 | SQLALCHEMY_TRACK_MODIFICATIONS = True 23 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 24 | 25 | # to get a string like this run: 26 | # openssl rand -hex 32 27 | SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" 28 | ALGORITHM = "HS256" 29 | 30 | # SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') 31 | 32 | 33 | class DevelopmentConfig(Config): 34 | DEBUG = True 35 | 36 | SQLALCHEMY_ECHO = False 37 | 38 | MYSQL_USER = 'pony' 39 | MYSQL_PASS = '' 40 | MYSQL_HOST = 'rm-2zee5e5ytvd02o9e9no.mysql.rds.aliyuncs.com' 41 | MYSQL_PORT = '3306' 42 | MYSQL_DB = 'bookcrawl' 43 | 44 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://%s:%s@%s:%s/%s' % (MYSQL_USER, MYSQL_PASS, MYSQL_HOST, MYSQL_PORT, MYSQL_DB) 45 | 46 | 47 | class ProductionConfig(Config): 48 | DEBUG = True 49 | 50 | # mysql configuration 51 | MYSQL_USER = '' 52 | MYSQL_PASS = '' 53 | MYSQL_HOST = '' 54 | MYSQL_PORT = '3306' 55 | MYSQL_DB = '' 56 | 57 | if (len(MYSQL_USER) > 0 and len(MYSQL_PASS) > 0 and len(MYSQL_HOST) > 0 and len(MYSQL_PORT) > 0 and len(MYSQL_DB) > 0): 58 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://%s:%s@%s:%s/%s' % (MYSQL_USER, MYSQL_PASS, MYSQL_HOST, MYSQL_PORT, MYSQL_DB) 59 | 60 | 61 | config = { 62 | 'default': DevelopmentConfig, 63 | 'development': DevelopmentConfig, 64 | 'production': ProductionConfig, 65 | } 66 | 67 | 68 | def get_config(): 69 | config_name = os.getenv('FASTAPI_ENV') or 'default' 70 | return config[config_name] 71 | -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright(C) 2018-2019 TEST 4 | All rights reserved 5 | 6 | File : database.py 7 | Time : 2020/08/01 20:21:48 8 | Author : mazhiyong 9 | Version : 1.0 10 | """ 11 | 12 | from sqlalchemy import create_engine 13 | from sqlalchemy.orm import sessionmaker 14 | from sqlalchemy.ext.declarative import declarative_base 15 | 16 | from app.config import get_config 17 | 18 | 19 | # 创建对象的基类: 20 | Base = declarative_base() 21 | 22 | # 初始化数据库连接: 23 | engine = create_engine(get_config().SQLALCHEMY_DATABASE_URI) 24 | 25 | # SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 26 | SessionLocal = sessionmaker(bind=engine) 27 | 28 | 29 | # db Dependency 30 | def get_db(): 31 | db = SessionLocal() 32 | try: 33 | yield db 34 | finally: 35 | db.close() 36 | 37 | 38 | def get_db_local(): 39 | return SessionLocal() 40 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhiyongma/fastproject/1dd2edef68a109d927663143707881dfaad78d64/app/models/__init__.py -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | ## -*- encoding: utf-8 -*- 2 | """ 3 | Copyright(C) 2018-2019 TEST 4 | All rights reserved 5 | 6 | File : user 7 | Time : 2020/07/02 16:45:40 8 | Author : mazhiyong 9 | Version : 1.0 10 | """ 11 | 12 | from sqlalchemy import Column, DateTime, String, text 13 | from sqlalchemy.dialects.mysql import INTEGER, VARCHAR 14 | from sqlalchemy.orm import Session 15 | from app.database import Base 16 | from pydantic import BaseModel 17 | from typing import Optional 18 | 19 | 20 | # Pydantic model 21 | class User(BaseModel): 22 | id: Optional[int] = None 23 | username: str 24 | sex: Optional[str] = None 25 | login_time: Optional[int] = None 26 | 27 | class Config: 28 | orm_mode = True 29 | 30 | 31 | # DB model 32 | class DBUser(Base): 33 | __tablename__ = 'test_user' 34 | 35 | id = Column(INTEGER(64), primary_key=True, comment='编号') 36 | username = Column(String(100)) 37 | password = Column(String(100)) 38 | sex = Column(VARCHAR(10), server_default=text("''"), comment='性别') 39 | login_time = Column(INTEGER(11), server_default=text("'0'"), comment='登陆时间,主要为了登陆JWT校验使用') 40 | create_date = Column(DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP")) 41 | update_date = Column(DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")) 42 | 43 | @classmethod 44 | def add(cls, db: Session, data): 45 | db.add(data) 46 | db.commit() 47 | # db.refresh(data) 48 | 49 | @classmethod 50 | def get_by_user_id(cls, db: Session, user_id): 51 | data = db.query(cls).filter_by(id=user_id).first() 52 | 53 | return data 54 | 55 | @classmethod 56 | def get_by_username(cls, db: Session, username): 57 | data = db.query(cls).filter_by(username=username).first() 58 | 59 | return data 60 | 61 | @classmethod 62 | def update(cls, db: Session, username, sex): 63 | db.query(cls).filter_by(username=username).update({cls.sex: sex}) 64 | 65 | db.commit() 66 | 67 | @classmethod 68 | def update_login_time(cls, db: Session, user_id, login_time): 69 | db.query(cls).filter_by(id=user_id).update({cls.login_time: login_time}) 70 | 71 | db.commit() 72 | -------------------------------------------------------------------------------- /app/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhiyongma/fastproject/1dd2edef68a109d927663143707881dfaad78d64/app/routers/__init__.py -------------------------------------------------------------------------------- /app/routers/items.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright(C) 2018-2019 TEST 4 | All rights reserved 5 | 6 | File : items.py 7 | Time : 2020/07/27 14:32:51 8 | Author : mazhiyong 9 | Version : 1.0 10 | """ 11 | 12 | from fastapi import APIRouter, HTTPException 13 | 14 | router = APIRouter() 15 | 16 | 17 | @router.get("/") 18 | async def read_items(): 19 | return [{"name": "Item Foo"}, {"name": "item Bar"}] 20 | 21 | 22 | @router.get("/{item_id}") 23 | async def read_item(item_id: str): 24 | return {"name": "Fake Specific Item", "item_id": item_id} 25 | 26 | 27 | @router.put( 28 | "/{item_id}", 29 | tags=["custom"], 30 | responses={403: {"description": "Operation forbidden"}}, 31 | ) 32 | async def update_item(item_id: str): 33 | if item_id != "foo": 34 | raise HTTPException(status_code=403, detail="You can only update the item: foo") 35 | return {"item_id": item_id, "name": "The Fighters"} -------------------------------------------------------------------------------- /app/routers/users.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright(C) 2018-2019 TEST 4 | All rights reserved 5 | 6 | File : users.py 7 | Time : 2020/07/27 14:32:44 8 | Author : mazhiyong 9 | Version : 1.0 10 | """ 11 | 12 | from fastapi import APIRouter, Depends, Request 13 | from fastapi.security import OAuth2PasswordRequestForm 14 | from sqlalchemy.orm import Session 15 | from app.database import get_db 16 | from app.auth.auths import Auth 17 | from app.models.user import User, DBUser 18 | 19 | router = APIRouter() 20 | 21 | 22 | @router.post("/register", response_model=User) 23 | async def register(request: Request, form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): 24 | # 密码加密 25 | password = Auth.get_password_hash(form_data.password) 26 | 27 | db_user = DBUser.get_by_username(db, form_data.username) 28 | if db_user: 29 | return db_user 30 | 31 | db_user = DBUser(username=form_data.username, password=password) 32 | DBUser.add(db, db_user) 33 | 34 | request.session['test'] = "test" 35 | 36 | return db_user 37 | 38 | 39 | @router.post("/login") 40 | async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): 41 | return Auth.login_authenticate(form_data.username, form_data.password, db) 42 | 43 | 44 | @router.post("/users/me/", response_model=User) 45 | async def read_users_me(request: Request): 46 | user = request.state.user 47 | return user 48 | -------------------------------------------------------------------------------- /app/util/common.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright(C) 2018-2019 TEST 4 | All rights reserved 5 | 6 | File : common.py 7 | Time : 2020/08/03 11:46:23 8 | Author : mazhiyong 9 | Version : 1.0 10 | """ 11 | 12 | from fastapi.responses import JSONResponse 13 | 14 | 15 | def trueReturn(data, msg): 16 | """ 操作成功结果 """ 17 | result = { 18 | "status": True, 19 | "data": data, 20 | "msg": msg 21 | } 22 | return JSONResponse(content=result) 23 | 24 | 25 | def falseReturn(data, msg): 26 | """ 操作成功结果 """ 27 | result = { 28 | "status": False, 29 | "data": data, 30 | "msg": msg 31 | } 32 | return JSONResponse(content=result) 33 | 34 | 35 | def trueContent(data, msg): 36 | """ 操作成功结果 """ 37 | result = { 38 | "status": True, 39 | "data": data, 40 | "msg": msg 41 | } 42 | return result 43 | 44 | 45 | def falseContent(data, msg): 46 | """ 操作成功结果 """ 47 | result = { 48 | "status": False, 49 | "data": data, 50 | "msg": msg 51 | } 52 | return result 53 | -------------------------------------------------------------------------------- /app/util/dateencoder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright (C) 2018 PPCMS 4 | All rights reserved 5 | 6 | Filename : dateencoder.py 7 | Description : 处理datetime类型数据json转化异常问题. 8 | ex:datetime.datetime is not JSON serializable 9 | 10 | Created by mazhiyong at 2018-11-01 14:42:26 11 | """ 12 | 13 | import datetime 14 | import json 15 | 16 | 17 | class DateEncoder(json.JSONEncoder): 18 | def default(self, obj): 19 | if isinstance(obj, datetime.datetime): 20 | print(obj.strftime('%Y-%m-%d %H:%M:%S')) 21 | return obj.strftime('%Y-%m-%d %H:%M:%S') 22 | else: 23 | return json.JSONEncoder.default(self, obj) 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/util/sms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # Copyright (c) 2014 The CCP project authors. All Rights Reserved. 3 | # 4 | # Use of this source code is governed by a Beijing Speedtong Information Technology Co.,Ltd license 5 | # that can be found in the LICENSE file in the root of the web site. 6 | # 7 | # http://www.yuntongxun.com 8 | # 9 | # An additional intellectual property rights grant can be found 10 | # in the file PATENTS. All contributing project authors may 11 | # be found in the AUTHORS file in the root of the source tree. 12 | 13 | import hashlib 14 | import base64 15 | import datetime 16 | import urllib.request 17 | import json 18 | 19 | 20 | class SMS: 21 | STATUS_SUCCESS = '000000' 22 | 23 | AccountSid = '' 24 | AccountToken = '' 25 | AppId = '' 26 | templateid = '' 27 | 28 | ServerIP = 'app.cloopen.com' 29 | ServerPort = '8883' 30 | SoftVersion = '2013-12-26' 31 | Iflog = True # 是否打印日志 32 | Batch = '' # 时间戳 33 | BodyType = 'xml' # 包体格式,可填值:json 、xml 34 | 35 | def log(self, url, body, data): 36 | print('这是请求的URL:') 37 | print(url) 38 | print('这是请求包体:') 39 | print(body) 40 | print('这是响应包体:') 41 | print(data) 42 | print('********************************') 43 | 44 | # 发送模板短信 45 | # @param to 必选参数 短信接收彿手机号码集合,用英文逗号分开 46 | # @param datas 可选参数 内容数据 47 | # @param tempId 必选参数 模板Id 48 | def sendTemplateSMS(self, to, datas): 49 | nowdate = datetime.datetime.now() 50 | self.Batch = nowdate.strftime("%Y%m%d%H%M%S") 51 | 52 | # 生成sig 53 | signature = self.AccountSid + self.AccountToken + self.Batch 54 | sig = hashlib.md5(signature.encode()).hexdigest().upper() 55 | 56 | # 拼接URL 57 | url = "https://" + self.ServerIP + ":" + self.ServerPort + "/" + self.SoftVersion\ 58 | + "/Accounts/" + self.AccountSid + "/SMS/TemplateSMS?sig=" + sig 59 | print(url) 60 | 61 | # 生成auth 62 | src = self.AccountSid + ":" + self.Batch 63 | auth = base64.encodestring(src.encode()).strip() 64 | req = urllib.request.Request(url) 65 | self.setHttpHeader(req) 66 | req.add_header("Authorization", auth) 67 | 68 | # 创建包体 69 | b = '' 70 | for a in datas: 71 | b += '%s' % (a) 72 | 73 | body = '' + b + '%s%s%s\ 74 | \ 75 | ' % (to, self.templateid, self.AppId) 76 | 77 | if self.BodyType == 'json': 78 | # if this model is Json ..then do next code 79 | b = '[' 80 | for a in datas: 81 | b += '"%s",' % (a) 82 | b += ']' 83 | body = '''{"to": "%s", "datas": %s, "templateId": "%s", "appId": "%s"}''' % ( 84 | to, b, self.templateid, self.AppId) 85 | 86 | # req.add_data(body) 87 | 88 | data = '' 89 | try: 90 | res = urllib.request.urlopen(req, data=body.encode()) 91 | data = res.read() 92 | res.close() 93 | 94 | if self.BodyType == 'json': 95 | # json格式 96 | locations = json.loads(data) 97 | else: 98 | # xml格式 99 | xtj = xmltojson() 100 | locations = xtj.main(data) 101 | if self.Iflog: 102 | self.log(url, body, data) 103 | except Exception as error: 104 | print('Exception------------------------') 105 | print(error) 106 | if self.Iflog: 107 | self.log(url, body, data) 108 | locations = {'172001': '网络错误'} 109 | 110 | # parse result 111 | return locations['statusCode'] # '000000' 为成功,其他为失败 112 | 113 | # 设置包头 114 | def setHttpHeader(self, req): 115 | if self.BodyType == 'json': 116 | req.add_header("Accept", "application/json") 117 | req.add_header("Content-Type", "application/json;charset=utf-8") 118 | 119 | else: 120 | req.add_header("Accept", "application/xml") 121 | req.add_header("Content-Type", "application/xml;charset=utf-8") 122 | -------------------------------------------------------------------------------- /deploy/gunicorn_fast.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Gunicorn fast 3 | After=syslog.target network.target remote-fs.target nss-lookup.target 4 | 5 | [Service] 6 | Type=forking 7 | 8 | PIDFile=/data/log/fast.pid 9 | 10 | ExecStart=/root/.virtualenvs/ppcms/bin/gunicorn -c /data/fastest/gunicorn.py -e FASTAPI_ENV=production run:app 11 | ExecReload=/bin/kill -s HUP $MAINPID 12 | ExecStop=/bin/kill -s QUIT $MAINPID 13 | PrivateTmp=true 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | 18 | -------------------------------------------------------------------------------- /deploy/test_user.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat MySQL Data Transfer 3 | 4 | Source Server : pony 5 | Source Server Type : MySQL 6 | Source Server Version : 80018 7 | Source Host : rm-2zee5e5ytvd02o9e9no.mysql.rds.aliyuncs.com 8 | Source Database : bookcrawl 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 80018 12 | File Encoding : utf-8 13 | 14 | Date: 08/05/2020 10:15:21 AM 15 | */ 16 | 17 | SET NAMES utf8; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for `test_user` 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `test_user`; 24 | CREATE TABLE `test_user` ( 25 | `id` int(64) NOT NULL AUTO_INCREMENT COMMENT '编号', 26 | `username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 27 | `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 28 | `sex` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '性别', 29 | `login_time` int(11) DEFAULT '0' COMMENT '登陆时间,主要为了登陆JWT校验使用', 30 | `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 31 | `update_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 32 | PRIMARY KEY (`id`,`username`) 33 | ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='用户表'; 34 | 35 | SET FOREIGN_KEY_CHECKS = 1; 36 | -------------------------------------------------------------------------------- /gunicorn.py: -------------------------------------------------------------------------------- 1 | # gunicorn.py 2 | 3 | debug = True 4 | daemon = True 5 | 6 | bind = '0.0.0.0:9000' # 绑定ip和端口号 7 | # backlog = 512 # 监听队列 8 | chdir = '/data/fastest' # gunicorn要切换到的目的工作目录 9 | timeout = 30 # 超时 10 | # worker_class = 'gevent' #使用gevent模式,还可以使用sync 模式,默认的是sync模式 11 | work_class = 'uvicorn.workers.UvicornWorker' 12 | 13 | # workers = multiprocessing.cpu_count() * 2 + 1 # 进程数 14 | # threads = 2 #指定每个进程开启的线程数 15 | loglevel = 'debug' # 日志级别,这个日志级别指的是错误日志的级别,而访问日志的级别无法设置 16 | access_log_format = '%(t)s %(p)s %(h)s "%(r)s" %(s)s %(L)s %(b)s %(f)s" "%(a)s"' # 设置gunicorn访问日志格式,错误日志无法设置 17 | 18 | """ 19 | 其每个选项的含义如下: 20 | h remote address 21 | l '-' 22 | u currently '-', may be user name in future releases 23 | t date of the request 24 | r status line (e.g. ``GET / HTTP/1.1``) 25 | s status 26 | b response length or '-' 27 | f referer 28 | a user agent 29 | T request time in seconds 30 | D request time in microseconds 31 | L request time in decimal seconds 32 | p process ID 33 | """ 34 | 35 | pidfile = "/data/log/fast.pid" 36 | accesslog = "/data/log/gunicorn_fasttest_access.log" # 访问日志文件 37 | errorlog = "/data/log/gunicorn_fasttest_error.log" # 错误日志文件 38 | -------------------------------------------------------------------------------- /local.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright(C) 2018-2019 PAPERPIE 4 | All rights reserved 5 | 6 | File : run.py 7 | Time : 2019/03/20 16:21:11 8 | Author : mazhiyong 9 | Version : 1.0 10 | """ 11 | 12 | from app import create_app 13 | 14 | app = create_app() 15 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright(C) 2018-2019 PAPERPIE 4 | All rights reserved 5 | 6 | File : run.py 7 | Time : 2019/03/20 16:21:11 8 | Author : mazhiyong 9 | Version : 1.0 10 | """ 11 | 12 | from app import create_app 13 | import logging 14 | from fastapi.logger import logger as fastapi_logger 15 | from logging.handlers import RotatingFileHandler 16 | 17 | app = create_app() 18 | 19 | 20 | # 将日志保存到文件中 21 | formatter = logging.Formatter( 22 | "[%(asctime)s.%(msecs)03d] %(levelname)s [%(thread)d] - %(message)s", "%Y-%m-%d %H:%M:%S") 23 | handler = RotatingFileHandler('/data/log/fastapi.log', backupCount=0) 24 | logging.getLogger().setLevel(logging.NOTSET) 25 | fastapi_logger.addHandler(handler) 26 | handler.setFormatter(formatter) 27 | 28 | fastapi_logger.info('****************** Starting Server *****************') 29 | --------------------------------------------------------------------------------