提交 38d1d2722e9c83f99e58c40558389d9fcdde61f0

作者 nheweijun
2 个父辈 c0b2dc09 6f120876

Merge branch 'master' of http://gitlab.ctune.cn/weijunh/DMapManager

merge
1 1 import decimal
  2 +from re import template
2 3
3 4 from flask import Flask as _Flask
4 5 from flask.json import JSONEncoder as _JSONEncoder
... ... @@ -58,18 +59,47 @@ def create_app():
58 59 app.config['OAUTH2_JWT_KEY'] = 'secret-key'
59 60 app.config['OAUTH2_JWT_ALG'] = 'HS256'
60 61 # app.config['SQLALCHEMY_ECHO'] = True
61   -
  62 +
62 63 # allows cookies and credentials to be submitted across domains
63 64 app.config['CORS_SUPPORTS_CREDENTIALS'] = true
64   - app.config['CORS_ORIGINS ']="*"
  65 + app.config['CORS_ORIGINS '] = "*"
65 66
66 67 # 跨域设置
67 68 CORS(app)
68 69
69 70 # swagger设置
70 71 swagger_config = Swagger.DEFAULT_CONFIG
71   - swagger_config.update({"title": "DMapManager"})
72   - Swagger(app, config=swagger_config)
  72 +
  73 + SWAGGER_TEMPLATE = {
  74 + # "openapi": "3.0.0",
  75 + # "components": {
  76 + # "securitySchemes": {
  77 + # "bearerAuth": {
  78 + # "type": "http",
  79 + # "scheme": "bearer",
  80 + # "bearerFormat": "JWT"
  81 + # },
  82 + # }
  83 + # },
  84 + "securityDefinitions": {
  85 + "APIKeyHeader": {
  86 + "type": "apiKey",
  87 + "in": "header",
  88 + "name": "Authorization"
  89 + }
  90 + },
  91 +
  92 + "security": [{
  93 + "APIKeyHeader": []
  94 + }
  95 + ]
  96 + }
  97 +
  98 + swagger_config = Swagger.DEFAULT_CONFIG
  99 + swagger_config.update(configure.swagger_configure)
  100 +
  101 + Swagger(app, config=swagger_config,
  102 + template=SWAGGER_TEMPLATE)
73 103
74 104 # 创建数据库
75 105 db.init_app(app)
... ... @@ -78,9 +108,12 @@ def create_app():
78 108 # 日志
79 109
80 110 logging.basicConfig(level=configure.log_level)
81   - log_file = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "logs", "log.txt")
82   - handler = logging.FileHandler(log_file, encoding='UTF-8') # 设置日志字符集和存储路径名字
83   - logging_format = logging.Formatter('[%(levelname)s] %(asctime)s %(message)s')
  111 + log_file = os.path.join(os.path.dirname(os.path.dirname(
  112 + os.path.realpath(__file__))), "logs", "log.txt")
  113 + handler = logging.FileHandler(
  114 + log_file, encoding='UTF-8') # 设置日志字符集和存储路径名字
  115 + logging_format = logging.Formatter(
  116 + '[%(levelname)s] %(asctime)s %(message)s')
84 117 handler.setFormatter(logging_format)
85 118
86 119 app.logger.addHandler(handler)
... ... @@ -99,6 +132,7 @@ def create_app():
99 132 # start_schedule()
100 133 return app
101 134
  135 +
102 136 def create_schedule():
103 137 monitor = Flask(__name__)
104 138 monitor.config['SQLALCHEMY_DATABASE_URI'] = configure.SQLALCHEMY_DATABASE_URI
... ... @@ -109,12 +143,12 @@ def create_schedule():
109 143
110 144 # allows cookies and credentials to be submitted across domains
111 145 monitor.config['CORS_SUPPORTS_CREDENTIALS'] = true
112   - monitor.config['CORS_ORIGINS ']="*"
113   -
  146 + monitor.config['CORS_ORIGINS '] = "*"
  147 +
114 148 # swagger设置
115 149 swagger_config = Swagger.DEFAULT_CONFIG
116 150 Swagger(monitor, config=swagger_config)
117   -
  151 +
