100 行代码实现用户登录注册与 RESTful 接口 - 手把手教程附 Python 源码

在开发大多数应用时,用户系统都是必不可少的部分,而我们总是需要开发围绕用户的登录,注册,获取,更新等接口。在这篇文章将带你用一百多行代码简洁地实现一套这样的用户鉴权与 RESTful 接口,并使用 Session 来处理用户的登录登出

我们将使用 UtilMeta 框架 完成接口开发,这是一个开源的 Python 后端元框架,同时支持接入与适配 Django, Flask, FastAPI 等主流 Python 框架,并且能简洁高效地开发 RESTful 接口

0. 安装框架

使用如下命令即可安装 UtilMeta 框架

pip install utilmeta

UtilMeta 框架需要 Python 版本 >= 3.8

1. 创建项目

我们使用如下命令来创建一个新项目

meta setup demo-user

我们的项目将会使用 Django 作为底层框架,所以在提示选择 backend 的时候我们可以输入 django

项目创建好后,我们需要先对服务的数据库连接进行配置,打开 server.py,在 service 的声明下面插入以下代码

service = UtilMeta(
    __name__,
    name='demo-user',
    backend=django,
)

# new +++++
from utilmeta.core.server.backends.django import DjangoSettings
from utilmeta.core.orm import DatabaseConnections, Database

service.use(DjangoSettings(
    secret_key='YOUR_SECRET_KEY',
))

service.use(DatabaseConnections({
    'default': Database(
        name='db',
        engine='sqlite3',
    )
}))

在插入的代码中,我们声明了 Django 的配置信息与数据库连接的配置
由于 Django 使用 app (应用) 的方式来管理数据模型,接下来我们使用如下的命令来创建一个名为 user 的 app

cd demo-user
meta add user

可以看到在我们的项目文件夹中新创建出了一个 user 文件夹,其中包括

/user
    /migrations
    api.py
    models.py
    schema.py

其中 migrations 文件夹是 Django 用来处理数据库迁移文件的,models.py 是我们编写数据模型的地方

应用创建完成后,我们将 server.py 的 Django 设置中插入一行代码来注入新创建的 user app

service.use(DjangoSettings(
    secret_key='YOUR_SECRET_KEY',
    apps=['user']     # new
))

至此我们完成了项目的配置和初始化

2. 编写用户模型

用户的登录注册 API 当然是围绕 “用户” 进行的了,在开发 API 之前,我们需要先编写好用户的数据模型,我们打开 user/models.py,编写

from django.db import models
from utilmeta.core.orm.backends.django.models import AbstractSession, PasswordField

class User(models.Model):
    username = models.CharField(max_length=20, unique=True)
    password = PasswordField(max_length=100)
    signup_time = models.DateTimeField(auto_now_add=True)

class Session(AbstractSession):
    user = models.ForeignKey(
        User, related_name='sessions', 
        null=True, default=None, on_delete=models.CASCADE
    )

