fastapi:管理后台:用户注册
一,要注意的环节:
用户的密码保存时不能用明文,要用bcrypt加密保存
有写库操作,要做try except,发生异常时要做写日志,
有多个参数,要用wtforms做参数的校验
二,代码
python代码:
# 得到bcrypt加密后的密码
def get_password_hash(password: str) -> str:
"""Hash a password using bcrypt"""
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
# 1. 定义 WTForms 表单
class RegistrationForm(WTForm):
account = StringField('用户名', [validators.Length(min=4, max=25)])
password = PasswordField('密码', validators=[
DataRequired(message="密码不能为空!"),
Length(min=6, max=20, message="用户名长度必须在 6 到 20 之间")
])
# 用 EqualTo 指向需要对比的字段名 (fieldname='password')
password_confirm = PasswordField('确认密码', [
DataRequired(message="请再次输入密码以确认"),
EqualTo(fieldname='password', message='两次输入的密码不一致,请重新输入')
])
nickname = StringField('昵称', [validators.Length(min=2, max=25)])
# 2. GET 路由:渲染空表单
@router.get("/register", response_class=HTMLResponse)
async def get_register(request: Request):
form = RegistrationForm()
return templates.TemplateResponse(
request=request,
name="account/register.html",
context={"request": request, "form": form,"title": "注册用户"})
# 3. POST 路由:处理表单提交
@router.post("/register")
async def post_register(request: Request,db: AsyncSession = Depends(get_db)):
# 将 FastAPI 接收到的 FormData 转换为 WTForms 需要的格式
form_data = await request.form()
form = RegistrationForm(form_data)
if form.validate():
# 检查是否已注册
count_stmt = select(func.count()).select_from(User).where(User.username == form.account.data)
total_items = await db.scalar(count_stmt)
if total_items>0:
raise HTTPException(
status_code=400,
detail="用户名已被占用,请换一个"
)
# 2. 密码哈希处理
hashed_password = get_password_hash(form.password.data)
# 3. 构建新的 User 实体
new_user = User(
username=form.account.data,
password=hashed_password, # 存入哈希值
nickname=form.nickname.data
)
try:
# 4. 写入数据库
db.add(new_user)
await db.commit()
await db.refresh(new_user) # 刷新以获取数据库生成的 id
except Exception as err:
await db.rollback()
stack = traceback.format_exc()
mylogger.bind(log_type="system").info(f'注册用户失败: {err}')
mylogger.bind(log_type="system").info(stack)
raise HTTPException(
status_code=400,
detail="注册用户失败"
)
# 返回成功
return JSONResponse({"status_code":200,"detail": "用户注册成功", "data":{}})
else:
print(form.errors)
error_msg = "; ".join([f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()])
# 返回异常
return JSONResponse({"status_code":400,"detail": error_msg,"data":{}})
html代码:
{% extends "layout/column.html" %}
{% block title %}{{ title }}{% endblock %}
{% block layout_css %}
<style type="text/css">
body {
background-color: #f5f5f5;
}
.auth-login {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
min-height: 780px;
}
.form-signin {
max-width: 330px;
padding: 15px;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.b-example-divider {
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
form .error {
padding-left:120px;
font-size: 14px;
}
</style>
{% endblock %}
{% block layout_js %}
<script src="{{ url_for('static', path='js/jquery/jquery.form.min.js') }}"></script>
<script src="{{ url_for('static', path='js/jquery/jquery.validate.min.js') }}"></script>
<script type="text/javascript">
$("#item-form").validate({
//ignore: ".ignore",
//debug: true,
rules: {
account: {
required: true,
},
password: {
required: true,
},
password_confirm: {
required: true,
equalTo: "#password",
},
nickname: {
required: true,
},
},
messages: {
account: {
required: "请输入账号",
},
password: {
required: "请输入密码",
},
password_confirm: {
required: "请再次输入密码",
equalTo: '密码不一致',
},
nickname: {
required: "请输入昵称",
},
},
submitHandler: function(form) {
$(form).ajaxSubmit({
dataType: 'json',
beforeSubmit: function(){
},
success: function(rs){
if(rs.status_code == 200){
phenix.show_success_note('注册成功,正在跳转登录页面...')
setTimeout(() => {
window.location.href = "{{ url_for('get_login') }}"
}, 2000);
}else{
phenix.show_error_note(rs.msg)
}
},
error: function(xhr, status, err){
phenix.show_error_note(xhr.responseJSON.meta.message)
}
});
}
});
</script>
{% endblock %}
{% block content %}
<div class="auth-login text-center">
<main class="form-signin w-100 m-auto text-align">
<form id="item-form" action="{{ url_for('post_register') }}" method="POST">
<img class="mb-4 rounded-circle" src="{{ url_for('static', path='images/logo/logo_300.jpg') }}" alt="" width="100">
<h1 class="h3 mb-3 fw-normal">注册</h1>
<div class="form-floating">
<input type="text" name="account" class="form-control" placeholder="">
<label for="account">账号</label>
</div>
<div class="form-floating mt-2">
<input id="password" type="password" name="password" class="form-control" placeholder="Password">
<label for="password">密码</label>
</div>
<div class="form-floating mt-2">
<input type="password" name="password_confirm" class="form-control" placeholder="Password Confirm">
<label for="password_confirm">验证密码</label>
</div>
<div class="form-floating mt-2">
<input type="text" name="nickname" class="form-control" placeholder="">
<label for="nickname">昵称</label>
</div>
<button class="w-100 btn btn-lg btn-primary mt-3" type="submit">注册</button>
</form>
</main>
</div>
{% endblock %}
三,测试效果:

浙公网安备 33010602011771号