• flask 之(七) --- 认证|文件|部署


    登陆注册

    说明:

      令牌Token认证,在对HTTP形式的API发请求时,大部分情况我们不是通过用户名密码做验证,而是通过一个令牌[Token来做验证]。

      RESTful API无法使用Flask-Login扩展来实现用户认证。因为其没有客户端,通过postman请求,无法设置cookie和session 

      RESTful API不保存状态,无法依赖Cookie及Session来保存用户信息。需要使用Flask-HTTPAuth扩展,完成RESTful API的用户认证工作

      Flask-HTTPAuth提供了几种不同的Auth方法,比如:HTTPBasicAuth、HTTPTokenAuth、MultiAuth、HTTPDigestAuth。 

      此时,我们就要使用Flask-HTTPAuth扩展中的HTTPTokenAuth对象中提供

      ”login_required”装饰器来认证视图函数、”error_handler”装饰器来处理错误、”verify_token”装饰器来验证令牌。

    认证

      两种方式:一种:包含加密过的用户数据 ;二种:不包含加密的用户数据

      1、使用flask的session,但没有使用flask-session扩展,session数据无法存储在服务器上,

        所以session的数据会被加密后保存到浏览器的cookies中。

       此种方式相当于 token 中包含加密过的用户数据 的情况。

        session['uid'] = 123

        {'uid': 123}

        sadufijklf260398riowehqklasdfnlk;d8rif2309wqeopsk

      2、使用flask-session在服务器上存储session数据,session数据不需要存储在客户端中,

        但是需要在浏览器的cookies 中存储一个session_id 的值,来标记当前请求对用的session是谁。

        session_id = 875456786sdagadh

        此种方式相当于 token 中不包含用户数据

    流程:

      1、用户登录,登录成功后,服务器为此用户生成一个 token(包含代表用户身份信息的加密字符串)

        {'uid': 123} 加密成 asdfjpoqwiefojklsd09823uiowejnfy8ij239-0eopifhdv789uiohjrefd8u9ioj

        此加密字符串中也包含 时间信息,用于 token 的过期验证

      2、在登录请求的响应中,向客户端返回 上一步 生成的 token

      3、客户端再次请求服务器时,会在 请求头 中携带 token

      4、服务器从请求头中获取 token,进行解密工作,如果解密失败:

        1、不是服务器加密的数据,会解密失败

        2、超过了有效期

      5、如果解密成功,则从 token 数据中获得用户的身份信息,如:uid,可以通过 uid 获得当前登录用户的用户对象

    加密解密:

      itsdangerous库提供了对信息加签名(Signature)的功能,我们可以通过它来生成并验证令牌。Flask 默认已经安装。

     1 from flask import Flask, g
     2 from flask_httpauth import HTTPTokenAuth
     3 from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
     4 from itsdangerous import BadSignature, SignatureExpired
     5 
     6 auth = HTTPTokenAuth()
     7 
     8 app = Flask(__name__)
     9 app.config['SECRET_KEY'] = '110'
    10 # 实例化了一个针对JSON的签名序列化对象token_serializer。它是有时效性的,60分钟后序列化后的签名即会失效,就无法解密
    11 token_serializer = Serializer(app.config['SECRET_KEY'], expires_in=3600)    # 参数:key ; 过期时间(秒)
    12 
    13 users = ['jack', 'tom']
    14 for user in users:
    15     # .dump(加密信息)对用户信息进行加密,然后生成token;.loads(要解密的加密值)对数据解密
    16     token = token_serializer.dumps({'username': user}).decode('utf-8')
    17     print('*** token for {}: {}
    '.format(user, token))

      

    安装:

    •  pip install flask-httpauth
    • 为了简化,我们将Token与用户的关系保存在一个字典中:

    使用:

     1 import datetime
     2 from flask import current_app
     3 from flask_sqlalchemy import SQLAlchemy
     4 from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
     5 
     6 db = SQLAlchemy()
     7 
     8 # 频道 1
     9 class Channel(db.Model):
    10     __tablename__ = "channels"
    11 
    12     id = db.Column(db.Integer, primary_key=True)
    13     name = db.Column(db.String(16), unique=True, nullable=False)
    14     sort = db.Column(db.Integer, nullable=False)
    15     articles = db.relationship('Article', backref='channel', lazy='dynamic')
    16 
    17 # 文章 N
    18 class Article(db.Model):
    19     __tablename__ = "articles"
    20 
    21     id = db.Column(db.Integer, primary_key=True)
    22     created_at = db.Column(db.DateTime, default=datetime.datetime.now())
    23     updated_at = db.Column(db.DateTime, default=datetime.datetime.now(), onupdate=datetime.datetime.now())
    24     title = db.Column(db.String(256), nullable=False)
    25     content = db.Column(db.String(5000), nullable=False)
    26 
    27     channel_id = db.Column(db.Integer, db.ForeignKey("channels.id"))
    28     author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    29 
    30 # 用户 1
    31 class User(db.Model):
    32     __tablename__ = 'users'
    33 
    34     id = db.Column(db.Integer, primary_key=True)
    35     username = db.Column(db.String(32), unique=True, nullable=False)
    36     password = db.Column(db.String(256), nullable=False)
    37     articles = db.relationship('Article', backref='author', lazy='dynamic')
    38 
    39     # 通过类方法,生成一个加密数据赋值给token变量,进行加密。
    40     def generate_auth_token(self, expires_in=3600):
    41         # Serializer(key验证值密钥,过期时间)  设置key密钥和expires_in过期时间实例化Serializer。
    42         serializer = Serializer(current_app.config['SECRET_KEY'], expires_in=expires_in)
    43         # 通过Serializer实例化的serializer对象 把当前用户的id用序列化器进行了加密,格式为urf-8。
    44         token = serializer.dumps({'user_id': self.id}).decode('utf-8')
    45         return token
    46 
    47     @classmethod
    48     def check_auth_token(cls, token):
    49         serializer = Serializer(current_app.config['SECRET_KEY'])
    50         try:
    51             data = serializer.loads(token)  # 从token中,解密数据
    52         except:
    53             return None
    54         if 'user_id' in data:
    55             return cls.query.get(data['user_id'])  # User.query.get()
    56         else:
    57             return None
    模型
     1 # 在 __init__.py 文件中
     2 
     3 from flask import Flask
     4 from flask_restful import Api
     5 from app import views, ext
     6 from app.apis import ChannelList, ChannelDetail, ArticleList, ArticleDetail, UserRegister, UserLogin
     7 from app.views import restful_bp
     8 
     9 
    10 def create_app():
    11     app = Flask(__name__)
    12     # session数据加密:from itsdangerous import TimedJSONWebSignatureSerializer
    13     # TimedJSONWebSignatureSerializer 数据加密
    14     # generate_password_hash,check_password_hash都会依赖app中的secret_key
    15     app.config['SECRET_KEY'] = '110'
    16 
    17     ext.init_db(app)
    18     ext.init_migrate(app)
    19 
    20     # 注册实例化api扩展。prefix前缀名
    21     api = Api(app, prefix='/api')
    22     # 注册频道路由
    23     api.add_resource(ChannelList, '/ChannelLists', endpoint='ChannelLists')
    24     api.add_resource(ChannelDetail, '/ChannelDetails/<int:id>', endpoint='ChannelDetails')
    25     # 注册文章路由
    26     api.add_resource(ArticleList, '/Articles', endpoint='Articles')
    27     api.add_resource(ArticleDetail, '/ArticleDetails/<int:id>', endpoint='ArticleDetails')
    28     # 用户注册登陆
    29     api.add_resource(UserRegister, '/auth/register', endpoint='user_register')
    30     api.add_resource(UserLogin, '/auth/login', endpoint='user_login')
    31 
    32     # 蓝图
    33     app.register_blueprint(blueprint=restful_bp)
    34     return app
    路由
     1 import os
     2 from flask_httpauth import HTTPTokenAuth
     3 from flask_migrate import Migrate
     4 from app.models import db
     5 
     6 migrate = Migrate()
     7 auth = HTTPTokenAuth()
     8 
     9 def init_db(app):
    10     app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(app.root_path, 'sqlite3.db')
    11     app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    12     db.init_app(app=app)
    13 
    14 def init_migrate(app):
    15     migrate.init_app(app=app,db=db)
    扩展
     1 import datetime
     2 
     3 from flask import g
     4 from flask_restful import Resource, reqparse, fields, marshal_with, abort
     5 from requests import auth
     6 from werkzeug.security import generate_password_hash, check_password_hash
     7 from app.models import Channel, db, Article, User
     8 
     9 # 输入格式化验证
    10 user_parser = reqparse.RequestParser()
    11 user_parser.add_argument('username', required=True, type=str)
    12 user_parser.add_argument('password', required=True, type=str)
    13 # 输出格式化设置
    14 user_fields = {
    15     'id': fields.Integer,
    16     'username': fields.String
    17 }
    18 
    19 # 用户注册
    20 class UserRegister(Resource):
    21     @marshal_with(fields=user_fields)
    22     def post(self):
    23         args = user_parser.parse_args()
    24         # 需要自定义验证,验证用户名是否重复。未写
    25         user = User()
    26         user.username = args.get('username')
    27         user.password = generate_password_hash(args['password'])
    28 
    29         db.session.add(user)
    30         db.session.commit()
    31         return user, 201
    32 
    33 # 用户登陆 34 class UserLogin(Resource): 35 def post(self): 36 args = user_parser.parse_args() 37 user = User.query.filter_by(username=args['username']).first() 38 if user is None or not check_password_hash(user.password, args['password']): 39 return {'msg': '用户名密码错误'}, 400 40 token = user.generate_auth_token() # 登陆成功为该用户生成一个token认证值,返回给客户端。 41 return {'token': token}        # 返回给客户端保存 42 # token验证。装饰器验证成功则True;否则False 43 @auth.verify_token 44 def verify_token(token): 46 # 验证token的回调函数。如果验证成功则使用token中的用户身份信息(user_id)从数据库中查询当前登录用户数据,返回uesr对象;如果验证不成功,则返回None 51 user = User.check_auth_token(token) 52 if user is None: 53 return False 54 # 验证成功后,将当前登录用户的对象设置到g对象中,供后续使用 55 g.user = user 56 return True

      @auth.verify_token和@auth.login_required 帮助我们对生成返回给客户端的token进行沿验证。

      auth = HTTPTokenAuth()扩展初始化实例时不需要传入app对象,也不需要调用auth.init_app(app)注入应用对象

    保护:

      @auth.login_required对频道模块的get请求进行保护

     1 # 频道模块。get、put、patch、delete
     2 class ChannelDetail(Resource):
     3     """
     4     GET     /channels/123
     5     PUT     /channels/234
     6     PATCH   /channels/123
     7     DELETE  /channels/123
     8     """
     9     def get_object(self,id):
    10         channel = Channel.query.get(id)
    11         if channel is None:
    12             return abort(404,message="找不到对象")
    13         return channel
    14 
    15     @auth.login_required # 对该函数的请求进行保护,token用户验证。如果该用户的token值错误或者过期,该用户将无法访问此函数
    16     @marshal_with(fields=channel_article_fields)
    17     def get(self,id):
    18         channel = self.get_object(id)
    19         return channel,200
     1 import datetime
     2 from flask import g
     3 from flask_restful import Resource, reqparse, fields, marshal_with, abort, marshal
     4 from werkzeug.security import generate_password_hash, check_password_hash
     5 
     6 from app.ext import auth
     7 from app.models import Channel, db, Article, User
     8 
     9 # ============================ N ===================================
    10 # 自定义一个类,用于时间格式化
    11 class MyDTFmt(fields.Raw):
    12     def format(self, value):
    13         return datetime.datetime.strftime(value, '%Y-%m-%d %H:%M:%S')
    14 
    15 
    16 # 定义参数验证格式
    17 article_parser = reqparse.RequestParser()
    18 article_parser.add_argument('title', required=True, type=str, help="标题必填")
    19 article_parser.add_argument('content', required=True, type=str, help="正文必填")
    20 article_parser.add_argument('channel_id', required=True, type=int, help="频道必填")
    21 
    22 # 定义返回输出格式
    23 article_fields = {
    24     'id': fields.Integer,
    25     'url': fields.Url(endpoint='ArticleDetails', absolute=True),
    26     'title': fields.String,
    27     'content': fields.String,
    28     # 等同于:'channel':fields.Nested(channel_fields),
    29     'channel': fields.Nested({  # 通过Nested将对象解开
    30         'name': fields.String,
    31         'url': fields.Url(endpoint="ChannelDetails", absolute=True),
    32         "sort": fields.Integer,
    33     }),
    34     "author": fields.Nested({
    35         'id': fields.Integer,
    36         'name': fields.String(attribute='username'),
    37     }),
    38     'created_at': MyDTFmt,  # 进行自定义时间格式化
    39     'updated_at': fields.DateTime(dt_format="iso8601")
    40 }
    41 
    42 
    43 # 文章模块。get、post
    44 class ArticleList(Resource):
    45 
    46     @auth.login_required
    47     @marshal_with(fields=article_fields)
    48     def get(self):
    49         articles = Article.query.all()
    50         return articles, 200
    51 
    52     @auth.login_required
    53     @marshal_with(fields=article_fields)
    54     def post(self):
    55         args = article_parser.parse_args()
    56 
    57         article = Article()
    58         article.title = args.get('title')
    59         article.content = args.get('content')
    60         article.channel_id = args.get('channel_id')
    61         article.author_id = g.user.id  # 登陆状态下设置文章作者
    62 
    63         db.session.add(article)
    64         db.session.commit()
    65         return article, 201
    文章保护 

    验证:

      通过postman请求登陆时返回的此用户的token值,在postman中设置上此用户的token值请求登陆访问频道列表

      

    补充:

       marshal的使用。api.py

     1 import datetime
     2 
     3 from flask import g
     4 from flask_restful import Resource, reqparse, fields, marshal_with, abort, marshal
     5 from werkzeug.security import generate_password_hash, check_password_hash
     6 
     7 from app.ext import auth
     8 from app.models import Channel, db, Article, User
     9 
    10 # 输出格式化设置
    11 user_fields = {
    12     'id': fields.Integer,
    13     'username': fields.String
    14 }
    15 
    16 # 用户登陆
    17 class UserLogin(Resource):
    18     def post(self):
    19         args = user_parser.parse_args()
    20         user = User.query.filter_by(username=args['username']).first()
    21         if user is None or not check_password_hash(user.password, args['password']):
    22             return {'msg': '用户名密码错误'}, 400
    23         token = user.generate_auth_token()  # 登陆成功生成一个token为该用户
    24         # return {'token': token} # 返回给客户端保存
    25 
    26         ret = {
    27             'code': 0,
    28             'msg': '',
    29             'data': {
    30                 # marshal相当于'user':'user.to_dict()' 将对象user与user_fileds中的同名参数进行匹配,
    31                 # marshal将user对象通过user_fields转化为字典形式传递出去
    32                 'user': marshal(user, user_fields),
    33                 'token': token,
    34             }
    35         }
    36         return ret


    文件上传

    原生实现:在实际生产中上传大型文件,一般采用分段上传

    • 上传表单  
    <form action="/upload" method="post" enctype="multipart/form-data">
          <input type="file" name="file">
          <input type="submit" value="Upload">
    </form>
    
    {# 设置表单类型为enctype="multipart/form-data" 上传文件才可以生效#}
    • 视图函数

        通过request对象上的files"file = request.files['file']"获取文件;使用 save() 方法保存文件到指定位置

     1 import os
     2 from flask import Flask, request, render_template, send_from_directory, url_for
     3 from werkzeug.utils import secure_filename
     4 
     5 app = Flask(__name__)
     6 # 设置上传存放文件的文件夹。uploads是此项目下的手动创建的目录。注意:手动创建
     7 app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'uploads')
     8 # 设置最大上传文件的大小值。10 M;一定要设置此值,不设置可能存在漏洞
     9 app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024
    10 
    11 # 判断文件的类型。原理:对文件名右边以点切割,获取脚标为1的参数,即是扩展名
    12 def allowed_file(filename):
    13     return '.' in filename and 
    14            filename.rsplit('.', 1)[1] in {'png', 'jpg', 'jpeg', 'gif'}
    15 
    16 @app.route('/uploads/<filename>')
    17 def uploaded_file(filename):
    18     """
    19     send_from_directory,从指定的目录中(第一个参数)查找一个指定名字的文件(第二个参数)
    20     如果找到,则将文件读入内存,并且最为响应返回给浏览器。只会在开发环境中使用
    21     """
    22     return send_from_directory(app.config['UPLOAD_FOLDER'],filename)
    23 
    24 @app.route('/upload-test/', methods=['POST', 'GET'])
    25 def upload_test():
    26     if request.method == 'POST':
    27         # 从表单中获得名为photo的上传文件对象,通过request.files.get获取。
    28         # files类型是字典,一个表单中可以有多个上传文件,可以通过遍历获取多个上传文件
    29         file = request.files.get('photo')
    30 
    31         if file:
    32             # 首先判断文件的 扩展名 是否被允许
    33             if allowed_file(file.filename):
    34                 # 将文件名做一个安全处理:'my movie.mp4' -> 'my_movie.mp4'
    35                 filename = secure_filename(file.filename)
    36                 # 将文件保存到指定目录(第一个参数),以某个文件名(第二个参数)
    37                 # 第二个参数是保存文件的文件名,可以自定义修改。filename = 'abc.def'
    38                 file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
    39                 # 通过url_for和uploaded_file视图函数,反向解析出文件的访问地址。供前端访问
    40                 file_url = url_for('uploaded_file', filename=filename)
    41                 return render_template('upload.html', photo_url=file_url)
    42             else:
    43                 return render_template('upload.html', err='不支持的文件类型')
    44     return render_template('upload.html')
    45 
    46 
    47 if __name__ == '__main__':
    48     app.run()

    结合flask-wtf实现:

    • 表单验证 form
    1 from flask_wtf import Form
    2 from flask_wtf.file import FileField, FileAllowed, FileRequired
    3 
    4 class PhotoForm(Form):
    5     photo = FileField('photo', validators=[
    6         FileRequired(), # 不能为空
    7         # 验证文件格式
    8         FileAllowed(['jpg', 'png', 'webp'], '只能上传图片')
    9     ])
    • 视图

       form.photo.data.filenameform.photo.data.save进行获取和保存

     1 import os
     2 from flask import Flask, render_template, url_for
     3 from werkzeug.utils import secure_filename
     4 from forms import PhotoForm
     5 
     6 app = Flask(__name__)
     7 # flask-wtf中生成csrf token必须依赖SECRET_KEY
     8 app.config['SECRET_KEY'] = '110'
     9 # 设置上传文件的文件夹,在static中
    10 app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'static/uploads')
    11 
    12 
    13 @app.route('/upload/', methods=('GET', 'POST'))
    14 def upload():
    15     form = PhotoForm()
    16     file_url = ''
    17 
    18     if form.validate_on_submit():
    19         # 获取photo中的数据
    20         filename = secure_filename(form.photo.data.filename)
    21         form.photo.data.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
    22         file_url = url_for('static', filename='uploads/' + filename)
    23     return render_template('upload.html', form=form, file_url=file_url)
    24 
    25 
    26 if __name__ == '__main__':
    27     app.run()
    • 模板
     1 <body>
     2 {% if form.errors %}
     3     <div>
     4     {{ form.errors }}
     5     </div>
     6 {% endif %}
     7 
     8 <form action="" method="post" enctype="multipart/form-data">
     9     {{ form.csrf_token() }}
    10     <input type="file" name="photo">
    11     <input type="submit" value="Upload">
    12 </form>
    13 
    14 <div>
    15     photo: <img src="{{ file_url }}" alt="">
    16 </div>
    17 </body>

    flask-uploads实现:

    安装:

    • pip install flask-uploads  
    • 视图
     1 import os
     2 from flask import Flask, render_template
     3 from flask_uploads import UploadSet, configure_uploads, IMAGES, patch_request_class
     4 from flask_wtf import FlaskForm
     5 from flask_wtf.file import FileField, FileRequired, FileAllowed
     6 
     7 app = Flask(__name__)
     8 app.config['SECRET_KEY'] = '110'
     9 
    10 # 为 flask_uploads 创建一个上传目录
    11 app.config['UPLOADED_PHOTOS_DEST'] = os.path.join(app.root_path, 'uploads')
    12 # 设置上传文件类型集合。可以将IMAGES[第二个参数]文件中的文件类型上传到名字叫photos[第一个参数]的文件中
    13 photos = UploadSet('photos', IMAGES)
    14 configure_uploads(app, photos)
    15 # 设置最大上传大小, 默认 64 MB
    16 patch_request_class(app)
    17 
    18 class UploadForm(FlaskForm):
    19     photo = FileField(validators=[
    20         FileAllowed(photos, '只能上传图片!'),
    21         FileRequired('文件未选择')])
    22 
    23 @app.route('/upload/', methods=['GET', 'POST'])
    24 def upload_file():
    25     form = UploadForm()
    26     if form.validate_on_submit():
    27         # 将 文件(form.photo.data)保存到 photos 上传集合中
    28         filename = photos.save(form.photo.data)
    29         # 获得文件的 url
    30         file_url = photos.url(filename)
    31         # 实际项目中,会把文件的 url 保存到数据库中
    32     else:
    33         file_url = None
    34     return render_template('upload.html', form=form, file_url=file_url)
    35 
    36 
    37 if __name__ == '__main__':
    38     app.run()
    • 模版
     1 <body>
     2 <form method="post" enctype="multipart/form-data">
     3      {{ form.csrf_token() }}
     4      {{ form.photo }}
     5      {% for error in form.photo.errors %}
     6          <p>{{ error }}</p>
     7      {% endfor %}
     8     <input type="submit" value="upload">
     9 </form>
    10 
    11 {% if file_url %}
    12 photo:<img src="{{ file_url }}">
    13 {% endif %}
    14 </body>

    前后端的分离实现:

      目前企业中,大多数上传文件的业务会把文件存储在 云存储 中,每个项目会向云存储申请上传的 secret_key,通过 secret_key 和 云存储上传api,将用户的文件上传到云存储中,并返回文件url

    1. 前端向后端发起申请上传的请求
    2. 后端根据云存储的 secret_key 及 云存储要求的特定算法(一会会由云存储的sdk提供)计算一个包含有效期的上传令牌(token),并将此 token 返回给前端
    3. 前端拿到上传 token 后,从本地直接将文件上传到云存储服务器(上传时会一起将 token 提交),上传成功后,云存储返回 url
    4. 前端将上传后的文件url及表单中的其他信息一起提交给后端进行保存 

    上线部署

    介绍:

       WSGI:W web、S server、G gateway、I interface。俗称:web容器,web服务器

       Nginx:也是一个服务器软件,反向代理、负载均衡;Nginx 背后可以有多个 WSGI 服务器

       Nginx 是不无论什么编程语言都可以使用的代理服务器软件。性能强悍、用户多、漏洞少、稳定性强。

      它接收用户的请求后,会转发给真正的运行python代码的服务器WSGI容器,WSGI容器内部会去执行python代码[python代码放在WSGI容器中才可以运行起来]

    过程:  

      用户访问时通过Nginx,Nginx会负载均衡、反向代理到WSGI容器上。python的代码是不可以直接与Ngins交互的,Nginx是不知道如何执行python代码的,

      所以需要python代码是在WSGI SERVER容器中运行的,通过WSGI运行就可以提供http服务;执行完后会返回app响应,相应会交给Nginx。最终返回给客户端 


      WSGI,  是一个协议,pep 333/3333 规范的协议,规定了服务器如何与应用程序交互web容器,执行我们的应用程序(比如:flask + 业务代码)

      uWSGI,是一个 服务器软件,实现了 WSGI 协议,可以运行标准的 WSGI 应用程序(Flask 应用)。同时也有自己的特有协议:uwsgi

      uwsgi, 是 uWSGI 特有的协议

                             / WSGI SERVER 1     <======>    Python(Flask app)
    client  <-----> Nginx    - WSGI SERVER 2     <======>    Python(Flask app)
                              WSGI SERVER 3     <======>    Python(Flask app)

    WSGI 就像油箱容器,我们的程序就像汽油。把程序放到这个容器中,就可以启动了 

    安装: 

    配置:

    部署: 

     

    生如逆旅 一苇以航
  • 相关阅读:
    iOS重签名及问题总结
    安装class-dump
    UISearchController 很坑
    hashmap 之哈希冲突
    wait()与sleep()的区别
    docker之es+es-head+kibana+ik分词器安装
    MySQL很有用的命令
    分布式事务执行逻辑
    索引 创建原则
    合理使用存储引擎
  • 原文地址:https://www.cnblogs.com/TMMM/p/11684553.html
Copyright © 2020-2023  润新知