118 152 # 创建数据库
119 153 db.init_app(monitor)
120 154 db.create_all(app=monitor)
... ... @@ -124,9 +158,12 @@ def create_schedule():
124 158
125 159 # 日志
126 160 logging.basicConfig(level=configure.log_level)
127   - log_file = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "logs", "monitor_log.txt")
128   - handler = logging.FileHandler(log_file, encoding='UTF-8') # 设置日志字符集和存储路径名字
129   - logging_format = logging.Formatter('[%(levelname)s] %(asctime)s %(message)s')
  161 + log_file = os.path.join(os.path.dirname(os.path.dirname(
  162 + os.path.realpath(__file__))), "logs", "monitor_log.txt")
  163 + handler = logging.FileHandler(
  164 + log_file, encoding='UTF-8') # 设置日志字符集和存储路径名字
  165 + logging_format = logging.Formatter(
  166 + '[%(levelname)s] %(asctime)s %(message)s')
130 167 handler.setFormatter(logging_format)
131 168 monitor.logger.addHandler(handler)
132 169
... ...
... ... @@ -4,20 +4,62 @@ import os
4 4 from flask_sqlalchemy import SQLAlchemy
5 5 import glob
6 6 import traceback
7   -from pyDes import des,PAD_PKCS5,ECB
8   -
  7 +from pyDes import des, PAD_PKCS5, ECB
  8 +import json
9 9 import base64
  10 +from gmssl import sm3, func
  11 +from Crypto.Cipher import AES
  12 +from Crypto.Util.Padding import unpad, pad
  13 +from cryptography.hazmat.primitives import padding
  14 +from cryptography.hazmat.primitives.ciphers import algorithms
  15 +from binascii import b2a_hex, a2b_hex
  16 +
  17 +
  18 +class AESHelper():
  19 + key = 'w03MyIgc3zMHM5Qe'.encode('utf-8')
  20 + iv = '8765432187654321'.encode('utf-8')
  21 +
  22 + @classmethod
  23 + def encode(self, instr):
  24 + plaintxt = self._pad(instr)
  25 + aes = AES.new(self.key, AES.MODE_CBC, self.iv)
  26 + ciphertext = aes.encrypt(plaintxt)
  27 + return b2a_hex(ciphertext).decode('utf-8')
  28 +
  29 + @classmethod
  30 + def decode(self, instr):
  31 + encryptedData = instr
  32 + aes = AES.new(self.key, AES.MODE_CBC, self.iv)
  33 + plaintxt = aes.decrypt(a2b_hex(encryptedData))
  34 + return bytes.decode(unpad(plaintxt, AES.block_size))
  35 +
  36 + def _pad(data):
  37 + if not isinstance(data, bytes):
  38 + data = data.encode()
  39 + padder = padding.PKCS7(algorithms.AES.block_size).padder()
  40 + padded_data = padder.update(data)+padder.finalize()
  41 + return padded_data
  42 +
  43 +
  44 +class SM3():
  45 + @classmethod
  46 + def encode(self, data=""):
  47 + by_str = bytes(data, 'utf-8')
  48 + result = sm3.sm3_hash(func.bytes_to_list(by_str))
  49 + return result
  50 +
  51 +
10 52 class DES():
11 53 '''
12 54 DES密码加解密
13 55 '''
14   - Des: des = des("Chinadci", ECB, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5)
  56 + Des: des = des("Chinadci", ECB, "\0\0\0\0\0\0\0\0",
  57 + pad=None, padmode=PAD_PKCS5)
15 58
16 59 @classmethod
17 60 def encode(cls, data):
18 61 return str(base64.b64encode(cls.Des.encrypt(data)), encoding="utf8")
19 62
20   -
21 63 @classmethod
22 64 def decode(cls, data):
23 65 if data:
... ... @@ -26,6 +68,7 @@ class DES():
26 68 else:
27 69 return data
28 70
  71 +
29 72 db = SQLAlchemy()
30 73
31 74 # 动态加载Model
... ... @@ -33,13 +76,14 @@ current_dir = os.path.abspath(os.path.dirname(__file__))
33 76 pkgs = list(glob.glob('%s/modules/*/models' % (current_dir)))
34 77 pkgs.extend(list(glob.glob('%s/modules/*/*/models' % (current_dir))))
35 78
36   -for pkg in pkgs :
  79 +for pkg in pkgs:
37 80 pkg = os.path.normpath(pkg)
38 81 node_list = pkg.split(os.path.sep)
39   - pkg_name = "app.{}".format(".".join(node_list[node_list.index("modules"):]))
  82 + pkg_name = "app.{}".format(
  83 + ".".join(node_list[node_list.index("modules"):]))
