news 2026/11/5 3:49:36

Photon Fusion 2自定义身份验证实战:从匿名连接到账号体系

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Photon Fusion 2自定义身份验证实战:从匿名连接到账号体系

去年在做一款基于 Fusion 2 的多人联机项目时,我在自定义身份验证(Custom Authentication)上整整折腾了两天。官方 Demo 默认是匿名连接,拿个 AppId 就能进房间,但真要对接账号体系、做封号处理、同步玩家数据,匿名连接完全不够用。我的做法是让玩家先通过自己的服务器登录换取 token,再用这个 token 去连接 Photon Fusion 2,由 Photon Cloud 回调我自己的认证服务器,确认“这个人是不是合法用户”。这套机制在 Fusion 2 里就叫 Custom Authentication。如果你也在做 Unity 联机游戏,并且不想被默认匿名验证卡住,这篇笔记应该能帮你省下不少翻文档的时间。

1. 为什么需要自定义身份验证

1.1 Fusion 2 默认身份验证的局限

Fusion 2 底层基于 Photon Realtime,最省事的接入方式就是填一个 AppId,然后直接调StartGame。在没有配置任何认证逻辑时,Photon 会给每个客户端分配一个随机的 PlayerId。这个 PlayerId 只在当次连接中作为玩家标识存在,你没办法把它当成真正的用户账号来用。玩家清缓存、换设备、重装游戏之后,这个 ID 基本就会变化,服务器没有任何稳定字段来识别同一个用户。

更麻烦的是,Fusion 的房主迁移、断线重连虽然做得不错,但身份层面它是“认连接不认人”。如果两个玩家各自用匿名身份连进来,你在游戏逻辑里无法判断他们是否对应数据库里的同一个人,也就做不到跨设备恢复进度、同账号多处登录限制、封禁账号拒绝进入这些基础运营需求。

所以只要产品需要账号体系,默认匿名验证基本不可用。自定义身份验证的意义,就是把“这个人是谁、能不能进”这个问题交给你自己的服务器。Photon Cloud 负责转发和落地,你负责给出最终裁决。

1.2 自定义认证能解决的几类真实需求

第一类是账号绑定。玩家在客户端先用自建登录接口拿到 userId 和 token,然后带着 token 去连 Photon,Photon 回调我的服务器,服务器确认 token 有效就放行,Fusion 里就用这个 userId 作为玩家标识。这样无论玩家换了几次设备,我在服务端都能认出来。

第二类是封禁与黑名单。哪怕玩家的 token 本身有效,如果服务端查到该账号已经被封禁,或者设备在风控黑名单里,直接返回Result=false,Photon 就不会放他进游戏。这套逻辑放在认证阶段,比进了房间再踢人节省大量带宽和逻辑处理。

第三类是第三方登录打通。比如玩家通过微信、QQ、Steam 之类平台登录后,我的服务器先验证平台返回的票据,再签发一个短期 token 给客户端,客户端拿这个 token 去连 Photon。这样第三方平台的密钥不会暴露到客户端,所有敏感验证都在自己的服务器上完成。

第四类是设备级风控。认证阶段可以顺便校验客户端版本、IP 归属、设备型号,发现异常就直接拒绝。有些自动化脚本连游戏都进不去,比在游戏内部做反作弊省力得多。

1.3 使用边界:认证接口不是业务接口

自定义认证本质上是一次轻量 HTTP 回调,Photon 对回调响应时间是有要求的。如果你在认证接口里做数据库全表查询、同步调用第三方 HTTP 接口,很容易触发 Photon 端超时,客户端拿到的结果就是连接失败。

我自己踩过这个坑。最初把“查询玩家所有装备和好友列表”的逻辑也放在认证阶段,结果数据库数据量上来之后,认证耗时从几十毫秒涨到两秒多,线上开始频繁出现认证失败。后面把装备、好友这些数据挪到进入房间之后,再通过游戏服务器单独下发,认证接口稳定在 100ms 以内。记住这句话:认证只回答“who are you”,不回答“what do you have”。

2. 自定义身份验证的完整链路

2.1 客户端如何携带认证信息

