Phoenix API 认证实战:基于 mix phx.gen.auth 构建 Bearer Token 认证体系

发布时间:2026/9/20 11:54:27
Phoenix API 认证实战:基于 mix phx.gen.auth 构建 Bearer Token 认证体系 Phoenix API 认证实战基于 mix phx.gen.auth 构建 Bearer Token 认证体系【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix本指南讲解如何在 Phoenix 应用中、基于mix phx.gen.auth生成的认证体系之上快速叠加一套面向 API 的 Bearer Token 认证方案。文章完整覆盖「上下文Context层新增建 Token 与校验 Token 函数」「UserToken 模块实现 Token 校验查询」「UserAuth 模块编写认证 Plug」「将 Plug 接入 :api 管道」四个步骤并给出测试代码与失败调试路径。读完本文你将掌握如何复用mix phx.gen.auth已有的令牌表与散列机制以最少的代码为 JSON API 或移动端、第三方客户端提供安全的 API 认证能力。背景与前置条件本指南假设你已经按照mix phx.gen.auth指南完成了认证系统的生成。mix phx.gen.auth会生成一个包含User与UserToken两个 schema 的Accounts上下文模块其中UserToken负责管理各类令牌session、magic link、修改邮箱验证等。本方案的核心思路是既然生成器已经为我们创建了 token 表就直接复用它来存放 API Token遵循最佳安全实践而不是另起炉灶。为方便讲解全文假设执行过如下命令$ mix phx.gen.auth Accounts User users如果你的命名不同将下文中的Accounts、User、UserToken相应替换即可。关于生成代码的细节可参考仓库中的模板源码schema_token.ex.eexUserToken 模块、context_functions.ex.eexAccounts 上下文函数、auth.ex.eexUserAuth 认证模块。从 migration.ex.eex 可以看到生成的users_tokens表包含tokenbinary、contextstring非空、sent_tostring等字段并建立了[:context, :token]的唯一索引——这正是同一 context 下同一 token 唯一这一约束的数据库层面保障也是我们新增api-token这一 context 类型时的直接受益点。第一部分为上下文Context新增 API 函数我们的 API 认证体系只需要两个函数一个用于创建API Token一个用于校验API Token。打开lib/my_app/accounts.ex在Accounts模块中新增## API doc Creates a new api token for a user. The token returned must be saved somewhere safe. This token cannot be recovered from the database. def create_user_api_token(user) do {encoded_token, user_token} UserToken.build_email_token(user, api-token) Repo.insert!(user_token) encoded_token end doc Fetches the user by API token. def fetch_user_by_api_token(token) do with {:ok, query} - UserToken.verify_api_token_query(token), %User{} user - Repo.one(query) do {:ok, user} else _ - :error end end这两个函数全部复用UserToken已有的能力create_user_api_token/1调用UserToken.build_email_token(user, api-token)以api-token作为 context 生成一枚新的 email 型令牌并入库最后只把编码后的原文 token 返回给调用方。参照模板 schema_token.ex.eex 中的build_email_token/2与私有函数build_hashed_token/3可知原始 token 由:crypto.strong_rand_bytes(rand_size)rand_size 32即 256 位强随机数生成随后用:crypto.hash(hash_algorithm, token)hash_algorithm :sha256散列再经Base.url_encode64(token, padding: false)编码后对外返回——数据库里只存散列值明文 token 无法从数据库恢复这是对抗只读数据库泄露的关键设计。调用方必须妥善保管返回值因为泄露后无法找回。fetch_user_by_api_token/1调用UserToken.verify_api_token_query/1拿到底层查询后执行Repo.one/1命中用户则返回{:ok, user}否则返回:error。为什么是 fetch_* 而不是 get_*注意我们刻意命名为fetch_user_by_api_token而非get_user_by_api_token。这是 Elixir 社区约定get_*通常在没有结果时返回nil而fetch_*返回{:ok, value}/:error这样的元组。之所以这样做是因为在 API 场景下我们要根据用户是否存在渲染不同的 HTTP 状态码例如 401 Unauthorized 而非 404元组返回值让调用方可以精确区分令牌无效与用户不存在。编写测试并预期它失败打开test/my_app/accounts_test.exs新增 describe 块describe create_user_api_token/1 and fetch_user_by_api_token/1 do test creates and fetches by token do user user_fixture() token Accounts.create_user_api_token(user) assert Accounts.fetch_user_by_api_token(token) {:ok, user} assert Accounts.fetch_user_by_api_token(invalid) :error end end运行测试它会失败报错形如1) test create_user_api_token/1 and fetch_user_by_api_token/1 creates and fetches by token (Demo.AccountsTest) test/demo/accounts_test.exs:380 ** (UndefinedFunctionError) function Demo.Accounts.UserToken.verify_api_token_query/1 is undefined or private. Did you mean: * verify_change_email_token_query/2 * verify_magic_link_token_query/1 * verify_session_token_query/1 code: assert Accounts.fetch_user_by_api_token(token) {:ok, user} stacktrace: (demo 0.1.0) Demo.Accounts.UserToken.verify_api_token_query(sTpJg7rt-KQ9gZ7xLMtn2keusGk9N2JpPwkXDx7LmHU) (demo 0.1.0) lib/demo/accounts.ex:325: Demo.Accounts.fetch_user_by_api_token/1 test/demo/accounts_test.exs:383: (test)这正是我们想要的红灯UserToken模块目前只有verify_change_email_token_query/2、verify_magic_link_token_query/1、verify_session_token_query/1还没有verify_api_token_query/1。你可以先尝试自行修复提示参考已有的 verify 函数照葫芦画瓢也可以直接看下面的实现。在 UserToken 中实现 verify_api_token_query打开lib/my_app/accounts/user_token.ex参照verify_magic_link_token_query/1模板见 schema_token.ex.eex新增doc Checks if the API token is valid and returns its underlying lookup query. The query returns the user found by the token, if any. The given token is valid if it matches its hashed counterpart in the database and the user email has not changed. This function also checks if the token is being used within 365 days. def verify_api_token_query(token) do case Base.url_decode64(token, padding: false) do {:ok, decoded_token} - hashed_token :crypto.hash(hash_algorithm, decoded_token) query from token in by_token_and_context_query(hashed_token, api-token), join: user in assoc(token, :user), where: token.inserted_at ago(^api_token_validity_in_days, day) and token.sent_to user.email, select: user {:ok, query} :error - :error end end同时在文件顶部的模块属性区补上有效期常量magic_link_validity_in_minutes 15 change_email_validity_in_days 7 session_validity_in_days 60 api_token_validity_in_days 365这段实现背后的原理对照模板中的verify_magic_link_token_query/1可以逐行拆解这段代码的意图URL-safe Base64 解码Base.url_decode64(token, padding: false)与生成时的Base.url_encode64(token, padding: false)严格对应。解码失败说明 token 格式非法直接返回:error。散列后再查库hashed_token :crypto.hash(hash_algorithm, decoded_token)用与生成时相同的:sha256算法散列请求中携带的 token再去比对数据库里存的散列值。请求方携带的永远是明文库里存储的永远是散列二者在查询时才对账。复用私有查询助手by_token_and_context_query(hashed_token, api-token)是模板中已有的私有函数from UserToken, where: [token: ^token, context: ^context]配合数据库[:context, :token]唯一索引保证api-token这一 context 下的 token 精确匹配。有效期约束token.inserted_at ago(^api_token_validity_in_days, day)判断令牌是否在 365 天内使用。API Token 的有效期完全取决于你的应用对安全敏感程度的判断可以根据业务调整这个天数。与用户当前邮箱绑定token.sent_to user.email这一条件正是文档强调的因为这是 email 令牌如果用户修改了邮箱这些令牌将全部过期失效的机制所在——build_email_token/2会把签发时的邮箱写入sent_to字段见模板build_hashed_token/3中的sent_to: user.email只要用户邮箱变更旧令牌立即失效无需额外清理逻辑。完成以上修改后之前的测试应该转为绿灯。至此上下文层就绪接下来进入 HTTP 层。第二部分API 认证 Plugmix phx.gen.auth生成代码时会在MyAppWeb.UserAuth模块中产出若干 plug——它们是接收conn并定制请求/响应生命周期的小函数如fetch_current_scope_for_user、require_authenticated_user等详见 auth.ex.eex 模板与 mix_phx_gen_auth.md 中的 Forbidding access 一节。打开lib/my_app_web/user_auth.ex新增如下 plugdef fetch_current_scope_for_api_user(conn, _opts) do with [bearer::binary-size(6), , token::binary] - get_req_header(conn, authorization), true - String.downcase(bearer) bearer, {:ok, user} - Accounts.fetch_user_by_api_token(token) do assign(conn, :current_scope, Scope.for_user(user)) else _ - conn | send_resp(:unauthorized, No access for you) | halt() end end逐段解读 plug 逻辑解析 Authorization 请求头get_req_header(conn, authorization)取出请求头。[bearer::binary-size(6), , token::binary] - ...是一个带二进制定长的模式匹配要求请求头形如Bearer TOKEN其中bearer恰好占 6 字节、后跟一个空格与 token 本体。注意 Phoenix 生成的MyAppWeb.UserAuth模块顶部已import Plug.Conn与import Phoenix.Controller见 auth.ex.eex因此get_req_header/2、assign/3、send_resp/3、halt/1均可直接使用。大小写不敏感校验true - String.downcase(bearer) bearer允许客户端写成bearer、Bearer等任意大小写组合。校验并取出用户{:ok, user} - Accounts.fetch_user_by_api_token(token)调用第一部分实现的上下文函数。注意这里使用了with的多个子句串联——任意一环失败头缺失、格式不对、scheme 不是 bearer、token 无效都会落入else分支。失败即中止请求else分支用send_resp(:unauthorized, No access for you)返回401 Unauthorized并halt()终止后续管道这正是上一节选择fetch_*返回元组的意义所在——失败路径被统一收敛无需在 plug 内重复判断。成功则写入 scopeassign(conn, :current_scope, Scope.for_user(user))将当前用户封装进Scope结构并挂到conn.assigns。Scope.for_user/1是mix phx.gen.auth自动生成的 scope 工厂函数见 scope.ex.eexdefstruct user: nilfor_user(%User{} user)返回%__MODULE__{user: user}传入nil返回nil。这与浏览器管道中fetch_current_scope_for_user的做法保持一致详见 scopes.md后续控制器、LiveView 以及未来的生成器代码都能通过统一的:current_scopeassign 拿到当前请求的上下文。将 Plug 接入 :api 管道最后一步是注册 plug。打开lib/my_app_web/router.ex找到已有的 API 管道把新 plug 加进去pipeline :api do plug :accepts, [json] plug :fetch_current_scope_for_api_user end至此所有pipe_through [:api]的路由都会先经过 Bearer Token 校验令牌有效则conn.assigns.current_scope可用无效则直接 401 中止。为 plug 编写测试文档建议打开test/my_app_web/user_auth_test.exs以其他 plug 的测试为模板编写自己的用例。可覆盖的典型场景包括携带合法Authorization: Bearer token头时请求通过且current_scope.user为对应用户不携带请求头时返回 401携带格式错误非Bearer 前缀或伪造 token 时返回 401用户修改邮箱后旧 token 立即失效对应sent_to user.email约束。实战延伸API 认证流在你的应用中如何落地整体 API 认证流取决于你的具体应用场景文档给出了两条典型路径面向 JavaScript / 移动端客户端的认证如果你希望自家前端如 JS 客户端使用该 token需要略微改动UserSessionController在登录成功后调用Accounts.create_user_api_token/1生成令牌并以 JSON 响应将 token 返回给客户端。参考生成的 session_controller.ex.eex 中create/2的流程magic link 与邮箱密码两条登录分支在成功分支中追加生成 API token 并json(conn, %{token: token})即可。客户端拿到 token 后保存在安全位置如内存或安全的本地存储后续请求统一携带Authorization: Bearer token头。面向第三方开发者的开放 API如果你想为第三方用户提供 API就需要提供一个让他们自助创建 token的入口例如设置页中的一个生成 API Token表单并在创建成功后把Accounts.create_user_api_token/1的返回值展示给对方。第三方必须自行安全保存这些 token并在每次请求时通过authorization请求头携带。由于数据库只存散列、token 无法找回务必在页面上提示用户token 仅显示一次请立即保存并在需要时提供撤销/重新生成能力删除对应context: api-token的记录即可UserToken表结构与唯一索引完全支持这一点。与现有安全设计的协同改邮箱即失效API token 与签发邮箱绑定sent_to user.email用户改邮箱后旧 token 全部作废避免令牌在邮箱变更后继续有效令牌可追踪所有 token 统一存放在users_tokens表并带有context标记你可以统计每个账号有效的 API token 数量甚至像 session 一样向用户展示并允许手动吊销参见 mix_phx_gen_auth.md 的 Tracking sessions 一节有效期可调api_token_validity_in_days是模块属性可根据业务安全等级调整例如敏感操作类 API 建议缩短有效期或引入刷新机制。总结本指南以mix phx.gen.auth生成的认证体系为基础用最小的改动完成了 API Bearer Token 认证的完整闭环Context 层新增Accounts.create_user_api_token/1与Accounts.fetch_user_by_api_token/1复用UserToken的 email 令牌机制与api-tokencontext令牌校验层在UserToken中实现verify_api_token_query/1沿用 SHA-256 散列比对 有效期 邮箱绑定三重校验并配置api_token_validity_in_daysHTTP 层在MyAppWeb.UserAuth中新增fetch_current_scope_for_api_user/2plug解析Authorization: Bearer头、失败返回 401 并 halt、成功写入:current_scopeassign路由层将 plug 挂载到:api管道使所有 API 路由自动获得认证能力。全程仅需四个文件的少量改动即可获得与既有 session/magic link 体系同源的散列存储、过期控制和邮箱联动失效能力——这正是复用生成器、遵循最佳安全实践这一设计思路的价值所在。后续若项目升级phx.gen.auth生成逻辑也只需对照 CHANGELOG.md 评估是否需要将改进同步到你的自定义代码中。【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考