年齢認証ページの作り方

この記事は [驚異のFANZA女優検索](https://fanza-actress-bust-search.azurewebsites.net/) Advent Calendar 2021 の 14 日目の記事です。

# 年齢認証
自分のサービスではTOPページで女優をクリックすると女優のページに飛ぶ前に年齢認証ページに行く。年齢認証の仕組みは初めて作ったので、一般的な正解は分からないが、色々気をつけたことがあるので今日はその話。認証の仕組みはなるべく自分のサービスに依らないように書いたつもり。

# 特定のページから年齢認証ページへリダイレクト
年齢認証が必要なページはあらかじめ決まっている。自分のサービスで言うと女優ページと作品ページだ。年齢認証で「はい」が押されていない状態で、これらのページに行く場合は、まず年齢認証ページにリダイレクトする。ここでポイントは年齢認証ではいを押したら元々行こうとしていたページに戻さなくてはならない。つまり元々行こうとしていたページにさらにリダイレクトする必要がある。また年齢認証は1回でいいので、一度はいを押したらその事をクッキーに保存して、次は年齢認証しなくする必要がある。

   
# 年齢認証ページに飛ばすための実装
まずサーバで年齢認証が終わっているかを判断するメソッドを作る。クッキーを使って判定だ。

```cs:HomeController.cs
//年齢認証ページではいが押されていれば、クッキーに保存されているのでクッキーの値をみてboolを返す。
private bool IsVerification() => Request.Cookies["Verification"] == "Yes";
```

このIsVerificationを認証が必要な各ページの先頭で呼ぶ。

```cs:HomeController.cs
//女優ページ
[Route("/actress/{id}")]
public async Task<IActionResult> Actress(int id, int page = 0, string count = "")
{
    //各メソッドの先頭で認証済みかチェックする。OKの場合は再びこのページに戻るためパラメータでurlを渡す
    if (!IsVerification()) return Redirect($"/home/verification?url=/actress/{id}");
        …

//作品ページ
[Route("/actress/{actressId}/product/{id}")]
public async Task<IActionResult> Product(string id, int actressId)
{
    //各メソッドの先頭で認証済みかのチェックする。OKの場合は再びこのページに戻るためパラメータでurlを渡す
    if (!IsVerification()) return Redirect($"/home/verification?url=/actress/{actressId}/product/{id}");
        …
```
これで、まず認証ページへリダイレクトされる。

# 年齢認証ページの実装
で、認証ページは以下のようにして、リダイレクト元のURLをViewDataに入れる。

```cs:HomeController.cs
public async Task<IActionResult> Verification(string url)
{
    ViewData["Url"] = url;
    return View();
}
```

認証ページは以下のように、はいを押されたらクッキーに保存して、元のURLに再度リダイクレトする。

```html:verification.cshtml
<div>
    <div class="m-2 d-flex justify-content-center small">
        <a class="m-2 btn btn-light" href='/'>いいえ</a>
        <a class="m-2 btn btn-primary" onclick="jumpUrl('@ViewData["Url"]')">はい</a>
    </div>
</div>
<script>
    function jumpUrl(url) {
        document.cookie = "Verification=Yes;Path=/";
        location.replace(url); //location.replaceで飛ぶのがポイント
    }
</script>
```
ポイントはこのリダイレクトをする時はjavascriptでlocation.hrefではなくlocation.replaceを呼ぶこと。location.hrefで移動すると閲覧履歴に追加されてしまうので、ブラウザの戻るボタンを押されると再度年齢認証画面に行ってしまう。戻るボタンでも再度認証させないにはlocation.replaceを使う。

# クローラー対策
これで年齢認証は実装できたが、あくまでこれだと対人間用だ。この作りだとクローラーも年齢認証にひっかかってしまう。人間がサービスに来た時は認証ページへリダイレクトし、クローラーが来た時はリダイレクトさせない仕組みが欲しい。人間はJavascriptを解釈するが、クローラーはJavascriptを解釈しない（最近のgoogleはするのかな？）のでそれを使う。つまりクッキーの値がYesならリダイレクトしないとしていたが、このサービスに来た瞬間にJavaScriptでクッキーにNotyetと書き込んむようにして、判定はYesかnullの場合はOKとしてやる。これでクローラーはnullのままだし人間はNotYetがついている。

```cs:HomeController.cs
//年齢認証は「Yes」か「何も設定されていなければ」trueにする
private bool IsVerification() => Request.Cookies["Verification"] == null || Request.Cookies["Verification"] == "Yes";
```

ただし、この方法は初回アクセス時だけは使えない。サーバーでのチェックを終えていないとJavaScriptは走らない。つまり初回アクセスの場合まずサーバーで認証チェックをするが、その時は何も書き込まれていない状態なので成功して女優ページなどに行けてしまう。なので、女優ページのjavascriptでリダイレクトする必要がある。この際、jsがbodyの後、footerなどに書かれているとその分は一瞬描画されてしまう。そこでこの判定をするjsだけはbosyの前のheadで定義する。


```html:index.cshtml
<head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="~/css/site.css" />
　　<!-- ここで定義したjsは下のbodyに書かれる内容よりも先に呼ばれる -->
    <script src="~/js/top.js"></script>
</head>
<body>
    <header>
        …
    </header>
    <div class="container">
        <main role="main" class="p-3">
            …
            …
        </main>
    </div>
    <footer>
        …
    </footer>
　　<!-- 普通のjsはこの辺で定義するが、ここより上に書かれた内容が先に描画されてしまう -->
    <script src="~/lib/jquery.min.js"></script>
    <script src="~/lib/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js"></script>
</body>
```


```typescript:top.js
//初回のチェックはjsで人間だけが行う。クローラーはここには来ない
const cookieArray = getCookieArray();
//クッキーで年齢認証されていない場合
if (cookieArray['Verification'] !== 'Ok') {
    //クッキーにNotyetを入れる(nullではなくなる)
    document.cookie = "Verification=Notyet;Path=/";
    //初回に年齢認証が必要なページが指定されている場合、既にサーバーのチェックはnullで通過してしまっているので、ここで年齢認証ページにリダイレクトする
    if (location.pathname.indexOf("/actress/") !== -1) {
        location.href = `verification?url=${location.href}`;
    }
}
```

これで、クローラーは年齢認証があるページもクローリング出来て、かつ人間は年齢認証を聞くように出来る。