诸神缄默不语-个人CSDN博文目录
本文介绍如何用Flask-Login库和阿里云短信推送服务实现网站注册登录功能。
大致逻辑是在注册和找回密码时调用阿里云短信服务,登录时使用手机号+密码登录(别的安全功能还没有加)。
很多代码都是直接由ChatGPT生成的,所以前后可能不太统一。
能用就得了,要什么自行车。
文章目录
- 1. 阿里云短信推送服务
- 2. Flask-Login库
- 2.1 初始化
- 2.2 定义用户
- 2.3 显示账号信息
- 2.4 注册
- 2.5 登录
- 2.6 退出
- 2.7 重置密码
- 2.8 限制功能必须登录使用
- 3. 本文撰写过程中的其他参考资料
1. 阿里云短信推送服务
可以薅100条的新人免费羊毛:https://www.aliyun.com/activity/daily/cloudcommunication-daily
OpenAPI调用访问的AccessKey信息,官方建议是创建一个RAM子用户,用它的AccessKey信息来调用。网站是这个:https://ram.console.aliyun.com/users/create
权限的话只用开启 OpenAPI 调用访问就够了。
如果在此时直接用这个AccessKey来请求短信推送服务的话,会返回:{'headers': {'date': 'omit', 'content-type': 'application/json;charset=utf-8', 'content-length': '171', 'connection': 'keep-alive', 'access-control-allow-origin': '*', 'access-control-expose-headers': '*', 'x-acs-request-id': 'omit', 'x-acs-trace-id': 'omit'}, 'statusCode': 200, 'body': {'Code': 'isp.RAM_PERMISSION_DENY', 'Message': 'RAM权限不足,请为当前使用的AccessKey对应RAM用户进行授权', 'RequestId': 'omit'}}
所以要先在https://ram.console.aliyun.com/users里面找到这个子用户,赋予这个系统权限(可以直接模糊查询“短信”):
设置签名:https://dysms.console.aliyun.com/domestic/text/sign(网站没备份的话就只能用测试账号,在“快速学习与测试”页面(https://dysms.console.aliyun.com/quickstart)绑定用以测试的手机号,除这些号以外的手机号都发不到)
设置模版:https://dysms.console.aliyun.com/domestic/text/template
参考模版:
- (注册)
您正在注册成为新用户,验证码为${code},验证码10分钟有效。如非本人操作,请忽略本短信。
- (找回密码)
您正在找回密码,验证码为${code},验证码10分钟有效。如非本人操作,请忽略本短信。
短信服务SDK的文档:短信服务_SDK中心-阿里云OpenAPI开发者门户
因为我用的是测试账号,所以如果下面填写测试手机号之外的账号,就会返回:{'headers': {'date': 'omit', 'content-type': 'application/json;charset=utf-8', 'content-length': '171', 'connection': 'keep-alive', 'access-control-allow-origin': '*', 'access-control-expose-headers': '*', 'x-acs-request-id': 'omit', 'x-acs-trace-id': 'omit'}, 'statusCode': 200, 'body': {'Code': 'isv.SMS_TEST_NUMBER_LIMIT', 'Message': '只能向已回复授权信息的手机号发送', 'RequestId': 'omit'}}
安装包:
pip install alibabacloud_ecs20140526==3.0.7
pip install alibabacloud_dysmsapi20170525==2.0.23
代码:(你可以发现在这里我事实上只处理了main()
,没管异步代码……)
# -*- coding: utf-8 -*-
import sys
from typing import List
from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_util.client import Client as UtilClient
class AliyunDuanxin:
def __init__(self):
pass
@staticmethod
def create_client(
access_key_id: str,
access_key_secret: str,
) -> Dysmsapi20170525Client:
"""
使用AK&SK初始化账号Client
@param access_key_id:
@param access_key_secret:
@return: Client
@throws Exception
"""
config = open_api_models.Config(
# 必填,您的 AccessKey ID,
access_key_id=access_key_id,
# 必填,您的 AccessKey Secret,
access_key_secret=access_key_secret
)
# 访问的域名
config.endpoint = f'dysmsapi.aliyuncs.com'
return Dysmsapi20170525Client(config)
@staticmethod
def main(
accessKeyId:str,
accessKeySecret:str,
sign_name:str,
template_code:str,
phone_numbers:str,
validation_code:str,
) -> None:
client = AliyunDuanxin.create_client(accessKeyId,accessKeySecret)
send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
sign_name=sign_name,
template_code=template_code,
phone_numbers=phone_numbers,
template_param='{"code":"'+str(validation_code)+'"}'
)
runtime = util_models.RuntimeOptions()
try:
return client.send_sms_with_options(send_sms_request, runtime)
except Exception as error:
# 如有需要,请打印 error
UtilClient.assert_as_string(error.message)
@staticmethod
async def main_async(
accessKeyId:str,
accessKeySecret:str,
sign_name:str,
template_code:str,
phone_numbers:str,
validation_code:str,
) -> None:
client = AliyunDuanxin.create_client(accessKeyId,accessKeySecret)
send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
sign_name=sign_name,
template_code=template_code,
phone_numbers=phone_numbers,
template_param='{"code":"'+str(validation_code)+'"}'
)
runtime = util_models.RuntimeOptions()
try:
# 复制代码运行请自行打印 API 的返回值
await client.send_sms_with_options_async(send_sms_request, runtime)
except Exception as error:
# 如有需要,请打印 error
UtilClient.assert_as_string(error.message)
if __name__ == '__main__':
AliyunDuanxin.main(omit)
正常运行后的输出就是:{'headers': {'date': 'omit', 'content-type': 'application/json;charset=utf-8', 'content-length': '171', 'connection': 'keep-alive', 'access-control-allow-origin': '*', 'access-control-expose-headers': '*', 'x-acs-request-id': 'omit', 'x-acs-trace-id': 'omit'}, 'statusCode': 200, 'body': {'BizId': 'omit', 'Code': 'OK', 'Message': 'OK', 'RequestId': 'omit'}
另外还有一种情况是发送太频繁了:{'headers': {'date': 'omit', 'content-type': 'application/json;charset=utf-8', 'content-length': '131', 'connection': 'keep-alive', 'access-control-allow-origin': '*', 'access-control-expose-headers': '*', 'x-acs-request-id': 'omit', 'x-acs-trace-id': 'omit'}, 'statusCode': 200, 'body': {'Code': 'isv.BUSINESS_LIMIT_CONTROL', 'Message': '触发小时级流控Permits:5', 'RequestId': 'omit'}}
2. Flask-Login库
安装:pip install flask-login
官方文档:Flask-Login — Flask-Login 0.7.0 documentation
flask的session官方文档:https://flask.palletsprojects.com/en/latest/quickstart/#sessions
官方GitHub项目:https://github.com/maxcountryman/flask-login
在这个博文中写过的内容将不会重复描述:在云服务器上安装MySQL (MariaDB) 数据库并与Python连接和互动
其他需要安装的工具包:
- Flask-WTF
官方文档:Flask-WTF — Flask-WTF Documentation (1.0.x)
安装方式:pip install Flask-WTF
(会同时安装WTForms) - WTForms
官方GitHub项目:wtforms/wtforms: A flexible forms validation and rendering library for Python.
官方文档:WTForms — WTForms Documentation (3.0.x)
2.1 初始化
from flask import request,render_template,session,redirect,url_for,flash
from flask_login import login_user, LoginManager, logout_user, current_user,login_required
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, ValidationError, EqualTo
from tables import User
app.secret_key=app_secret_key
这里的secret_key
要是一个随机初始化的字节对象,如b'_5#y2L"F4Q8z\n\xec]/'
(但是不要用这个,意思是让你自己生成一个)。
可以用代码生成:
import secrets
print(secrets.token_hex())
2.2 定义用户
#定义用户
login_manager=LoginManager()
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
2.3 显示账号信息
#我的账号
@app.route('/myaccount')
def myaccount():
if 'user_id' in session:
return f'Logged in as {session["nickname"]}'
return 'You are not logged in'
2.4 注册
class RegistrationForm(FlaskForm):
phone = StringField('请填入您的手机号:', validators=[DataRequired()])
nickname=StringField('请填写用户昵称:(可选)')
password = PasswordField('请填写密码:', validators=[DataRequired()])
password2 = PasswordField(
'请再次确认密码:', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('注册')
def validate_phone(self, phone):
user = User.query.filter_by(phone_number=phone.data).first()
if user is not None:
raise ValidationError('您的手机号已经被注册过,请重新注册')
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
#发送验证码
yanzhengma=''.join([str(random.randint(0,9)) for _ in range(6)])
session['verification_code']=yanzhengma
validation_code_json=AliyunDuanxin.main(accessKeyId,accessKeySecret,sign_name,template_codes['register'],form.phone.data,yanzhengma)
if validation_code_json.body.code=='OK':
#短信发送成功,跳转到验证界面
session['phone'] = form.phone.data
session['password'] = form.password.data
session["nickname"]=form.nickname.data
return redirect(url_for('verify'))
elif validation_code_json.body.code=='isv.SMS_TEST_NUMBER_LIMIT':
flash('网站开发者没有开通正规短信服务,如需使用,请联系开发者将您的手机号加入测试服务')
elif validation_code_json.body.code=='isv.BUSINESS_LIMIT_CONTROL':
flash('发送验证码次数过多,请稍后重试!')
else:
return str(validation_code_json)
return render_template('register.html', form=form)
class VerificationForm(FlaskForm):
code = StringField('请输入您收到的6位数验证码:', validators=[DataRequired()])
submit = SubmitField('确定')
@app.route('/verify', methods=['GET', 'POST'])
def verify():
form = VerificationForm()
if form.validate_on_submit():
# 验证码正确
if form.code.data == session.get('verification_code'):
user = User(phone_number=session.get('phone'))
user.set_password(session.get('password'))
db.session.add(user)
db.session.commit()
flash('您已成功注册,请享受ScholarEase之旅吧!')
return redirect(url_for('login'))
else:
flash('验证码错误')
return render_template('verify.html', form=form)
register.html
<!DOCTYPE html>
<html>
<head>
<title>Register</title>
<!-- Include Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<h2>Register</h2>
<form method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.phone.label }} {{ form.phone(class="form-control") }}
{% if form.phone.errors %}
<ul class="errors">
{% for error in form.phone.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="form-group">
{{ form.nickname.label }} {{ form.nickname(class="form-control") }}
{% if form.nickname.errors %}
<ul class="errors">
{% for error in form.nickname.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="form-group">
{{ form.password.label }} {{ form.password(class="form-control") }}
{% if form.password.errors %}
<ul class="errors">
{% for error in form.password.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="form-group">
{{ form.password2.label }} {{ form.password2(class="form-control") }}
{% if form.password2.errors %}
<ul class="errors">
{% for error in form.password2.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="form-group">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
</body>
</html>
verify.html
<!DOCTYPE html>
<html>
<head>
<title>Verify</title>
<!-- Include Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<h2>Verify</h2>
<form method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.code.label }} {{ form.code(class="form-control") }}
{% if form.code.errors %}
<ul class="errors">
{% for error in form.code.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="form-group">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
</body>
</html>
注册成功之后跳转回登录界面:
2.5 登录
login_manager.login_view='login'
login_manager.login_message='您访问的页面需要登录使用'
class LoginForm(FlaskForm):
phone = StringField('请填入您的手机号:', validators=[DataRequired()])
password = PasswordField('请填写密码:', validators=[DataRequired()])
remember_me = BooleanField('记住我')
submit = SubmitField('登录')
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
# 如果用户已经登录,显示一个消息并重定向到主页
flash('您已成功登录,现在将您跳转到首页')
return render_template('message.html')
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(phone_number=form.phone.data).first()
if user is None or not user.check_password(form.password.data):
flash('手机号或密码错误')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('get_home'))
return render_template('login.html', form=form)
login_message
是用于设置@login_required
函数在未登录状态下被点击后的显示内容。
login.html
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<!-- Include Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<h2>Login</h2>
<form method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.phone.label }} {{ form.phone(class="form-control") }}
{% if form.phone.errors %}
<ul class="errors">
{% for error in form.phone.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="form-group">
{{ form.password.label }} {{ form.password(class="form-control") }}
{% if form.password.errors %}
<ul class="errors">
{% for error in form.password.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="form-group">
{{ form.remember_me(class="form-check-input") }} {{ form.remember_me.label(class="form-check-label") }}
</div>
<div class="form-group">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
</body>
</html>
登录成功后跳转回主页:
2.6 退出
@app.route('/logout')
def logout():
# 登出用户
logout_user()
# 显示一个消息
flash('You have been logged out.')
# 重定向到登录页面
return redirect(url_for('login'))
2.7 重置密码
@app.route('/reset_password', methods=['GET', 'POST'])
def reset_password():
form = ResetPasswordForm()
if form.validate_on_submit():
#发送验证码
yanzhengma=''.join([str(random.randint(0,9)) for _ in range(6)])
session['verification_code']=yanzhengma
validation_code_json=AliyunDuanxin.main(accessKeyId,accessKeySecret,sign_name,template_codes['register'],form.phone.data,yanzhengma)
if validation_code_json.body.code=='OK':
# 保存用户信息到 session
session['phone'] = form.phone.data
session['new_password'] = form.new_password.data
return redirect(url_for('verify_reset_password'))
elif validation_code_json.body.code=='isv.SMS_TEST_NUMBER_LIMIT':
flash('网站开发者没有开通正规短信服务,如需使用,请联系开发者将您的手机号加入测试服务')
elif validation_code_json.body.code=='isv.BUSINESS_LIMIT_CONTROL':
flash('发送验证码次数过多,请稍后重试!')
else:
return validation_code_json
return render_template('reset_password.html',form=form)
@app.route('/verify_reset_password', methods=['GET', 'POST'])
def verify_reset_password():
#限制发起请求的url
referrer = request.referrer
reset_password_url = url_for('reset_password')
verify_reset_password_url = url_for('verify_reset_password')
if referrer not in [reset_password_url, verify_reset_password_url]:
flash("您的requests URL错误!")
return redirect(url_for('reset_password'))
form = VerificationForm()
if form.validate_on_submit():
# 验证码正确
if form.code.data == session.get('verification_code'):
user = User.query.filter_by(phone_number=session.get('phone')).first()
if user is None:
flash('Invalid phone number.')
return redirect(url_for('reset_password'))
user.set_password(session.get('new_password'))
db.session.commit()
flash('Your password has been reset.')
return redirect(url_for('login'))
else:
flash('Invalid verification code.')
return render_template('verify_reset_password.html', form=form)
reset_password.html
<!DOCTYPE html>
<html>
<head>
<title>Reset Password</title>
</head>
<body>
<h1>Reset Password</h1>
<form action="{{ url_for('reset_password') }}" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.phone.label }}<br>
{{ form.phone(size=20) }}
</p>
<p>
{{ form.new_password.label }}<br>
{{ form.new_password(size=20) }}
{% for error in form.new_password.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
</p>
<p>
{{ form.new_password2.label }}<br>
{{ form.new_password2(size=20) }}
{% for error in form.new_password2.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
</body>
</html>
verify_reset_password.html
<!DOCTYPE html>
<html>
<head>
<title>Verify Reset Password</title>
</head>
<body>
<h1>Verify Reset Password</h1>
<form action="{{ url_for('verify_reset_password') }}" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.code.label }}<br>
{{ form.code(size=20) }}
</p>
<p>{{ form.submit() }}</p>
</form>
</body>
</html>
2.8 限制功能必须登录使用
在@app.route()
后面加一行@login_required
3. 本文撰写过程中的其他参考资料
- Flask+python3+阿里云平台发送短信 最简单最笨的那种_mingkoukou的博客-CSDN博客
- 注册登录功能设计:3种常见注册登录方案逻辑解析 | 人人都是产品经理