### 2021/05/19 -- 更新:

+ 添加了企业微信支持,修改pwdselfservice/local_settings.py中的SCAN_CODE_TYPE = 'DING'或SCAN_CODE_TYPE = 'WEWORK',区分使用哪个应用扫码验证
+ 添加Reids缓存Token支持,如果不配置Redis则使用MemoryStorage缓存到内存中
This commit is contained in:
向乐🌌 2021-05-19 17:07:26 +08:00
parent 00d1d9a03c
commit 89b1c0de46
29 changed files with 753 additions and 440 deletions

View File

@ -2,6 +2,7 @@
import os
import sys
from utils.ad_ops import AdOps
from django_redis import get_redis_connection
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pwdselfservice.settings')

View File

@ -0,0 +1,18 @@
import datetime
from django_redis import get_redis_connection
from utils.storage.memorystorage import MemoryStorage
from utils.storage.kvstorage import KvStorage
try:
redis_conn = get_redis_connection()
cache_storage = KvStorage(redis_conn)
cache_storage.set('redis_connection', str(datetime.datetime))
cache_storage.get('redis_connection')
print("Redis连接成功set/get测试通过Token缓存将使用Redis处理")
except Exception as e:
cache_storage = MemoryStorage()
print("Redis无法连接Token缓存将使用MemoryStorage处理")
print("如果确定需要使用Redis作为缓存请排查Redis配置")
print("Exception: {}".format(e))

View File

@ -2,7 +2,7 @@
# AD主机可以是IP或主机域名例如可以是: abc.com或172.16.122.1
AD_HOST = r'修改成自己的'
# AD域控的DOMAIN名例如abc、abc.com
# AD域控的DOMAIN名例如abc
AD_DOMAIN = r'修改成自己的'
# 用于登录AD做用户信息处理的账号需要有修改用户账号密码或信息的权限。
@ -20,9 +20,12 @@ AD_USE_SSL = True
# 连接的端口如果启用SSL默认是636否则就是389
AD_CONN_PORT = 636
# 扫码验证的类型
# 钉钉 / 企业微信,自行修改
# 值是DING / WEWORK
SCAN_CODE_TYPE = 'DING'
# ########## 钉钉
# 钉钉配置
# ########## 钉钉 《如果不使用钉钉扫码,可不用配置》##########
# 钉钉接口主地址,不可修改
DING_URL = r'https://oapi.dingtalk.com'
@ -34,11 +37,26 @@ DING_AGENT_ID = r'修改为自己的'
DING_APP_KEY = r'修改为自己的'
DING_APP_SECRET = r'修改为自己的'
# 移动应用接入 主要为了实现通过扫码拿到用户的unioid
# 移动应用接入 主要为了实现通过扫码拿到用户的unionid
DING_MO_APP_ID = r'修改为自己的'
DING_MO_APP_SECRET = r'修改为自己的'
# 执行python3 ./resetpwd/utils/crypto.py 生成
# ####### 企业微信《如果不使用企业微信扫码,可不用配置》 ##########
# 企业微信的企业ID
WEWORK_CORP_ID = r'修改为自己的'
# 应用的AgentId
WEWORK_AGENT_ID = r'修改为自己的'
# 应用的Secret
WEWORK_AGNET_SECRET = r'修改为自己的'
# Redis配置
# redis的连接地址redis://<Ip/Host>:<Port>/<数据库>
REDIS_LOCATION = r'redis://127.0.0.1:6379/1'
REDIS_PASSWORD = r'12345678'
# ##########################
# 执行python3 ./utils/crypto.py 生成
# 可自行生成后替换
CRYPTO_KEY = b'dp8U9y7NAhCD3MoNwPzPBhBtTZ1uI_WWSdpNs6wUDgs='
@ -46,4 +64,5 @@ CRYPTO_KEY = b'dp8U9y7NAhCD3MoNwPzPBhBtTZ1uI_WWSdpNs6wUDgs='
TMPID_COOKIE_AGE = 300
# 主页域名钉钉跳转等需要指定域名格式pwd.abc.com。
# 如果是自定义安装,请修改成自己的域名
HOME_URL = 'PWD_SELF_SERVICE_DOMAIN'

View File

