用Flask-Login库和阿里云短信推送服务实现网站注册登录功能

诸神缄默不语-个人CSDN博文目录

本文介绍如何用Flask-Login库和阿里云短信推送服务实现网站注册登录功能。

大致逻辑是在注册和找回密码时调用阿里云短信服务,登录时使用手机号+密码登录(别的安全功能还没有加)。

很多代码都是直接由ChatGPT生成的,所以前后可能不太统一。
能用就得了,要什么自行车。

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开发者门户

扫描二维码关注公众号,回复: 15342100 查看本文章

因为我用的是测试账号,所以如果下面填写测试手机号之外的账号,就会返回:{'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连接和互动

其他需要安装的工具包:

  1. Flask-WTF
    官方文档:Flask-WTF — Flask-WTF Documentation (1.0.x)
    安装方式:pip install Flask-WTF
    (会同时安装WTForms)
  2. 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. 本文撰写过程中的其他参考资料

  1. Flask+python3+阿里云平台发送短信 最简单最笨的那种_mingkoukou的博客-CSDN博客
  2. 注册登录功能设计:3种常见注册登录方案逻辑解析 | 人人都是产品经理

猜你喜欢

转载自blog.csdn.net/PolarisRisingWar/article/details/130975157
今日推荐