如何限制API仅对Unity开放访问,阻止浏览器及未授权客户端接入
Unity专属Node.js/Express API安全实现方案
核心思路
要实现仅允许Unity客户端访问API,关键是利用Unity客户端独有的请求特征,结合多重验证机制——单一方法易被绕过,组合验证才能大幅提升安全性。
具体实现方案
1. 自定义请求头+HMAC签名验证
这种方式通过"专属标识+请求签名"双重验证,既区分客户端类型,又防止请求被篡改或重放。
- 自定义专属头:Unity发起请求时固定携带
X-Unity-Client头,值设为约定字符串(比如MyUnityGame_v1.2.0) - 签名生成逻辑:Unity端用请求路径+时间戳+专属密钥生成SHA256 HMAC签名,随请求一同发送
- Express端验证步骤:
- 检查
X-Unity-Client头是否存在且符合约定值 - 验证时间戳是否在有效窗口内(比如5分钟),防止重放攻击
- 用相同密钥重新计算签名,与请求中的
X-Signature头对比,一致则通过
- 检查
Unity端示例代码(C#)
using System; using System.Security.Cryptography; using System.Text; using UnityEngine.Networking; public class ApiClient { private const string SecretKey = "你的专属密钥"; private const string UnityClientFlag = "MyUnityGame_v1.2.0"; public IEnumerator SendProtectedRequest(string apiUrl) { long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); string signature = GenerateHmacSignature(apiUrl, timestamp.ToString()); UnityWebRequest request = UnityWebRequest.Get(apiUrl); request.SetRequestHeader("X-Unity-Client", UnityClientFlag); request.SetRequestHeader("X-Timestamp", timestamp.ToString()); request.SetRequestHeader("X-Signature", signature); yield return request.SendWebRequest(); // 处理响应逻辑 if (request.result == UnityWebRequest.Result.Success) { Debug.Log("请求成功:" + request.downloadHandler.text); } else { Debug.LogError("请求失败:" + request.error); } } private string GenerateHmacSignature(string data, string timestamp) { string rawContent = $"{data}{timestamp}{SecretKey}"; using (HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(SecretKey))) { byte[] hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawContent)); return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); } } }
Express端中间件示例
const crypto = require('crypto'); const SECRET_KEY = '你的专属密钥'; const VALID_UNITY_FLAG = 'MyUnityGame_v1.2.0'; const VALID_TIME_WINDOW = 300; // 5分钟,单位秒 function validateUnityRequest(req, res, next) { // 验证专属客户端头 const unityFlag = req.headers['x-unity-client']; if (!unityFlag || unityFlag !== VALID_UNITY_FLAG) { return res.status(403).send('未授权客户端'); } // 验证时间戳有效期 const timestamp = req.headers['x-timestamp']; if (!timestamp) { return res.status(403).send('缺少请求时间戳'); } const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - parseInt(timestamp)) > VALID_TIME_WINDOW) { return res.status(403).send('请求已过期'); } // 验证签名 const signature = req.headers['x-signature']; if (!signature) { return res.status(403).send('缺少请求签名'); } const rawContent = `${req.originalUrl}${timestamp}${SECRET_KEY}`; const computedSignature = crypto.createHmac('sha256', SECRET_KEY) .update(rawContent) .digest('hex'); if (computedSignature !== signature) { return res.status(403).send('签名无效'); } next(); } // 将中间件应用到所有API路由 app.use('/api', validateUnityRequest);
2. JWT结合设备标识验证
通过颁发短期有效令牌,结合Unity设备唯一标识,进一步绑定客户端身份。
- 流程:Unity首次启动时,用
SystemInfo.deviceUniqueIdentifier作为设备标识,向API请求JWT令牌;API验证标识格式后颁发有效期为1天的令牌 - 后续请求:Unity每次携带JWT令牌,Express端验证令牌有效性,同时可额外校验请求中的设备标识与令牌内的标识是否一致
Express端JWT示例
const jwt = require('jsonwebtoken'); const JWT_SECRET = '你的JWT密钥'; // 颁发令牌接口 app.post('/api/auth', (req, res) => { const deviceId = req.body.deviceId; // 简单验证Unity设备ID格式(可根据平台调整规则) if (!deviceId || deviceId.length < 10) { return res.status(403).send('无效设备标识'); } const token = jwt.sign({ deviceId }, JWT_SECRET, { expiresIn: '1d' }); res.json({ token }); }); // JWT验证中间件 function validateJwt(req, res, next) { const token = req.headers['authorization']?.split(' ')[1]; if (!token) { return res.status(403).send('缺少授权令牌'); } try { const decoded = jwt.verify(token, JWT_SECRET); // 可选:验证请求中的设备标识与令牌一致 const requestDeviceId = req.headers['x-device-id']; if (requestDeviceId && requestDeviceId !== decoded.deviceId) { return res.status(403).send('设备标识不匹配'); } req.authInfo = decoded; next(); } catch (err) { return res.status(403).send('令牌无效或已过期'); } } // 应用到受保护路由 app.use('/api/protected', validateJwt);
Unity端获取并使用JWT示例
using UnityEngine; using UnityEngine.Networking; public class JwtAuthClient { public IEnumerator GetTokenAndAccessProtectedApi() { string deviceId = SystemInfo.deviceUniqueIdentifier; WWWForm form = new WWWForm(); form.AddField("deviceId", deviceId); // 请求令牌 UnityWebRequest authReq = UnityWebRequest.Post("http://localhost:3000/api/auth", form); yield return authReq.SendWebRequest(); if (authReq.result == UnityWebRequest.Result.Success) { TokenResponse response = JsonUtility.FromJson<TokenResponse>(authReq.downloadHandler.text); PlayerPrefs.SetString("ApiToken", response.token); PlayerPrefs.Save(); // 访问受保护接口 UnityWebRequest protectedReq = UnityWebRequest.Get("http://localhost:3000/api/protected/data"); protectedReq.SetRequestHeader("Authorization", $"Bearer {response.token}"); protectedReq.SetRequestHeader("X-Device-Id", deviceId); yield return protectedReq.SendWebRequest(); if (protectedReq.result == UnityWebRequest.Result.Success) { Debug.Log("受保护接口响应:" + protectedReq.downloadHandler.text); } } } [System.Serializable] private class TokenResponse { public string token; } }
3. 额外辅助限制
- 拦截OPTIONS请求:浏览器发起跨域请求时会先发OPTIONS预请求,Unity不会,可直接拦截返回403
- 校验User-Agent:UnityWebRequest默认User-Agent以
UnityPlayer/开头,可增加此校验(虽可模拟,但多一层防护)
Express端示例
// 拦截OPTIONS预请求 app.options('*', (req, res) => { res.status(403).send('禁止预请求'); }); // User-Agent校验中间件 function validateUserAgent(req, res, next) { const userAgent = req.headers['user-agent']; if (!userAgent || !userAgent.startsWith('UnityPlayer/')) { return res.status(403).send('无效客户端'); } next(); } app.use('/api', validateUserAgent);
关键注意事项
- 密钥混淆:Unity端的密钥不要明文存储,用IL2CPP编译+代码混淆工具降低泄露风险
- 多机制组合:不要依赖单一验证方式,建议将自定义头、签名、JWT校验组合使用
- 环境区分:开发阶段可临时跳过部分验证,生产环境必须启用所有安全规则
内容的提问来源于stack exchange,提问作者Ninhko
相关产品推荐
相关产品推荐

