Starlette 认证与权限实战从自定义 AuthenticationBackend 到 scopes 权限控制【免费下载链接】starletteThe little ASGI framework that shines. 项目地址: https://gitcode.com/gh_mirrors/st/starletteStarlette 为 ASGI 应用提供了一套简洁而强大的认证authentication与权限permission接口通过AuthenticationMiddleware挂载认证后端后你的端点即可直接使用request.user与request.auth两个接口来识别用户与凭据并用requires装饰器完成细粒度的 scopes 权限校验。本文以 docs/authentication.md 为主体结合starlette.authentication、starlette.middleware.authentication的源码与测试带你完整掌握从 Basic Auth 后端编写、用户模型定制到权限装饰器、自定义错误响应、WebSocket 认证的完整链路。认证架构总览AuthenticationMiddleware 的工作方式在 Starlette 中认证的核心不是某个具体登录方案而是一层可插拔的中间件 后端抽象。你只需两步即可让全站端点具备用户身份能力实现一个AuthenticationBackend子类覆写authenticate方法用Middleware(AuthenticationMiddleware, backend...)把它装进应用。装好后request.user与request.auth便会出现在所有端点以及位于其后的其他中间件中。从 starlette/middleware/authentication.py 的源码可以看到完整的处理流程async def __call__(self, scope, receive, send): if scope[type] not in [http, websocket]: await self.app(scope, receive, send) return conn HTTPConnection(scope) try: auth_result await self.backend.authenticate(conn) except AuthenticationError as exc: response self.on_error(conn, exc) if scope[type] websocket: await send({type: websocket.close, code: 1000}) else: await response(scope, receive, send) return if auth_result is None: auth_result AuthCredentials(), UnauthenticatedUser() scope[auth], scope[user] auth_result await self.app(scope, receive, send)这段实现揭示了几条关键语义只对 HTTP 与 WebSocket 生效scope[type]为其他类型如lifespan的调用会直接透传不做任何认证处理后端返回None表示匿名中间件会自动兜底为AuthCredentials()UnauthenticatedUser()因此你的后端可以识别不了就直接return无需抛错后端抛出AuthenticationError会短路整个请求中间件不再调用下游应用而是直接使用on_error生成响应WebSocket 场景则直接发送websocket.close帧关闭码为 1000结果写入scopescope[user]、scope[auth]是身份信息的最终载体request.user/request.auth不过是它们的便捷属性见 starlette/requests.py当未安装中间件时访问会触发断言提示 AuthenticationMiddleware must be installed to access request.user/auth。因为Request与WebSocket都继承自HTTPConnection见 starlette/requests.py所以这套user/auth接口在两类连接上完全一致认证逻辑可以无缝复用在 WebSocket 场景。编写认证后端Basic Auth 完整示例AuthenticationBackend的定义非常轻量starlette/authentication.pyclass AuthenticationBackend: async def authenticate(self, conn): raise NotImplementedError()它接收一个HTTPConnection返回两种结果之一返回(AuthCredentials, BaseUser)元组 —— 认证成功返回None—— 未认证中间件会注入匿名身份。官方文档给出了一个完整的 Basic Auth 后端可以直接复制运行from starlette.applications import Starlette from starlette.authentication import ( AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser ) from starlette.middleware import Middleware from starlette.middleware.authentication import AuthenticationMiddleware from starlette.responses import PlainTextResponse from starlette.routing import Route import base64 import binascii class BasicAuthBackend(AuthenticationBackend): async def authenticate(self, conn): if Authorization not in conn.headers: return auth conn.headers[Authorization] try: scheme, credentials auth.split() if scheme.lower() ! basic: return decoded base64.b64decode(credentials).decode(ascii) except (ValueError, UnicodeDecodeError, binascii.Error) as exc: raise AuthenticationError(Invalid basic auth credentials) username, _, password decoded.partition(:) # TODO: Youd want to verify the username and password here. return AuthCredentials([authenticated]), SimpleUser(username) async def homepage(request): if request.user.is_authenticated: return PlainTextResponse(Hello, request.user.display_name) return PlainTextResponse(Hello, you) routes [ Route(/, endpointhomepage) ] middleware [ Middleware(AuthenticationMiddleware, backendBasicAuthBackend()) ] app Starlette(routesroutes, middlewaremiddleware)这段示例值得逐行拆解无Authorization头 → 匿名直接return中间件自动构造匿名身份非 Basic 协议 → 匿名即使带了头但 scheme 不是basic同样静默放行把判定交给其他机制解析失败 → 抛AuthenticationError解码异常时主动抛出触发中间件的on_error短路路径默认返回 400 响应见后文认证成功 → 返回凭据与用户AuthCredentials([authenticated])授予一个authenticatedscopeSimpleUser(username)携带用户名。这一示例的等价实现也出现在仓库测试中tests/test_authentication.py 的BasicAuth类是官方验证过的标准写法。Middleware的cls kwargs机制见 starlette/middleware/init.py它只是延迟构造中间件实例的配置载体。用户接口BaseUser、SimpleUser 与 UnauthenticatedUser安装AuthenticationMiddleware后request.user对端点和其他中间件可用。该接口约定继承BaseUser除了你自己的用户模型字段外基类定义了两个属性源码中还额外定义了第三个identity见 starlette/authentication.py.is_authenticated—— 是否已认证布尔值.display_name—— 展示名字符串.identity—— 用户唯一标识源码中定义默认要求子类实现。Starlette 内置了两个开箱即用的用户实现类构造参数is_authenticateddisplay_nameSimpleUser(username)用户名True即usernameUnauthenticatedUser()无False空字符串对应实现见 starlette/authentication.pySimpleUser把username同时作为展示名返回而UnauthenticatedUser的is_authenticated恒为False、display_name恒为空串。实际业务中你可以继承BaseUser定义更丰富的用户模型例如from starlette.authentication import BaseUser class User(BaseUser): def __init__(self, user_id: int, name: str, email: str): self.user_id user_id self.name name self.email email property def is_authenticated(self) - bool: return True property def display_name(self) - str: return self.name property def identity(self) - str: return str(self.user_id)测试中的行为验证tests/test_authentication.py也印证了这套接口匿名访问/返回{authenticated: False, user: }带 Basic Auth 访问则返回{authenticated: True, user: tomchristie}——即UnauthenticatedUser与SimpleUser的实际效果。凭据与用户分离AuthCredentials 与 scopesStarlette 强调一个重要的设计原则认证凭据credentials必须与用户身份user视为两个独立概念。理由在于一种认证方案应当能够独立于用户身份去授予或限制特定权限——同一个用户在不同上下文里可能拥有不同的权限集合。AuthCredentials提供了request.auth暴露的基础接口.scopes—— 凭据携带的权限范围字符串列表其实现极简starlette/authentication.py构造时把传入的序列拷贝为列表缺省为空列表。class AuthCredentials: def __init__(self, scopesNone): self.scopes [] if scopes is None else list(scopes)scopes 本质上就是一组权限标签字符串比如[authenticated]、[admin]、[read:posts]。权限校验正是基于conn.auth.scopes是否包含所需标签来判定的见has_required_scopestarlette/authentication.pydef has_required_scope(conn, scopes): for scope in scopes: if scope not in conn.auth.scopes: return False return True注意这是全部满足语义要求多个 scope 时凭据必须同时包含每一个才会放行。权限控制requires 装饰器权限在 Starlette 中实现为端点装饰器强制要求入站请求包含所需的认证 scopes。核心签名如下starlette/authentication.pydef requires(scopes, status_code: int 403, redirect: str | None None):单个或多个必需 scope要求一个 scopefrom starlette.authentication import requires requires(authenticated) async def dashboard(request): ...要求多个 scope全部满足才放行from starlette.authentication import requires requires([authenticated, admin]) async def dashboard(request): ...自定义状态码默认情况下权限不足返回403 Forbidden响应。某些场景你可能想自定义状态码例如对未认证用户隐藏 URL 布局信息返回 404 比 403 更不暴露资源是否存在from starlette.authentication import requires requires([authenticated, admin], status_code404) async def dashboard(request): ...注意status_code参数不适用于 WebSocketWebSocket 场景始终使用 403Forbidden语义。从源码看WebSocket 分支starlette/authentication.py在权限不足时直接调用await websocket.close()关闭连接并不渲染任何 HTTP 响应。重定向到其他页面另一种常见需求是把未认证用户重定向到登录页等页面from starlette.authentication import requires async def homepage(request): ... requires(authenticated, redirecthomepage) async def dashboard(request): ...redirect接收的是路由的name不是路径字符串装饰器内部通过request.url_for(redirect)解析目标地址。重定向会携带用户最初请求的 URL 作为next查询参数starlette/authentication.pyorig_request_qparam urlencode({next: str(request.url)}) next_url f{request.url_for(redirect)}?{orig_request_qparam} return RedirectResponse(urlnext_url, status_code303)可以看到重定向使用303 See Other状态码保证登录完成后用户能被送回原始目标页。配套的登录处理逻辑如下from starlette.authentication import requires from starlette.responses import RedirectResponse requires(authenticated, redirectlogin) async def admin(request): ... async def login(request): if request.method POST: # Now that the user is authenticated, # we can send them to their original request destination if request.user.is_authenticated: next_url request.query_params.get(next) if next_url: return RedirectResponse(next_url) return RedirectResponse(/)即登录成功后读取next参数回跳到用户最初想访问的页面没有next则回首页。测试 tests/test_authentication.py 验证了该行为匿名访问/admin会得到形如http://testserver/?nexthttp://testserver/admin的重定向。类视图HTTPEndpoint中使用对于基于类的端点需要把装饰器包裹在类的某个方法上from starlette.authentication import requires from starlette.endpoints import HTTPEndpoint class Dashboard(HTTPEndpoint): requires(authenticated) async def get(self, request): ...函数签名约束requires装饰器要求被装饰函数必须有一个名为requestHTTP 端点或websocketWebSocket 端点的参数否则会在定义阶段抛出Exceptionstarlette/authentication.py。同时它支持三种形态的函数异步函数async def走async_wrapper同步函数def走sync_wrapper权限不足时抛出HTTPException(status_code)starlette/exceptions.py 定义由异常处理机制渲染为对应状态码响应WebSocket 端点走websocket_wrapper直接关闭连接。测试覆盖了同步端点dashboard_sync、类视图Dashboard、以及叠加了额外关键字注入装饰器的复杂场景decorated_async/decorated_sync/websocket_endpoint_decorated见 tests/test_authentication.py说明装饰器对参数注入是兼容的。自定义认证错误响应on_error当认证后端抛出AuthenticationError时默认行为由AuthenticationMiddleware.default_on_error提供starlette/middleware/authentication.pystaticmethod def default_on_error(conn, exc): return PlainTextResponse(str(exc), status_code400)即返回400 Bad Request纯文本响应内容为异常信息。如果你想自定义例如返回 JSON 格式的 401可以通过on_error参数传入回调from starlette.applications import Starlette from starlette.middleware import Middleware from starlette.middleware.authentication import AuthenticationMiddleware from starlette.requests import Request from starlette.responses import JSONResponse def on_auth_error(request: Request, exc: Exception): return JSONResponse({error: str(exc)}, status_code401) app Starlette( middleware[ Middleware(AuthenticationMiddleware, backendBasicAuthBackend(), on_erroron_auth_error), ], )回调签名约定为(conn, exc) - Response其中conn是HTTPConnectionRequest是它的子类均可传入。中间件在构造时会把传入的on_error与默认实现统一收口starlette/middleware/authentication.py。测试 tests/test_authentication.py 验证了该行为带上非法Authorization: basic foobar访问受保护端点时返回401及{error: Invalid basic auth credentials}而合法凭据依然正常通过200。测试验证与实战要点仓库的 tests/test_authentication.py 是上述所有行为的权威验证集可以作为你自查的清单匿名与认证用户接口test_user_interface验证request.user.is_authenticated与request.user.display_name的两种状态HTTP 权限拦截test_authentication_required验证无凭据访问返回 403、带 Basic Auth 访问返回 200覆盖异步、同步、类视图三种端点以及非法凭据触发 400WebSocket 认证test_websocket_authentication_required验证无凭据连接会以WebSocketDisconnect关闭、合法凭据可正常收发消息重定向test_authentication_redirect验证next参数携带原始 URL自定义错误test_custom_on_error验证 401 JSON 响应。实战中还有几个容易踩坑的点值得记住中间件顺序敏感AuthenticationMiddleware必须在依赖request.user的中间件如权限判断、日志记录之前挂载因为身份信息写入scope后只对后续层可见scopes 是权限判定的唯一依据requires只看conn.auth.scopes与用户名无关。要给用户授更多权限在authenticate返回的AuthCredentials里多放几个 scope 即可匿名是显式的后端返回None会被规范化为AuthCredentials()UnauthenticatedUser()所以端点里无需空值判断直接读request.user.is_authenticated即可identity属性是基类约定的第三接口自定义用户模型时建议一并实现便于后续对接 session、JWT 等方案的用户查找。至此你已经掌握了 Starlette 认证体系的完整拼图AuthenticationBackend负责你是谁AuthCredentials负责你能做什么requires负责你没权限时怎么办而on_error负责认证失败时怎么应答。基于这套抽象你可以自由扩展出 Session、JWT、OAuth 等各种真实世界的认证方案同时保持端点代码与具体认证手段完全解耦。【免费下载链接】starletteThe little ASGI framework that shines. 项目地址: https://gitcode.com/gh_mirrors/st/starlette创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