Fusion 2 客户端并不是直接把自定义认证服务器的地址告诉 Photon,而是在StartGameArgs里传入AuthenticationValues,SDK 会在连接请求中自动携带这些信息。AuthenticationValues是 Photon Realtime 里的类,认证类型要设置成CustomAuthenticationType.Custom,然后通过SetAuthParameters传入自定义参数。

这里有个容易忽略的细节:参数不能随便用逗号拼接。我看到过有人把参数写成"user01,token123",Photon 默认按键值对解析时根本识别不了。正确做法是显式拼接成 query string 风格,比如"username=user01&token=token123",并且每个值都要单独做 URL 编码。

客户端在拿到认证结果之前,连接流程是阻塞的。也就是说,你要在StartGame这一层等待 Photon 完成认证,认证成功后才开始进入房间逻辑。如果认证失败,玩家是进不了游戏流程的。

2.2 Photon Cloud 到你的认证服务器

Photon 收到客户端的连接请求后,会查询当前 AppId 是否配置了自定义认证地址。如果配置了,Photon Cloud 会向该地址发起 HTTP 请求,同时把客户端传过来的参数附加到请求里。这一步发生在 Photon 自己的服务器上,而不是客户端,所以你的认证服务器默认拿不到客户端的真实 IP。如果业务需要 IP,可以让客户端在参数里传,但服务端不能完全信任这个值,只能把它当作参考。

重点来了:Photon 要求回调地址必须是 HTTPS,并且证书要能被公网正常验证。自签名证书、内网地址、localhost 统统不行。我刚开始在本地联调时用https://localhost:44300,Photon 直接拒绝认证。后面我临时用内网穿透工具暴露一个公网 HTTPS 地址,才把流程跑通。

Photon 还会在请求里带上专门的认证 Header,用来证明这个请求确实来自 Photon Cloud。我在 Demo 阶段先校验 Header 中的 secret 字段,生产环境则必须按照官方文档校验完整签名和请求时间戳,否则任何人都可以直接伪造请求,绕过你的认证服务器。

2.3 认证服务器返回结果的约定

认证服务器返回的是 JSON。成功时的最小响应是:

{"Result":true}

但为了业务方便,一般会带上UserId和Nickname:

{"Result":true,"UserId":"db_uuid_12345","Nickname":"playerA"}

失败时可以返回:

{"Result":false,"Message":"banned user"}

Photon 拿到Result=false,或者返回非 200 状态码,就会判定认证失败并拒绝连接。如果你的认证服务器返回 200 但 JSON 格式解析不出来,Photon 同样会判定失败。这里要特别注意 JSON 字段的大小写,我自己因为把Result写成了result,导致认证始终不通过,后来看官方示例才发现字段名是约定好的,不能随意修改。

2.4 客户端如何拿到最终的认证结果

客户端这边,认证成功后会进入正常的连接流程,你可以通过 Fusion 的OnPlayerJoined、OnShutdown等回调跟踪状态。认证失败时,Photon Realtime 会触发失败回调,Fusion 的连接过程会中断。很多新手在StartGame的返回值里只看到Ok=false,就以为是自己房间名的问题,实际上是卡在认证阶段。

排查这个阶段的问题有个笨办法:在启动NetworkRunner之前,先把AuthenticationValues的内容完整打印出来。我被 token 编码问题坑了很久,最后发现是 token 里有个+号没被正确编码,Photon 解析后把+变成空格,服务端验签一直失败。日志里看到SetAuthParameters之后的原始字符串,基本就能定位是编码问题还是服务端验签问题。

3. Fusion 2 客户端接入实操

3.1 在 Photon Dashboard 配置自定义认证地址

打开 Photon Dashboard,找到你的 Fusion 2 应用,进入“管理”菜单下的“自定义身份验证”设置页。需要填写两项:回调 URL 和 Secret。URL 示例:

https://api.yourgame.com/photon/auth

Secret 建议用随机生成长字符串,我一般用openssl rand -hex 32生成。Secret 要同时保存在服务端环境变量里,不要写死在客户端代码或前端配置中。

配置完成后,Photon 并不会立刻开始回调,必须等客户端带着认证值发起连接时才会触发。所以配置完后可以先不启动游戏,直接用浏览器打开认证地址看能不能返回合法 JSON,这样可以提前排查 URL 和服务端代码的问题。

3.2 服务端认证接口示例(ASP.NET Core)

