诸神缄默不语-个人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 sysfrom typing import Listfrom 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 UtilClientclass AliyunDuanxin:def __init__(self):pass@staticmethoddef 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)@staticmethoddef 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:# 如有需要,请打印 errorUtilClient.assert_as_string(error.message)@staticmethodasync 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:# 如有需要,请打印 errorUtilClient.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 Userapp.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']=yanzhengmavalidation_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.datasession['password'] = form.password.datasession["nickname"]=form.nickname.datareturn 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']=yanzhengmavalidation_code_json=AliyunDuanxin.main(accessKeyId,accessKeySecret,sign_name,template_codes['register'],form.phone.data,yanzhengma)if validation_code_json.body.code=='OK':# 保存用户信息到 sessionsession['phone'] = form.phone.datasession['new_password'] = form.new_password.datareturn 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_jsonreturn render_template('reset_password.html',form=form)@app.route('/verify_reset_password', methods=['GET', 'POST'])
def verify_reset_password():#限制发起请求的urlreferrer = request.referrerreset_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种常见注册登录方案逻辑解析 | 人人都是产品经理