Skip to content

短縮 URL 機能の設計 ​

第1版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

画面右上のリンクコピーボタンでコピーされる URL には、ビューの設定が JSON のままクエリに入るため、数百文字になることがあります。このページは、ボタンを押したときに短縮 URL(https://{host}/s/{code})を発行してコピーする機能を追加する本体改修の設計メモです。本体の標準機能ではありません。 本体を改修せずに短縮 URL を扱う方法は URL で既定値を渡す・短縮 URL を参照してください。

要件内容優先度
リンクコピーボタンとの統合ボタンを押すと短縮 URL を発行してクリップボードに入れる主
コントローラの追加リダイレクト用と管理用主
有効期限無期限・期限付きを選べる。期限付きの既定日数はパラメータで持つ主
期限切れの削除バックグラウンドのタイマーで定期的に削除する主
任意の URLサーバースクリプトや API から任意の URL を短縮できる副

前提にした現行実装(1.5.8.1) ​

リンクコピーボタン ​

パンくずの右に出るボタン(#CopyToClipboards の中の #CopyDirectUrlToClipboard)は、サーバー側で HTML を作るときに URL を確定し、onclick に文字列として埋め込みます(HtmlBreadcrumb.cs)。

  • URL は現在のクエリに View(ビューの JSON)を足したもの。gridrows のアクションは index に置き換え、ポート 80 / 443 は省く(HtmlBreadcrumb.cs)
  • ブラウザ側の $p.copyDirectUrlToClipboard は、画面外に置いた要素を選択して document.execCommand('copy') でコピーし、alert で知らせるだけで、サーバーとは通信しない(clipboard.js)
  • ボタンはサイト・レコードの画面のほか、組織・グループ・ユーザー管理の画面などにも出る

ルートとコントローラ ​

MVC のコントローラは Controllers/、API は Controllers/Api/([Route("api/[controller]")])にあります。ルートは FormBinaries → Default({controller}/{action})→ Others → Item({controller}/{id}/{action})→ Binaries({controller}/{guid}/{action})の順に登録されています(Startup.cs)。/s/{code} はこのままだと Default ルート(コントローラ s、アクション {code})に一致してしまうので、専用ルートを先に登録します。

バックグラウンドのタイマー ​

定期処理は Quartz.NET のジョブで、各タイマーの Param(IExecutionTimerBaseParam)が有効フラグ・ジョブのキー・実行時刻の一覧を返します(IExecutionTimerBaseParam.cs)。

  • 実行時刻の一覧は "02:00" のような HH:mm の文字列で、Service.TimeZoneDefault(未設定なら UTC)のタイムゾーンの毎日その時刻に動く cron トリガーに変換される(TimerBackground.cs)
  • タイマーは TimerBackground の一覧に並べて登録する(TimerBackground.cs)
  • 一覧全体は BackgroundService.TimerEnabled が true のときだけ動き、この判定は既存のタイマーの有効フラグの OR(BackgroundService.cs)。新しいタイマーだけを有効にしても動かないので、ここにも条件を足す

URL と短縮コード ​

text
https://{host}/s/{code}
方式推測されにくさ衝突採用
暗号論的乱数の Base62(8 文字)高い再生成で回避推奨
SHA-256 の先頭 N 文字低い(元 URL から計算できる)あり得る―
連番の Base62低いなし―

Base62 の 8 文字は 62^8(約 218 兆)通りです。

csharp
private static string GenerateCode(int length = 8)
{
    const string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    return RandomNumberGenerator.GetString(chars, length);
}

接頭辞は /s/(短い)、/go/(用途が分かりやすい)などが候補です。

テーブル ​

図を読み込み中…

インデックス列用途
主キーTenantId, Code
一意Codeリダイレクト時の検索
非一意ExpiresAt期限切れの削除
非一意TenantId, SiteIdサイトごとの一覧
非一意TenantId, Url, SiteId重複登録の確認

テーブルは CodeDefiner の定義(App_Data/Definitions/)に追加し、SQL Server・PostgreSQL・MySQL のどれでも動くよう Rds の抽象化を使います。

コントローラ ​

図を読み込み中…

csharp
endpoints.MapControllerRoute(
    name: "ShortenedUrls",
    pattern: "s/{code}",
    defaults: new { Controller = "ShortenedUrls", Action = "Redirect" },
    constraints: new { code = "[A-Za-z0-9]{1,16}" });

リダイレクトは匿名で受け付け、コードが見つからないか期限切れなら 404、有効ならアクセス回数と最終アクセス日時を更新して 302 で転送します。作成・取得・削除の API は既存の API と同じく POST で、認証を必須にします。

API本文・応答
POST /api/shortenedurls/createUrl(必須)、SiteId、Permanent、ExpiresAt → Code、ShortUrl、期限
POST /api/shortenedurls/{code}/getUrl、AccessCount、LastAccessedTime など
POST /api/shortenedurls/{code}/delete削除
json
{
    "StatusCode": 200,
    "Response": {
        "Code": "aB3xK9mQ",
        "Url": "https://pleasanter.example.com/items/12345/index?View=...",
        "ShortUrl": "https://pleasanter.example.com/s/aB3xK9mQ",
        "Permanent": false,
        "ExpiresAt": "2027-03-02T00:00:00"
    }
}

パラメータ ​

json
{
    "Enabled": false,
    "DefaultExpirationDays": 365,
    "CodeLength": 8,
    "MaxUrlLength": 2048,
    "MaxPerSite": 1000,
    "MaxPerTenant": 100000,
    "AllowedDomains": [],
    "ShowPreview": false
}
パラメータ説明
Enabled機能の有効・無効
DefaultExpirationDays期限付きのときの既定日数
CodeLength短縮コードの長さ
MaxUrlLength登録できる URL の最大長
MaxPerSite / MaxPerTenant登録数の上限
AllowedDomains転送先を許すドメイン(空なら制限なし)
ShowPreview転送前に転送先を表示する確認画面を挟む

期限切れの削除は BackgroundService に DeleteExpiredShortenedUrls(有効フラグ)と DeleteExpiredShortenedUrlsTime(例: ["03:00"])を追加します。

期限切れの削除タイマー ​

既存の DeleteTrashBoxTimer(DeleteTrashBoxTimer.cs)と同じ形で作り、TimerBackground の一覧と TimerEnabled の条件に追加します。

csharp
public class DeleteExpiredShortenedUrlsTimer : ClusterExecutionTimerBase
{
    public class Param : IExecutionTimerBaseParam
    {
        public static readonly JobKey jobKey = new JobKey("DeleteExpiredShortenedUrlsTimer", "ExecutionTimerBase");
        public Type JobType => typeof(DeleteExpiredShortenedUrlsTimer);
        public IEnumerable<string> TimeList => Parameters.BackgroundService.DeleteExpiredShortenedUrlsTime;
        public bool Enabled => Parameters.BackgroundService.DeleteExpiredShortenedUrls;
        public JobKey JobKey => jobKey;
        public string JobName => "DeleteExpiredShortenedUrlsService";
        public Task<bool> SetCustomTimer(IScheduler scheduler) => Task.FromResult(false);
    }

    public override async Task Execute(IJobExecutionContext jobContext)
    {
        // Permanent = false かつ ExpiresAt < 現在時刻 を物理削除し、SysLogs に件数を残す
        await Task.CompletedTask;
    }
}

ClusterExecutionTimerBase は中身の無い抽象クラス(ClusterExecutionTimerBase.cs)です。複数台構成で多重に動かさないためには Quartz のクラスタリング設定と合わせて検討します。

リンクコピーボタンとの統合 ​

方針内容
置き換え操作はそのまま(ボタン 1 回)。中で短縮 URL を発行する
Shift + クリック短縮せず、従来の長い URL をコピーする
重複の排除同じ URL には同じ短縮コードを返すので、2 回目からは検索だけで済む
フォールバック機能が無効、または API がエラーのときは従来の URL をコピーする

サーバー側は、機能が有効なときだけ onclick の呼び出し先を新しい関数に変え、event とサイト ID を渡します。

csharp
var directUrl = DirectUrl(context: context, view: view);
var onClick = Parameters.ShortenedUrl?.Enabled == true
    ? $"$p.copyShortenedUrlToClipboard(event, '{directUrl}', {context.SiteId});"
    : $"$p.copyDirectUrlToClipboard('{directUrl}');";

ブラウザ側は既存の $p.copyDirectUrlToClipboard を残し、新しい関数を足します。API の呼び出しには既存の $p.apiExec(POST、画面の #Token を付ける。_api.js)を使えます。

javascript
$p.copyShortenedUrlToClipboard = function (event, directUrl, siteId) {
    if (event && event.shiftKey) {
        $p.copyDirectUrlToClipboard(directUrl);
        return;
    }
    $p.apiExec($('#ApplicationPath').val() + 'api/shortenedurls/create', {
        data: { Url: directUrl, SiteId: siteId, Permanent: false },
        done: function (data) {
            var shortUrl = data && data.Response && data.Response.ShortUrl;
            if (!shortUrl || !navigator.clipboard) {
                $p.copyDirectUrlToClipboard(directUrl);
                return;
            }
            navigator.clipboard.writeText(shortUrl).then(function () {
                alert($p.display('ShortenedUrlCopied'));
            });
        },
        fail: function () {
            $p.copyDirectUrlToClipboard(directUrl);
        }
    });
};

navigator.clipboard は HTTPS(安全なコンテキスト)でしか使えないため、無い場合も従来の方法に戻します。表示文字列 ShortenedUrlCopied は App_Data/Displays/ に追加します。

図を読み込み中…

同じ URL の重複登録 ​

同じテナント・同じ URL・同じサイト ID の組み合わせは、新しく作らず既存のコードを返します(DB の肥大化を防ぎ、アクセス数も 1 つにまとまる)。

既存今回の要求動作
無期限期限付き既存をそのまま返す
期限付き無期限既存を無期限に変えて返す
期限付き期限付き(期限が違う)長いほうの期限に更新して返す
期限切れ―既存を消して新しく作る

セキュリティ ​

脅威対策
オープンリダイレクト転送先は http / https だけ許す。必要なら AllowedDomains で絞る
javascript: などのスキーム拒否する
内部ネットワークへの転送ループバック・プライベート IP・リンクローカルを拒否する
コードの推測・列挙暗号論的乱数で十分な長さにし、リダイレクトにもレート制限をかける
大量作成作成は認証必須、テナント・サイトごとの上限
不正な URL最大長、Uri.TryCreate での検証
リファラーからの漏洩転送の応答に Referrer-Policy: no-referrer を付ける
フィッシング外部公開するなら ShowPreview で確認画面を挟む
コードの再利用期限切れで消したコードは別の URL に割り当てない
追跡作成・削除・転送を SysLogs に残す
csharp
private static bool IsValidUrl(string url)
{
    if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
    if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) return false;
    if (IPAddress.TryParse(uri.Host, out var ip))
    {
        if (IPAddress.IsLoopback(ip)) return false;
        var b = ip.GetAddressBytes();
        if (b.Length == 4
            && (b[0] == 10
                || (b[0] == 172 && b[1] >= 16 && b[1] <= 31)
                || (b[0] == 192 && b[1] == 168)
                || (b[0] == 169 && b[1] == 254)))
        {
            return false;
        }
    }
    return true;
}

サーバースクリプトから使う(副) ​

ホストオブジェクト shortenedUrls を追加し、任意の URL を短縮できるようにします。

javascript
var result = shortenedUrls.Create({
    url: 'https://external.example.com/document/123',
    permanent: true
});
// result.Code, result.ShortUrl
var info = shortenedUrls.Get(result.Code);
shortenedUrls.Delete(result.Code);

そのほかの注意点 ​

項目内容
拡張サーバースクリプトコントローラ名 shortenedurls で条件を書けるようにする
テナント転送時はコードからテナントを引くので、ログインは要らない
キャッシュよく使われるコードはメモリにキャッシュする

関連ページ ​

変更履歴

第1版外部連携の改修・設計メモ(iCal・RSS/Atom・Webhook 送受信・iPaaS・短縮 URL・POP 受信・マスターデータ同期)を追加