@ -1,16 +1,5 @@
"""
Django settings for pwdselfservice project.
Generated by 'django-admin startproject' using Django 2.1.8.
For more information on this file, see
https://docs.djangoproject.com/en/2.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/ref/settings/
"""
import os
from pwdselfservice.local_settings import REDIS_PASSWORD, REDIS_LOCATION
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -127,17 +116,18 @@ TEMPLATES = [
WSGI_APPLICATION = 'pwdselfservice.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
# }
# }
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_LOCATION,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PASSWORD": REDIS_PASSWORD,
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
"IGNORE_EXCEPTIONS": True,
}
}
}
AUTH_PASSWORD_VALIDATORS = [
{
@ -170,4 +160,3 @@ STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]

View File

@ -6,7 +6,7 @@
用户自行重置密码时如果通过手机号来进行钉钉与AD之间的验证就行不通了。
### 新版本逻辑:
>用户扫码通过之后通过临时授权码提取用户的unionid再通过unionid判断用户在本企业中是否存在。如果存在,提取用户钉钉账号的邮箱通过邮箱转成账号将账号拿到AD中进行比对来验证账号在AD中是否存在并账号状态是激活的。满足以上条件的账号就会视为可自行重置密码。
>用户扫码通过之后通过临时授权码提取用户的userid再通过userid断用户在本企业中是否存在。如果存在,提取钉钉/企业微信用户的邮箱通过邮箱转成账号将账号拿到AD中进行比对来验证账号在AD中是否存在并账号状态是激活的。满足以上条件的账号就会视为可自行重置密码。
### 代码提交到--新分支:
```
@ -29,6 +29,14 @@ AD必须使用SSL才能修改密码这里被坑了N久...
+ 重写了用户账号的格式兼容现在用户账号可以兼容username、DOMAIN\username、username@abc.com这三种格式。
+ 优化了整体的代码逻辑,去掉一些冗余重复的代码。
### 2021/05/19 -- 更新:
+ 添加了企业微信支持修改pwdselfservice/local_settings.py中的SCAN_CODE_TYPE = 'DING'或SCAN_CODE_TYPE = 'WEWORK',区分使用哪个应用扫码验证
+ 添加Reids缓存Token支持如果不配置Redis则使用MemoryStorage缓存到内存中
Redis的安装和配置方法请自行百度比较简单
> 切记Redis一定请配置密码弱密码或没有密码的Redis如果不小心暴露到公网极其容易导致机器被黑用来挖矿。
整体验证逻辑不变如果需要使用其它字段关联到AD验证的请自行修改代码。
## 线上环境需要的基础环境:
+ Python 3.8.9 (可自行下载源码包放到项目目录下,使用一键安装)
@ -37,6 +45,7 @@ AD必须使用SSL才能修改密码这里被坑了N久...
## 截图
![截图1](screenshot/Snipaste_2019-07-15_20-05-49.jpg)
![截图2](screenshot/Snipaste_2019-07-15_20-06-14.jpg)
@ -48,7 +57,9 @@ AD必须使用SSL才能修改密码这里被坑了N久...
参考截图配置:
![截图3](screenshot/h5微应用.png)
![截图4](screenshot/h5微应用--开发管理.png)
![截图5](screenshot/h5微应用--权限管理.png)
#### 移动接入应用--登录权限:
@ -59,6 +70,19 @@ AD必须使用SSL才能修改密码这里被坑了N久...
![截图6](screenshot/移动应用接入--登录.png)
## 企业微信必要条件:
* 创建应用记录下企业的CorpId应用的ID和Secret。
参考截图:
![截图7](screenshot/微扫码13.png)
![截图8](screenshot/微扫码14.png)
![截图9](screenshot/微扫码15.png)
![截图10](screenshot/微扫码16.png)
## 使用脚本自动部署:
使用脚本自动快速部署只适合Centos其它发行版本的Linux请自行修改相关命令。
@ -72,7 +96,7 @@ AD必须使用SSL才能修改密码这里被坑了N久...
# AD主机可以是IP或主机域名例如可以是: abc.com或172.16.122.1
AD_HOST = r'修改成自己的'
# AD域控的DOMAIN名例如abc、abc.com
# AD域控的DOMAIN名例如abc
AD_DOMAIN = r'修改成自己的'
# 用于登录AD做用户信息处理的账号需要有修改用户账号密码或信息的权限。
@ -90,9 +114,12 @@ AD_USE_SSL = True
# 连接的端口如果启用SSL默认是636否则就是389
AD_CONN_PORT = 636
# 扫码验证的类型
# 钉钉 / 企业微信,自行修改
# 值是DING / WEWORK
SCAN_CODE_TYPE = 'DING'
# ########## 钉钉
# 钉钉配置
# ########## 钉钉 《如果不使用钉钉扫码,可不用配置》##########
# 钉钉接口主地址,不可修改
DING_URL = r'https://oapi.dingtalk.com'
@ -104,11 +131,26 @@ DING_AGENT_ID = r'修改为自己的'
DING_APP_KEY = r'修改为自己的'
DING_APP_SECRET = r'修改为自己的'
# 移动应用接入 主要为了实现通过扫码拿到用户的unioid
# 移动应用接入 主要为了实现通过扫码拿到用户的unionid
DING_MO_APP_ID = r'修改为自己的'
DING_MO_APP_SECRET = r'修改为自己的'
# 执行python3 ./resetpwd/utils/crypto.py 生成
# ####### 企业微信《如果不使用企业微信扫码,可不用配置》 ##########
# 企业微信的企业ID
WEWORK_CORP_ID = r'修改为自己的'
# 应用的AgentId
WEWORK_AGENT_ID = r'修改为自己的'
# 应用的Secret
WEWORK_AGNET_SECRET = r'修改为自己的'
# Redis配置
# redis的连接地址redis://<Ip/Host>:<Port>/<数据库>
REDIS_LOCATION = r'redis://127.0.0.1:6379/1'
REDIS_PASSWORD = r'12345678'
# ##########################
# 执行python3 ./utils/crypto.py 生成
# 可自行生成后替换
CRYPTO_KEY = b'dp8U9y7NAhCD3MoNwPzPBhBtTZ1uI_WWSdpNs6wUDgs='
@ -116,6 +158,7 @@ CRYPTO_KEY = b'dp8U9y7NAhCD3MoNwPzPBhBtTZ1uI_WWSdpNs6wUDgs='
TMPID_COOKIE_AGE = 300
# 主页域名钉钉跳转等需要指定域名格式pwd.abc.com。
# 如果是自定义安装,请修改成自己的域名
HOME_URL = 'PWD_SELF_SERVICE_DOMAIN'
````
### 执行部署脚本
@ -146,7 +189,7 @@ chmod +x auto-install.sh
# AD主机可以是IP或主机域名例如可以是: abc.com或172.16.122.1
AD_HOST = r'修改成自己的'
# AD域控的DOMAIN名例如abc、abc.com
# AD域控的DOMAIN名例如abc
AD_DOMAIN = r'修改成自己的'
# 用于登录AD做用户信息处理的账号需要有修改用户账号密码或信息的权限。
@ -164,9 +207,12 @@ AD_USE_SSL = True
# 连接的端口如果启用SSL默认是636否则就是389
AD_CONN_PORT = 636
# 扫码验证的类型
# 钉钉 / 企业微信,自行修改
# 值是DING / WEWORK
SCAN_CODE_TYPE = 'DING'
# ########## 钉钉
# 钉钉配置
# ########## 钉钉 《如果不使用钉钉扫码,可不用配置》##########
# 钉钉接口主地址,不可修改
DING_URL = r'https://oapi.dingtalk.com'
@ -178,11 +224,26 @@ DING_AGENT_ID = r'修改为自己的'
DING_APP_KEY = r'修改为自己的'
DING_APP_SECRET = r'修改为自己的'
# 移动应用接入 主要为了实现通过扫码拿到用户的unioid
# 移动应用接入 主要为了实现通过扫码拿到用户的unionid
DING_MO_APP_ID = r'修改为自己的'
DING_MO_APP_SECRET = r'修改为自己的'
# 执行python3 ./resetpwd/utils/crypto.py 生成
# ####### 企业微信《如果不使用企业微信扫码,可不用配置》 ##########
# 企业微信的企业ID
WEWORK_CORP_ID = r'修改为自己的'
# 应用的AgentId
WEWORK_AGENT_ID = r'修改为自己的'
# 应用的Secret
WEWORK_AGNET_SECRET = r'修改为自己的'
# Redis配置
# redis的连接地址redis://<Ip/Host>:<Port>/<数据库>
REDIS_LOCATION = r'redis://127.0.0.1:6379/1'
REDIS_PASSWORD = r'12345678'
# ##########################
# 执行python3 ./utils/crypto.py 生成
# 可自行生成后替换
CRYPTO_KEY = b'dp8U9y7NAhCD3MoNwPzPBhBtTZ1uI_WWSdpNs6wUDgs='
@ -190,6 +251,7 @@ CRYPTO_KEY = b'dp8U9y7NAhCD3MoNwPzPBhBtTZ1uI_WWSdpNs6wUDgs='
TMPID_COOKIE_AGE = 300
# 主页域名钉钉跳转等需要指定域名格式pwd.abc.com。
# 如果是自定义安装,请修改成自己的域名
HOME_URL = 'PWD_SELF_SERVICE_DOMAIN'
````

View File

@ -2,5 +2,6 @@ Django==3.2
dingtalk-sdk==1.3.8
cryptography==3.4.7
ldap3==2.9
django-redis==4.12.1
requests
uwsgi

View File

@ -1,40 +1,51 @@
import logging
import sys
from django.http import *
from django.shortcuts import render
from pwdselfservice.local_settings import SCAN_CODE_TYPE, DING_MO_APP_ID, WEWORK_CORP_ID, WEWORK_AGENT_ID, HOME_URL, CRYPTO_KEY, TMPID_COOKIE_AGE
from utils.ad_ops import *
from utils.ad_ops import AdOps
from utils.crypto import Crypto
from utils.dingding_ops import *
from utils.format_username import format2username
from utils.format_username import format2username, get_user_is_active
from .form import CheckForm
msg_template = 'messages.html'
logger = logging.getLogger('django')
try:
class PARAMS(object):
if SCAN_CODE_TYPE == 'DING':
app_id = DING_MO_APP_ID
agent_id = None
SCAN_APP = '钉钉'
from utils.dingding_ops import DingDingOps
ops = DingDingOps()
elif SCAN_CODE_TYPE == 'WEWORK':
app_id = WEWORK_CORP_ID
agent_id = WEWORK_AGENT_ID
SCAN_APP = '微信'
from utils.wework_ops import WeWorkOps
ops = WeWorkOps()
scan_params = PARAMS()
_ops = scan_params.ops
ad_ops = AdOps()
except Exception as e:
print(e)
sys.exit(1)
try:
ding_ops = DingDingOps()
except Exception as e:
print(e)
sys.exit(1)
def index(request):
"""
用户自行修改密码
用户自行修改密码/首页
:param request:
:return:
"""
home_url = '%s://%s' % (request.scheme, HOME_URL)
app_id = DING_MO_APP_ID
if request.method == 'GET':
return render(request, 'index.html', locals())
app_id = scan_params.app_id
agent_id = scan_params.agent_id
if request.method == 'GET' and SCAN_CODE_TYPE == 'DING':
return render(request, 'ding_index.html', locals())
elif request.method == 'GET' and SCAN_CODE_TYPE == 'WEWORK':
return render(request, 'we_index.html', locals())
else:
logger.error('[异常] 请求方法:%s,请求路径:%s' % (request.method, request.path))
@ -68,8 +79,9 @@ def index(request):
return render(request, msg_template, context)
# 514 66050是AD中账号被禁用的特定代码这个可以在微软官网查到。
# 可能不是太准确
if ad_ops.ad_get_user_status_by_account(username) == 514 or ad_ops.ad_get_user_status_by_account(username) == 66050:
# 可能不是太准确,如果使用者能确定还有其它状态码,可以自行在此处添加
account_code = ad_ops.ad_get_user_status_by_account(username)
if account_code == 514 or account_code == 66050:
context = {
'msg': "此账号状态为己禁用请联系HR确认账号是否正确。",
'button_click': "window.location.href='%s'" % home_url,
@ -113,65 +125,57 @@ def callback_check(request):
logger.info('[成功] 请求方法:%s,请求路径:%sCODE%s' % (request.method, request.path, code))
else:
logger.error('[异常] 请求方法:%s,请求路径:%s未能拿到CODE。' % (request.method, request.path))
try:
union_status, union_id = ding_ops.ding_get_union_id_by_code(code)
# 判断 union_id 在本企业钉钉中是否存在
if not union_status:
logger.error('[异常] 请求方法:%s,请求路径:%s未能拿到union_id。' % (request.method, request.path))
_status, user_id = _ops.get_user_id_by_code(code)
# 判断 user_id 在本企业钉钉/微信中是否存在
if not _status:
context = {
'msg': '未能在企业钉钉中检索到用户信息,错误信息:{}' .format(union_id),
'msg': '获取钉钉userid失败错误信息{}'.format(user_id),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
userid_status, user_result = ding_ops.ding_get_userid_by_union_id(union_id)
if not userid_status:
context = {
'msg': '获取钉钉userid失败错误信息{}'.format(user_result),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
detail_status, ding_user_info = ding_ops.ding_get_userinfo_detail(user_result)
detail_status, user_info = _ops.get_user_detail_by_user_id(user_id)
if not detail_status:
context = {
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(ding_user_info),
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
# 钉钉中此账号是否可用
if ding_user_info['active']:
# 账号是否是激活的
if get_user_is_active(user_info):
crypto = Crypto(CRYPTO_KEY)
# 对union_id进行加密因为union_id基本上固定不变的为了防止union_id泄露而导致重复使用进行加密后再传回。
union_id_cryto = crypto.encrypt(union_id)
# 配置cookie通过cookie把加密后的用户union_id传到重置密码页面并重定向到重置密码页面。
# 对user_id进行加密因为user_id基本上固定不变的为了防止user_id泄露而导致重复使用进行加密后再传回。
_id_cryto = crypto.encrypt(user_id)
# 配置cookie通过cookie把加密后的用户user_id传到重置密码页面并重定向到重置密码页面。
set_cookie = HttpResponseRedirect('resetPassword')
set_cookie.set_cookie('tmpid', union_id_cryto, expires=TMPID_COOKIE_AGE)
set_cookie.set_cookie('tmpid', _id_cryto, expires=TMPID_COOKIE_AGE)
return set_cookie
else:
context = {
'msg': '[%s]在钉钉中未激活或可能己离职' % format2username(ding_user_info['name']),
'msg': '[%s]在钉钉中未激活或可能己离职' % format2username(user_info.get('name')),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except KeyError:
context = {
'msg': "错误,钉钉临时Code己失效请从主页重新扫码",
'msg': "错误,临时授权码己失效,请从主页重新扫码验证",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
logger.error('[异常] %s' % str(KeyError))
return render(request, msg_template, context)
except Exception as e:
except Exception as callback_e:
context = {
'msg': "错误[%s],请与管理员联系." % str(e),
'msg': "错误[%s],请与管理员联系." % str(callback_e),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
logger.error('[异常] %s' % str(e))
logger.error('[异常] %s' % str(callback_e))
return render(request, msg_template, context)
@ -181,17 +185,17 @@ def reset_pwd_by_ding_callback(request):
:param request:
:return:
"""
global union_id_crypto
global tmpid_crypto
home_url = '%s://%s' % (request.scheme, HOME_URL)
# 从cookie中提取union_id并解密然后对当前union_id的用户进行重置密码
if request.method == 'GET':
try:
union_id_crypto = request.COOKIES.get('tmpid')
tmpid_crypto = request.COOKIES.get('tmpid')
except Exception as e:
union_id_crypto = None
tmpid_crypto = None
logger.error('[异常] %s' % str(e))
if not union_id_crypto:
logger.error('[异常] 请求方法:%s,请求路径:%s,未能拿到CODE或CODE己超时。' % (request.method, request.path))
if not tmpid_crypto:
logger.error('[异常] 请求方法:%s,请求路径:%s,未能拿到TmpID或会话己超时。' % (request.method, request.path))
context = {
'msg': "会话己超时,请重新扫码验证用户信息。",
'button_click': "window.location.href='%s'" % home_url,
@ -200,35 +204,27 @@ def reset_pwd_by_ding_callback(request):
return render(request, msg_template, context)
# 解密
crypto = Crypto(CRYPTO_KEY)
union_id = crypto.decrypt(union_id_crypto)
# 通过union_id在钉钉中拿到用户的邮箱并格式化为username
userid_status, user_result = ding_ops.ding_get_userid_by_union_id(union_id)
user_id = crypto.decrypt(tmpid_crypto)
# 通过user_id拿到用户的邮箱并格式化为username
userid_status, user_result = _ops.get_user_detail_by_user_id(user_id)
if not userid_status:
context = {
'msg': '获取钉钉userid失败,错误信息:{}'.format(user_result),
'msg': '获取{}用户信息失败,错误信息:{}'.format(user_result, scan_params.SCAN_APP),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
detail_status, ding_user_info = ding_ops.ding_get_userinfo_detail(user_result)
if not detail_status:
context = {
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(ding_user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
username = format2username(ding_user_info['email'])
# 如果邮箱在钉钉中能提取到,则格式化之后,提取出账号提交到前端绑定
username = format2username(user_result['email'])
# 如果邮箱能提取到,则格式化之后,提取出账号提交到前端绑定
if username:
context = {
'username': username,
}
return render(request, 'resetPassword.html', context)
# 否则就是钉钉中此用户未配置邮箱,返回相关提示
# 否则就是用户未配置邮箱,返回相关提示
else:
context = {
'msg': "%s您好企业钉钉中未能找到您账号的邮箱配置请联系HR完善信息。" % ding_user_info['name'],
'msg': "{},您好,企业{}中未能找到您账号的邮箱配置请联系HR完善信息。" .format(user_result.get('name'), scan_params.SCAN_APP),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
@ -238,11 +234,11 @@ def reset_pwd_by_ding_callback(request):
elif request.method == 'POST':
_new_password = request.POST.get('new_password').strip()
try:
union_id_crypto = request.COOKIES.get('tmpid')
tmpid_crypto = request.COOKIES.get('tmpid')
except Exception as e:
union_id_crypto = None
tmpid_crypto = None
logger.error('[异常] %s' % str(e))
if not union_id_crypto:
if not tmpid_crypto:
logger.error('[异常] 请求方法:%s,请求路径:%s未能拿到CODE或CODE己超时。' % (request.method, request.path))
context = {
'msg': "会话己超时,请重新扫码验证用户信息。",
@ -251,24 +247,16 @@ def reset_pwd_by_ding_callback(request):
}
return render(request, msg_template, context)
crypto = Crypto(CRYPTO_KEY)
union_id = crypto.decrypt(union_id_crypto)
userid_status, user_result = ding_ops.ding_get_userid_by_union_id(union_id)
user_id = crypto.decrypt(tmpid_crypto)
userid_status, user_result = _ops.get_user_detail_by_user_id(user_id)
if not userid_status:
context = {
'msg': '获取钉钉userid失败,错误信息:{}'.format(user_result),
'msg': '获取企业{}用户信息失败,错误信息:{}'.format(scan_params.SCAN_APP, user_result),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
detail_status, ding_user_info = ding_ops.ding_get_userinfo_detail(user_result)
if not detail_status:
context = {
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(ding_user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
username = format2username(ding_user_info['email'])
username = format2username(user_result['email'])
if ad_ops.ad_ensure_user_by_account(username) is False:
context = {
'msg': "账号[%s]在AD中不存在请确认当前钉钉扫码账号绑定的邮箱是否和您正在使用的邮箱一致或者该账号己被禁用\n猜测:您的账号或邮箱是否是带有数字或其它字母区分?" % username,
@ -276,7 +264,11 @@ def reset_pwd_by_ding_callback(request):
'button_display': "返回主页"
}
return render(request, msg_template, context)
if ad_ops.ad_get_user_status_by_account(username) == 514 or ad_ops.ad_get_user_status_by_account(username) == 66050:
# 514 66050是AD中账号被禁用的特定代码这个可以在微软官网查到。
# 可能不是太准确,如果使用者能确定还有其它状态码,可以自行在此处添加
account_code = ad_ops.ad_get_user_status_by_account(username)
if account_code == 514 or account_code == 66050:
context = {
'msg': "此账号状态为己禁用请联系HR确认账号是否正确。",
'button_click': "window.location.href='%s'" % home_url,
@ -328,24 +320,16 @@ def unlock_account(request):
}
return render(request, msg_template, context)
crypto = Crypto(CRYPTO_KEY)
union_id = crypto.decrypt(_union_id_crypto)
userid_status, user_result = ding_ops.ding_get_userid_by_union_id(union_id)
user_id = crypto.decrypt(_union_id_crypto)
userid_status, user_info = _ops.get_user_detail_by_user_id(user_id)
if not userid_status:
context = {
'msg': '获取钉钉userid失败错误信息{}'.format(user_result),
'msg': '获取{}用户信息失败,错误信息:{}'.format(scan_params.SCAN_APP, user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
detail_status, ding_user_info = ding_ops.ding_get_userinfo_detail(user_result)
if not detail_status:
context = {
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(ding_user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
username = format2username(ding_user_info['email'])
username = format2username(user_info['email'])
context = {
'username': username,
}
@ -361,24 +345,16 @@ def unlock_account(request):
}
return render(request, msg_template, context)
crypto = Crypto(CRYPTO_KEY)
union_id = crypto.decrypt(_union_id_crypto)
userid_status, user_result = ding_ops.ding_get_userid_by_union_id(union_id)
user_id = crypto.decrypt(_union_id_crypto)
userid_status, user_info = _ops.get_user_detail_by_user_id(user_id)
if not userid_status:
context = {
'msg': '获取钉钉userid失败错误信息{}'.format(user_result),
'msg': '获取{}用户信息失败,错误信息:{}'.format(scan_params.SCAN_APP, user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
detail_status, ding_user_info = ding_ops.ding_get_userinfo_detail(user_result)
if not detail_status:
context = {
'msg': '获取钉钉用户信息失败,错误信息:{}'.format(ding_user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
username = format2username(ding_user_info['email'])
username = format2username(user_info['email'])
if ad_ops.ad_ensure_user_by_account(username=username) is False:
context = {
'msg': "账号[%s]在AD中未能正确检索到请确认当前钉钉扫码账号绑定的邮箱是否和您正在使用的邮箱一致或者该账号己被禁用\n猜测:您的账号或邮箱是否是带有数字或其它字母区分?" %

BIN
screenshot/微扫码11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

BIN
screenshot/微扫码12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
screenshot/微扫码13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
screenshot/微扫码14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
screenshot/微扫码15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
screenshot/微扫码16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -1,119 +0,0 @@
a, body, button, dd, div, dl, dt, h1, h2, h3, h4, h5, h6, input, li, ol, p, td, textarea, ul { margin: 0; padding: 0; }
body, button, input, select, textarea { font: 9pt/1.5 tahoma,arial,Hiragino Sans GB,\5b8b\4f53,sans-serif; }
button, h1, h2, h3, h4, h5, h6, input, select, textarea { font-size: 100%; }
/*background-image: linear-gradient(160deg, #2f548e 20%,#043559 80%);*/
html{height: 100%; background-image: linear-gradient(160deg, #2f548e 20%,#043559 80%);}
ol, ul { list-style: none;}
a { color: #666; text-decoration: none; }
a:hover { color: #043559; text-decoration: underline; }
body { font-size: 9pt; height: 100%;
font-family: 'microsoft yahei', sans-serif; min-width: 750pt; margin: 0; overflow: hidden}
img { border: 0; vertical-align: top; }
textarea { resize: none; }
a, button, input, select, textarea { outline: 0; }
a, button { cursor: pointer; }
button { border: none; }
.errorlist {font-size: 16px; color: #333333}
.pagewrap {height: 100% }
.main { position: relative; margin-top:0; width: 100%; height: 100%}
.header {height: 100px;margin-bottom: 5%;margin-left: 50px; background: url(/static/img/rgec.png) left center no-repeat; }
.header h1 a { display: block; }
.content { overflow: hidden; margin-left: 10% }
.content .con_left { float: left; height: 450px; width: 50%; margin-top: 65px}
/*.content .con_left .box {position: absolute; width: 400px; height:400px; left: 50%; right: 50%; margin-left: -100px; margin-right: -100px;}*/
.content .con_left p { padding: 0 0 3px; width: 10pc; color: #040000; font-size: 1pc; font-family: 'microsoft yahei', sans-serif; }
.content .con_left a { padding: 0 0 0 2pc; color: #2f548e; text-decoration: underline; font-size: 1pc; font-family: 'microsoft yahei', sans-serif; }
.content .con_right { float: left; margin: 65px 0 0; width: 28pc; height: 450px; border: 1px solid #dedede; background: #fff; }
.content .con_right .con_r_top { padding: 0 0 0 39px; width: 409px; height: 110px; border-top: 8px solid #2e558e; }
.content .con_right .con_r_top .left, .content .con_right .con_r_top .right { float: left; padding: 35px 0 0; width: 186px; height: 35px; text-align: center; text-decoration: none; font-size: 18px; font-family: "微软雅黑"; }
.content .con_right .con_r_top .left { border-bottom: 2px solid #dedede; color: #999; }
.content .con_right ul .con_r_left .erweima { text-align: center; }
.content .con_right ul .con_r_left p {color: #2f548e; font-size: 25px; font-family: 'microsoft yahei', sans-serif; }
.content .con_right ul .con_r_left .user input { margin: 0 0 1px -1px; padding-left: 7px; width: 324px; height: 33px; border: 1px solid #dedede; color: #999; font-size: 14px; font-family: "微软雅黑"; line-height: 2pc; }
.content .con_right ul .con_r_left .user { padding: 0 0 0 39px; }
.content .con_right ul .con_r_left .user ul{font-size: 16px; color: #333333}
.content .con_right ul .con_r_left .user li{font-size: 16px; color: #333333}
.content .con_right ul .con_r_left .user .user-icon { float: left; width: 36px; height: 35px; background: url(../img/user-icon.jpg) left top no-repeat; }
.content .con_right ul .con_r_left .user .mima-icon { float: left; width: 36px; height: 35px; background: url(../img/mima-icon.jpg) left top no-repeat; }
.content .con_right ul .con_r_left .user .unlock-icon { float: left; width: 36px; height: 35px; background: url(../img/unlock.jpg) left top no-repeat; }
.content .con_right ul .con_r_left p { overflow: hidden; padding: 0 39px 37px; color: #666; font-size: 13px; font-family: 'microsoft yahei', sans-serif; }
.content .con_right ul .con_r_left p .mima { float: left; padding-left: 5px; text-decoration: none; }
.content .con_right ul .con_r_left p .zhuce { float: right; text-decoration: none; }
.content .con_right ul .con_r_left button { margin: 0 0 0 75pt; width: 250px; height: 44px; background: #2e558e; color: #fff; font-size: 1pc; font-family: 'microsoft yahei', sans-serif; }
.content .con_right .con_r_top .right { border-bottom: 2px solid #2e558e; color: #333; }
.content .con_right ul .con_r_right .user input { margin: 0 0 1px -1px; padding-left: 7px; width: 324px; height: 33px; border: 1px solid #dedede; color: #999; font-size: 14px; font-family: "微软雅黑"; line-height: 2pc; }
.content .con_right ul .con_r_right .user { padding: 0 0 0 39px; }
.content .con_right ul .con_r_right .user ul{font-size: 16px; color: #333333}
.content .con_right ul .con_r_right .user li{font-size: 16px; color: #333333}
.content .con_right ul .con_r_right .user .user-icon { float: left; width: 36px; height: 35px; background: url(../img/user-icon.jpg) left top no-repeat; }
.content .con_right ul .con_r_right .user .mima-icon { float: left; width: 36px; height: 35px; background: url(../img/mima-icon.jpg) left top no-repeat; }
.content .con_right ul .con_r_right .user .unlock-icon { float: left; width: 36px; height: 35px; background: url(../img/unlock.jpg) left top no-repeat; }
.content .con_right ul .con_r_right p { overflow: hidden; padding: 0 39px 37px; color: #666; font-size: 13px; font-family: 'microsoft yahei', sans-serif; }
.content .con_right ul .con_r_right p .mima { float: left; padding-left: 5px; text-decoration: none; }
.content .con_right ul .con_r_right p .zhuce { float: right; text-decoration: none; }
.content .con_right ul .con_r_right button { margin: 0 0 0 75pt; width: 250px; height: 44px; background: #2e558e; color: #fff; font-size: 1pc; font-family: 'microsoft yahei', sans-serif; }
.content .con_right ul .con_r_left { display: none; }
.con_right ul .con_r_left .erweima { position: relative; margin: 0 auto; width: 365px; height: 330px; }
.qrcode { position: absolute; top: 0; left: 0; width: 174px; height: 11pc; }
.divimg { position: absolute; top: 50%; left: 50%; z-index: 100; overflow: hidden; margin-top: -15px; margin-left: -30px; padding: 1px; width: 60px; height: 30px; border: 1px solid #eee; border-radius: .5rem; background: #fff; opacity: .9; filter: alpha(opacity=90); -moz-opacity: .9; }
.content .con_right ul .con_r_right .user .yanzheng { width: 150px; margin: 0 5px 10px 1px; padding-left: 5px; }
.content .con_right ul .con_r_right .user .next { font-size: 12px; width: 40px; height: 33px; float: right; margin-right: 40px; }
.content .con_right .con_r_top { *height: 90px; }
.autoWidth{margin:0 auto;min-width:1000px;max-width:1200px}
.auto{margin:0 auto;min-width:1000px;max-width:1200px}
@media screen and (max-width:1233px){.auto{padding-left:10px}
}
.clearfix:after,.clearfix:before{display:table;line-height:0;content:""}
.clearfix:after{clear:both}
.clear-float{clear:both}
.footer{background-color:#009fd9;font-family: 'microsoft yahei', sans-serif; }
.footer-floor1{width:100%;padding:36px 0 60px}
.footer-list{width:69%;height:100%;float:left}
.footer-list ul{float:left;margin-right:13%}
.footer-list .flist-4{margin-right:0}
.footer-list li{line-height:32px}
.footer-list li a{color:#b6e2f2;font-size:12px;text-decoration:none}
.footer-list li a:hover{text-decoration:underline;color:#fff}
.footer-list .flist-title{font-size:16px;color:#fff;margin-bottom:15px}
.footer-floor2{width:100%;border-top:1px solid #4cc3ed;padding:20px 0;text-align:center}
.footer-floor2 p{text-align:center;color:#b6e2f2;font-size:12px;line-height:30px}
.footer-floor2 p span{font-family:PingFangSC-Light,'helvetica neue','hiragino sans gb',tahoma,'microsoft yahei ui','microsoft yahei',sans-serif}
.footer-floor2 a{color:#b6e2f2}
.footer-floor2 a:hover{color:#a8d0e0;text-decoration:underline}
.foot-link{margin:0 15px;text-decoration:none;color:#b6e2f2}
.foot-link:hover{text-decoration:underline}
.footer-right{width:300px;float:right}
.telephone{width:100%;height:32px;line-height:32px;color:#fff}
.telephone .tel-number{font-size:30px;font-weight:400;text-align:right}
.official-plat{width:100%;height:100%;margin-top:20px;position:relative}
.official-plat ul{float:right;margin-top:7px}
.official-plat ul li{height:45px}
.official-plat ul a{display:inline-block;height:32px;width:100%;line-height:32px;color:#fff;text-decoration:none;font-size:12px}
.official-plat>p{display:inline-block;width:132px;height:132px;border:1px solid #ddd;background-color:#fff}
#wx-corner{border:10px solid transparent;border-left:10px solid #fff;position:absolute;top:12px;right:-20px;z-index:10}
#wb-corner{border:10px solid transparent;border-left:10px solid #fff;position:absolute;top:58px;right:-20px;z-index:10}
.five-superiority{width:100%;border-bottom:1px solid #27aede;padding:10px 0 20px}
.five-superiority-list li{float:left;width:20%;height:36px;text-align:center;border-left:1px solid #27aede}
.five-superiority-list li:first-child{border-left:none}
.five-superiority-list li a{display:inline-block;position:relative;width:100%;height:36px;line-height:36px;background:no-repeat 2% center;text-indent:2em;color:#fff;font-size:16px}
.five-superiority-list li a:hover{color:#bfe7f5}
.five-superiority-list li a.superiority-text{text-indent:4em}
.compensate_ico .superiority-icon{background-position:0 0}
.compensate_ico:hover .superiority-icon{background-position:0 -50px}
.retreat_ico .superiority-icon{background-position:0 -100px}
.retreat_ico:hover .superiority-icon{background-position:0 -150px}
.technology_ico .superiority-icon{background-position:0 -200px}
.technology_ico:hover .superiority-icon{background-position:0 -250px}
.prepare_ico .superiority-icon{background-position:0 -300px}
.prepare_ico:hover .superiority-icon{background-position:0 -350px}
.service_ico .superiority-icon{background-position:0 -400px}
.service_ico:hover .superiority-icon{background-position:0 -450px}
.marquee-box{overflow:hidden;width:100%;position:absolute;left:0;top:0}
.marquee{width:8000%;height:60px}
.wave-list-box{float:left}
.wave-list-box ul{float:left;height:60px;overflow:hidden;zoom:1}
.wave-list-box ul li{height:60px;width:100%;float:left;line-height:30px;list-style:none}
.wave-box{position:relative;height:60px;background:#fff}

View File

@ -1,66 +0,0 @@
*{margin:0;padding:0;box-sizing:border-box;list-style:none}
html{height: 100%; width:100%}
body{font-family:"Microsoft Yahei";min-width:1000px}
a{outline:0;text-decoration:none}
strong{font-weight:400}
.strong{font-weight:700}
::selection{background:#1EACDF;color:#fff}
img{border:0}
::-moz-selection{background:#1EACDF;color:#fff}
::-webkit-selection{background:#1EACDF;color:#fff}
.autoWidth{margin:0 auto;min-width:1000px;max-width:1200px}
.auto{margin:0 auto;min-width:1000px;max-width:1200px}
@media screen and (max-width:1233px){.auto{padding-left:10px}
}
.clearfix:after,.clearfix:before{display:table;line-height:0;content:""}
.clearfix:after{clear:both}
.clear-float{clear:both}
.footer{background-color:#009fd9;font-family:"Microsoft Yahei"}
.footer-floor1{width:100%;padding:36px 0 60px}
.footer-list{width:69%;height:100%;float:left}
.footer-list ul{float:left;margin-right:13%}
.footer-list .flist-4{margin-right:0}
.footer-list li{line-height:32px}
.footer-list li a{color:#b6e2f2;font-size:12px;text-decoration:none}
.footer-list li a:hover{text-decoration:underline;color:#fff}
.footer-list .flist-title{font-size:16px;color:#fff;margin-bottom:15px}
.footer-floor2{width:100%;border-top:1px solid #4cc3ed;padding:20px 0;text-align:center}
.footer-floor2 p{text-align:center;color:#b6e2f2;font-size:12px;line-height:30px}
.footer-floor2 p span{font-family:PingFangSC-Light,'helvetica neue','hiragino sans gb',tahoma,'microsoft yahei ui','microsoft yahei',simsun,sans-serif}
.footer-floor2 a{color:#b6e2f2}
.footer-floor2 a:hover{color:#a8d0e0;text-decoration:underline}
.foot-link{margin:0 15px;text-decoration:none;color:#b6e2f2}
.foot-link:hover{text-decoration:underline}
.footer-right{width:300px;float:right}
.official-plat{width:100%;height:100%;margin-top:20px;position:relative}
.official-plat ul{float:right;margin-top:7px}
.official-plat ul li{height:45px}
.official-plat ul a{display:inline-block;height:32px;width:100%;line-height:32px;color:#fff;text-decoration:none;font-size:12px}
.official-plat>p{display:inline-block;width:132px;height:132px;border:1px solid #ddd;background-color:#fff}
#wx-corner{border:10px solid transparent;border-left:10px solid #fff;position:absolute;top:12px;right:-20px;z-index:10}
#wb-corner{border:10px solid transparent;border-left:10px solid #fff;position:absolute;top:58px;right:-20px;z-index:10}
.five-superiority{width:100%;border-bottom:1px solid #27aede;padding:10px 0 20px}
.five-superiority-list li{float:left;width:20%;height:36px;text-align:center;border-left:1px solid #27aede}
.five-superiority-list li:first-child{border-left:none}
.five-superiority-list li a{display:inline-block;position:relative;width:100%;height:36px;line-height:36px;background:no-repeat 2% center;text-indent:2em;color:#fff;font-size:16px}
.five-superiority-list li a:hover{color:#bfe7f5}
.five-superiority-list li a.superiority-text{text-indent:4em}
.compensate_ico .superiority-icon{background-position:0 0}
.compensate_ico:hover .superiority-icon{background-position:0 -50px}
.retreat_ico .superiority-icon{background-position:0 -100px}
.retreat_ico:hover .superiority-icon{background-position:0 -150px}
.technology_ico .superiority-icon{background-position:0 -200px}
.technology_ico:hover .superiority-icon{background-position:0 -250px}
.prepare_ico .superiority-icon{background-position:0 -300px}
.prepare_ico:hover .superiority-icon{background-position:0 -350px}
.service_ico .superiority-icon{background-position:0 -400px}
.service_ico:hover .superiority-icon{background-position:0 -450px}
.marquee-box{overflow:hidden;width:100%;position:absolute;left:0;top:0}
.marquee{width:8000%;height:60px}
.wave-list-box{float:left}
.wave-list-box ul{float:left;height:60px;overflow:hidden;zoom:1}
.wave-list-box ul li{height:60px;width:100%;float:left;line-height:30px;list-style:none}
.wave-box{position:relative;height:60px;background:#fff}

View File

@ -1 +0,0 @@
"use strict";window.bubbly=function(t){var n=t||{},o=function(){return Math.random()},r=n.canvas||document.createElement("canvas"),e=r.width,a=r.height;null===r.parentNode&&(r.setAttribute("style","position:fixed;z-index:-1;left:0;top:0;min-width:100vw;min-height:100vh;"),e=r.width=window.innerWidth,a=r.height=window.innerHeight,document.body.appendChild(r));var i=r.getContext("2d");i.shadowColor=n.shadowColor||"#fff",i.shadowBlur=n.blur||4;var l=i.createLinearGradient(0,0,e,a);l.addColorStop(0,n.colorStart||"#2AE"),l.addColorStop(1,n.colorStop||"#17B");for(var c=n.bubbles||Math.floor(.02*(e+a)),u=[],d=0;d<c;d++)u.push({f:(n.bubbleFunc||function(){return"hsla(0, 0%, 100%, "+.1*o()+")"}).call(),x:o()*e,y:o()*a,r:(n.radiusFunc||function(){return 4+o()*e/25}).call(),a:(n.angleFunc||function(){return o()*Math.PI*2}).call(),v:(n.velocityFunc||function(){return.1+.5*o()}).call()});!function t(){if(null===r.parentNode)return cancelAnimationFrame(t);!1!==n.animate&&requestAnimationFrame(t),i.globalCompositeOperation="source-over",i.fillStyle=l,i.fillRect(0,0,e,a),i.globalCompositeOperation=n.compose||"lighter",u.forEach(function(t){i.beginPath(),i.arc(t.x,t.y,t.r,0,2*Math.PI),i.fillStyle=t.f,i.fill(),t.x+=Math.cos(t.a)*t.v,t.y+=Math.sin(t.a)*t.v,t.x-t.r>e&&(t.x=-t.r),t.x+t.r<0&&(t.x=e+t.r),t.y-t.r>a&&(t.y=-t.r),t.y+t.r<0&&(t.y=a+t.r)})}()};

File diff suppressed because one or more lines are too long

View File

@ -1,41 +0,0 @@
$(document).ready(function () {
$(".official-plat ul li:first-child").hover(function () {
$(".weixin").show();
$(".weibo").hide();
});
$("li[title='点击打开官方微博']").hover(function () {
$(".weixin").hide();
$(".weibo").show();
});
//href="#a_null"的统一设置为无效链接
$("a[href='#a_null']").click(function () {
return false;
});
});
//波浪动画
$(function () {
var marqueeScroll = function (id1, id2, id3, timer) {
var $parent = $("#" + id1);
var $goal = $("#" + id2);
var $closegoal = $("#" + id3);
$closegoal.html($goal.html());
function Marquee() {
if (parseInt($parent.scrollLeft()) - $closegoal.width() >= 0) {
$parent.scrollLeft(parseInt($parent.scrollLeft()) - $goal.width());
}
else {
$parent.scrollLeft($parent.scrollLeft() + 1);
}
}
setInterval(Marquee, timer);
}
var marqueeScroll1 = new marqueeScroll("marquee-box", "wave-list-box1", "wave-list-box2", 20);
var marqueeScroll2 = new marqueeScroll("marquee-box3", "wave-list-box4", "wave-list-box5", 40);
});

98
templates/we_index.html Normal file
View File

@ -0,0 +1,98 @@
{% load static %}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>密码自助服务</title>
<link type="text/css" rel="stylesheet" href="{% static 'css/login.css' %}">
<script type="text/javascript" src="{% static 'js/jquery-1.8.3.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/check.js' %}"></script>
<script src="https://rescdn.qqmail.com/node/ww/wwopenmng/js/sso/wwLogin-1.0.0.js"></script>
</head>
<body>
<div class="pagewrap">
<div class="main">
<div class="header">
</div>
<div class="content">
<div class="con_left">
<div style="margin: 0 auto; width:100%; height: 200px; line-height: 200px;" align="center">
<p style="margin: 0 auto; color: #fdfdfe; font-size: 36px; width:100%; ">「域账号或邮箱」<small>密码自助平台</small></p>
</div>
<div style="margin: 0 auto; width:400px; height: 240px;">
<p style="margin: 0 auto; color: #fdfdfe; font-size: 16px; width:100%;
">提示新密码要求满足8至30位长度(不包含空格),至少包含大小写字母及数字组成。</p>
<p style="margin: 0 auto; color: #fdfdfe; font-size: 16px; width:100%;
">如果密码己遗忘,可点击[重置/解锁],使用钉钉扫描验证后直接重置密码。</p>
</div>
</div>
<div class="con_right">
<div class="con_r_top">
<a href="javascript:" class="right"
style="color: rgb(51, 51, 51); border-bottom-width: 2px; border-bottom-style: solid; border-bottom-color: rgb(46, 85, 142);">修改密码</a>
<a href="javascript:" class="left"
style="color: rgb(153, 153, 153); border-bottom-width: 2px; border-bottom-style: solid; border-bottom-color: rgb(222, 222, 222);">重置/解锁</a>
</div>
<ul>
<li class="con_r_right" style="display: block;">
<form name="modifypwd" method="post" action="" autocomplete="off">
{% csrf_token %}
<div class="user">
<div><span class="user-icon"></span>
<input type="text" id="username" name="username" placeholder="格式abc\lisi、lisi、lisi@abc.com" value="">
</div>
<div><span class="mima-icon"></span>
<input type="password" id="old_password" name="old_password"
placeholder=" 输入旧密码" value="">
</div>
<div><span class="mima-icon"></span>
<input type="password" id="new_password" name="new_password"
placeholder=" 输入新密码" value="">
</div>
<div><span class="mima-icon"></span>
<input type="password" id="ensure_password" name="ensure_password"
placeholder=" 再次输入新密码" value="">
</div>
</div>
<br>
<button id="btn_modify" type="submit">修改密码</button>
</form>
</li>
<li class="con_r_left" style="display: none;">
<div style="margin-top: -30px" class="erweima">
<div style="width: 300px; height: 300px; margin: 0 auto" id="we_code"></div>
<script type="text/javascript">
let home_url = "{{ home_url }}";
let app_id = "{{ app_id }}";
let agent_id = "{{ agent_id }}"
let redirect_url = encodeURIComponent(home_url + '/callbackCheck');
window.WwLogin({
id: "we_code",
appid: app_id,
agentid: agent_id,
redirect_uri: redirect_url,
// 样式使用base64加密而不使用https的方式
href: 'data:text/css;base64, ' +
'LmltcG93ZXJCb3ggLnRpdGxlIHtkaXNwbGF5OiBub25lO30KLmltcG93ZXJCb3ggLnFyY29kZSB7d2lkdGg6IDIyMHB4O30KLmltcG93ZXJCb3ggLmluZm8ge3dpZHRoOiAyMjBweDt9Ci5zdGF0dXNfaWNvbiB7ZGlzcGxheTogbm9uZSAgIWltcG9ydGFudH0KLmltcG93ZXJCb3ggLnN0YXR1cy5zdGF0dXNfYnJvd3NlciB7ZGlzcGxheTogbm9uZTt9Ci5pbXBvd2VyQm94IC5zdGF0dXMge3RleHQtYWxpZ246IGNlbnRlcjt9'
});
</script>
</div>
<div style="height: 70px; margin-top: -30px">
<p style="font-size: 18px; color: #2e558e" align="center">企业微信扫码验证用户信息</p>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<script type="text/javascript">
window.onload = function () {
if (!!window.ActiveXObject || "ActiveXObject" in window)
alert("您当前使用的浏览器为IE或IE内核因为IE各种体验问题本网站不对IE兼容。\n为能正常使用密码自助修改服务请更换谷歌、火狐等非IE核心的浏览器。\n如果是360、Maxthon等这类双核心浏览器请切换至[极速模式]亦可。")
}
</script>
</body>
</html>

View File

@ -46,7 +46,7 @@ class AdOps(object):
self.authentication = authentication
self.auto_bind = auto_bind
server = Server(host='%s' % AD_HOST, use_ssl=self.use_ssl, port=port, get_info=ALL)
server = Server(host='%s' % AD_HOST, connect_timeout=1, use_ssl=self.use_ssl, port=port, get_info=ALL)
try:
self.conn = Connection(server, auto_bind=self.auto_bind, user=r'{}\{}'.format(self.domain, self.user), password=self.password,
authentication=self.authentication, raise_exceptions=True)
@ -115,7 +115,7 @@ class AdOps(object):
def ad_get_user_dn_by_account(self, username):
"""
通过mail查询某个用户的完整DN
通过username查询某个用户的完整DN
:param username:
:return: DN
"""
@ -179,7 +179,7 @@ class AdOps(object):
def ad_get_user_locked_status_by_account(self, username):
"""
通过mail获取某个用户账号是否被锁定
通过username获取某个用户账号是否被锁定
:param username:
:return: 如果结果是1601-01-01说明账号未锁定返回0
"""

View File

@ -9,47 +9,31 @@ from urllib.parse import quote
import requests
from dingtalk.client import AppKeyClient
from pwdselfservice import cache_storage
from pwdselfservice.local_settings import DING_APP_KEY, DING_APP_SECRET, DING_CORP_ID, DING_URL, DING_MO_APP_ID, DING_MO_APP_SECRET
class DingDingOps(object):
def __init__(self, corp_id=DING_CORP_ID, app_key=DING_APP_KEY, app_secret=DING_APP_SECRET, mo_app_id=DING_MO_APP_ID, mo_app_secret=DING_MO_APP_SECRET):
def __init__(self, corp_id=DING_CORP_ID, app_key=DING_APP_KEY, app_secret=DING_APP_SECRET, mo_app_id=DING_MO_APP_ID, mo_app_secret=DING_MO_APP_SECRET, storage=cache_storage):
self.corp_id = corp_id
self.app_key = app_key
self.app_secret = app_secret
self.mo_app_id = mo_app_id
self.mo_app_secret = mo_app_secret
self.storage = storage
@property
def ding_client_connect(self):
def _client_connect(self):
"""
钉钉连接器
:return:
"""
return AppKeyClient(corp_id=self.corp_id, app_key=self.app_key, app_secret=self.app_secret)
@property
def ding_get_access_token(self):
def get_union_id_by_code(self, code):
"""
获取企业内部应用的access_token
:return:
"""
return self.ding_client_connect.access_token
def ding_get_dept_user_list_detail(self, dept_id, offset, size):
"""
获取部门中的用户列表详细清单
:param dept_id: 部门ID
:param offset: 偏移量可理解为步进量
:param size: 一次查询多少个
:return:
"""
return self.ding_client_connect.user.list(department_id=dept_id, offset=offset, size=size)
def ding_get_union_id_by_code(self, code):
"""
通过移动应用接入扫码返回的临时授权码获取用户的unionid
通过移动应用接入扫码返回的临时授权码使用临时授权码换取用户的unionid
:param code:
:return:
"""
@ -68,57 +52,36 @@ class DingDingOps(object):
json=dict(tmp_auth_code=code),
)
resp = resp.json()
print(resp)
try:
if resp['errcode'] != 0:
return False, 'ding_get_union_id_by_code: %s' % str(resp)
return False, 'get_union_id_by_code: %s' % str(resp)
else:
return True, resp["user_info"]["unionid"]
except Exception:
return False, 'ding_get_union_id_by_code: %s' % str(resp)
return False, 'get_union_id_by_code: %s' % str(resp)
def ding_get_userid_by_union_id(self, union_id):
def get_user_id_by_code(self, code):
"""
通过unionid获取用户的userid
:param union_id: 用户在当前钉钉开放平台账号范围内的唯一标识
通过code获取用户的userid
:param id: 用户在当前钉钉开放平台账号范围内的唯一标识
:return:
"""
try:
return True, self.ding_client_connect.user.get_userid_by_unionid(union_id)['userid']
except Exception as e:
return False, 'ding_get_union_id_by_code: %s' % str(e)
_status, union_id = self.get_union_id_by_code(code)
if _status:
return True, self._client_connect.user.get_userid_by_unionid(union_id).get('userid')
else:
return False, 'get_user_id_by_code: %s' % str(union_id)
except (KeyError, IndexError) as k_error:
return False, 'ding_get_union_id_by_code: %s' % str(k_error)
@property
def ding_get_org_user_count(self):
"""
企业员工数量
only_active 是否包含未激活钉钉的人员数量
:return:
"""
return self.ding_client_connect.user.get_org_user_count('only_active')
def ding_get_userinfo_detail(self, user_id):
def get_user_detail_by_user_id(self, user_id):
"""
通过user_id 获取用户详细信息
user_id 用户ID
:return:
"""
try:
return True, self.ding_client_connect.user.get(user_id)
return True, self._client_connect.user.get(user_id)
except Exception as e:
return False, 'ding_get_union_id_by_code: %s' % str(e)
return False, 'get_user_detail_by_user_id: %s' % str(e)
except (KeyError, IndexError) as k_error:
return False, 'ding_get_union_id_by_code: %s' % str(k_error)
if __name__ == '__main__':
start = time.time()
d = DingDingOps().ding_client_connect
unicode = ''
# print(d.)
end = time.time()
print("running:" + str(round((end - start), 3)))
return False, 'get_user_detail_by_user_id: %s' % str(k_error)

View File

@ -24,12 +24,22 @@ def format2username(account):
elif re.fullmatch(domain_compile, account):
return re.fullmatch(domain_compile, account).group(2)
else:
return account
return account.lower()
else:
raise NameError("输入的账号不能为空..")
def get_user_is_active(user_info):
try:
return True, user_info.get('active') or user_info.get('status')
except Exception as e:
return False, 'get_user_is_active: %s' % str(e)
except (KeyError, IndexError) as k_error:
return False, 'get_user_is_active: %s' % str(k_error)
if __name__ == '__main__':
user = 'aaa\jf.com'
user = 'jf.com\XiangLe'
username = format2username(user)
print(username)

23
utils/storage/__init__.py Normal file
View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
class BaseStorage(object):
def get(self, key, default=None):
raise NotImplementedError()
def set(self, key, value, ttl=None):
raise NotImplementedError()
def delete(self, key):
raise NotImplementedError()
def __getitem__(self, key):
self.get(key)
def __setitem__(self, key, value):
self.set(key, value)
def __delitem__(self, key):
self.delete(key)

58
utils/storage/cache.py Normal file
View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import inspect
from utils.storage import BaseStorage
def _is_cache_item(obj):
return isinstance(obj, CacheItem)
class CacheItem(object):
def __init__(self, cache=None, name=None):
self.cache = cache
self.name = name
def key_name(self, key):
if isinstance(key, (tuple, list)):
key = ':'.join(key)
k = '{0}:{1}'.format(self.cache.prefix, self.name)
if key is not None:
k = '{0}:{1}'.format(k, key)
return k
def get(self, key=None, default=None):
return self.cache.storage.get(self.key_name(key), default)
def set(self, key=None, value=None, ttl=None):
return self.cache.storage.set(self.key_name(key), value, ttl)
def delete(self, key=None):
return self.cache.storage.delete(self.key_name(key))
class BaseCache(object):
def __new__(cls, *args, **kwargs):
self = super(BaseCache, cls).__new__(cls)
api_endpoints = inspect.getmembers(self, _is_cache_item)
for name, api in api_endpoints:
api_cls = type(api)
api = api_cls(self, name)
setattr(self, name, api)
return self
def __init__(self, storage, prefix='client'):
assert isinstance(storage, BaseStorage)
self.storage = storage
self.prefix = prefix
class WeWorkCache(BaseCache):
access_token = CacheItem()

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import json
from dingtalk.core.utils import to_text
from utils.storage import BaseStorage
class KvStorage(BaseStorage):
def __init__(self, kvdb, prefix='wework'):
for method_name in ('get', 'set', 'delete'):
assert hasattr(kvdb, method_name)
self.kvdb = kvdb
self.prefix = prefix
def key_name(self, key):
return '{0}:{1}'.format(self.prefix, key)
def get(self, key, default=None):
key = self.key_name(key)
value = self.kvdb.get(key)
if value is None:
return default
return json.loads(to_text(value))
def set(self, key, value, ttl=None):
if value is None:
return
key = self.key_name(key)
value = json.dumps(value)
self.kvdb.set(key, value, ttl)
def delete(self, key):
key = self.key_name(key)
self.kvdb.delete(key)

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import time
from utils.storage import BaseStorage
class MemoryStorage(BaseStorage):
def __init__(self):
self._data = {}
def get(self, key, default=None):
ret = self._data.get(key, None)
if ret is None or len(ret) != 2:
return default
else:
value = ret[0]
expires_at = ret[1]
if expires_at is None or expires_at > time.time():
return value
else:
return default
def set(self, key, value, ttl=None):
if value is None:
return
self._data[key] = (value, int(time.time()) + ttl)
def delete(self, key):
self._data.pop(key, None)

View File

@ -0,0 +1,113 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import json
import requests
DEBUG = False
class ApiException(Exception):
def __init__(self, errCode, errMsg):
self.errCode = errCode
self.errMsg = errMsg
class AbstractApi(object):
def __init__(self):
return
def access_token(self):
raise NotImplementedError
def http_call(self, url_type, args=None):
short_url = url_type[0]
method = url_type[1]
response = {}
for retryCnt in range(0, 3):
if 'POST' == method:
url = self.__make_url(short_url)
response = self.__http_post(url, args)
elif 'GET' == method:
url = self.__make_url(short_url)
url = self.__append_args(url, args)
response = self.__http_get(url)
else:
raise ApiException(-1, "unknown method type")
# check if token expired
if self.__token_expired(response.get('errcode')):
self.__refresh_token(short_url)
retryCnt += 1
continue
else:
break
return self.__check_response(response)
@staticmethod
def __append_args(url, args):
if args is None:
return url
for key, value in args.items():
if '?' in url:
url += ('&' + key + '=' + value)
else:
url += ('?' + key + '=' + value)
return url
@staticmethod
def __make_url(short_url):
base = "https://qyapi.weixin.qq.com"
if short_url[0] == '/':
return base + short_url
else:
return base + '/' + short_url
def __append_token(self, url):
if 'ACCESS_TOKEN' in url:
return url.replace('ACCESS_TOKEN', self.access_token())
else:
return url
def __http_post(self, url, args):
real_url = self.__append_token(url)
if DEBUG is True:
print(real_url, args)
return requests.post(real_url, data=json.dumps(args, ensure_ascii=False).encode('utf-8')).json()
def __http_get(self, url):
real_url = self.__append_token(url)
if DEBUG is True:
print(real_url)
return requests.get(real_url).json()
def __post_file(self, url, media_file):
return requests.post(url, file=media_file).json()
@staticmethod
def __check_response(response):
errCode = response.get('errcode')
errMsg = response.get('errmsg')
if errCode == 0:
return response
else:
raise ApiException(errCode, errMsg)
@staticmethod
def __token_expired(errCode):
if errCode == 40014 or errCode == 42001 or errCode == 42007 or errCode == 42009:
return True
else:
return False
def __refresh_token(self, url):
if 'ACCESS_TOKEN' in url:
self.access_token()

140
utils/wework_ops.py Normal file
View File

@ -0,0 +1,140 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @FileName WEWORK_ops.py
# @Software:
# @Author: Leven Xiang
# @Mail: xiangle0109@outlook.com
# @Date 2021/5/18 16:55
from __future__ import absolute_import, unicode_literals
from pwdselfservice import cache_storage
from pwdselfservice.local_settings import *
from utils.storage.cache import WeWorkCache
from utils.wework_api.AbstractApi import *
CORP_API_TYPE = {
'GET_ACCESS_TOKEN': ['/cgi-bin/gettoken', 'GET'],
'USER_CREATE': ['/cgi-bin/user/create?access_token=ACCESS_TOKEN', 'POST'],
'USER_GET': ['/cgi-bin/user/get?access_token=ACCESS_TOKEN', 'GET'],
'USER_UPDATE': ['/cgi-bin/user/update?access_token=ACCESS_TOKEN', 'POST'],
'USER_DELETE': ['/cgi-bin/user/delete?access_token=ACCESS_TOKEN', 'GET'],
'USER_BATCH_DELETE': ['/cgi-bin/user/batchdelete?access_token=ACCESS_TOKEN', 'POST'],
'USER_SIMPLE_LIST': ['/cgi-bin/user/simplelist?access_token=ACCESS_TOKEN', 'GET'],
'USER_LIST': ['/cgi-bin/user/list?access_token=ACCESS_TOKEN', 'GET'],
'USERID_TO_OPENID': ['/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN', 'POST'],
'OPENID_TO_USERID': ['/cgi-bin/user/convert_to_userid?access_token=ACCESS_TOKEN', 'POST'],
'USER_AUTH_SUCCESS': ['/cgi-bin/user/authsucc?access_token=ACCESS_TOKEN', 'GET'],
'DEPARTMENT_CREATE': ['/cgi-bin/department/create?access_token=ACCESS_TOKEN', 'POST'],
'DEPARTMENT_UPDATE': ['/cgi-bin/department/update?access_token=ACCESS_TOKEN', 'POST'],
'DEPARTMENT_DELETE': ['/cgi-bin/department/delete?access_token=ACCESS_TOKEN', 'GET'],
'DEPARTMENT_LIST': ['/cgi-bin/department/list?access_token=ACCESS_TOKEN', 'GET'],
'TAG_CREATE': ['/cgi-bin/tag/create?access_token=ACCESS_TOKEN', 'POST'],
'TAG_UPDATE': ['/cgi-bin/tag/update?access_token=ACCESS_TOKEN', 'POST'],
'TAG_DELETE': ['/cgi-bin/tag/delete?access_token=ACCESS_TOKEN', 'GET'],
'TAG_GET_USER': ['/cgi-bin/tag/get?access_token=ACCESS_TOKEN', 'GET'],
'TAG_ADD_USER': ['/cgi-bin/tag/addtagusers?access_token=ACCESS_TOKEN', 'POST'],
'TAG_DELETE_USER': ['/cgi-bin/tag/deltagusers?access_token=ACCESS_TOKEN', 'POST'],
'TAG_GET_LIST': ['/cgi-bin/tag/list?access_token=ACCESS_TOKEN', 'GET'],
'BATCH_JOB_GET_RESULT': ['/cgi-bin/batch/getresult?access_token=ACCESS_TOKEN', 'GET'],
'BATCH_INVITE': ['/cgi-bin/batch/invite?access_token=ACCESS_TOKEN', 'POST'],
'AGENT_GET': ['/cgi-bin/agent/get?access_token=ACCESS_TOKEN', 'GET'],
'AGENT_SET': ['/cgi-bin/agent/set?access_token=ACCESS_TOKEN', 'POST'],
'AGENT_GET_LIST': ['/cgi-bin/agent/list?access_token=ACCESS_TOKEN', 'GET'],
'MENU_CREATE': ['/cgi-bin/menu/create?access_token=ACCESS_TOKEN', 'POST'],
'MENU_GET': ['/cgi-bin/menu/get?access_token=ACCESS_TOKEN', 'GET'],
'MENU_DELETE': ['/cgi-bin/menu/delete?access_token=ACCESS_TOKEN', 'GET'],
'MESSAGE_SEND': ['/cgi-bin/message/send?access_token=ACCESS_TOKEN', 'POST'],
'MESSAGE_REVOKE': ['/cgi-bin/message/revoke?access_token=ACCESS_TOKEN', 'POST'],
'MEDIA_GET': ['/cgi-bin/media/get?access_token=ACCESS_TOKEN', 'GET'],
'GET_USER_INFO_BY_CODE': ['/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN', 'GET'],
'GET_USER_DETAIL': ['/cgi-bin/user/getuserdetail?access_token=ACCESS_TOKEN', 'POST'],
'GET_TICKET': ['/cgi-bin/ticket/get?access_token=ACCESS_TOKEN', 'GET'],
'GET_JSAPI_TICKET': ['/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN', 'GET'],
'GET_CHECKIN_OPTION': ['/cgi-bin/checkin/getcheckinoption?access_token=ACCESS_TOKEN', 'POST'],
'GET_CHECKIN_DATA': ['/cgi-bin/checkin/getcheckindata?access_token=ACCESS_TOKEN', 'POST'],
'GET_APPROVAL_DATA': ['/cgi-bin/corp/getapprovaldata?access_token=ACCESS_TOKEN', 'POST'],
'GET_INVOICE_INFO': ['/cgi-bin/card/invoice/reimburse/getinvoiceinfo?access_token=ACCESS_TOKEN', 'POST'],
'UPDATE_INVOICE_STATUS':
['/cgi-bin/card/invoice/reimburse/updateinvoicestatus?access_token=ACCESS_TOKEN', 'POST'],
'BATCH_UPDATE_INVOICE_STATUS':
['/cgi-bin/card/invoice/reimburse/updatestatusbatch?access_token=ACCESS_TOKEN', 'POST'],
'BATCH_GET_INVOICE_INFO':
['/cgi-bin/card/invoice/reimburse/getinvoiceinfobatch?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_CREATE': ['/cgi-bin/appchat/create?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_GET': ['/cgi-bin/appchat/get?access_token=ACCESS_TOKEN', 'GET'],
'APP_CHAT_UPDATE': ['/cgi-bin/appchat/update?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_SEND': ['/cgi-bin/appchat/send?access_token=ACCESS_TOKEN', 'POST'],
'MINIPROGRAM_CODE_TO_SESSION_KEY': ['/cgi-bin/miniprogram/jscode2session?access_token=ACCESS_TOKEN', 'GET'],
}
class WeWorkOps(AbstractApi):
def __init__(self, corp_id=WEWORK_CORP_ID, agent_id=WEWORK_AGENT_ID, agent_secret=WEWORK_AGNET_SECRET, storage=cache_storage, prefix='wework'):
super().__init__()
self.corp_id = corp_id
self.agent_id = agent_id
self.agent_secret = agent_secret
self.storage = storage
self.cache = WeWorkCache(self.storage, "%s:%s" % (prefix, "corp_id:%s" % self.corp_id))
def access_token(self):
access_token = self.cache.access_token.get()
if access_token is None:
ret = self.get_access_token()
access_token = ret['access_token']
expires_in = ret.get('expires_in', 7200)
self.cache.access_token.set(value=access_token, ttl=expires_in)
return access_token
def get_access_token(self):
return self.http_call(
CORP_API_TYPE['GET_ACCESS_TOKEN'],
{
'corpid': self.corp_id,
'corpsecret': self.agent_secret,
})
def get_user_id_by_code(self, code):
try:
return True, self.http_call(
CORP_API_TYPE['GET_USER_INFO_BY_CODE'],
{
'code': code,
}).get('UserId')
except ApiException as e:
return False, "get_user_id_by_code: {}-{}" .format(e.errCode, e.errMsg)
except Exception as e:
return False, "get_user_id_by_code: {}".format(e)
def get_user_detail_by_user_id(self, user_id):
try:
return True, self.http_call(
CORP_API_TYPE['USER_GET'],
{
'userid': user_id,
})
except ApiException as e:
return False, "get_user_detail_by_user_id: {}-{}" .format(e.errCode, e.errMsg)
except Exception as e:
return False, "get_user_detail_by_user_id: {}".format(e)
if __name__ == '__main__':
wx = WeWorkOps()
print(wx.get_user_detail_by_user_id('XiangLe'))