我用 ASP.NET Core 写了一个非常精简的认证接口。服务端语言不一定要 C#,这里只是因为它和 Unity 同语言,Unity 开发者看起来最省力。

[ApiController] [Route("api/[controller]")] public class PhotonAuthController : ControllerBase { private readonly IUserAuthService _userAuthService; public PhotonAuthController(IUserAuthService userAuthService) { _userAuthService = userAuthService; } [HttpGet("auth")] [HttpPost("auth")] public async Task<IActionResult> Authenticate( [FromQuery] string username, [FromQuery] string token, CancellationToken cancellationToken) { var secret = Environment.GetEnvironmentVariable("PHOTON_SECRET"); var photonAuthHeader = Request.Headers["X-Photon-Authentication"].ToString(); if (string.IsNullOrEmpty(photonAuthHeader)) { return Unauthorized(new { Result = false, Message = "Missing auth header" }); } // 注意:生产环境必须按官方文档校验完整的签名和时间戳。 // 这里只做最小验证,目的是先把认证链路跑通。 if (!photonAuthHeader.StartsWith(secret)) { return Unauthorized(new { Result = false, Message = "Secret mismatch" }); } var result = await _userAuthService.ValidateAsync(username, token, cancellationToken); if (!result.Allowed) { return Ok(new { Result = false, Message = result.Message }); } return Ok(new { Result = true, UserId = result.UserId, Nickname = result.Nickname }); } }

对应的服务逻辑大致是这样:

public class UserAuthService : IUserAuthService { private readonly IUserRepository _repository; public async Task<AuthResult> ValidateAsync(string username, string token, CancellationToken ct) { var user = await _repository.GetByUsernameAsync(username, ct); if (user == null || !user.TokenEquals(token)) { return AuthResult.Fail("username or token error"); } if (user.Banned) { return AuthResult.Fail("user banned"); } return AuthResult.Success(user.Id, user.Nickname); } }

看到没有?认证接口只做三件事:验证用户名密码或 token,检查封禁状态,返回稳定的 UserId。千万不要在接口里写日志表、道具表、好友列表这些重逻辑。

3.3 Unity 客户端设置 AuthValues 并启动 Fusion

客户端代码其实不长。关键点是构造AuthenticationValues并赋值给StartGameArgs.AuthValues。

using Photon.Realtime; using Fusion; public class PhotonConnector : MonoBehaviour { [SerializeField] private NetworkRunner runner; public async void ConnectWithToken(string username, string token) { var auth = new AuthenticationValues(); auth.AuthType = CustomAuthenticationType.Custom; auth.SetAuthParameters($"username={Uri.EscapeDataString(username)}&token={Uri.EscapeDataString(token)}"); var args = new StartGameArgs { SessionName = "default-room", AuthValues = auth, PlayerCount = 0 }; var result = await runner.StartGame(args); if (!result.Ok) { Debug.LogWarning($"连接失败: {result.ShutdownReason}"); } } }

CustomAuthenticationType.Custom是一个枚举值,它告诉 Photon 不要走默认匿名认证,而是走你配置的自定义回调。SetAuthParameters接收的是字符串,这个字符串会原样拼到 Photon 的认证请求里。

需要特别提醒:不同版本的 Fusion 2 SDK 对StartGameArgs.AuthValues的命名可能有差异。我用的版本是直接暴露AuthValues属性;如果你的版本没有这个字段,大概率是 SDK 版本过旧,建议先升级到官方推荐版本,再去翻对应版本的 API 文档。

3.4 调试技巧:先用浏览器打通认证链路

我强烈建议在集成 Unity 之前,先用浏览器打通认证链路。直接在浏览器地址栏输入完整的回调 URL,并加上测试参数:

https://api.yourgame.com/photon/auth?username=test&token=demo123

看返回结果是不是标准 JSON。如果浏览器能通过,说明服务端逻辑和 URL 配置没问题,接下来再排查 Unity 端。

我在真实项目中还会用 Postman 保存一组测试用例,包括成功、封禁用户、用户名不存在、token 过期等场景。每次改完服务端代码,先跑一遍 Postman,确认接口不会因为输入变化返回异常状态码,再回到 Unity 里测集成,效率会高很多。

4. 常见问题与排查实录

4.1 常见现象速查表

现象可能原因排查方向
认证服务器日志没有任何请求客户端没有传入 AuthValues,或 Dashboard 没配置 URL检查 StartGameArgs 和仪表盘配置
浏览器访问 URL 正常,Unity 连接失败客户端传参格式不对打印 SetAuthParameters 后的字符串,确认 URL 编码
Photon 回调能到服务器,但结果始终 false服务端校验 secret 失败,或返回 JSON 字段不对确认 Secret 一致,检查 Result 字段大小写
连接偶尔超时认证接口里有耗时操作给认证接口加耗时日志,把重逻辑移出认证阶段
同一玩家被识别为不同 ID没有正确返回 UserId,或 UserId 不稳定固定用数据库主键,而不是随机字符串
自签名证书导致认证失败Photon 要求可信 CA 证书换正规 HTTPS 证书,或临时用公网穿透工具联调

这张表是我自己遇到过的场景整理出来的,不一定覆盖全部情况。碰到问题先顺着表排查,能省很多时间。

4.2 我踩过的几个坑

第一个坑是本地联调。Photon 回调不会访问内网地址,所以开发阶段我选择用公网穿透工具把一个本地端口映射成公网 HTTPS 地址,再把这个地址填到 Dashboard。这样我可以一边改服务端代码一边调试,不用每次都部署到测试服务器。需要注意免费穿透域名经常变,Dashboard 配置要跟着更新,否则联调到一半突然认证失败。

第二个坑是参数编码。token 这类敏感信息里经常出现+、=、&等字符,如果不做 URL 编码,直接在 query string 里拼,服务端拿到的参数就是错的。我在客户端用Uri.EscapeDataString编码,服务端用内置的 URL 解码,两边对齐之后问题才消失。

第三个坑是返回 JSON 字段大小写。Photon 的文档里示例用的是驼峰命名,但我一开始按照习惯写成小写result,结果服务器返回了 200 但 Photon 就是不认。后来把字段名严格对齐官方示例,才恢复正常。建议不管你的服务端用什么框架,都统一输出驼峰格式的 JSON,并且在序列化配置里忽略命名大小写,减少这类问题。

第四个坑是认证接口的日志。为了排查问题,我在认证接口里打印了完整的Request.Headers和 Query 参数,结果发现 token 全被记录到了日志文件。线上这是安全隐患。后来我把日志改成只记录 username、认证结果和耗时,token 一律脱敏。

4.3 上线前检查清单

上线之前,我建议按下面这个清单过一遍:

  • 认证 URL 是公网可访问的 HTTPS,证书由可信 CA 签发。
  • Secret 没有出现在客户端代码或前端 bundle 里。
  • 认证接口有完整的签名校验和时间戳防重放逻辑。
  • 认证接口做了频率限制,防止被恶意刷请求。
  • 日志不记录 token、密码等敏感信息。
  • 认证接口的 P99 响应时间小于 500ms。
  • 游戏服务器再次校验认证结果中的 UserId 与客户端的业务逻辑是否一致。
  • 准备好认证服务器的降级开关,Photon 认证挂掉时能快速切换回备用方案。

这份清单是我从上一次上线事故里总结出来的。当时只做了功能验证,没考虑频率限制,结果有玩家抓包后用脚本疯狂调认证接口,把数据库连接占满了,导致整个登录入口雪崩。

5. 个人经验与后续扩展

5.1 连接失效与重新认证

自定义认证解决的是连接那一刻的身份校验,不解决长期会话过期的问题。玩家可能连着游戏超过一小时,认证 token 过期了,但他的连接仍然有效。这里要分两种情况:如果玩家不掉线,可以不管;一旦玩家断线重连,就必须重新走一遍认证流程。

我在项目里的做法是:客户端在断线重连前,先向自建服务器申请新的 token,再拿着新 token 去重连 Fusion。千万不要在客户端缓存过期 token 后反复重试,否则服务端验签一直失败,玩家会卡在“重连中”界面。

5.2 多端登录与同账号踢人

自定义认证接口可以在认证时检查该 userId 是否已经有一个正在使用的 Photon 连接。如果存在,可以主动拒绝新连接,也可以允许新连接并把旧连接踢下线,这取决于业务需求。

Fusion 2 本身不提供直接通过 userId 踢人的 API,但你可以结合自定义认证返回的 UserId,在游戏逻辑里维护“当前在线玩家”的映射。服务端要踢人时,向指定房间发一条自定义消息,房间内持有该 UserId 的客户端再主动断线。这样做比在认证阶段直接拒绝更平滑,玩家能收到明确的“账号已在其他设备登录”提示。

5.3 别把认证接口变成万能接口

最后唠叨一句:自定义身份验证是一个入口,但它不是数据中转站,也不是防作弊的全部。认证只做一次,之后玩家在游戏里做什么事,还得靠游戏服务器持续校验。

我现在的项目分了三层:最前端是 Photon 自定义认证,负责门禁;中间是游戏逻辑服务器,负责所有关键操作校验;最后是异步数据服务,负责存档、道具、排行榜。三层职责分开,每一层都很轻。如果你发现认证接口里塞了一堆跟身份无关的逻辑,建议拆出去重新设计。

从最早被自签名证书折磨,到后来把整条认证链路跑通,我最深的体会有两点:第一,先跑通最小链路再补安全细节,浏览器能验证成功、集成 Unity 能进房间,比任何文档都管用;第二,认证接口一定要保持轻量,它只是开始,不是终点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 21:19:31

发那科机器人二次开发:C#读写数据与点位信息获取实战

简介&#xff1a;这份资源面向从事工业自动化与机器人二次开发的C#开发者&#xff0c;聚焦发那科&#xff08;FANUC&#xff09;机器人的数据读写与点位信息获取。通过调用发那科机器人控制SDK中的API&#xff0c;开发者可读取机器人当前位置、速度、加速度等关键参数&#xff…

作者头像 李华
网站建设 2026/10/27 22:43:34

飞行轨迹预测实战:双向LSTM与GRU选型及数据切分避坑

简介&#xff1a;面向本科及以上学历、有一定MATLAB基础的开发者&#xff0c;提供一套完整的飞行轨迹预测实验方案&#xff0c;涵盖双向LSTM与GRU两种神经网络的建模、训练、评估与可视化。压缩包共18个文件&#xff0c;大小约319KB&#xff1b;核心为6个.m脚本&#xff0c;包括…

作者头像 李华
网站建设 2026/10/27 9:41:52

双NPN三极管恒流源设计:低成本高精度方案与温漂补偿实战

恒流源这个东西&#xff0c;说简单也简单&#xff0c;说讲究也是真讲究。一个运放加一个MOS管能搭&#xff0c;一个基准源加一个三极管也能搭&#xff0c;但真要把成本压到几毛钱、温漂控制在可接受范围内、还要在宽负载范围内稳定工作&#xff0c;双NPN三极管方案就值得拿出来…

作者头像 李华
网站建设 2026/10/23 6:04:56

Redis 生产环境最佳实践:7 大维度 43 条规范全详解

去年帮一家电商团队排查线上故障&#xff0c;现象很经典&#xff1a;Redis 内存告警、慢查询刷屏、数据库连接被打满。我打开他们的 Redis 一看——大 key、永不过期的 key、还有大量用 String 存的复杂结构&#xff0c;遍地都是雷。这几乎是每个没定过使用规范的团队&#xff…

作者头像 李华
网站建设 2026/10/24 4:00:16

宿州30米DEM数据处理三步法:坐标系、无效值、范围对齐

简介&#xff1a;本资源为安徽省宿州市及周边区域30米分辨率数字高程模型&#xff08;DEM&#xff09;地理信息数据集&#xff0c;面向GIS从业人员、地理信息专业师生、城市规划与环境研究者&#xff0c;支撑地形分析、坡度计算、流域模拟、地质灾害评估等空间分析任务。压缩包…

作者头像 李华
网站建设 2026/10/23 10:13:17

Linux启动引导与systemd服务控制:从开机到故障排查完整指南

刚接手一批新服务器的时候&#xff0c;我最怕听到的一句话就是&#xff1a;机器重启之后起不来了。明明昨天还好好的&#xff0c;就因为装了个软件、改了行配置、或者单纯是机房断电后来电&#xff0c;系统就卡在一个黑屏、一个 grub> 提示符、或者一串 kernel panic 日志面…

作者头像 李华