You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何限制API仅对Unity开放访问,阻止浏览器及未授权客户端接入

Unity专属Node.js/Express API安全实现方案

核心思路

要实现仅允许Unity客户端访问API,关键是利用Unity客户端独有的请求特征,结合多重验证机制——单一方法易被绕过,组合验证才能大幅提升安全性。

具体实现方案

1. 自定义请求头+HMAC签名验证

这种方式通过"专属标识+请求签名"双重验证,既区分客户端类型,又防止请求被篡改或重放。

  • 自定义专属头:Unity发起请求时固定携带X-Unity-Client头,值设为约定字符串(比如MyUnityGame_v1.2.0)
  • 签名生成逻辑:Unity端用请求路径+时间戳+专属密钥生成SHA256 HMAC签名,随请求一同发送
  • Express端验证步骤:
    1. 检查X-Unity-Client头是否存在且符合约定值
    2. 验证时间戳是否在有效窗口内(比如5分钟),防止重放攻击
    3. 用相同密钥重新计算签名,与请求中的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 21:35:24