前后端交互模式:
查询字符串: 前端: location.search/params(放的url?后面的东西) 后端接收 request.query_params
请求体: 前端: 表单提交/ajax的put putch post 的第二个参数{} 后端接收: request.data.get()put。。。 也可以用location.search发查询字段串 后端用request.query_params
找回密码(忘记密码)
业务流程实现vue客户端提供找回密码的页面,供用户输入当前帐号绑定的邮箱地址
在用户点击发送邮件以后,ajax请求api服务端发送邮件到指定邮箱,并指定邮件内容为链接地址(链接中附带访问票据参数),点击链接地址跳转到荏苒网的修改密码页面,新页面提供修改密码的表单。
提供用户修改密码的服务端接口。
代码实现
显示找回密码页面
用邮箱重置密码
发送邮件
import "../../static/js/TCaptcha";
export default {
name: "FindPassword",
data(){
return {
email:"",
password:"",
}
},
methods:{
send_email(){
if(!/^\w+@\w+\.\w+$/.test(this.email)){
this.$message.error("邮箱地址格式有误!");
return false;
}
},
show_captcha(){
// 显示验证码
}
},
}
input{
outline: none;
}
*, :after, :before {
box-sizing: border-box;
}
.sign {
height: 100%;
min-height: 750px;
text-align: center;
font-size: 14px;
background-color: #f1f1f1
}
.sign:before {
content: "";
display: inline-block;
height: 85%;
vertical-align: middle
}
.sign .disable,.sign .disable-gray {
opacity: .5;
pointer-events: none
}
.sign .disable-gray {
background-color: #969696
}
.sign .tooltip-error {
font-size: 14px;
line-height: 25px;
white-space: nowrap;
background: none
}
.sign .tooltip-error .tooltip-inner {
max-width: 280px;
color: #333;
border: 1px solid #ea6f5a;
background-color: #fff
}
.sign .tooltip-error .tooltip-inner i {
position: static;
margin-right: 5px;
font-size: 20px;
color: #ea6f5a;
vertical-align: middle
}
.sign .tooltip-error .tooltip-inner span {
vertical-align: middle;
display: inline-block;
white-space: normal;
max-width: 230px
}
.sign .tooltip-error.right .tooltip-arrow-border {
border-right-color: #ea6f5a
}
.sign .tooltip-error.right .tooltip-arrow-bg {
left: 2px;
border-right-color: #fff
}
.sign .slide-error {
position: relative;
padding: 10px 0;
border: 1px solid #c8c8c8;
border-radius: 4px
}
.sign .slide-error i {
position: static!important;
margin-right: 10px;
color: #ea6f5a!important;
vertical-align: middle
}
.sign .slide-error span {
font-size: 15px;
vertical-align: middle
}
.sign .slide-error div {
margin-top: 10px;
font-size: 13px
}
.sign .slide-error a {
color: #3194d0
}
.sign .js-sign-up-forbidden {
color: #999;
padding: 80px 0 100px
}
.sign .js-sign-up-container .slide-error {
border-bottom: none;
border-radius: 0
}
.sign .logo {
position: absolute;
top: 56px;
margin-left: 50px
}
.sign .logo img {
width: 100px
}
.sign .main {
width: 400px;
margin: 60px auto 0;
padding: 50px 50px 30px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 0 8px rgba(0,0,0,.1);
vertical-align: middle;
display: inline-block
}
.sign .reset-title,.sign .title {
margin: 0 auto 50px;
padding: 10px;
font-weight: 400;
color: #969696
}
.sign .reset-title a,.sign .title a {
padding: 10px;
color: #969696
}
.sign .reset-title a:hover,.sign .title a:hover {
border-bottom: 2px solid #ea6f5a
}
.sign .reset-title .active,.sign .title .active {
font-weight: 700;
color: #ea6f5a;
border-bottom: 2px solid #ea6f5a
}
.sign .reset-title b,.sign .title b {
padding: 10px
}
.sign .reset-title {
color: #333;
font-weight: 700
}
.sign form {
margin-bottom: 30px
}
.sign form .input-prepend {
position: relative;
width: 100%
}
.sign form .input-prepend input {
width: 100%;
height: 50px;
margin-bottom: 0;
padding: 4px 12px 4px 35px;
border: 1px solid #c8c8c8;
border-radius: 0 0 4px 4px;
background-color: hsla(0,0%,71%,.1);
vertical-align: middle
}
.sign form .input-prepend i {
position: absolute;
top: 14px;
left: 10px;
font-size: 18px;
color: #969696
}
.sign form .input-prepend span {
color: #333
}
.sign form .input-prepend .ic-show {
top: 18px;
left: auto;
right: 8px;
font-size: 12px
}
.sign form .geetest-placeholder {
height: 44px;
border-radius: 4px;
background-color: hsla(0,0%,71%,.1);
text-align: center;
line-height: 44px;
font-size: 14px;
color: #999
}
.sign form .restyle {
margin-bottom: 0
}
.sign form .restyle input {
border-bottom: none;
border-radius: 4px 4px 0 0
}
.sign form .no-radius input {
border-radius: 0
}
.sign form .slide-security-placeholder {
height: 32px;
background-color: hsla(0,0%,71%,.1);
border-radius: 4px
}
.sign form .slide-security-placeholder p {
padding-top: 7px;
color: #999;
margin-right: -7px
}
.sign .overseas-btn {
font-size: 14px;
color: #999
}
.sign .overseas-btn:hover {
color: #2f2f2f
}
.sign .remember-btn {
float: left;
margin: 15px 0
}
.sign .remember-btn span {
margin-left: 5px;
font-size: 15px;
color: #969696;
vertical-align: middle
}
.sign .forget-btn {
float: right;
position: relative;
margin: 15px 0;
font-size: 14px
}
.sign .forget-btn a {
color: #999
}
.sign .forget-btn a:hover {
color: #333
}
.sign .forget-btn .dropdown-menu {
top: 20px;
left: auto;
right: 0;
border-radius: 4px
}
.sign .forget-btn .dropdown-menu a {
padding: 10px 20px;
color: #333
}
.sign #sign-in-loading {
position: relative;
width: 20px;
height: 20px;
vertical-align: middle;
margin-top: -4px;
margin-right: 2px;
display: none
}
.sign #sign-in-loading:after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: transparent
}
.sign #sign-in-loading:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border-radius: 10px;
border: 2px solid #fff;
border-bottom-color: transparent;
vertical-align: middle;
-webkit-animation: rolling .8s infinite linear;
animation: rolling .8s infinite linear;
z-index: 1
}
.sign .sign-in-button,.sign .sign-up-button {
margin-top: 20px;
width: 100%;
padding: 9px 18px;
font-size: 18px;
border: none;
border-radius: 25px;
color: #fff;
background: #42c02e;
cursor: pointer;
outline: none;
display: block;
clear: both
}
.sign .sign-in-button:hover,.sign .sign-up-button:hover {
background: #3db922
}
.sign .sign-in-button {
background: #3194d0
}
.sign .sign-in-button:hover {
background: #187cb7
}
.sign .btn-in-resend,.sign .btn-up-resend {
position: absolute;
top: 7px;
right: 7px;
width: 100px;
height: 36px;
font-size: 13px;
color: #fff;
background-color: #42c02e;
border-radius: 20px;
line-height: 36px
}
.sign .btn-in-resend {
background-color: #3194d0
}
.sign .sign-up-msg {
margin: 10px 0;
padding: 0;
text-align: center;
font-size: 12px;
line-height: 20px;
color: #969696
}
.sign .sign-up-msg a,.sign .sign-up-msg a:hover {
color: #3194d0
}
.sign .overseas input {
padding-left: 110px!important
}
.sign .overseas .overseas-number {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 50px;
font-size: 18px;
color: #969696;
border-right: 1px solid #c8c8c8
}
.sign .overseas .overseas-number span {
margin-top: 17px;
padding-left: 35px;
text-align: left;
font-size: 14px;
display: block
}
.sign .overseas .dropdown-menu {
width: 100%;
max-height: 285px;
font-size: 14px;
border-radius: 0 0 4px 4px;
overflow-y: auto
}
.sign .overseas .dropdown-menu li .nation-code {
width: 65px;
display: inline-block
}
.sign .overseas .dropdown-menu li a {
padding: 6px 20px;
font-size: 14px;
line-height: 20px
}
.sign .overseas .dropdown-menu li a::hover {
color: #fff;
background-color: #f5f5f5
}
.sign .more-sign {
margin-top: 50px
}
.sign .more-sign h6 {
position: relative;
margin: 0 0 10px;
font-size: 12px;
color: #b5b5b5
}
.sign .more-sign h6:before {
left: 30px
}
.sign .more-sign h6:after,.sign .more-sign h6:before {
content: "";
border-top: 1px solid #b5b5b5;
display: block;
position: absolute;
width: 60px;
top: 5px
}
.sign .more-sign h6:after {
right: 30px
}
.sign .more-sign ul {
margin-bottom: 10px;
list-style: none
}
.sign .more-sign ul li {
margin: 0 5px;
display: inline-block
}
.sign .more-sign ul a {
width: 50px;
height: 50px;
line-height: 50px;
display: block
}
.sign .more-sign ul i {
font-size: 28px
}
.sign .more-sign .ic-weibo {
color: #e05244
}
.sign .more-sign .ic-wechat {
color: #00bb29
}
.sign .more-sign .ic-qq_connect {
color: #498ad5
}
.sign .more-sign .ic-douban {
color: #00820f
}
.sign .more-sign .ic-more {
color: #999
}
.sign .more-sign .weibo-loading {
pointer-events: none;
cursor: pointer;
position: relative
}
.sign .more-sign .weibo-loading:after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #fff
}
body.reader-night-mode .sign .more-sign .weibo-loading:after {
background-color: #3f3f3f
}
.sign .more-sign .weibo-loading:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border-radius: 10px;
border: 2px solid #e05244;
border-bottom-color: transparent;
vertical-align: middle;
-webkit-animation: rolling .8s infinite linear;
animation: rolling .8s infinite linear;
z-index: 1
}
@keyframes rolling {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg)
}
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn)
}
}
@-webkit-keyframes rolling {
0% {
-webkit-transform: rotate(0deg)
}
to {
-webkit-transform: rotate(1turn)
}
}
.sign .reset-password-input {
border-radius: 4px!important
}
.sign .return {
margin-left: -8px;
color: #969696
}
.sign .return:hover {
color: #333
}
.sign .return i {
margin-right: 5px
}
.sign .icheckbox_square-green {
display: inline-block;
*display: inline;
vertical-align: middle;
margin: 0;
padding: 0;
width: 18px;
height: 18px;
background: url(/static/image/green.png) no-repeat;
border: none;
cursor: pointer;
background-position: 0 0
}
.sign .icheckbox_square-green.hover {
background-position: -20px 0
}
.sign .icheckbox_square-green.checked {
background-position: -40px 0
}
.sign .icheckbox_square-green.disabled {
background-position: -60px 0;
cursor: default
}
.sign .icheckbox_square-green.checked.disabled {
background-position: -80px 0
}
.geetest_panel_box>* {
box-sizing: content-box
}
@media (max-width:768px) {
body {
min-width: 0
}
.sign {
height: auto;
min-height: 0;
background-color: transparent
}
.sign .logo {
display: none
}
.sign .main {
position: absolute;
left: 50%;
margin: 0 0 0 -200px;
box-shadow: none
}
}
在登录组件Login.vue文件中,提供找回密码的跳转页面按钮。Login.vue,代码:a标签改为router-link,href改为to
登录遇到问题?
路由代码:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
// ...import FindPassword from "../components/FindPassword"
export default new Router({
mode: "history",
routes: [
// .... ,{
path: '/find_password',
name: "FindPassword",
component: FindPassword,
},
]
})
服务端提供接收客户端请求和发送邮件的api接口
视图代码:
import re
class FindPasswordAPIView(APIView):
def get(self,request):
"""发送邮件"""
email = request.query_params.get("email")
# 验证邮件 xx@xx.xxx
if not re.match("^\w+@\w+\.\w+$",email):
return Response({"detail":"邮箱格式不正确!"}, status=status.HTTP_400_BAD_REQUEST)
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return Response({"detail": "邮箱没有被注册,请重新确认!"}, status=status.HTTP_400_BAD_REQUEST)
# 发送邮件
return Response({"detail":"ok"})
路由地址:
from django.urls import path,re_path
from rest_framework_jwt.views import obtain_jwt_token
from . import views
urlpatterns = [
path("login/", obtain_jwt_token),
path("captcha/", views.CaptchaAPIView.as_view() ),
path("", views.UserAPIView.as_view() ),
re_path("sms/(?P1[3-9]\d{9})/", views.SMSAPIView.as_view() ),
re_path("mobile/(?P1[3-9]\d{9})/", views.MobileAPIView.as_view() ),
re_path("find_password/", views.FindPasswordAPIView.as_view() ),
]
使用Django发送邮件
Django中内置了邮件发送功能,被定义在django.core.mail模块中。发送邮件需要使用SMTP服务器,常用的免费服务器有:163、126、QQ,下面以163邮件为例。SMTP,简单邮件发送协议,是程序发送邮件必须实现的协议。
1)注册163邮箱,登录后设置。
2)在新页面中点击“客户端授权密码”,勾选“开启”,弹出新窗口填写手机验证码。
3)填写授权码。
4)提示开启成功。
5) 在Django配置文件settings/dev.py中,设置邮箱的配置信息
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 25
#发送邮件的邮箱
EMAIL_HOST_USER = 'mooluoo@163.com'
#在邮箱中设置的客户端授权密码
EMAIL_HOST_PASSWORD = 'renranwang2020'
#收件人看到的发件人
EMAIL_FROM = 'renran<649641514@163.com>'
6) 使用Django提供的模块发送邮件
在django.core.mail模块提供了send_mail来发送邮件。
send_mail(subject, message, from_email, recipient_list,html_message=None)subject 邮件标题
message 普通邮件正文, 普通字符串
from_email 发件人
recipient_list 收件人列表
html_message 多媒体邮件正文,可以是html字符串
视图代码:
import re
from django.core.mail import send_mail
from django.conf import settings
class FindPasswordAPIView(APIView):
def get(self,request):
"""发送邮件"""
email = request.query_params.get("email")
# 验证邮件 xx@xx.xxx
if not re.match("^\w+@\w+\.\w+$",email):
return Response({"detail":"邮箱格式不正确!"}, status=status.HTTP_400_BAD_REQUEST)
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return Response({"detail": "邮箱没有被注册,请重新确认!"}, status=status.HTTP_400_BAD_REQUEST)
# 发送邮件
url = settings.CLIENT_HOST + "/reset_password"
send_mail(subject="【荏苒】找回密码",message="",
html_message='尊敬的%s,您好!
您在荏苒网申请了找回密码,
请点击重置密码进行密码修改操
作.
如果不是您本人的操作,请忽略本次邮件!' % (user.username,url),
from_email=settings.EMAIL_FROM, recipient_list=[email])
return Response({"detail":"ok"})
setting/dev.py配置:
# 客户端相关配置
CLIENT_HOST = "http://www.moluo.net:8080"
客户端ajax请求发送邮件,FindPassword.vue,代码:
用邮箱重置密码
发送邮件
import "../../static/js/TCaptcha";
export default {
name: "FindPassword",
data(){
return {
email:"",
password:"",
}
},
methods:{
send_email(){
if(!/^\w+@\w+\.\w+$/.test(this.email)){
this.$message.error("邮箱地址格式有误!");
return false;
}
// 请求发送邮件
this.$axios.get(`${this.$settings.Host}/users/find_password/`,{
params:{
email: this.email,
}
}).then(response=>{
this.$message.success("发送邮件成功!请留意您的邮箱!");
}).catch(error=>{
this.$message.error("网络异常,无法发送邮件!");
})
},
show_captcha(){
// 显示验证码
if(this.email.length<1){
// 阻止代码继续往下执行
return false;
}
var captcha1 = new TencentCaptcha(this.$settings.TC_captcha.app_id, res=>{
this.$axios.get(`${this.$settings.Host}/users/captcha/`,{
params:{
ticket: res.ticket,
randstr: res.randstr
}
}).then(response=>{
if(response.data.detail){
// 继续进行登录处理
this.send_email();
}
}).catch(error=>{
this.$message.error("对不起,验证码校验不通过!");
});
});
captcha1.show(); // 显示验证码
}
},
}
客户端提供重置密码的页面
ResetPassword.vue
使用邮箱重置密码
重置密码
export default {
name: "ResetPassword",
data(){
return {
email: "",
password:"",
password2:"",
}
},
created() {
},
methods:{
// 重置密码
passwordhander(){
},
show_captcha(){
}
},
}
input{
outline: none;
}
*, :after, :before {
box-sizing: border-box;
}
.sign {
height: 100%;
min-height: 750px;
text-align: center;
font-size: 14px;
background-color: #f1f1f1
}
.sign:before {
content: "";
display: inline-block;
height: 85%;
vertical-align: middle
}
.sign .disable,.sign .disable-gray {
opacity: .5;
pointer-events: none
}
.sign .disable-gray {
background-color: #969696
}
.sign .tooltip-error {
font-size: 14px;
line-height: 25px;
white-space: nowrap;
background: none
}
.sign .tooltip-error .tooltip-inner {
max-width: 280px;
color: #333;
border: 1px solid #ea6f5a;
background-color: #fff
}
.sign .tooltip-error .tooltip-inner i {
position: static;
margin-right: 5px;
font-size: 20px;
color: #ea6f5a;
vertical-align: middle
}
.sign .tooltip-error .tooltip-inner span {
vertical-align: middle;
display: inline-block;
white-space: normal;
max-width: 230px
}
.sign .tooltip-error.right .tooltip-arrow-border {
border-right-color: #ea6f5a
}
.sign .tooltip-error.right .tooltip-arrow-bg {
left: 2px;
border-right-color: #fff
}
.sign .slide-error {
position: relative;
padding: 10px 0;
border: 1px solid #c8c8c8;
border-radius: 4px
}
.sign .slide-error i {
position: static!important;
margin-right: 10px;
color: #ea6f5a!important;
vertical-align: middle
}
.sign .slide-error span {
font-size: 15px;
vertical-align: middle
}
.sign .slide-error div {
margin-top: 10px;
font-size: 13px
}
.sign .slide-error a {
color: #3194d0
}
.sign .js-sign-up-forbidden {
color: #999;
padding: 80px 0 100px
}
.sign .js-sign-up-container .slide-error {
border-bottom: none;
border-radius: 0
}
.sign .logo {
position: absolute;
top: 56px;
margin-left: 50px
}
.sign .logo img {
width: 100px
}
.sign .main {
width: 400px;
margin: 60px auto 0;
padding: 50px 50px 30px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 0 8px rgba(0,0,0,.1);
vertical-align: middle;
display: inline-block
}
.sign .reset-title,.sign .title {
margin: 0 auto 50px;
padding: 10px;
font-weight: 400;
color: #969696
}
.sign .reset-title a,.sign .title a {
padding: 10px;
color: #969696
}
.sign .reset-title a:hover,.sign .title a:hover {
border-bottom: 2px solid #ea6f5a
}
.sign .reset-title .active,.sign .title .active {
font-weight: 700;
color: #ea6f5a;
border-bottom: 2px solid #ea6f5a
}
.sign .reset-title b,.sign .title b {
padding: 10px
}
.sign .reset-title {
color: #333;
font-weight: 700
}
.sign form {
margin-bottom: 30px
}
.sign form .input-prepend {
position: relative;
width: 100%
}
.sign form .input-prepend input {
width: 100%;
height: 50px;
margin-bottom: 0;
padding: 4px 12px 4px 35px;
border: 1px solid #c8c8c8;
border-radius: 0 0 4px 4px;
background-color: hsla(0,0%,71%,.1);
vertical-align: middle
}
.sign form .input-prepend i {
position: absolute;
top: 14px;
left: 10px;
font-size: 18px;
color: #969696
}
.sign form .input-prepend span {
color: #333
}
.sign form .input-prepend .ic-show {
top: 18px;
left: auto;
right: 8px;
font-size: 12px
}
.sign form .geetest-placeholder {
height: 44px;
border-radius: 4px;
background-color: hsla(0,0%,71%,.1);
text-align: center;
line-height: 44px;
font-size: 14px;
color: #999
}
.sign form .restyle {
margin-bottom: 0
}
.sign form .restyle input {
border-bottom: none;
border-radius: 4px 4px 0 0
}
.sign form .no-radius input {
border-radius: 0
}
.sign form .slide-security-placeholder {
height: 32px;
background-color: hsla(0,0%,71%,.1);
border-radius: 4px
}
.sign form .slide-security-placeholder p {
padding-top: 7px;
color: #999;
margin-right: -7px
}
.sign .overseas-btn {
font-size: 14px;
color: #999
}
.sign .overseas-btn:hover {
color: #2f2f2f
}
.sign .remember-btn {
float: left;
margin: 15px 0
}
.sign .remember-btn span {
margin-left: 5px;
font-size: 15px;
color: #969696;
vertical-align: middle
}
.sign .forget-btn {
float: right;
position: relative;
margin: 15px 0;
font-size: 14px
}
.sign .forget-btn a {
color: #999
}
.sign .forget-btn a:hover {
color: #333
}
.sign .forget-btn .dropdown-menu {
top: 20px;
left: auto;
right: 0;
border-radius: 4px
}
.sign .forget-btn .dropdown-menu a {
padding: 10px 20px;
color: #333
}
.sign #sign-in-loading {
position: relative;
width: 20px;
height: 20px;
vertical-align: middle;
margin-top: -4px;
margin-right: 2px;
display: none
}
.sign #sign-in-loading:after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: transparent
}
.sign #sign-in-loading:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border-radius: 10px;
border: 2px solid #fff;
border-bottom-color: transparent;
vertical-align: middle;
-webkit-animation: rolling .8s infinite linear;
animation: rolling .8s infinite linear;
z-index: 1
}
.sign .sign-in-button,.sign .sign-up-button {
margin-top: 20px;
width: 100%;
padding: 9px 18px;
font-size: 18px;
border: none;
border-radius: 25px;
color: #fff;
background: #42c02e;
cursor: pointer;
outline: none;
display: block;
clear: both
}
.sign .sign-in-button:hover,.sign .sign-up-button:hover {
background: #3db922
}
.sign .sign-in-button {
background: #3194d0
}
.sign .sign-in-button:hover {
background: #187cb7
}
.sign .btn-in-resend,.sign .btn-up-resend {
position: absolute;
top: 7px;
right: 7px;
width: 100px;
height: 36px;
font-size: 13px;
color: #fff;
background-color: #42c02e;
border-radius: 20px;
line-height: 36px
}
.sign .btn-in-resend {
background-color: #3194d0
}
.sign .sign-up-msg {
margin: 10px 0;
padding: 0;
text-align: center;
font-size: 12px;
line-height: 20px;
color: #969696
}
.sign .sign-up-msg a,.sign .sign-up-msg a:hover {
color: #3194d0
}
.sign .overseas input {
padding-left: 110px!important
}
.sign .overseas .overseas-number {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 50px;
font-size: 18px;
color: #969696;
border-right: 1px solid #c8c8c8
}
.sign .overseas .overseas-number span {
margin-top: 17px;
padding-left: 35px;
text-align: left;
font-size: 14px;
display: block
}
.sign .overseas .dropdown-menu {
width: 100%;
max-height: 285px;
font-size: 14px;
border-radius: 0 0 4px 4px;
overflow-y: auto
}
.sign .overseas .dropdown-menu li .nation-code {
width: 65px;
display: inline-block
}
.sign .overseas .dropdown-menu li a {
padding: 6px 20px;
font-size: 14px;
line-height: 20px
}
.sign .overseas .dropdown-menu li a::hover {
color: #fff;
background-color: #f5f5f5
}
.sign .more-sign {
margin-top: 50px
}
.sign .more-sign h6 {
position: relative;
margin: 0 0 10px;
font-size: 12px;
color: #b5b5b5
}
.sign .more-sign h6:before {
left: 30px
}
.sign .more-sign h6:after,.sign .more-sign h6:before {
content: "";
border-top: 1px solid #b5b5b5;
display: block;
position: absolute;
width: 60px;
top: 5px
}
.sign .more-sign h6:after {
right: 30px
}
.sign .more-sign ul {
margin-bottom: 10px;
list-style: none
}
.sign .more-sign ul li {
margin: 0 5px;
display: inline-block
}
.sign .more-sign ul a {
width: 50px;
height: 50px;
line-height: 50px;
display: block
}
.sign .more-sign ul i {
font-size: 28px
}
.sign .more-sign .ic-weibo {
color: #e05244
}
.sign .more-sign .ic-wechat {
color: #00bb29
}
.sign .more-sign .ic-qq_connect {
color: #498ad5
}
.sign .more-sign .ic-douban {
color: #00820f
}
.sign .more-sign .ic-more {
color: #999
}
.sign .more-sign .weibo-loading {
pointer-events: none;
cursor: pointer;
position: relative
}
.sign .more-sign .weibo-loading:after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #fff
}
body.reader-night-mode .sign .more-sign .weibo-loading:after {
background-color: #3f3f3f
}
.sign .more-sign .weibo-loading:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border-radius: 10px;
border: 2px solid #e05244;
border-bottom-color: transparent;
vertical-align: middle;
-webkit-animation: rolling .8s infinite linear;
animation: rolling .8s infinite linear;
z-index: 1
}
@keyframes rolling {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg)
}
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn)
}
}
@-webkit-keyframes rolling {
0% {
-webkit-transform: rotate(0deg)
}
to {
-webkit-transform: rotate(1turn)
}
}
.sign .reset-password-input {
border-radius: 4px!important
}
.sign .return {
margin-left: -8px;
color: #969696
}
.sign .return:hover {
color: #333
}
.sign .return i {
margin-right: 5px
}
.sign .icheckbox_square-green {
display: inline-block;
*display: inline;
vertical-align: middle;
margin: 0;
padding: 0;
width: 18px;
height: 18px;
background: url(/static/image/green.png) no-repeat;
border: none;
cursor: pointer;
background-position: 0 0
}
.sign .icheckbox_square-green.hover {
background-position: -20px 0
}
.sign .icheckbox_square-green.checked {
background-position: -40px 0
}
.sign .icheckbox_square-green.disabled {
background-position: -60px 0;
cursor: default
}
.sign .icheckbox_square-green.checked.disabled {
background-position: -80px 0
}
.geetest_panel_box>* {
box-sizing: content-box
}
@media (max-width:768px) {
body {
min-width: 0
}
.sign {
height: auto;
min-height: 0;
background-color: transparent
}
.sign .logo {
display: none
}
.sign .main {
position: absolute;
left: 50%;
margin: 0 0 0 -200px;
box-shadow: none
}
}
注册路由:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router);
import Home from "@/components/Home";
import Login from "@/components/Login"
import Register from "@/components/Register"
import FindPassword from "@/components/FindPassword"
import ResetPassword from "@/components/ResetPassword"
export default new Router({
mode: "history",
routes: [
{
name:"Home",
path:"/",
component:Home,
},
{
name:"Home",
path:"/home",
component:Home,
},
{
name:"Login",
path:"/user/login",
component:Login,
},
{
name:"Register",
path:"/user/register",
component: Register,
},
{
name:"FindPassword",
path:"/find_password",
component: FindPassword,
},
{
name:"ResetPassword",
path:"/reset_password",
component: ResetPassword,
},
]
})
因为我们需要在重置页面组件中,需要识别当前用户身份而且不能给外界直接看到,所以我们可以把用户识别用户身份的数据使用itsdangerous进行加密
itsdangerous模块的官方文档:https://pythonhosted.org/itsdangerous/
安装
pip install -U itsdangerous
TimedJSONWebSignatureSerializer的使用
使用TimedJSONWebSignatureSerializer可以生成带有有效期的token
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings
"""加密"""
# serializer = Serializer(秘钥, 加密数据的有效期(秒))
serializer = Serializer(settings.SECRET_KEY, 300)
# serializer.dumps(数据), 返回bytes类型
access_token = serializer.dumps({'email': '649641514@qq.com'})
access_token = access_.decode()
# 检验token
# 验证失败,会抛出itsdangerous.BadData异常
serializer = Serializer(settings.SECRET_KEY, 300)
try:
data = serializer.loads(access_token)
except BadData:
return None
基于Itsdangerous修改视图中的发送邮件的api接口
import re
from django.core.mail import send_mail
from django.conf import settings
from itsdangerous import TimedJSONWebSignatureSerializer
class FindPasswordAPIView(APIView):
def get(self,request):
"""发送邮件"""
email = request.query_params.get("email")
# 验证邮件 xx@xx.xxx
if not re.match("^\w+@\w+\.\w+$",email):
return Response({"detail":"邮箱格式不正确!"}, status=status.HTTP_400_BAD_REQUEST)
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return Response({"detail": "邮箱没有被注册,请重新确认!"}, status=status.HTTP_400_BAD_REQUEST)
# 使用itsdangerous对链接中的数据进行加密
serializer = TimedJSONWebSignatureSerializer(secret_key=settings.SECRET_KEY, expires_in=constants.EMAIL_EXPIRE_TIME)
token = serializer.dumps({"email": email}).decode()
url = settings.CLIENT_HOST + "/reset_password?token=%s" % token
# 发送邮件
send_mail(subject="【荏苒】找回密码",
message="",
html_message='尊敬的%s,您好!
您在荏苒网申请了找回密码,请点击重置密码进行密码修改操作.
如果不是您本人的操作,请忽略本次邮件!' % (user.username,url),from_email=settings.EMAIL_FROM, recipient_list=[email])
return Response({"detail":"ok"})
客户端重置密码页面组件ResetPassword.vue中接收并转发token到服务端验证toke。视图中接受来自客户端token数据,并进行校验
视图代码:
class CheckTokenAPIView(APIView):
def get(self,request):
token_string = request.query_params.get("token")
serializer = TimedJSONWebSignatureSerializer(secret_key=settings.SECRET_KEY,
expires_in=constants.EMAIL_EXPIRE_TIME)
try:
token = serializer.loads(token_string)
except BadData:
return Response({"detail":"对不起,当前链接地址不存在或者已过期!"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"email": token.get("email")})
路由,代码:
from django.urls import path,re_path
from rest_framework_jwt.views import obtain_jwt_token
from . import views
urlpatterns = [
path("login/", obtain_jwt_token),
path("captcha/", views.CaptchaAPIView.as_view() ),
path("", views.UserAPIView.as_view() ),
re_path("sms/(?P1[3-9]\d{9})/", views.SMSAPIView.as_view() ),
re_path("mobile/(?P1[3-9]\d{9})/", views.MobileAPIView.as_view() ),
re_path("find_password/", views.FindPasswordAPIView.as_view() ),
re_path("token/", views.CheckTokenAPIView.as_view() ),
]
客户端发送ajax请求检测token是否正确并提取token中的信息!
使用邮箱重置密码
重置密码
export default {
name: "ResetPassword",
data(){
return {
email: "",
password:"",
password2:"",
}
},
created() {
this.get_token();
},
methods:{
get_token(){
// 获取链接中的token
this.$axios.get(`${this.$settings.Host}/users/token/`+location.search).then(response=>{
this.email = response.data.email;
}).catch(error=>{
this.$message.error(error.response.data.detail);
})
},
// 重置密码
passwordhander(){
},
show_captcha(){
}
},
}
服务端提供修改重置密码接口
视图代码:
class ResetPasswordAPIView(APIView):
def put(self,request):
token_string = request.query_params.get("token")
serializer = TimedJSONWebSignatureSerializer(secret_key=settings.SECRET_KEY,
expires_in=constants.EMAIL_EXPIRE_TIME)
try:
token = serializer.loads(token_string)
except BadData:
return Response({"detail": "对不起,当前链接地址不存在或者已过期!"}, status=status.HTTP_400_BAD_REQUEST)
password = request.data.get("password")
password2 = request.data.get("password2")
if password != password2:
return Response({"detail":"密码和确认密码不一致!"}, status=status.HTTP_400_BAD_REQUEST)
user = User.objects.get(email=token.get("email"))
user.set_password(password)
user.save()
return Response({"detail":"修改密码成功!"}):
路由代码:
re_path("reset_password/", views.ResetPasswordAPIView.as_view() ),
客户端提交数据请求重置密码
使用邮箱重置密码
重置密码
export default {
name: "ResetPassword",
data(){
return {
email: "",
password:"",
password2:"",
}
},
created() {
this.get_token();
},
methods:{
get_token(){
// 获取链接中的token
this.$axios.get(`${this.$settings.Host}/users/token/`+location.search).then(response=>{
this.email = response.data.email;
}).catch(error=>{
this.$message.error(error.response.data.detail);
})
},
// 重置密码
passwordhander(){
this.$axios.put(`${this.$settings.Host}/users/reset_password/`+location.search,{
password: this.password,
password2: this.password2
}).then(response=>{
this.$confirm('是否跳转到登录页面', '重置密码成功', {
confirmButtonText: '登录页面',
cancelButtonText: '返回首页',
type: 'success'
}).then(() => {
// 前往登录页面
this.$router.push("/login");
}).catch(() => {
// 返回站点首页
this.$router.push("/");
});
}).catch(error=>{
this.$message.error("重置密码失败!请重新尝试发送邮件找回密码!");
})
},
show_captcha(){
// 显示验证码
if(this.password.length<1){
// 阻止代码继续往下执行
this.$message.error("对不起,密码不能为空!");
return false;
}
if(this.password != this.password2){
this.$message.error("对不起,密码和米确认密码必须一致!");
return false;
}
var captcha1 = new TencentCaptcha(this.$settings.TC_captcha.app_id, res=>{
this.$axios.get(`${this.$settings.Host}/users/captcha/`,{
params:{
ticket: res.ticket,
randstr: res.randstr
}
}).then(response=>{
if(response.data.detail){
// 继续进行登录处理
this.passwordhander();
}
}).catch(error=>{
this.$message.error("对不起,验证码校验不通过!");
});
});
captcha1.show(); // 显示验证码
}
},
}
使用celery异步发送找回密码的邮件
在mycelery目录下创建异步发送邮件的任务目录email,并创建任务文件tasks.py,代码:
from mycelery.main import app
import logging
from django.conf import settings
log = logging.getLogger("django")
from django.core.mail import send_mail
@app.task(name="send_email")
def send_email(username, recipient_list, url):
"""异步发送找回密码的邮件"""
try:
send_mail(subject="【荏苒】找回密码",message="",html_message='尊敬的%s,您好!
您在荏苒网申请了找回密码, 请点击重置密码进行密码修改操作.
如果不是您本人的操作,请忽略本次邮件!' % (username,url),from_email=settings.EMAIL_FROM, recipient_list=recipient_list)
except:
log.error("发送邮件失败!")
注册任务
mycelery.main.py
# 主程序
import os
from celery import Celery
# 创建celery实例对象
app = Celery("renran")
# 把celery和django进行组合,识别和加载django的配置文件
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'renranapi.settings.dev')
# 对django框架执行初始化
import django
django.setup()
# 通过app对象加载配置
app.config_from_object("mycelery.config")
# 加载任务
# 参数必须必须是一个列表,里面的每一个任务都是任务的路径名称
# app.autodiscover_tasks(["任务1","任务2"])
app.autodiscover_tasks(["mycelery.sms","mycelery.email"])
# 启动Celery的命令
# 切换目录到mycelery的上一级目录下启动
# celery -A mycelery.main worker --loglevel=info
重启celery以后, 修改视图users.views.py中的发送邮件代码
from renranapi.mycelery.email.tasks import send_email
class FindPasswordAPIView(APIView):
def get(self,request):
"""发送邮件"""
email = request.query_params.get("email")
# 验证邮件 xx@xx.xxx
if not re.match("^\w+@\w+\.\w+$",email):
return Response({"detail":"邮箱格式不正确!"}, status=status.HTTP_400_BAD_REQUEST)
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return Response({"detail": "邮箱没有被注册,请重新确认!"}, status=status.HTTP_400_BAD_REQUEST)
# 使用itsdangerous对链接中的数据进行加密
serializer = TimedJSONWebSignatureSerializer(secret_key=settings.SECRET_KEY, expires_in=constants.EMAIL_EXPIRE_TIME)
token = serializer.dumps({"email": email}).decode()
url = settings.CLIENT_HOST + "/reset_password?token=%s" % token
# 发送邮件
# send_mail(subject="【荏苒】找回密码",message="",html_message='尊敬的%s,您好!
您在荏苒网申请了找回密码, 请点击重置密码进行密码修改操作.
如果不是您本人的操作,请忽略本次邮件!' % (user.username,url),from_email=settings.EMAIL_FROM, recipient_list=[email])
send_email.delay(user.username,[email], url )
return Response({"detail":"ok"})