40 84 try:
41 85 if pkg_name.__contains__("models"):
42 86 __import__(pkg_name)
43 87 except Exception as e:
44 88 print(traceback.format_exc())
45   - pass
\ No newline at end of file
  89 + pass
... ...
1 1 from enum import auto
2 2 from logging import error
  3 +from unittest import result
3 4 from flasgger import swag_from
4 5 from app.util import BlueprintApi
5 6 from app.util import BlueprintApi
6 7 from flask import Blueprint, render_template, redirect, request, session, jsonify
7   -from sqlalchemy import and_
8 8 from .models import *
9   -from .oauth2 import authorization, generate_user_info,require_oauth
  9 +from .oauth2 import authorization, generate_user_info, require_oauth
10 10 from authlib.oauth2 import OAuth2Error
11 11 from authlib.integrations.flask_oauth2 import current_token
12 12 from . import user_create, client_create, client_query, user_query, user_update, user_delete
13 13 import configure
14 14 from app.decorators.auth_decorator import auth_decorator
  15 +import time
  16 +from app.models import SM3, AESHelper
15 17
16 18
17 19 def current_user():
... ... @@ -45,17 +47,22 @@ class DataManager(BlueprintApi):
45 47 except OAuth2Error as error:
46 48 return jsonify(dict(error.get_body()))
47 49 if not user:
48   - return render_template("auth/authorize.html", user=user, grant=grant)
49   - # return render_template("auth/login1.html", user=user, grant=grant)
  50 + # 生成验证码
  51 +
  52 + return render_template("auth/authorize.html",
  53 + user=user,
  54 + grant=grant)
50 55 error = ""
51 56 if not user:
  57 + # 验证码校验
  58 +
52 59 if not "username" in request.form or not request.form.get("username"):
53 60 error = "用户名不可为空"
54 61 elif not "password" in request.form or not request.form.get("password"):
55 62 error = "密码不可为空"
56 63 else:
57 64 username = request.form.get("username")
58   - password = request.form.get("password")
  65 + password = SM3.encode(request.form.get("password"))
59 66 user = User.query.filter_by(
60 67 username=username, password=password).first()
61 68 if not user:
... ... @@ -64,15 +71,14 @@ class DataManager(BlueprintApi):
64 71 if user:
65 72 session["id"] = user.id
66 73 grant_user = user
67   - return authorization.create_authorization_response(grant_user=grant_user)
  74 + return authorization.create_authorization_response(request=request, grant_user=grant_user)
68 75
69 76 try:
70 77 grant = authorization.validate_consent_request(end_user=user)
71 78 except OAuth2Error as error:
72 79 return jsonify(dict(error.get_body()))
73   - return render_template("auth/authorize.html", user=user, grant=grant, error=error)
74   -
75   -
  80 + # return render_template("auth/authorize.html", user=user, grant=grant, error=error)
  81 + return authorization.create_authorization_response(grant_user=None)
76 82
77 83 @staticmethod
78 84 @bp.route("/token", methods=["POST"])
... ... @@ -106,8 +112,7 @@ class DataManager(BlueprintApi):
106 112 except OAuth2Error as error:
107 113 return jsonify(dict(error.get_body()))
108 114 return redirect(url)
109   -
110   -
  115 +
