diff --git a/auto-install.sh b/auto-install.sh old mode 100644 new mode 100755 index 2dd3725..9306404 --- a/auto-install.sh +++ b/auto-install.sh @@ -1,7 +1,29 @@ #!/bin/bash -echo -e "此脚本为快速部署,目前只做了Centos版本的,如果是其它系统请自行修改下相关命令\n请准备一个新的环境运行\n本脚本会快速安装相关的环境和所需要的服务\n如果你运行脚本的服务器中已经存在如:Nginx、Python3等,可能会破坏掉原有的应用配置。" -##Check IP +SCRIPT=$(readlink -f $0) +CWD=$(dirname ${SCRIPT}) +os_distro='' +os_version='' +get_selinux='' + +function get_os_basic_info() { + if [[ -f /etc/lsb-release ]]; then + os_distro=$(lsb_release -d |awk '{print $2}') + os_version=$(lsb_release -d -s |awk '{print $2}') + elif [[ -f /etc/redhat-release ]]; then + os_distro=$(cat /etc/redhat-release |awk '{print $1}') + os_version=$(cat /etc/redhat-release |awk '{print $4}') + get_selinux=$(getenforce) + if [[ ${get_selinux} =~ enforcing|Enforcing ]];then + echo "请先禁用SELINUX~~! ..." + exit 1 + fi + else + echo "不能识别的操作系统,请选择使用Ubuntu或Centos! ..." + exit 1 + fi +} + function check_ip() { local IP=$1 VALID_CHECK=$(echo $IP|awk -F. '$1<=255&&$2<=255&&$3<=255&&$4<=255{print "yes"}') @@ -16,7 +38,6 @@ function check_ip() { fi } -##Check domain function check_domain() { local DOMAIN=$1 if echo $DOMAIN |grep -P "(?=^.{4,253}$)(^(?:[a-zA-Z0-9](?:(?:[a-zA-Z0-9\-]){0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$)" >/dev/null; then @@ -26,7 +47,6 @@ function check_domain() { fi } -##Check Port function check_port() { local PORT=$1 VALID_CHECK=$(echo $PORT|awk '$1<=65535&&$1>=1{print "yes"}') @@ -41,11 +61,38 @@ function check_port() { fi } +function safe_installer() { + local _run_cmd="$@" + if [[ ${os_distro} =~ (CentOS|Redhat) ]]; then + sudo yum makecache + sudo yum install -y ${_run_cmd} + elif [[ ${os_distro} =~ (xUbuntu|Debian) ]]; then + sudo apt-get update + sudo apt-get installer ${_run_cmd} + else + echo "未适配的操作系统 ${os_distro}" + exit 1 + fi + if [[ $? -ne 0 ]]; then + echo "安装 [${_run_cmd}] 失败" + exit 1 + fi +} + +get_os_basic_info +echo "=============================================================================" +echo " 此脚本为快速部署,支持[Ubuntu, Debian, Centos] + 请准备一个新的环境运行,本脚本会快速安装相关的环境和所需要的服务 + 如果你运行脚本的服务器中已经存在如:Nginx、Python3等,可能会破坏掉原有的应用配置" +echo " 当前目录:${CWD}" +echo " 操作系统发行版本:${os_distro}, 系统版本:${os_version} ..." +echo "=============================================================================" + while :; do echo - echo "请确认你此台服务器是全新干净的,以防此脚本相关操作对正在运行的服务造成影响(不可逆)。" + echo "请确认你此台服务器是全新干净的,以防此脚本相关操作对正在运行的服务造成影响(不可逆) ..." read -p "请确认是否继续执行,输入 [y/n]: " ensure_yn if [[ ! "${ensure_yn}" =~ ^[y,n]$ ]]; then - echo "输入有误,请输入 y 或 n" + echo "输入有误,请输入 y 或 n ..." else break fi @@ -55,182 +102,240 @@ if [[ "${ensure_yn}" = n ]]; then exit 0 fi -echo "=======================================================================" + while :; do echo read -p "请输入密码自助平台使用的本机IP: " PWD_SELF_SERVICE_IP - check_ip ${PWD_SELF_SERVICE_IP} + check_ip "${PWD_SELF_SERVICE_IP}" if [[ $? -ne 0 ]]; then - echo "---输入的IP地址格式有误,请重新输入。" + echo "---输入的IP地址格式有误,请重新输入 ..." else break fi done -echo "=======================================================================" while :; do echo - read -p "请输入密码自助平台使用的端口(不要和Nginx一样): " PWD_SELF_SERVICE_PORT - check_port ${PWD_SELF_SERVICE_PORT} + read -p "请输入密码自助平台使用的端口(不要和Nginx[80]一样): " PWD_SELF_SERVICE_PORT + check_port "${PWD_SELF_SERVICE_PORT}" if [[ $? -ne 0 ]]; then - echo "---输入的端口有误,请重新输入。" + echo "---输入的端口有误,请重新输入 ..." else break fi done -echo "=======================================================================" while :; do echo read -p "请输入密码自助平台使用域名,例如:pwd.abc.com(不需要加http://或https://) " PWD_SELF_SERVICE_DOMAIN - check_domain ${PWD_SELF_SERVICE_DOMAIN} + check_domain "${PWD_SELF_SERVICE_DOMAIN}" if [[ $? -ne 0 ]]; then - echo "---输入的域名格式有误,请重新输入。" + echo "---输入的域名格式有误,请重新输入 ..." else break fi done -##当前脚本的绝对路径 -SHELL_FOLDER=$(dirname $(readlink -f "$0")) +echo +echo "===============================================" +echo "开始部署 ..." - - -echo "关闭SELINUX" -sudo setenforce 0 -sudo sed -i 's@SELINUX=*@SELINUX=disabled@g' /etc/selinux/config -echo "DONE....." -echo "关闭防火墙" -sudo systemctl disable firewalld -sudo systemctl stop firewalld -echo "DONE....." - -echo "初始化编译环境----------" -sudo yum install gcc patch libffi-devel python-devel zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel wget psmisc -y -echo "=======================================================================" -echo "初始化编译环境完成" -echo "=======================================================================" - -##Quick install nginx -echo "=======================================================================" -echo "安装 Nginx" -sudo cat << EOF > /etc/yum.repos.d/nginx.repo -[nginx-stable] -name=nginx stable repo -baseurl=http://nginx.org/packages/centos/7/\$basearch/ -gpgcheck=1 -enabled=1 -gpgkey=https://nginx.org/keys/nginx_signing.key -module_hotfixes=true -EOF - -sudo yum makecache fast -sudo yum install nginx -y - -if [[ $? -eq 0 ]] -then - sudo systemctl enable nginx - sudo systemctl start nginx - echo "=======================================================================" - echo "nginx 安装成功!" - echo "=======================================================================" -else - echo "=======================================================================" - echo "nginx 安装失败!" - echo "=======================================================================" - exit 1 +if [[ ! -f "${CWD}/.init_package.Done" ]]; then + echo "初始化依赖包 ..." + if [[ ${os_distro} =~ (CentOS|Redhat) ]]; then + sudo yum makecache + sudo yum install -y @development zlib-devel bzip2 bzip2-devel readline-devel sqlite \ + sqlite-devel openssl openssl-devel xz xz-devel libffi-devel ncurses-devel readline-devel tk-devel \ + libpcap-devel findutils wget nginx curl tar initscripts + elif [[ ${os_distro} =~ (xUbuntu|Debian) ]]; then + sudo apt-get update + sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev \ + libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \ + xz-utils tk-dev libffi-dev liblzma-dev python-openssl wget nginx curl tar initscripts + fi + if [[ $? -eq 0 ]]; then + echo "初始化依赖包完成 ..." + touch ${CWD}/.init_package.Done + else + echo "初始化依赖包失败 ..." + exit 1 + fi fi -##install python3 -##如果之前用此脚本安装过python3,后续就不会再次安装。 -PYTHON_VER='3.8.9' -PYTHON_INSTALL_DIR=/usr/share/python-${PYTHON_VER} -if [[ -f "${PYTHON_INSTALL_DIR}/bin/python3" ]] -then - echo "己发现Python3,将不会安装。" -else - if [[ -f "Python-${PYTHON_VER}.tar.xz" ]] - then - echo "将安装Python${PYTHON_VER}" - tar xf Python-${PYTHON_VER}.tar.xz - cd Python-${PYTHON_VER} - sudo ./configure --prefix=${PYTHON_INSTALL_DIR} && make && make install +if [[ ! -f "${CWD}/.redis.Done" ]]; then + safe_installer redis + if [[ $? -eq 0 ]]; then + gen_password=$(echo "$(hostname)$(date)" |base64 |cut -b 1-24) + sed -i 's@^requirepass.*@@g' /etc/redis/redis.conf + sed -i "/# requirepass foobared/a requirepass ${gen_password}" /etc/redis/redis.conf + sed -i "s@REDIS_PASSWORD.*@REDIS_PASSWORD = r'${gen_password}'@g" ${CWD}/conf/local_settings.py + touch ${CWD}/.redis.Done + echo "安装 redis-server 成功" else - echo "脚本目录下没有发现Python${PYTHON_VER}.tar.xz,将会下载python ${PYTHON_VER}" - sudo wget https://www.python.org/ftp/python/${PYTHON_VER}/Python-${PYTHON_VER}.tar.xz - if [[ $? -eq 0 ]]; then - tar xf Python-${PYTHON_VER}.tar.xz - cd Python-${PYTHON_VER} - sudo ./configure --prefix=${PYTHON_INSTALL_DIR} && make && make install - else - echo "下载${PYTHON_VER}/Python-${PYTHON_VER}.tar.xz失败,请重新运行本脚本再次重试" + echo "安装 redis-server 失败,请重新运行本脚本再试" + fi +fi + +redis_service='' +if [[ -f /usr/lib/systemd/system/redis.service ]];then + redis_service=redis +elif [[ -f /usr/lib/systemd/system/redis-server.service ]]; then + redis_service='redis-server' +fi + +if [[ -z ${redis_service} ]]; then + echo "Redis服务名未能识别到,请自行手动重启本机的Redis服务 ..." +else + rm -f /etc/nginx/conf.d/default.conf + rm -f /etc/nginx/sites-enabled/default.conf + systemctl restart ${redis_service} +fi + +NGINX_USER=$(grep -E '^user' /etc/nginx/nginx.conf |sed 's@user @@g' |sed 's@;@@g' |awk '{print $1}') + +cat <<'EOF' >/etc/nginx/nginx.conf +worker_processes auto; +pid /run/nginx.pid; + +events { + use epoll; + worker_connections 2048; +} + +http { + include mime.types; + default_type application/octet-stream; + server_tokens off; + client_header_buffer_size 16k; + client_body_buffer_size 128k; + keepalive_timeout 65; + keepalive_requests 120; + sendfile on; + tcp_nodelay on; + tcp_nopush on; + charset utf-8; + autoindex off; + # gzip + gzip on; + gzip_min_length 1k; + gzip_buffers 4 16k; + gzip_comp_level 3; + gzip_disable "MSIE [1-6]\."; + gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/json; + gzip_vary on; + gzip_static on; + # upload file + client_max_body_size 0; + proxy_buffering off; + proxy_send_timeout 10m; + proxy_read_timeout 10m; + proxy_connect_timeout 10m; + proxy_request_buffering off; + # log + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log; + # config file + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*.conf; +} +EOF +sed -i "1i\user ${NGINX_USER};" /etc/nginx/nginx.conf + +PYTHON_VER='3.8.16' +PYTHON_INSTALL_DIR=/usr/share/python-${PYTHON_VER} +PYTHON_VENV_DIR=${CWD}/pwd_venv + +if [[ -f "${CWD}/.python3.Done" ]];then + echo "Python3己部署,跳过 ..." +else + if [[ -f "${CWD}/Python-${PYTHON_VER}.tar.xz" ]] && [[ -f "${CWD}/python.${PYTHON_VER}.md5" ]]; then + python3_md5=$(md5sum "${CWD}/Python-${PYTHON_VER}.tar.xz" |awk '{print $1}') + python3_md5_record=$(cat ${CWD}/python.${PYTHON_VER}.md5) + if [[ x"${python3_md5}" != x"${python3_md5_record}" ]]; then + rm -f "${CWD}/Python-${PYTHON_VER}.tar.xz" + rm -f "${CWD}/python.${PYTHON_VER}.md5" + fi + else + echo "无Python${PYTHON_VER}.tar.xz,执行下载python ${PYTHON_VER} ..." + rm -f "${CWD}/Python-${PYTHON_VER}.tar.xz" + + sudo wget -c -t 10 -T 120 https://repo.huaweicloud.com/python/${PYTHON_VER}/Python-${PYTHON_VER}.tar.xz -O ${CWD}/Python-${PYTHON_VER}.tar.xz + + md5sum "${CWD}/Python-${PYTHON_VER}.tar.xz" |awk '{print $1}' > ${CWD}/python.${PYTHON_VER}.md5 + if [[ $? -ne 0 ]]; then + echo "下载${PYTHON_VER}/Python-${PYTHON_VER}.tar.xz失败,请重新运行本脚本再次重试 ..." + exit 1 fi fi + echo "执行安装Python${PYTHON_VER} ..." + tar xf ${CWD}/Python-${PYTHON_VER}.tar.xz -C ${CWD}/ + cd "${CWD}/Python-${PYTHON_VER}" || exit 1 + sudo ./configure --prefix=${PYTHON_INSTALL_DIR} && make && make install - if [[ $? -eq 0 ]] - then - echo "创建python3和pip3的软件链接" - cd ${SHELL_FOLDER} - sudo ln -svf ${PYTHON_INSTALL_DIR}/bin/python3 /usr/bin/python3 - sudo ln -svf ${PYTHON_INSTALL_DIR}/bin/pip3 /usr/bin/pip3 - echo "=======================================================================" - echo "Python3 安装成功!" - echo "=======================================================================" + if [[ $? -eq 0 ]]; then + echo "创建python虚拟环境 -> ${PYTHON_VENV_DIR} ..." + rm -rf "${PYTHON_VENV_DIR}" + ${PYTHON_INSTALL_DIR}/bin/python3 -m venv --copies "${PYTHON_VENV_DIR}" + touch ${CWD}/.python3.Done + echo "Python3 安装成功 ..." else - echo "=======================================================================" - echo "Python3 安装失败!" - echo "=======================================================================" + echo "Python3 安装失败,请重试 ..." exit 1 fi fi ##修改PIP源为国内 mkdir -p ~/.pip -cat << EOF > ~/.pip/pip.conf +cat <<'EOF' > ~/.pip/pip.conf [global] -index-url = https://pypi.tuna.tsinghua.edu.cn/simple +index-url=https://pypi.tuna.tsinghua.edu.cn/simple [install] trusted-host=pypi.tuna.tsinghua.edu.cn EOF -cd ${SHELL_FOLDER} -echo "====升级pip================" -/usr/bin/pip3 install --upgrade pip -/usr/bin/pip3 install -r requestment - -if [[ $? -eq 0 ]] -then - echo "=======================================================================" - echo "Pip3 requestment 安装成功!" - echo "=======================================================================" -else - echo "=======================================================================" - echo "Pip3 requestment 安装失败!" - echo "=======================================================================" - exit 1 +if [[ ! -f "${CWD}/.pip3.Done" ]]; then + echo "部署pip依赖 ..." + ${PYTHON_VENV_DIR}/bin/pip3 install --upgrade pip + ${PYTHON_VENV_DIR}/bin/pip3 install wheel setuptools + ${PYTHON_VENV_DIR}/bin/pip3 install -r ${CWD}/requirement + if [[ $? -eq 0 ]]; then + touch ${CWD}/.pip3.Done + echo "Pip3 Requirement 安装成功 ..." + else + echo "Pip3 Requirement 安装失败 ..." + exit 1 + fi fi ##处理配置文件 -echo "=======================================================================" -echo "处理uwsgi.ini配置文件" +echo "处理uwsgi.ini配置文件 ..." CPU_NUM=$(cat /proc/cpuinfo | grep processor | wc -l) -sed -i "s@CPU_NUM@${CPU_NUM}@g" ${SHELL_FOLDER}/uwsgi.ini -sed -i "s@PWD_SELF_SERVICE_HOME@${SHELL_FOLDER}@g" ${SHELL_FOLDER}/uwsgi.ini -sed -i "s@PWD_SELF_SERVICE_IP@${PWD_SELF_SERVICE_IP}@g" ${SHELL_FOLDER}/uwsgi.ini -sed -i "s@PWD_SELF_SERVICE_PORT@${PWD_SELF_SERVICE_PORT}@g" ${SHELL_FOLDER}/uwsgi.ini -echo "处理uwsgi.ini配置文件完成" +sed -i "s@CPU_NUM@${CPU_NUM}@g" ${CWD}/uwsgi.ini +sed -i "s@PYTHON_VENV_DIR@${PYTHON_VENV_DIR}@g" ${CWD}/uwsgi.ini +sed -i "s@PWD_SELF_SERVICE_HOME@${CWD}@g" ${CWD}/uwsgi.ini +sed -i "s@PWD_SELF_SERVICE_IP@${PWD_SELF_SERVICE_IP}@g" ${CWD}/uwsgi.ini +sed -i "s@PWD_SELF_SERVICE_PORT@${PWD_SELF_SERVICE_PORT}@g" ${CWD}/uwsgi.ini +echo "处理uwsgi.ini配置文件完成 ..." echo -echo "处理uwsgiserver启动脚本" -sed -i "s@PWD_SELF_SERVICE_HOME@${SHELL_FOLDER}@g" ${SHELL_FOLDER}/uwsgiserver -sed -i "s@PYTHON_INSTALL_DIR@${PYTHON_INSTALL_DIR}@g" ${SHELL_FOLDER}/uwsgiserver +echo "处理uwsgiserver启动脚本 ..." +sed -i "s@PWD_SELF_SERVICE_HOME@${CWD}@g" ${CWD}/uwsgiserver +sed -i "s@PYTHON_VENV_DIR@${PYTHON_VENV_DIR}@g" ${CWD}/uwsgiserver + alias cp='cp' -cp -f ${SHELL_FOLDER}/uwsgiserver /etc/init.d/uwsgiserver +cp -rfp ${CWD}/uwsgiserver /etc/init.d/uwsgiserver + chmod +x /etc/init.d/uwsgiserver -chkconfig uwsgiserver on -echo "处理uwsgiserver启动脚本完成" + +systemctl daemon-reload + +systemctl enable uwsgiserver + +echo "处理uwsgiserver启动脚本完成 ..." echo -sed -i "s@PWD_SELF_SERVICE_DOMAIN@${PWD_SELF_SERVICE_DOMAIN}@g" ${SHELL_FOLDER}/conf/local_settings.py +sed -i "s@PWD_SELF_SERVICE_DOMAIN@${PWD_SELF_SERVICE_DOMAIN}@g" ${CWD}/conf/local_settings.py ##Nginx vhost配置 -cat << EOF > /etc/nginx/conf.d/pwdselfservice.conf +cat << EOF >/etc/nginx/conf.d/pwdselfservice.conf server { listen 80; server_name ${PWD_SELF_SERVICE_DOMAIN} ${PWD_SELF_SERVICE_IP}; @@ -245,20 +350,22 @@ server { access_log off; } EOF -rm -f /etc/nginx/conf.d/default.conf -systemctl restart nginx + +systemctl start nginx + +systemctl start uwsgi echo -echo "=======================================================================" echo -echo "密码自助服务平台的访问地址是:http://${PWD_SELF_SERVICE_DOMAIN}或http://${PWD_SELF_SERVICE_IP}" -echo "请确保以上域名能正常解析,否则使用域名无法访问。" +echo "密码自助服务平台的访问地址是:http://${PWD_SELF_SERVICE_DOMAIN}或http://${PWD_SELF_SERVICE_IP} ..." +echo "请确保以上域名能正常解析,否则使用域名无法访问 ..." +echo "如果本机防火墙是开启状态,请自行放行端口: [80, ${PWD_SELF_SERVICE_PORT}]" echo -echo "Uwsgi启动:/etc/init.d/uwsgi start" -echo "Uwsgi停止:/etc/init.d/uwsgi stop" -echo "Uwsgi重启:/etc/init.d/uwsgi restart" +echo "Uwsgi启动:/etc/init.d/uwsgi start ..." +echo "Uwsgi停止:/etc/init.d/uwsgi stop ..." +echo "Uwsgi重启:/etc/init.d/uwsgi restart ..." echo +echo "Redis Server密码是:${gen_password},可在/etc/redis.conf中查到 ..." echo -echo "文件${SHELL_FOLDER}/conf/local_setting.py中配置参数请自动确认下是否完整" +echo "文件${CWD}/conf/local_setting.py中配置参数请自行确认下是否完整 ..." echo -echo "=======================================================================" diff --git a/conf/local_settings.py b/conf/local_settings.py index 0f9445c..b0774ed 100644 --- a/conf/local_settings.py +++ b/conf/local_settings.py @@ -68,3 +68,7 @@ WEWORK_AGNET_SECRET = r'修改为自己的' HOME_URL = 'PWD_SELF_SERVICE_DOMAIN' # 平台显示的标题 TITLE = 'Self-Service' + +# ####### Redis ########## +REDIS_LOCATION = r'127.0.0.1:6379' +REDIS_PASSWORD = r'PWD_SELF_REDIS_PASSWORD' diff --git a/pwdselfservice/__init__.py b/pwdselfservice/__init__.py index 186c35f..cb16ef4 100644 --- a/pwdselfservice/__init__.py +++ b/pwdselfservice/__init__.py @@ -1,14 +1,22 @@ -import sys - import datetime -from utils.storage.memorystorage import MemoryStorage -from traceback import format_exc +import sys +import traceback +import logging +from django_redis import get_redis_connection +from utils.storage.kvstorage import KvStorage +logger = logging.getLogger(__name__) try: - cache_storage = MemoryStorage() - cache_storage.set('MemoryStorage', str(datetime.datetime.now())) - redis_get = cache_storage.get('MemoryStorage') + redis_conn = get_redis_connection() + cache_storage = KvStorage(redis_conn) + cache_storage.set('test_redis_connection', str(datetime.datetime)) + cache_storage.get('test_redis_connection') + cache_storage.delete('test_redis_connection') + logger.info("Redis连接成功,set/get/delete测试通过...") except Exception as e: - print("MemoryStorage Exception: {}".format(format_exc())) + cache_storage = None + logger.error("Redis无法连接,请排查Redis配置...") + logger.error("{}".format(traceback.format_exc())) sys.exit(1) + diff --git a/pwdselfservice/settings.py b/pwdselfservice/settings.py index c5a15fb..60d6881 100644 --- a/pwdselfservice/settings.py +++ b/pwdselfservice/settings.py @@ -5,8 +5,10 @@ from django.utils.log import DEFAULT_LOGGING APP_ENV = os.getenv('APP_ENV') if APP_ENV == 'dev': DEBUG = True + from conf.local_settings_dev import REDIS_LOCATION, REDIS_PASSWORD else: DEBUG = False + from conf.local_settings import REDIS_LOCATION, REDIS_PASSWORD # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -16,13 +18,8 @@ SECRET_KEY = 'nxnm3#&2tat_c2i6%$y74a)t$(3irh^gpwaleoja1kdv30fmcm' ALLOWED_HOSTS = ['*'] -# 不安全的内部初始密码,用于检验新密码 -UN_SEC_PASSWORD = ['1qaz@WSX', '1234@Abc'] - - # 创建日志的路径 LOG_PATH = os.path.join(BASE_DIR, 'log') -# 如果地址不存在,则会自动创建log文件夹 if not os.path.isdir(LOG_PATH): os.mkdir(LOG_PATH) @@ -44,7 +41,7 @@ logging.config.dictConfig({ 'handlers': { # console logs to stderr 'console': { - "level": "DEBUG", + "level": "ERROR", 'class': 'logging.StreamHandler', 'formatter': 'default', }, @@ -53,8 +50,8 @@ logging.config.dictConfig({ 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(LOG_PATH, "all.log"), 'formatter': 'default', - 'maxBytes': 1024 * 1024 * 50, # 日志大小 10M - 'backupCount': 24, # 备份数为 2# 简单格式 + 'maxBytes': 1024 * 1024 * 50, + 'backupCount': 24, 'encoding': 'utf-8', }, 'django.server': DEFAULT_LOGGING['handlers']['django.server'], @@ -63,7 +60,7 @@ logging.config.dictConfig({ # default for all undefined Python modules '': { 'level': LOGLEVEL, - 'handlers': ['file'], + 'handlers': ['console', 'file'], }, # Default runserver request logging 'django.server': DEFAULT_LOGGING['loggers']['django.server'], @@ -71,18 +68,7 @@ logging.config.dictConfig({ }) -# SESSION -# 只有在settings.SESSION_SAVE_EVERY_REQUEST 为True时才有效 -SESSION_SAVE_EVERY_REQUEST = True -# 过期时间分钟 -SESSION_COOKIE_AGE = 300 -# False 会话cookie可以在用户浏览器中保持有效期。True:关闭浏览器,则Cookie失效。 -SESSION_EXPIRE_AT_BROWSER_CLOSE = True -# session使用的存储方式 -SESSION_ENGINE = 'django.contrib.sessions.backends.cache' - INSTALLED_APPS = [ - 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', @@ -90,19 +76,12 @@ INSTALLED_APPS = [ 'resetpwd', ] -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'unique-snowflake', - } -} MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] @@ -119,7 +98,6 @@ TEMPLATES = [ 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, @@ -128,9 +106,18 @@ TEMPLATES = [ WSGI_APPLICATION = 'pwdselfservice.wsgi.application' -# 514 66050是AD中账号被禁用的特定代码,这个可以在微软官网查到。 -# 可能不是太准确,如果使用者能确定还有其它状态码,可以自行在此处添加 -AD_ACCOUNT_DISABLE_CODE = [514, 66050] +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://{}/1".format(REDIS_LOCATION), + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + "PASSWORD": REDIS_PASSWORD, + "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor", + "IGNORE_EXCEPTIONS": True, + } + } +} AUTH_PASSWORD_VALIDATORS = [ { diff --git a/pwdselfservice/wsgi.py b/pwdselfservice/wsgi.py index 6446a49..727ea0d 100644 --- a/pwdselfservice/wsgi.py +++ b/pwdselfservice/wsgi.py @@ -1,12 +1,3 @@ -""" -WSGI config for pwdselfservice project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ -""" - import os from django.core.wsgi import get_wsgi_application diff --git a/readme.md b/readme.md index 87bc46b..fabc94e 100644 --- a/readme.md +++ b/readme.md @@ -43,13 +43,20 @@ AD必须使用SSL才能修改密码(这里被坑了N久...) + 添加日志装饰器记录请求日志 + 优化ad_ops中异常的处理 + +### 2023/02/10 -- 更新: ++ 修复一些BUG ++ auto-install.sh 重写,支持Ubuntu\Centos ++ 添加Redis缓存,之前使用内存缓存有问题 + **如果想在点击应用之后自动跳转到重置页面,请将回调接口配置成YOURDOMAIN.com/auth** ## 线上环境需要的基础环境: -+ Python 3.8.9 (可自行下载源码包放到项目目录下,使用一键安装) ++ Python 3.8.16 (可自行下载源码包放到项目目录下,使用一键安装) + Nginx + Uwsgi ++ Redis ### 界面效果 diff --git a/requestment b/requirement similarity index 71% rename from requestment rename to requirement index b2bf104..b041965 100644 --- a/requestment +++ b/requirement @@ -1,11 +1,9 @@ Django==3.2 -pycryptodome==3.10.1 attrs==21.2.0 python-dateutil==2.8.1 dingtalk-sdk==1.3.8 cryptography==3.4.7 ldap3==2.9 django-redis==4.12.1 -django-redis==4.12.1 requests==2.28.1 uwsgi==2.0.21 \ No newline at end of file diff --git a/resetpwd/utils.py b/resetpwd/utils.py index 4571efe..afb6810 100644 --- a/resetpwd/utils.py +++ b/resetpwd/utils.py @@ -31,31 +31,6 @@ def code_2_user_detail(ops, home_url, code): return _, s, e -@decorator_logger(logger, log_head='AccountOps', pretty=True, indent=2, verbose=1) -def code_2_user_info_with_oauth2(ops, request, msg_template, home_url, code): - """ - 临时授权码换取userinfo - """ - _status, user_id = ops.get_user_id_by_code(code) - # 判断 user_id 在本企业钉钉/微信中是否存在 - if not _status: - context = {'global_title': TITLE, - 'msg': '获取userid失败,错误信息:{}'.format(user_id), - 'button_click': "window.location.href='%s'" % home_url, - 'button_display': "返回主页" - } - return False, context, user_id - detail_status, user_info = ops.get_user_detail_by_user_id(user_id) - if not detail_status: - context = {'global_title': TITLE, - 'msg': '获取用户信息失败,错误信息:{}'.format(user_info), - 'button_click': "window.location.href='%s'" % home_url, - 'button_display': "返回主页" - } - return False, context, user_info - return True, user_id, user_info - - @decorator_logger(logger, log_head='AccountOps', pretty=True, indent=2, verbose=1) def ops_account(ad_ops, request, msg_template, home_url, username, new_password): """ @@ -65,27 +40,30 @@ def ops_account(ad_ops, request, msg_template, home_url, username, new_password) print("ops_account: {}".format(username)) _status, _account = ad_ops.ad_ensure_user_by_account(username=username) if not _status: - context = {'global_title': TITLE, - 'msg': "账号[%s]在AD中不存在,请确认当前钉钉扫码账号绑定的邮箱是否和您正在使用的邮箱一致?或者该账号己被禁用!\n猜测:您的账号或邮箱是否是带有数字或其它字母区分?" % username, - 'button_click': "window.location.href='%s'" % home_url, - 'button_display': "返回主页" - } + context = { + 'global_title': TITLE, + 'msg': "账号[%s]在AD中不存在,请确认当前钉钉扫码账号绑定的邮箱是否和您正在使用的邮箱一致?或者该账号己被禁用!\n猜测:您的账号或邮箱是否是带有数字或其它字母区分?" % username, + 'button_click': "window.location.href='%s'" % home_url, + 'button_display': "返回主页" + } return render(request, msg_template, context) _status, account_code = ad_ops.ad_get_user_status_by_account(username) if _status and account_code in settings.AD_ACCOUNT_DISABLE_CODE: - context = {'global_title': TITLE, - 'msg': "此账号状态为己禁用,请联系HR确认账号是否正确。", - 'button_click': "window.location.href='%s'" % home_url, - 'button_display': "返回主页" - } + context = { + 'global_title': TITLE, + 'msg': "此账号状态为己禁用,请联系HR确认账号是否正确。", + 'button_click': "window.location.href='%s'" % home_url, + 'button_display': "返回主页" + } return render(request, msg_template, context) elif not _status: - context = {'global_title': TITLE, - 'msg': "错误:{}".format(account_code), - 'button_click': "window.location.href='%s'" % home_url, - 'button_display': "返回主页" - } + context = { + 'global_title': TITLE, + 'msg': "错误:{}".format(account_code), + 'button_click': "window.location.href='%s'" % home_url, + 'button_display': "返回主页" + } return render(request, msg_template, context) if new_password: @@ -94,39 +72,44 @@ def ops_account(ad_ops, request, msg_template, home_url, username, new_password) # 重置密码并执行一次解锁,防止重置后账号还是锁定状态。 unlock_status, result = ad_ops.ad_unlock_user_by_account(username) if unlock_status: - context = {'global_title': TITLE, - 'msg': "密码己修改成功,请妥善保管。你可以点击修改密码或直接关闭此页面!", - 'button_click': "window.location.href='%s'" % home_url, - 'button_display': "返回主页" - } + context = { + 'global_title': TITLE, + 'msg': "密码己修改成功,请妥善保管。你可以点击修改密码或直接关闭此页面!", + 'button_click': "window.location.href='%s'" % home_url, + 'button_display': "返回主页" + } return render(request, msg_template, context) else: - context = {'global_title': TITLE, - 'msg': "密码未修改/重置成功,错误信息:{}".format(result), - 'button_click': "window.location.href='%s'" % '/auth', - 'button_display': "重新认证授权" - } + context = { + 'global_title': TITLE, + 'msg': "密码未修改/重置成功,错误信息:{}".format(result), + 'button_click': "window.location.href='%s'" % '/auth', + 'button_display': "重新认证授权" + } return render(request, msg_template, context) else: unlock_status, result = ad_ops.ad_unlock_user_by_account(username) if unlock_status: - context = {'global_title': TITLE, - 'msg': "账号己解锁成功。你可以点击返回主页或直接关闭此页面!", - 'button_click': "window.location.href='%s'" % home_url, - 'button_display': "返回主页" - } + context = { + 'global_title': TITLE, + 'msg': "账号己解锁成功。你可以点击返回主页或直接关闭此页面!", + 'button_click': "window.location.href='%s'" % home_url, + 'button_display': "返回主页" + } return render(request, msg_template, context) else: - context = {'global_title': TITLE, - 'msg': "账号未能解锁,错误信息:{}".format(result), - 'button_click': "window.location.href='%s'" % '/auth', - 'button_display': "重新认证授权" - } + context = { + 'global_title': TITLE, + 'msg': "账号未能解锁,错误信息:{}".format(result), + 'button_click': "window.location.href='%s'" % '/auth', + 'button_display': "重新认证授权" + } return render(request, msg_template, context) except LDAPException as l_e: - context = {'global_title': TITLE, - 'msg': "账号未能解锁,错误信息:{}".format(l_e), - 'button_click': "window.location.href='%s'" % '/auth', - 'button_display': "重新认证授权" - } + context = { + 'global_title': TITLE, + 'msg': "账号未能解锁,错误信息:{}".format(l_e), + 'button_click': "window.location.href='%s'" % '/auth', + 'button_display': "重新认证授权" + } return render(request, msg_template, context) diff --git a/resetpwd/views.py b/resetpwd/views.py index 85533d7..11bdbd3 100644 --- a/resetpwd/views.py +++ b/resetpwd/views.py @@ -1,15 +1,16 @@ import json import logging import os +import traceback + from django.shortcuts import render from utils.ad_ops import AdOps import urllib.parse as url_encode from utils.format_username import format2username, get_user_is_active, get_email_from_userinfo from .form import CheckForm from .utils import code_2_user_detail, ops_account -from django.conf import settings -from utils.logger_filter import decorator_request_logger from utils.tracecalls import decorator_logger +from pwdselfservice import cache_storage APP_ENV = os.getenv('APP_ENV') if APP_ENV == 'dev': @@ -51,11 +52,9 @@ def auth(request): app_id = scan_params.app_id agent_id = scan_params.agent_id scan_app = scan_params.AUTH_APP - unsecpwd = settings.UN_SEC_PASSWORD redirect_url = url_encode.quote(home_url + '/resetPassword') app_type = INTEGRATION_APP_TYPE global_title = TITLE - if request.method == 'GET': return render(request, 'auth.html', locals()) else: @@ -129,30 +128,27 @@ def reset_password(request): """ home_url = '%s://%s' % (request.scheme, HOME_URL) if request.method == 'GET': - code = request.GET.get('code', None) - username = request.GET.get('username', None) - # 如果满足,说明已经授权免密登录 - if username and code and request.session.get(username) == code: + code = request.GET.get('code') + username = request.GET.get('username') + # 如果从GET路径中提取到username、code,并且在缓存中存在username对应的code值,说明已经认证过 + if username and code and cache_storage.get(username) == code: context = { 'global_title': TITLE, 'username': username, 'code': code, } return render(request, 'reset_password.html', context) + # 否则就是第一次认证,用code换取用户信息 else: - if code: - logger.info('[成功] 请求方法:%s,请求路径:%s,Code:%s' % (request.method, request.path, code)) - else: - logger.error('[异常] 请求方法:%s,请求路径:%s,未能拿到Code。' % (request.method, request.path)) + if not code: context = { 'global_title': TITLE, - 'msg': "错误,临时授权码己失效,请尝试重新认证授权..", + 'msg': "临时授权码己失效,请尝试重新认证授权...", 'button_click': "window.location.href='%s'" % '/auth', 'button_display': "重新认证授权" } return render(request, msg_template, context) try: - # 用code换取用户基本信息 _status, user_id, user_info = code_2_user_detail(_ops, home_url, code) if not _status: return render(request, msg_template, user_id) @@ -166,6 +162,44 @@ def reset_password(request): 'button_display': "返回主页" } return render(request, msg_template, context) + + # 通过user_info拿到用户邮箱,并格式化为username + _, email = get_email_from_userinfo(user_info) + if not _: + context = { + 'global_title': TITLE, + 'msg': email, + 'button_click': "window.location.href='%s'" % '/auth', + 'button_display': "重新认证授权" + } + return render(request, msg_template, context) + _, username = format2username(email) + if _ is False: + context = { + 'global_title': TITLE, + 'msg': username, + 'button_click': "window.location.href='%s'" % '/auth', + 'button_display': "重新认证授权" + } + return render(request, msg_template, context) + if username: + cache_storage.set(username, code, ttl=300) + context = { + 'global_title': TITLE, + 'username': username, + 'code': code, + } + return render(request, 'reset_password.html', context) + else: + context = { + 'global_title': TITLE, + 'msg': "{},您好,企业{}中未能找到您账号的邮箱配置,请联系HR完善信息。".format( + user_info.get('name'), scan_params.AUTH_APP), + 'button_click': "window.location.href='%s'" % '/auth', + 'button_display': "重新认证授权" + } + return render(request, msg_template, context) + except Exception as callback_e: context = { 'global_title': TITLE, @@ -176,50 +210,11 @@ def reset_password(request): logger.error('[异常] :%s' % str(callback_e)) return render(request, msg_template, context) - # 通过user_info拿到用户邮箱,并格式化为username - _, email = get_email_from_userinfo(user_info) - if not _: - context = { - 'global_title': TITLE, - 'msg': email, - 'button_click': "window.location.href='%s'" % '/auth', - 'button_display': "重新认证授权" - } - return render(request, msg_template, context) - - _, username = format2username(email) - if _ is False: - context = { - 'global_title': TITLE, - 'msg': username, - 'button_click': "window.location.href='%s'" % '/auth', - 'button_display': "重新认证授权" - } - return render(request, msg_template, context) - - if username: - request.session[username] = code - context = { - 'global_title': TITLE, - 'username': username, - 'code': code, - } - return render(request, 'reset_password.html', context) - else: - context = { - 'global_title': TITLE, - 'msg': "{},您好,企业{}中未能找到您账号的邮箱配置,请联系HR完善信息。".format( - user_info.get('name'), scan_params.AUTH_APP), - 'button_click': "window.location.href='%s'" % '/auth', - 'button_display': "重新认证授权" - } - return render(request, msg_template, context) - # 重置密码页面,输入新密码后点击提交 elif request.method == 'POST': username = request.POST.get('username') code = request.POST.get('code') - if username and code and request.session.get(username) == code: + if username and code and cache_storage.get(username) == code: _new_password = request.POST.get('new_password').strip() try: return ops_account(ad_ops=AdOps(), request=request, msg_template=msg_template, home_url=home_url, @@ -255,7 +250,7 @@ def unlock_account(request): if request.method == 'GET': code = request.GET.get('code') username = request.GET.get('username') - if username and code and request.session.get(username) == code: + if username and code and cache_storage.get(username) == code: context = { 'global_title': TITLE, 'username': username, @@ -274,7 +269,7 @@ def unlock_account(request): if request.method == 'POST': username = request.POST.get('username') code = request.POST.get('code') - if username and code and request.session.get(username) == code: + if username and code and cache_storage.get(username) == code: try: return ops_account(AdOps(), request, msg_template, home_url, username, None) except Exception as reset_e: @@ -284,7 +279,7 @@ def unlock_account(request): 'button_click': "window.location.href='%s'" % home_url, 'button_display': "返回主页" } - logger.error('[异常] :%s' % str(reset_e)) + logger.error('{}' .format(traceback.format_exc())) return render(request, msg_template, context) else: context = { diff --git a/utils/ad_ops.py b/utils/ad_ops.py index a7482d9..b2cba2e 100644 --- a/utils/ad_ops.py +++ b/utils/ad_ops.py @@ -63,8 +63,7 @@ class AdOps(object): def __server(self): if self.server is None: try: - self.server = Server(host='%s' % LDAP_HOST, connect_timeout=1, use_ssl=self.use_ssl, port=self.port, - get_info=ALL) + self.server = Server(host='%s' % LDAP_HOST, connect_timeout=1, use_ssl=self.use_ssl, port=self.port, get_info=ALL) except LDAPInvalidCredentialsResult as lic_e: return False, LDAPOperationResult("LDAPInvalidCredentialsResult: " + str(lic_e.message)) except LDAPOperationResult as lo_e: @@ -139,24 +138,7 @@ class AdOps(object): """ try: self.__conn() - return True, self.conn.search(BASE_DN, SEARCH_FILTER.format(username), - attributes=['sAMAccountName']) - except IndexError: - return False, "AdOps Exception: Connect.search未能检索到任何信息,当前账号可能被排除在之外,请联系管理员处理。" - except Exception as e: - return False, "AdOps Exception: {}".format(e) - - @decorator_logger(logger, log_head='AdOps', pretty=True, indent=2, verbose=1) - def ad_get_user_displayname_by_account(self, username): - """ - 通过username查询某个用户的显示名 - :param username: - :return: user_displayname - """ - try: - self.__conn() - self.conn.search(BASE_DN, SEARCH_FILTER.format(username), attributes=['name']) - return True, self.conn.entries[0]['name'] + return True, self.conn.search(BASE_DN, SEARCH_FILTER.format(username), attributes=['sAMAccountName']) except IndexError: return False, "AdOps Exception: Connect.search未能检索到任何信息,当前账号可能被排除在之外,请联系管理员处理。" except Exception as e: diff --git a/utils/dingding_ops.py b/utils/dingding_ops.py index 2b7b5d0..8a0482b 100644 --- a/utils/dingding_ops.py +++ b/utils/dingding_ops.py @@ -59,18 +59,20 @@ class DingDingOps(AppKeyClient): _status, user_id = self.get_user_id_by_code(code) # 判断 user_id 在本企业钉钉/微信中是否存在 if not _status: - context = {'global_title': TITLE, - 'msg': '获取userid失败,错误信息:{}'.format(user_id), - 'button_click': "window.location.href='%s'" % home_url, - 'button_display': "返回主页" - } + context = { + 'global_title': TITLE, + 'msg': '获取userid失败,错误信息:{}'.format(user_id), + 'button_click': "window.location.href='%s'" % home_url, + 'button_display': "返回主页" + } return False, context, user_id detail_status, user_info = self.get_user_detail_by_user_id(user_id) if not detail_status: - context = {'global_title': TITLE, - 'msg': '获取用户信息失败,错误信息:{}'.format(user_info), - 'button_click': "window.location.href='%s'" % home_url, - 'button_display': "返回主页" - } + context = { + 'global_title': TITLE, + 'msg': '获取用户信息失败,错误信息:{}'.format(user_info), + 'button_click': "window.location.href='%s'" % home_url, + 'button_display': "返回主页" + } return False, context, user_info return True, user_id, user_info diff --git a/utils/wework_ops.py b/utils/wework_ops.py index 9ca9dca..e22d65f 100644 --- a/utils/wework_ops.py +++ b/utils/wework_ops.py @@ -161,31 +161,33 @@ class WeWorkOps(AbstractApi): _status, ticket_data = self.get_user_ticket_by_code_with_oauth2(code) # 判断 user_ticket 是否存在 if not _status: - context = {'global_title': TITLE, - 'msg': '获取userid失败,错误信息:{}'.format(ticket_data), - 'button_click': "window.location.href='%s'" % '/auth', - 'button_display': "重新认证授权" - } + context = { + 'global_title': TITLE, + 'msg': '获取userid失败,错误信息:{}'.format(ticket_data), + 'button_click': "window.location.href='%s'" % '/auth', + 'button_display': "重新认证授权" + } return False, context, ticket_data user_id = ticket_data.get('userid') if ticket_data.get('user_ticket') is None: - context = {'global_title': TITLE, - 'msg': '获取用户Ticket失败,当前扫码用户[{}]可能未加入企业!'.format(user_id), - 'button_click': "window.location.href='%s'" % home_url, - 'button_display': "返回修改密码" - } + context = { + 'global_title': TITLE, + 'msg': '获取用户Ticket失败,当前扫码用户[{}]可能未加入企业!'.format(user_id), + 'button_click': "window.location.href='%s'" % home_url, + 'button_display': "返回修改密码" + } return False, context, user_id # 通过user_ticket获取企业微信用户详情信息 detail_status, user_info = self.get_user_info_by_ticket_with_oauth2(ticket_data.get('user_ticket')) print("get_user_info_by_ticket_with_oauth2 --- ", user_info) if not detail_status: - context = {'global_title': TITLE, - 'msg': '获取用户信息失败,错误信息:{}'.format(user_id), - 'button_click': "window.location.href='%s'" % '/auth', - 'button_display': "重新认证授权" - } + context = { + 'global_title': TITLE, + 'msg': '获取用户信息失败,错误信息:{}'.format(user_id), + 'button_click': "window.location.href='%s'" % '/auth', + 'button_display': "重新认证授权" + } return False, context return True, user_id, user_info - diff --git a/uwsgi.ini b/uwsgi.ini index 3a835a9..86903c4 100644 --- a/uwsgi.ini +++ b/uwsgi.ini @@ -5,7 +5,8 @@ chdir = PWD_SELF_SERVICE_HOME env=DJANGO_SETTINGS_MODULE=pwdselfservice.settings -module = pwdselfservice.wsgi:application +;module = pwdselfservice.wsgi:application +wsgi-file = PWD_SELF_SERVICE_HOME/pwdselfservice/wsgi.py master = true diff --git a/uwsgiserver b/uwsgiserver old mode 100644 new mode 100755 index d98a905..acc8b1e --- a/uwsgiserver +++ b/uwsgiserver @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # Startup script for the uwsgi server # chkconfig: - 85 15 # description: uwsgi server is Web Server @@ -6,7 +6,7 @@ # processname: uwsgiserver INI="PWD_SELF_SERVICE_HOME/uwsgi.ini" -UWSGI="PYTHON_INSTALL_DIR/bin/uwsgi" +UWSGI="PYTHON_VENV_DIR/bin/uwsgi" PSID="ps aux | grep "uwsgi"| grep -v "grep" | wc -l" if [ ! -n "$1" ]