节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
4.44 KiB
Stardust
using NewLife;
using NewLife.Remoting;
using Stardust.Data;
using Stardust.Models;

namespace Stardust.Server.Services;

/// <summary>应用令牌服务</summary>
public class AppTokenService
{
    /// <summary>验证应用密码,不存在时新增。密钥不匹配(含空密钥)时按重注册模式裁决</summary>
    /// <param name="username">应用标识</param>
    /// <param name="password">应用密钥。为空或错误时按该应用的重注册模式放行或拒绝</param>
    /// <param name="autoRegister">应用不存在时是否自动创建</param>
    /// <param name="ip">来源IP</param>
    /// <param name="clientId">客户端标识。仅用于写历史</param>
    /// <returns>验证通过的应用</returns>
    public App Authorize(String username, String password, Boolean autoRegister, String ip = null, String clientId = null)
    {
        if (username.IsNullOrEmpty()) throw new ArgumentNullException(nameof(username));
        //if (password.IsNullOrEmpty()) throw new ArgumentNullException(nameof(password));

        // 查找应用
        var app = App.FindByName(username);
        // 查找或创建应用,避免多线程创建冲突
        app ??= App.GetOrAdd(username, App.FindByName, k => new App
        {
            Name = username,
            Secret = password,
            Enable = autoRegister,
        });

        // 检查黑白名单
        if (!app.MatchIp(ip))
            throw new ApiException(ApiCode.Forbidden, L.T("Application [{0}] forbids access from {1}.", username, ip));
        if (app.Project != null && !app.Project.MatchIp(ip))
            throw new ApiException(ApiCode.Forbidden, L.T("Project [{0}] forbids access from {1}.", app.Project, ip));

        // 检查应用有效性
        if (!app.Enable) throw new ApiException(ApiCode.Forbidden, L.T("Application [{0}] is disabled.", username));

        // 密钥校验。空密钥或错误密钥时按重注册模式裁决,与 App/Login 的 Register 保持同一套语义:
        // 关闭=一律拒绝(默认安全);收敛窗口=临时放行,客户端携带正确密钥后自动关闭;开放=放行;可信接入暂不支持本接口
        if (app.Secret.IsNullOrEmpty() || password != app.Secret)
        {
            var mode = app.ReRegister;
            if (mode is not (ReRegisterModes.Auto or ReRegisterModes.Open))
            {
                // 安全纵深(V-1/V-2):空密钥/错误密钥默认禁止,防止身份冒用与真实密钥泄露
                if (mode == ReRegisterModes.Trusted)
                    throw new ApiException(ApiCode.Forbidden, L.T("The re-registration mode [trusted access] of application [{0}] is not supported by this API. Configure a secret or switch to the convergence window/open mode.", username));
                if (app.Secret.IsNullOrEmpty())
                    throw new ApiException(ApiCode.Forbidden, L.T("Application [{0}] has no secret configured and access is forbidden. Set a Secret for this application first.", username));
                throw new ApiException(ApiCode.Forbidden, L.T("Illegal access to application [{0}].", username));
            }

            // 收敛窗口/开放:放行,等待客户端收敛;密钥由令牌接口(OAuth/Token)随响应下发
            app.WriteHistory(L.T("Application re-registered"), true, L.T("[{0}] secret mismatch; allowed by [{1}] mode", app.Name, mode), null, ip, clientId);
        }
        // 收敛窗口:客户端已携带正确密钥,自动关闭窗口完成收敛
        else if (app.ReRegister == ReRegisterModes.Auto)
        {
            app.WriteHistory(L.T("Application re-registered"), true, L.T("[{0}] the client now carries the correct secret; the convergence window is closed automatically", app.Name), null, ip, clientId);
            app.ReRegister = ReRegisterModes.Off;
            app.Update();
        }

        return app;
    }

    public App ValidApp(String appId)
    {
        // 验证应用
        var app = App.FindByName(appId);
        if (app == null)
        {
            // 可能是StarAgent混用了token
            var node = Data.Nodes.Node.FindByCode(appId);
            if (node == null) throw new ApiException(ApiCode.Forbidden, L.T("Invalid application [{0}]", appId));

            app = new App { Name = node.Code, DisplayName = node.Name, Enable = true };
        }
        if (!app.Enable) throw new ApiException(ApiCode.Forbidden, L.T("Application [{0}] is disabled", appId));

        return app;
    }
}