111 116 """接口"""
112 117 @staticmethod
113 118 @bp.route("/users", methods=["GET"])
... ... @@ -166,3 +171,29 @@ class DataManager(BlueprintApi):
166 171 获取client列表
167 172 """
168 173 return client_query.Api().result
  174 +
  175 + @staticmethod
  176 + @bp.route("/init", methods=["GET"])
  177 + def init():
  178 + username = 'admin'
  179 + password = 'admin'
  180 + if not User.query.filter_by(username=username).one_or_none():
  181 + user = User(username=username, password=password, role='admin',
  182 + phone='', company='', position='', email='',
  183 + create_time=time.strftime(
  184 + "%Y-%m-%d %H:%M:%S", time.localtime()),
  185 + update_time=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
  186 + db.sesion.add(user)
  187 + db.session.commit()
  188 + return "创建默认用户成功"
  189 + else:
  190 + return "默认用户已存在"
  191 +
  192 + @staticmethod
  193 + @bp.route("/test", methods=["GET"])
  194 + def test():
  195 + result = 'c78ca1de8139e62c99d4c4d2050ddd16'
  196 + result = AESHelper.encode('dataman2')
  197 + # result=DES.decode('U2FsdGVkX199Ncuh7+0/HVmonwM9Uz7SYnmF73wtW7c=')
  198 + result2 = AESHelper.decode(result)
  199 + return result2
... ...
... ... @@ -40,6 +40,13 @@ class OAuth2Client(db.Model, OAuth2ClientMixin):
40 40 Integer, ForeignKey('dmap_user.id', ondelete='CASCADE'))
41 41 user = relationship('User')
42 42
  43 + def get_default_redirect_uri(self):
  44 + if self.redirect_uris:
  45 + return self.redirect_uris[0]
  46 +
  47 + def check_redirect_uri(self, redirect_uri):
  48 + return redirect_uri in self.redirect_uris
  49 +
43 50
44 51 class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin):
45 52 __tablename__ = 'dmap_oauth2_code'
... ... @@ -57,4 +64,4 @@ class OAuth2Token(db.Model, OAuth2TokenMixin):
57 64 user_id = Column(
58 65 Integer, ForeignKey('dmap_user.id', ondelete='CASCADE'))
59 66 # name = Column(Text)
60   - user = relationship('User')
\ No newline at end of file
  67 + user = relationship('User')
... ...
  1 +from authlib.oauth2.rfc6749 import grants
1 2 from os import access, remove
2 3 from time import time
3 4 from authlib.integrations.flask_oauth2 import (
... ... @@ -29,10 +30,11 @@ DUMMY_JWT_CONFIG = {
29 30 'exp': 7200,
30 31 }
31 32
  33 +
32 34 class myCodeIDToken(CodeIDToken):
33 35 def validate(self, now, leeway):
34 36 return super().validate(now=now, leeway=leeway)
35   -
  37 +
36 38 def validate_exp(self, now, leeway):
37 39 return super().validate_exp(now, leeway)
38 40
... ... @@ -68,7 +70,34 @@ def create_authorization_code(client, grant_user, request):
68 70 return code
69 71
70 72
71   -class AuthorizationCodeGrant(_AuthorizationCodeGrant):
  73 +class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
  74 +
  75 + def save_authorization_code(self, code, request):
  76 + client = request.client
  77 + auth_code = OAuth2AuthorizationCode(
  78 + code=code,
  79 + client_id=client.client_id,
  80 + redirect_uri=request.redirect_uri,
  81 + scope=request.scope,
  82 + user_id=request.user.id,
  83 + )
  84 + db.session.add(auth_code)
  85 + db.session.commit()
  86 + return auth_code
  87 +
  88 + def query_authorization_code(self, code, client):
  89 + item = OAuth2AuthorizationCode.query.filter_by(
  90 + code=code, client_id=client.client_id).first()
  91 + if item and not item.is_expired():
  92 + return item
  93 +
  94 + def delete_authorization_code(self, authorization_code):
  95 + db.session.delete(authorization_code)
  96 + db.session.commit()
  97 +
  98 + def authenticate_user(self, authorization_code):
  99 + return User.query.get(authorization_code.user_id)
  100 +
72 101 def create_authorization_code(self, client, grant_user, request):
73 102 return create_authorization_code(client, grant_user, request)
74 103
... ... @@ -122,6 +151,17 @@ class HybridGrant(_OpenIDHybridGrant):
122 151 return generate_user_info(user, scope)
123 152
124 153
  154 +class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
  155 + def authenticate_user(self, username, password):
  156 + user = User.query.filter_by(username=username).first()
  157 + if user.check_password(password):
  158 + return user
  159 +
  160 + TOKEN_ENDPOINT_AUTH_METHODS = [
  161 + 'client_secret_basic', 'client_secret_post'
  162 + ]
  163 +
  164 +
125 165 authorization = AuthorizationServer()
126 166 require_oauth = ResourceProtector()
127 167
... ... @@ -142,6 +182,7 @@ def config_oauth(app):
142 182 ])
143 183 authorization.register_grant(ImplicitGrant)
144 184 authorization.register_grant(HybridGrant)
  185 + authorization.register_grant(PasswordGrant)
145 186
146 187 # protect resource
147 188 bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
... ...
... ... @@ -6,6 +6,8 @@
6 6 from .models import *
7 7 from app.util.component.ApiTemplate import ApiTemplate
8 8 import time
  9 +from app.models import SM3
  10 +from app.models import AESHelper
9 11
10 12
11 13 class Api(ApiTemplate):
... ... @@ -26,13 +28,13 @@ class Api(ApiTemplate):
26 28 res["result"] = False
27 29 try:
28 30 # 业务逻辑
29   - username = self.para.get("username")
30   - password = self.para.get("pwd")
31   - role = self.para.get("role")
32   - company = self.para.get("company", None)
33   - position = self.para.get("position", None)
34   - email = self.para.get("email", None)
35   - phone = self.para.get("phone", None)
  31 + username = AESHelper.decode( self.para.get("username", ''))
  32 + password = SM3.encode(AESHelper.decode(self.para.get("pwd", '')))
  33 + role = AESHelper.decode(self.para.get("role", ''))
  34 + company = AESHelper.decode(self.para.get("company", ''))
  35 + position = AESHelper.decode(self.para.get("position", ''))
  36 + email = AESHelper.decode(self.para.get("email", ''))
  37 + phone = AESHelper.decode(self.para.get("phone", ''))
36 38 # 是否重名
37 39 if(User.query.filter_by(username=username).one_or_none()):
38 40 res["msg"] = "username 已存在"
... ...
1 1 from app.util.component.ApiTemplate import ApiTemplate
2   -import time
  2 +from app.models import SM3
3 3 from .models import *
  4 +from app.models import AESHelper
4 5
5 6
6 7 class Api(ApiTemplate):
... ... @@ -25,10 +26,13 @@ class Api(ApiTemplate):
25 26 else:
26 27 # 更新密码要求同时输入pwd/newPwd/reNewPwd
27 28 if self.para.__contains__("pwd") or self.para.__contains__("newPwd") or self.para.__contains__("reNewPwd"):
28   - password = self.para.get("pwd")
29   - new_password = self.para.get("newPwd")
30   - re_new_password = self.para.get("reNewPwd")
31   -
  29 + password = SM3.encode(
  30 + AESHelper.decode(self.para.get("pwd")))
  31 + new_password = SM3.encode(
  32 + AESHelper.decode(self.para.get("newPwd")))
  33 + re_new_password = SM3.encode(
  34 + AESHelper.decode(self.para.get("reNewPwd")))
  35 +
32 36 # validate pwd
33 37 if not password:
34 38 res["result"] = False
... ... @@ -42,17 +46,17 @@ class Api(ApiTemplate):
42 46 res["result"] = False
43 47 res["msg"] = "原密码输入错误"
44 48 return res
45   -
  49 +
46 50 # 更新密码
47 51 userinfo.update({"password": new_password})
48   -
49   - #更新用户基本信息
  52 +
  53 + # 更新用户基本信息
50 54 for key in obj_value:
51 55 if self.para.__contains__(obj_value[key]):
52   - value = self.para.get(obj_value[key])
  56 + value = self.para.get(AESHelper.decode(obj_value[key]))
53 57 value = "" if value == "None" or value == "none" else value
54 58 userinfo.update({key: value})
55   -
  59 +
56 60 db.session.commit()
57 61 res["result"] = True
58 62 res["msg"] = "更新用户信息成功"
... ...
  1 +'''
  2 +生成验证码图片
  3 +'''
  4 +
  5 +from PIL import Image, ImageDraw, ImageFont, ImageFilter
  6 +import random
  7 +
  8 +
  9 +# 随机字符
  10 +def rndChar():
  11 + num = 0
  12 + while num == 0 and ((num >= 58 and num <= 64) or (num >= 91 and num <= 96)):
  13 + num = random.randint(48, 122)
  14 + return num
  15 +
  16 +# 随机颜色
  17 +
  18 +
  19 +def rndColor():
  20 + return ''
\ No newline at end of file
... ...
... ... @@ -5,6 +5,7 @@ deploy_ip_host = "172.26.40.105:8840"
5 5 # 系统数据库
6 6 SQLALCHEMY_DATABASE_URI = "postgresql://postgres:chinadci@172.26.60.101:5432/dmap_manager"
7 7
  8 +
8 9 # 指定精华表所在位置(必须为空间库),设置为None则存放在各自的实体库中
9 10 #VACUATE_DB_URI = None
10 11 VACUATE_DB_URI = SQLALCHEMY_DATABASE_URI
... ... @@ -13,6 +14,8 @@ VACUATE_DB_URI = SQLALCHEMY_DATABASE_URI
13 14 dmap_engine = "http://172.26.99.160:8820"
14 15
15 16 # 固定配置不需要修改
  17 +
  18 +swagger_configure = {"title": "DMapManager"}
16 19 entry_data_thread = 3
17 20 SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/'
18 21 # 权限
... ...
  1 +101映射域名
  2 +dmap.apps.chinadci.com
  3 +# 鉴权
  4 +Authlib
  5 +## 1 授权服务器
  6 +为授权、颁发令牌、刷新令牌和撤销令牌提供多个端点。当资源拥有者(用户)获得授权时,授权服务器会向客户端颁发访问令牌。
  7 +### 资源拥有者:用户
  8 +### 客户端:客户端是一个代表资源拥有者并在授权情况下请求受保护资源的应用
  9 + * client_id是唯一标识
  10 + * client_secret是密码
  11 + * 客户端令牌端点认证方法
  12 +### 令牌
  13 +使用令牌访问用户资源,令牌发布时带有有效期,有访问范围等等。它至少包括:
  14 +* access_token
  15 +* refresh_token
  16 +*...
  17 +### 服务器
  18 +authlib使用工具AuthorizationServer管理请求和响应
  19 +
  20 +
  21 +
  22 +# Web安全
  23 +## 1 接口权限控制
  24 +[swagger](https://swagger.io/docs/specification/2-0/what-is-swagger/)
  25 +
  26 +**需求:api需要授权**
  27 +
  28 +1.确定当前使用openapi2版本
  29 +2.了解简单YAML使用方式,文档使用YAML格式
  30 +3.声明Swagger,传入配置template(json或者yaml)
  31 +```python
  32 +Swagger(app, config=swagger_config,template=SWAGGER_TEMPLATE)
  33 +```
  34 +
  35 +## 2 用户密码加密
  36 +[pipy](https://pypi.org/project/gmssl/)
  37 +[github](https://github.com/guanzhi/GmSSL)
  38 +[使用gmssl demo](https://www.cnblogs.com/rocedu/p/15518988.html)
  39 +
  40 +**用户密码加密不可逆,兼容国产化,使用sm3算法,采用gmssl开源组件**
  41 +
  42 +封装好的类使用如下:
  43 +```python
  44 +from app.models import SM3
  45 +password = SM3.encode('test')
  46 +```
  47 +## 3 通过网络传输的敏感信息要加密
  48 +~~使用sm2,通过gmssl工具生成sm2的公钥、私钥。~~
  49 +~~[教程](https://github.com/guanzhi/GmSSL)~~
  50 +
  51 +~~1.生成sm2公钥、私钥~~
  52 +~~privatekey使用的pem是`zhou@123`~~
  53 +
  54 +~~2. 使用公钥加密,私钥解密~~
  55 +~~前端使用sm-crypto,用户与python-gmssl互通~~
  56 +~~[npm_sm-crypto](https://www.npmjs.com/package/sm-crypto)~~
  57 +
  58 +使用AES对称加密敏感信息,前端加密,后端解密。偏移量iv、加密密钥key与前端保持一致,保证解密正确。封装在models.py中。
  59 +
  60 +依赖组件:
  61 +* pycryptodome
  62 +* Crypto
  63 +* binascii
  64 +参考资料
  65 +[pycryptodome]()
  66 +
  67 +**demo**
  68 +```python
  69 +from app.models import AESHelper
  70 +
  71 +encryption=AESHelper.encode('test_data')
  72 +result=AESHelper.decode(encryption)
  73 +```
  74 +
  75 +## 4 使用验证码,防止恶意破解密码、刷票、论坛灌水、刷页
  76 +验证码又叫CAPTCHA
  77 +
  78 +[验证码基础知识](https://baike.baidu.com/item/%E9%AA%8C%E8%AF%81%E7%A0%81/31701)
  79 +[使用python图像处理标准库](https://www.liaoxuefeng.com/wiki/1016959663602400/1017785454949568)
  80 +[pillow](https://pillow.readthedocs.io/en/stable/index.html)
\ No newline at end of file
... ...
... ... @@ -17,3 +17,5 @@ kazoo==2.8.0
17 17 paramiko==2.8.0
18 18 requests==2.26.0
19 19 schedule==1.1.0
  20 +gmssl==3.2.1
  21 +pycryptodome==3.13.0
... ...
注册登录 后发表评论