我们首先编写了一个用户模型 User, 其中包含了以下字段

  • username:用户名字段,需要是不能重复的(unique=True
  • password:密码字段,使用的 PasswordField 会自动对输入的明文密码进行哈希加密
  • signup_time:注册时间字段

可以看到除了 User 模型外,我们还编写了一个用户记录用户会话和登录状态的 Session 模型,继承自 UtilMeta 提供的模型基类 AbstractSession,我们将通过这个模型实现用户的登录与鉴权

初始化数据库

当我们编写好数据模型后即可使用 Django 提供的迁移命令方便地创建对应的数据表了,由于我们使用的是 SQLite,所以无需提前安装数据库软件,只需要运行以下两行命令即可完成数据库的创建

meta makemigrations
meta migrate

当看到以下输出时即表示你已完成了数据库的创建

Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying user.0001_initial... OK

数据库迁移命令根据 server.py 中的数据库配置,在项目文件夹中创建了一个名为 db 的 SQLite 数据库,其中已经完成了 User 和 Session 模型的建表

3. 配置 Session 与用户鉴权

编写完用户鉴权相关的模型,我们就可以开始开发鉴权相关的逻辑了,我们在 user 文件夹中新建一个 auth.py 文件,编写 Session 与用户鉴权的配置

from utilmeta.core import auth
from utilmeta.core.auth.session.db import DBSessionSchema, DBSession
from .models import Session, User

USER_ID = '_user_id'

class SessionSchema(DBSessionSchema):
    def get_session_data(self):
        data = super().get_session_data()
        data.update(user_id=self.get(USER_ID))
        return data

session_config = DBSession(
    session_model=Session,
    engine=SessionSchema,
    cookie=DBSession.Cookie(
        name='sessionid',
        age=7 * 24 * 3600,
        http_only=True
    )
)

user_config = auth.User(
    user_model=User,
    authentication=session_config,
    key=USER_ID,
    login_fields=User.username,
    password_field=User.password,
)

在这段代码中,SessionSchema 是处理和存储 Session 数据的核心引擎,session_config 是声明 Session 配置的组件,定义了我们刚编写的 Session 模型以及引擎,并且配置了相应的 Cookie 策略

为了简化案例,我们选择了基于数据库的 Session 实现(DBSession),实际开发中,我们常常使用 Redis 等缓存作为 Session 的存储实现,或者使用 缓存+数据库 的方式,这些实现方式 UtilMeta 都支持,你可以在 Session 鉴权文档 中找到更多的使用方式

另外在代码中我们也声明了 user_config 用户鉴权配置,其中的参数包括

  • user_model:指定鉴权的用户模型,就是我上一节中编写好的 User 模型
  • authentication:指定鉴权策略,我们传入刚刚定义的 session_config,表示着用户鉴权使用 Session 进行
  • key:在 Session 数据中保存当前用户 ID 的名称,默认是 '_user_id'
  • login_fields:能用于登录的字段,如用户标识名,邮箱等,需要是唯一的
  • password_field:用户的密码字段,声明这些可以让 UtilMeta 自动帮你处理登录校验逻辑

4. 编写用户 API

注册接口

我们首先来编写用户的注册接口,注册接口应该接收用户名,密码字段,校验用户名没有被占用后完成注册,并返回新注册的用户数据

我们打开 user/api.py 编写注册接口

from datetime import datetime
from utilmeta.core import api, orm
from utilmeta.utils import exceptions
from .models import User
from . import auth

class SignupSchema(orm.Schema[User]):
    username: str
    password: str
    
class UserSchema(orm.Schema[User]):
    id: int
    username: str
    signup_time: datetime

@auth.session_config.plugin
class UserAPI(api.API):
    @api.post
    def signup(self, data: SignupSchema = request.Body) -> UserSchema:
        if User.objects.filter(username=data.username).exists():
            raise exceptions.BadRequest('Username exists')
        data.save()
        auth.user_config.login_user(
            request=self.request,
            user=data.get_instance()
        )
        return UserSchema.init(data.pk)

我们使用 @api 装饰器定义提供接口服务的 API 函数,其中有 get / post / put / patch / delete 等 HTTP 方法,我们注册接口使用的是 post 方法,你可以使用装饰器的第一个参数指定接口的路径,如果没有的话(比如例子中),就会自动使用函数的名称(signup)作为路径

我们首先声明了注册接口所接受的数据结构 SignupSchema 作为请求体(request.Body)的类型声明,这样 UtilMeta 就会对注册接口的请求体进行解析并转化为一个 SignupSchema 实例,不符合要求的请求会被框架自动拒绝并返回 400 响应

注册接口中的逻辑为

  1. 检测请求中的 username 是否已被注册
  2. 调用 data.save() 方法保存数据
  3. 为当前请求使用 login_user 方法登录新注册的用户
  4. 使用 UserSchema.init(data.pk) 将新用户的数据初始化为 UserSchema 实例后返回

UtilMeta 实现了一套高效的声明式 ORM 查询体系,我们在声明 Schema 类时便使用 orm.Schema[User] 绑定了模型,这样我们就可以通过它的方法来实现数据的增删改查了,你可以在 数据查询与 ORM 文档 中查看它的更多用法

另外我们发现在 UserAPI 类被施加了 @auth.session_config.plugin 这一装饰器插件,这是 Session 配置应用到 API 上的方式,这个插件能在每次请求结束后对请求所更新的 Session 数据进行保存,并返回对应的 Set-Cookie

登录登出接口

接下来我们编写用户的登录与登出接口

from datetime import datetime
from utilmeta.core import api, orm, request
from utilmeta.utils import exceptions
from .models import User
from . import auth
import utype

class LoginSchema(utype.Schema):
    username: str
    password: str

@auth.session_config.plugin
class UserAPI(api.API):
    @api.post
    def signup(self): ...

    # new ++++
    @api.post
    def login(self, data: LoginSchema = request.Body) -> UserSchema:
        user = auth.user_config.login(
            request=self.request,
            ident=data.username,
            password=data.password
        )
        if not user:
            raise exceptions.PermissionDenied('Username of password wrong')
        return UserSchema.init(user)

    @api.post
    def logout(self, session: auth.SessionSchema = auth.session_config):
        session.flush()

在登录接口中,我们直接调用了鉴权配置中的 login() 方法来完成登录,由于我们已经配置好了登录字段与密码字段,UtilMeta 可以自动帮我们完成密码校验与登录,如果成功登录,便返回相应的用户实例

所以当返回为空时,我们便抛出错误返回登录失败,而成功登录后,我们调用 UserSchema.init 方法将登录的用户数据返回给客户端

而对于登出接口,我们只需将当前请求 Session 的数据清空即可,我们使用之前声明的 session_config 作为 API 函数参数的默认值从而接收当前请求的 Session 对象,然后在函数中调用 session.flush() 清空 Session 数据

用户信息的获取与更新

当我们了解了 Schema Query 的用法后,编写用户信息的获取与更新接口就非常简单了,如下

from datetime import datetime
from utilmeta.core import api, orm, request
from utilmeta.utils import exceptions
from .models import User
from . import auth
import utype

class UserUpdateSchema(orm.Schema[User]):
    id: int = orm.Field(no_input=True)
    username: str = orm.Field(required=False)
    password: str = orm.Field(required=False)

@auth.session_config.plugin
class UserAPI(api.API):
    @api.post
    def signup(self): ...
    @api.post
    def login(self): ...
    @api.post
    def logout(self): ...

    # new ++++
    def get(self, user: User = auth.user_config) -> UserSchema:
        return UserSchema.init(user)

    def put(self, data: UserUpdateSchema = request.Body, 
            user: User = auth.user_config) -> UserSchema:
        data.id = user.pk
        data.save()
        return UserSchema.init(data.pk)

当我们声明了用户鉴权配置后,在任何一个需要用户登录才能访问的接口,我们都可以在接口参数中声明 user: User = auth.user_config 从而拿到当前请求用户的实例,如果请求没有登录,则 UtilMeta 会自动处理并返回 401 Unauthorized

get 接口中,我们直接将当前的请求用户的数据用 UserSchema 初始化并返回给客户端
put 接口中,我们将当前用户的 ID 赋值给接收到 UserUpdateSchema 实例的 id 字段,然后保存并返回更新后的用户数据

由于我们不能允许请求用户任意指定要更新的用户 ID,所以对于请求数据的 id 字段我们使用了 no_input=True 的选项,这其实也是一种常见的权限策略,即一个用户只能更新自己的信息

当你的函数直接使用 get / put / patch / post / delete 等 HTTP 动词进行命名时,它们就会自动绑定对应的方法,路径与 API 类的路径保持一致,这些方法称为这个 API 类的核心方法

至此我们的 API 就全部开发完成了

整合 API

为了使我们开发的 UserAPI 能够提供访问,我们需要把它 挂载 到服务的根 API 上,我们回到 server.py,修改 RootAPI 的声明

from utilmeta.core.server.backends.django import DjangoSettings

service.use(DjangoSettings(
    secret_key='YOUR_SECRET_KEY',
    apps=['user']
))

# new +++
service.setup()

from user.api import UserAPI

class RootAPI(api.API):
    user: UserAPI
	
service.mount(RootAPI, route='/api')

我们将开发好的 UserAPI 挂载到了 RootAPI 的 user 属性,意味着 UserAPI 的路径被挂载到了 /api/user,其中定义的接口路径也相应延申,如

  • GET /api/user:获取用户信息
  • PUT /api/user:更新用户信息
  • POST /api/user/login:用户登录
  • POST /api/user/logout:用户登出
  • POST /api/user/signup:用户注册

这样的 API 树级挂载对于组织接口架构和定义树状的接口路由非常方便

对于使用 Django 的 API 服务,请在导入任何模型或 API 前加入 service.setup() 完成服务的初始化,这样 django 才能正确识别所有的数据模型

5. 运行 API

在项目文件夹中使用如下命令即可将 API 服务运行起来

meta run

或者你也可以使用

python server.py

当你看到如下输出时表示服务已成功启动

UtilMeta v2.4.1 starting service [demo-user]
...
Starting development server at http://127.0.0.1:8000/

你可以通过调整 server.py 中的 UtilMeta 服务声明里的 hostport 参数来改变 API 服务监听的地址

6. 调试 API

启动好 API 服务后我们就可以调试我们的接口了,我们可以使用 UtilMeta 自带的客户端测试工具方便地调试接口,我们在项目目录中新建一个 test.py 文件,写入调试 API 的代码

from server import service

if __name__ == '__main__':
    with service.get_client(live=True) as client:
        r1 = client.post('user/signup', data={
            'username': 'user1',
            'password': '123123'
        })
        r1.print()
        r2 = client.get('user')
        r2.print()

其中编写了用户注册接口和获取当前用户接口的调试代码,当我们启动服务并运行 test.py 时,我们可以看到的输出类似

Response [200 OK] "POST /api/user/signup"
application/json (76)
{'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T12:29:33.684594'}

Response [200 OK] "GET /api/user"
application/json (76)
{'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T12:29:33.684594'}

这说明我们的注册接口和获取用户的接口开发成功,首先注册接口返回了正确的结果,然后注册接口登录了新注册的用户,所以之后访问用户获取接口也得到了同样的结果

with 代码块中,客户端会记忆响应中 Set-Cookie 所存储的 cookies 并发送到接下来的请求中,所以我们可以看到与真实的浏览器类似的会话效果

UtilMeta 服务实例的 get_client 方法用于获取一个服务的客户端实例,你可以直接调用这个实例的 get, post 等方法发起 HTTP 请求,将会得到一个 utilmeta.core.response.Response 响应,这与我们在 API 服务中生成的响应类型一致,其中常用的属性有

  • status:响应的状态码
  • data:解析后的响应数据,如果是 JSON 响应体,则会得到一个 dictlist 类型的数据
  • headers:响应头
  • request:响应对应的请求对象,有请求的方法,路径等参数信息

get_client 方法中的 live 参数如果没有开启,则是直接调用对应的接口函数进行调试,无需启动服务

所以你也可以使用这个客户端编写单元测试,比如

from server import service

def test_signup():
    with service.get_client(live=True) as client:
        r1 = client.post('user/signup', data={
            'username': 'user1',
            'password': '123123'
        })
        assert r1.status == 200
        assert isinstance(r1.data, dict)
        assert r1.data.get('username') == 'user1'

我们还可以测试登录,登出与更新接口,比如在登出后 cookies 应该被清空,之后获取当前用户也应该返回空,最后完整的调试代码与对应的输出如下

from server import service

if __name__ == '__main__':
    with service.get_client(live=True) as client:
        r1 = client.post('user/signup', data={
            'username': 'user1',
            'password': '123123'
        })
        r1.print()
        # Response [200 OK] "POST /api/user/signup"
        # application/json (75)
        # {'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T13:29:03.336134'}
        r2 = client.get('user')
        r2.print()
        # Response [200 OK] "GET /api/user"
        # application/json (75)
        # {'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T13:29:03.336134'}
        r3 = client.post('user/logout')
        r3.print()
        # Response [200 OK] "POST /api/user/logout"
        # text/html (0)
        r4 = client.get('user')
        r4.print()
        # Response [401 Unauthorized] "GET /api/user"
        # text/html (0)
        r5 = client.post('user/login', data={
            'username': 'user1',
            'password': '123123'
        })
        # Response [200 OK] "POST /api/user/login"
        # application/json (75)
        # {'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T13:29:03.336134'}
        r5.print()
        r6 = client.get('user')
        r6.print()
        # Response [200 OK] "GET /api/user"
        # application/json (75)
        # {'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T13:29:03.336134'}
        r7 = client.put('user', data={
            'username': 'user-updated',
            'password': '123456'
        })
        r7.print()
        # Response [200 OK] "PUT /api/user"
        # application/json (82)
        # {'username': 'user-updated', 'id': 1, 'signup_time': '2024-01-29T13:44:30.095711'}

源码与资料

我同时也是 UtilMeta 框架的作者,如果你有什么问题也欢迎联系我,我的全网 ID 和微信号都是 voidZXL

posted @ 2024-02-17 20:25  voidZXL  阅读(478)  评论(0编辑  收藏  举报