修改钉钉/企业微信直接使用内部应用免密登录的方式来验证,不再支持扫码。
由于一些API的权限发生变化,导致一些关键信息无法获取,所以做以上改变。 删除了无用的代码,其它没啥变化,没太多时间重写,先就这么着吧。
This commit is contained in:
parent
2e886dc6e8
commit
c5bc154924
|
@ -31,10 +31,7 @@ AD_CONN_PORT = 636
|
|||
# 值是:DING / WEWORK
|
||||
AUTH_CODE_TYPE = 'DING'
|
||||
|
||||
# ########## 钉钉 《如果不使用钉钉扫码,可不用配置》##########
|
||||
# 钉钉接口主地址,不可修改
|
||||
DING_URL = r'https://oapi.dingtalk.com'
|
||||
|
||||
# ########## 钉钉 《如果不使用钉钉,可不用配置》##########
|
||||
# 钉钉企业ID <CorpId>,修改为自己的
|
||||
DING_CORP_ID = '修改为自己的'
|
||||
|
||||
|
@ -48,7 +45,7 @@ DING_MO_APP_ID = r'修改为自己的'
|
|||
DING_MO_APP_SECRET = r'修改为自己的'
|
||||
|
||||
|
||||
# ####### 企业微信《如果不使用企业微信扫码,可不用配置》 ##########
|
||||
# ####### 企业微信《如果不使用企业微信,可不用配置》 ##########
|
||||
# 企业微信的企业ID
|
||||
WEWORK_CORP_ID = r'修改为自己的'
|
||||
# 应用的AgentId
|
||||
|
@ -58,11 +55,9 @@ WEWORK_AGNET_SECRET = r'修改为自己的'
|
|||
|
||||
# Redis配置
|
||||
# redis的连接地址,redis://<Ip/Host>:<Port>/<数据库>
|
||||
REDIS_LOCATION = r'redis://127.0.0.1:6379/1'
|
||||
REDIS_PASSWORD = r'12345678'
|
||||
REDIS_LOCATION = r'redis://127.0.0.1:6379'
|
||||
REDIS_PASSWORD = r'修改为自己的'
|
||||
|
||||
# COOKIE超时时间,单位是秒,可不用修改
|
||||
TMPID_COOKIE_AGE = 300
|
||||
|
||||
# 主页域名,钉钉跳转等需要指定域名,格式:pwd.abc.com。
|
||||
# 如果是自定义安装,请修改成自己的域名
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import datetime
|
||||
from cryptography.fernet import Fernet
|
||||
import sys
|
||||
|
||||
from django_redis import get_redis_connection
|
||||
from utils.storage.kvstorage import KvStorage
|
||||
from utils.storage.memorystorage import MemoryStorage
|
||||
import datetime
|
||||
from traceback import format_exc
|
||||
|
||||
|
||||
try:
|
||||
redis_conn = get_redis_connection()
|
||||
cache_storage = KvStorage(redis_conn)
|
||||
cache_storage.set('redis_connection', str(datetime.datetime.now()))
|
||||
redis_get = cache_storage.get('redis_connection')
|
||||
# print("Redis连接成功,set/get测试通过--{},Token缓存将使用Redis处理".format(redis_get))
|
||||
except Exception as e:
|
||||
cache_storage = MemoryStorage()
|
||||
print("Redis无法连接,Token缓存将使用MemoryStorage处理")
|
||||
print("如果确定需要使用Redis作为缓存,请排查Redis配置,错误信息如下:")
|
||||
print("Redis Exception: {}".format(e))
|
||||
|
||||
crypto_key = Fernet.generate_key()
|
||||
print("请排查Redis配置,错误信息如下:")
|
||||
print("Redis Exception: {}".format(format_exc()))
|
||||
sys.exit(1)
|
||||
|
|
|
@ -8,7 +8,6 @@ else:
|
|||
from conf.local_settings import REDIS_PASSWORD, REDIS_LOCATION
|
||||
DEBUG = False
|
||||
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
@ -80,8 +79,10 @@ SESSION_SAVE_EVERY_REQUEST = True
|
|||
SESSION_COOKIE_AGE = 300
|
||||
# False 会话cookie可以在用户浏览器中保持有效期。True:关闭浏览器,则Cookie失效。
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
# 建议配置,阻止 javascript 对会话数据的访问,提高安全性。
|
||||
# SESSION_COOKIE_HTTPONLY= True
|
||||
# session使用的存储方式
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||
# 指明使用哪一个库保存session数据
|
||||
SESSION_CACHE_ALIAS = "session"
|
||||
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
@ -132,11 +133,19 @@ AD_ACCOUNT_DISABLE_CODE = [514, 66050]
|
|||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_LOCATION,
|
||||
"LOCATION": "{}/1".format(REDIS_LOCATION),
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"PASSWORD": REDIS_PASSWORD,
|
||||
"IGNORE_EXCEPTIONS": True,
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": "{}/3".format(REDIS_LOCATION), # 指明使用redis的3号数据库
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"PASSWORD": REDIS_PASSWORD,
|
||||
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
|
||||
"IGNORE_EXCEPTIONS": True,
|
||||
}
|
||||
}
|
||||
|
|
11
readme.md
11
readme.md
|
@ -159,14 +159,6 @@ WEWORK_AGNET_SECRET = r'修改为自己的'
|
|||
REDIS_LOCATION = r'redis://127.0.0.1:6379/1'
|
||||
REDIS_PASSWORD = r'12345678'
|
||||
|
||||
# ##########################
|
||||
# 执行:python3 ./utils/crypto.py 生成
|
||||
# 可自行生成后替换
|
||||
CRYPTO_KEY = b'dp8U9y7NAhCD3MoNwPzPBhBtTZ1uI_WWSdpNs6wUDgs='
|
||||
|
||||
# COOKIE 超时单位是秒,可不用修改
|
||||
TMPID_COOKIE_AGE = 300
|
||||
|
||||
# 主页域名,钉钉跳转等需要指定域名,格式:pwd.abc.com。
|
||||
# 如果是自定义安装,请修改成自己的域名
|
||||
HOME_URL = 'PWD_SELF_SERVICE_DOMAIN'
|
||||
|
@ -257,9 +249,6 @@ REDIS_PASSWORD = r'12345678'
|
|||
# 可自行生成后替换
|
||||
CRYPTO_KEY = b'dp8U9y7NAhCD3MoNwPzPBhBtTZ1uI_WWSdpNs6wUDgs='
|
||||
|
||||
# COOKIE 超时单位是秒,可不用修改
|
||||
TMPID_COOKIE_AGE = 300
|
||||
|
||||
# 主页域名,钉钉跳转等需要指定域名,格式:pwd.abc.com。
|
||||
# 如果是自定义安装,请修改成自己的域名
|
||||
HOME_URL = 'PWD_SELF_SERVICE_DOMAIN'
|
||||
|
|
|
@ -2,10 +2,10 @@ Django==3.2
|
|||
pycryptodome==3.10.1
|
||||
attrs==21.2.0
|
||||
python-dateutil==2.8.1
|
||||
alibabacloud_dingtalk==1.4.96
|
||||
dingtalk-sdk==1.3.8
|
||||
cryptography==3.4.7
|
||||
ldap3==2.9
|
||||
django-redis==4.12.1
|
||||
feishu-python-sdk==0.1.4
|
||||
requests
|
||||
uwsgi
|
||||
django-redis==4.12.1
|
||||
requests==2.28.1
|
||||
uwsgi==2.0.21
|
|
@ -13,7 +13,6 @@ from utils.crypto_ops import Crypto
|
|||
from ldap3.core.exceptions import LDAPInvalidCredentialsResult, LDAPOperationResult, LDAPExceptionError, LDAPException
|
||||
|
||||
from django.conf import settings
|
||||
from pwdselfservice import crypto_key
|
||||
import os
|
||||
APP_ENV = os.getenv('APP_ENV')
|
||||
if APP_ENV == 'dev':
|
||||
|
@ -56,70 +55,6 @@ def code_2_user_info_with_oauth2(ops, request, msg_template, home_url, code):
|
|||
return True, user_id, user_info
|
||||
|
||||
|
||||
def crypto_id_2_user_info(ops, request, msg_template, home_url, scan_app_tag):
|
||||
"""
|
||||
能过前端提交的加密的userid来获取用户信息<userinfo>
|
||||
"""
|
||||
try:
|
||||
crypto_tmp_id = request.COOKIES.get('tmpid')
|
||||
if not crypto_tmp_id:
|
||||
logger.error('[异常] 请求方法:%s,请求路径:%s,未能拿到TmpID或会话己超时。' % (request.method, request.path))
|
||||
context = {
|
||||
'msg': "会话己超时,请重新验证用户信息。",
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
return False, context
|
||||
# 解密
|
||||
crypto = Crypto(crypto_key)
|
||||
user_id = crypto.decrypt(crypto_tmp_id)
|
||||
# 通过user_id拿到用户的邮箱,并格式化为username
|
||||
userid_status, user_info = ops.get_user_detail_by_user_id(user_id)
|
||||
if not userid_status:
|
||||
context = {
|
||||
'msg': '获取{}用户信息失败,错误信息:{}'.format(user_info, scan_app_tag),
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
return False, context
|
||||
return True, user_info
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def crypto_user_id_2_cookie(user_id):
|
||||
"""
|
||||
加密userid,写入到cookie
|
||||
"""
|
||||
crypto = Crypto(crypto_key)
|
||||
# 对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', _id_cryto, expires=TMPID_COOKIE_AGE)
|
||||
return set_cookie
|
||||
|
||||
|
||||
def crypto_id_2_user_id(request, msg_template, home_url):
|
||||
"""
|
||||
前端提交的加密的userid,解密出真实的userid
|
||||
"""
|
||||
try:
|
||||
crypto_tmp_id = request.COOKIES.get('tmpid')
|
||||
# 解密
|
||||
crypto = Crypto(crypto_key)
|
||||
return True, crypto.decrypt(crypto_tmp_id)
|
||||
except Exception as e:
|
||||
logger.error('[异常] :%s' % str(e))
|
||||
logger.error('[异常] 请求方法:%s,请求路径:%s,未能拿到TmpID或会话己超时。' % (request.method, request.path))
|
||||
context = {
|
||||
'msg': "会话己超时,请重新扫码验证用户信息。",
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
return False, context
|
||||
|
||||
|
||||
def ops_account(ad_ops, request, msg_template, home_url, username, new_password):
|
||||
"""
|
||||
ad 账号操作,判断账号状态,重置密码或解锁账号
|
||||
|
|
|
@ -7,7 +7,7 @@ from ldap3.core.exceptions import LDAPException
|
|||
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, crypto_id_2_user_info, ops_account
|
||||
from .utils import code_2_user_detail, ops_account
|
||||
from django.conf import settings
|
||||
APP_ENV = os.getenv('APP_ENV')
|
||||
if APP_ENV == 'dev':
|
||||
|
@ -34,13 +34,6 @@ class PARAMS(object):
|
|||
AUTH_APP = '微信'
|
||||
from utils.wework_ops import WeWorkOps
|
||||
ops = WeWorkOps()
|
||||
else:
|
||||
corp_id = None
|
||||
app_id = WEWORK_CORP_ID
|
||||
agent_id = WEWORK_AGENT_ID
|
||||
AUTH_APP = '微信'
|
||||
from utils.wework_ops import WeWorkOps
|
||||
ops = WeWorkOps()
|
||||
|
||||
|
||||
scan_params = PARAMS()
|
||||
|
@ -63,8 +56,6 @@ def index(request):
|
|||
return render(request, 'ding_index.v1.html', locals())
|
||||
elif request.method == 'GET' and AUTH_CODE_TYPE == 'WEWORK':
|
||||
return render(request, 'we_index.v1.html', locals())
|
||||
elif request.method == 'GET' and AUTH_CODE_TYPE == 'FEISHU':
|
||||
return render(request, 'index.v1.html', locals())
|
||||
else:
|
||||
logger.error('[异常] 请求方法:%s,请求路径%s' % (request.method, request.path))
|
||||
#
|
||||
|
@ -123,7 +114,6 @@ def reset_password(request):
|
|||
:return:
|
||||
"""
|
||||
home_url = '%s://%s' % (request.scheme, HOME_URL)
|
||||
# 从cookie中提取union_id,并解密,然后对当前union_id的用户进行重置密码
|
||||
if request.method == 'GET':
|
||||
code = request.GET.get('code')
|
||||
if code:
|
||||
|
@ -138,13 +128,13 @@ def reset_password(request):
|
|||
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)
|
||||
# 账号是否是激活的
|
||||
# 账号在企业微信或钉钉中是否是激活的
|
||||
_, res = get_user_is_active(user_info)
|
||||
if not _:
|
||||
# 否则账号不存在或未激活
|
||||
context = {
|
||||
'msg': '当前扫码的用户未激活或可能己离职,用户信息如下:%s' % user_info,
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
|
@ -160,7 +150,7 @@ def reset_password(request):
|
|||
logger.error('[异常] :%s' % str(callback_e))
|
||||
return render(request, msg_template, context)
|
||||
|
||||
# 通过user_info拿到用户信息,并格式化为username
|
||||
# 通过user_info拿到用户邮箱,并格式化为username
|
||||
_, email = get_email_from_userinfo(user_info)
|
||||
if not _:
|
||||
context = {
|
||||
|
@ -181,8 +171,10 @@ def reset_password(request):
|
|||
|
||||
# 如果邮箱能提取到,则格式化之后,提取出账号提交到前端绑定
|
||||
if username:
|
||||
request.session[username] = code
|
||||
context = {
|
||||
'username': username,
|
||||
'code': code,
|
||||
}
|
||||
return render(request, 'resetPassword.v1.html', context)
|
||||
else:
|
||||
|
@ -195,43 +187,25 @@ def reset_password(request):
|
|||
|
||||
# 重置密码页面,输入新密码后点击提交
|
||||
elif request.method == 'POST':
|
||||
try:
|
||||
username = request.POST.get('username')
|
||||
code = request.POST.get('code')
|
||||
if request.session.get(username) and request.session.get(username) == code:
|
||||
_new_password = request.POST.get('new_password').strip()
|
||||
_status, user_info = crypto_id_2_user_info(_ops, request, msg_template, home_url, scan_params.AUTH_APP)
|
||||
if not _status:
|
||||
return render(request, msg_template, user_info)
|
||||
|
||||
# 通过user_info拿到用户信息,并格式化为username
|
||||
_, email = get_email_from_userinfo(user_info)
|
||||
if not _:
|
||||
try:
|
||||
return ops_account(ad_ops=AdOps(), request=request, msg_template=msg_template, home_url=home_url, username=username, new_password=_new_password)
|
||||
except Exception as reset_e:
|
||||
context = {
|
||||
'msg': email,
|
||||
'msg': "错误[%s],请与管理员联系." % str(reset_e),
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
logger.error('[异常] :%s' % str(reset_e))
|
||||
return render(request, msg_template, context)
|
||||
|
||||
# 格式化用户名
|
||||
_, username = format2username(email)
|
||||
if _ is False:
|
||||
context = {
|
||||
'msg': username,
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
return render(request, msg_template, context)
|
||||
return ops_account(ad_ops=AdOps(), request=request, msg_template=msg_template, home_url=home_url, username=username, new_password=_new_password)
|
||||
except Exception as reset_e:
|
||||
context = {
|
||||
'msg': "错误[%s],请与管理员联系." % str(reset_e),
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
logger.error('[异常] :%s' % str(reset_e))
|
||||
return render(request, msg_template, context)
|
||||
finally:
|
||||
del request.session[username]
|
||||
else:
|
||||
context = {
|
||||
'msg': "请从主页开始进行操作。",
|
||||
'msg': "认证已经失效,请从主页重新进行操作。",
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
|
@ -245,62 +219,26 @@ def unlock_account(request):
|
|||
:return:
|
||||
"""
|
||||
home_url = '%s://%s' % (request.scheme, HOME_URL)
|
||||
if request.method == 'GET':
|
||||
_status, user_info = crypto_id_2_user_info(_ops, request, msg_template, home_url, scan_params.AUTH_APP)
|
||||
if not _status:
|
||||
return render(request, msg_template, user_info)
|
||||
|
||||
# 通过user_info拿到用户信息,并格式化为username
|
||||
_, email = get_email_from_userinfo(user_info)
|
||||
if not _:
|
||||
context = {
|
||||
'msg': email,
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
return render(request, msg_template, context)
|
||||
|
||||
_, username = format2username(email)
|
||||
if _ is False:
|
||||
context = {
|
||||
'msg': username,
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
return render(request, msg_template, context)
|
||||
context = {
|
||||
'username': username,
|
||||
}
|
||||
return render(request, 'resetPassword.v1.html', context)
|
||||
|
||||
elif request.method == 'POST':
|
||||
_status, user_info = crypto_id_2_user_info(_ops, request, msg_template, home_url, scan_params.AUTH_APP)
|
||||
if not _status:
|
||||
return render(request, msg_template, user_info)
|
||||
|
||||
# 通过user_info拿到用户信息,并格式化为username
|
||||
_, email = get_email_from_userinfo(user_info)
|
||||
if not _:
|
||||
context = {
|
||||
'msg': email,
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
return render(request, msg_template, context)
|
||||
|
||||
# 格式化用户名
|
||||
_, username = format2username(email)
|
||||
if _ is False:
|
||||
context = {
|
||||
'msg': username,
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
return render(request, msg_template, context)
|
||||
return ops_account(AdOps(), request, msg_template, home_url, username, None)
|
||||
if request.method == 'POST':
|
||||
username = request.POST.get('username')
|
||||
code = request.POST.get('code')
|
||||
if request.session.get(username) and request.session.get(username) == code:
|
||||
try:
|
||||
return ops_account(AdOps(), request, msg_template, home_url, username, None)
|
||||
except Exception as reset_e:
|
||||
context = {
|
||||
'msg': "错误[%s],请与管理员联系." % str(reset_e),
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
logger.error('[异常] :%s' % str(reset_e))
|
||||
return render(request, msg_template, context)
|
||||
finally:
|
||||
del request.session[username]
|
||||
else:
|
||||
context = {
|
||||
'msg': "请从主页开始进行操作。",
|
||||
'msg': "认证已经失效,请从主页重新进行操作。",
|
||||
'button_click': "window.location.href='%s'" % home_url,
|
||||
'button_display': "返回主页"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
a, body, button, dd, div, dl, dt, h1, h2, h3, h4, h5, h6, input, li, ol, p, td, textarea, ul { margin: 0; padding: 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%);*/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
|
||||
function BtnClick(btn, type, unsecpwd) {
|
||||
$(btn).click(function () {
|
||||
// ^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$\%\^\&\*\(\)])[0-9a-zA-Z!@#$\%\^\&\*\(\)]{8,32}$ 要求密码了里面包含字母、数字、特殊字符。
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load static %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
<title>自助密码平台</title>
|
||||
<link rel="stylesheet" href="{% static 'css/dmaku.css' %}">
|
||||
<script type="text/javascript" src="{% static 'js/jquery-1.8.3.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/alert.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/check.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/ddLogin-0.0.5.js' %}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="head-container" id="head-container">
|
||||
|
@ -22,6 +20,7 @@
|
|||
{% csrf_token %}
|
||||
<h1>重置</h1>
|
||||
<input type="text" id="username" name="username" readonly placeholder="{{ username }}" value="{{ username }}">
|
||||
<input type="hidden" id="code" name="code" readonly value="{{ code }}">
|
||||
<p></p>
|
||||
<p></p>
|
||||
<button id="btn_unlock" type="submit">解锁账号</button>
|
||||
|
@ -34,6 +33,7 @@
|
|||
<h1>重置</h1>
|
||||
<span>新密码8至30位长度,要求包含大小写字母及数字。</span>
|
||||
<input type="text" id="username" name="username" readonly placeholder="{{ username }}" value="{{ username }}">
|
||||
<input type="hidden" id="code" name="code" readonly value="{{ code }}">
|
||||
<input type="password" id="new_password" name="new_password" placeholder="新密码">
|
||||
<input type="password" id="ensure_password" name="ensure_password" placeholder="再次确认新密码">
|
||||
<p></p>
|
||||
|
|
Loading…
Reference in New Issue