タスク管理ツールをReact + ASP.Coreで作る019JWTトークン機能追加

[前回](https://qiita.com/mosuma/items/035967eb98259bbd4934)の続きです


# 概要

016～018の記事で、認証機能の追加、ログイン用ユーザーの追加、ログイン画面、JWTトークンの生成・クライアントでの取得機能を作りました。
認証情報をやり取りする土台が出来たので、いよいよ認証されているユーザーにのみ特定の操作を許す。という仕様を盛り込むことが可能になります。
それでは構築していきましょう。


### ガイド
全体Index：[タスク管理ツールをReact + ASP.Coreで作る - Index](https://qiita.com/mosuma/items/88e40dc44e7bff1a54d8)


## 認証機能追加の一連の流れ
- [016サーバーサイドに認証機能関連のクラス追加](https://qiita.com/mosuma/items/728f501bcc7e54352fb8)
- [017クライアントにユーザー登録・ログイン画面と処理を追加](https://qiita.com/mosuma/items/e606cd33423edfdb4118)
- [018認証トークン関連機能をサーバー・クライアント双方に追加](https://qiita.com/mosuma/items/035967eb98259bbd4934)
- 019認証有無に従い制御を実施する機能を追加(本記事)
- [020認証機能をリファクタリング](https://qiita.com/mosuma/items/cf50dc4136fe06233412)
- [021認証状態に従い画面表示を切り替え](https://qiita.com/mosuma/items/5b7ed9431633800e7cc6)

# 構築


## 認証情報をクライアントサイドに保管する機能の追加


まず、認証された際のトークンをクライアントに保管し、認証後にクライアントに問い合わせるときは認証情報付きで問い合わせるように仕様を変更します。


次に、認証情報をAPIに乗せて、認証するようにします。

まず、認証時にjwtトークンを記録(「tasket_jwt_token」という名称でブラウザに保存)します


```diff_tsx:Login.tsx
    const submit = async (e: SyntheticEvent) => {
        e.preventDefault();
        const response = await fetch('https://localhost:5001/account/login',
        {
            method : 'POST',
            headers:{'Content-Type' : 'application/json'},
            credentials: 'include',
            body: JSON.stringify({
                email,
                password
            })
        });
        const content = await response.json();
        const status = await response.status

        setResultcode(status);
        setResultTitle(content.title);
        if(status==200){
            setName(content.username);
            setToken(content.token);

+            window.localStorage.setItem('tasket_jwt_token', content.token);

        }

    }
```



<details><summary>参考：TaskList.tsx全体(開くと表示)</summary><div>

```diff_tsx:Login.tsx
import React, { SyntheticEvent, useState } from 'react';

const Login = (
    ) => 
{
    
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [resultcode, setResultcode] = useState(0);
    const [resultTitle, setResultTitle] = useState('');
    const [token, setToken] = useState('');



    const submit = async (e: SyntheticEvent) => {
        e.preventDefault();
        const response = await fetch('https://localhost:5001/account/login',
        {
            method : 'POST',
            headers:{'Content-Type' : 'application/json'},
            body: JSON.stringify({
                email,
                password
            })
        });
        const content = await response.json();
        const status = await response.status

        setResultcode(status);
        setResultTitle(content.title);
        if(status==200){
            setName(content.username);
            setToken(content.token);
            
            window.localStorage.setItem('tasket_jwt_token', content.token);

        }

    }

    
    return (
        <>
        <form onSubmit={submit}>
            <h2>Sign in</h2>

            <ul>

                <li>
                    <label>email</label>
                    <input type="email" placeholder="name@example.com" required 
                        onChange = {e => setEmail(e.target.value)}            
                    />
                </li>

                <li>                    
                    <label>password</label>
                    <input type="password" placeholder="Password" required 
                        onChange = {e => setPassword(e.target.value)}            
                    />
                </li>
            </ul>

            <button type="submit">Sign in</button>

        </form>
        
        <h2>Response</h2>

        <ul>
            <li>
                {resultcode!=0 && <>{resultcode==200 ? <>Login Success</> : <>Login Fail</>}</>}
            </li>

            <li>
                {resultcode==200 && <>Name:{name}</>}
            </li>

            <li>
                {resultcode!=0 && <>Code:{resultcode}</>}
            </li>

            <li>
                {resultcode!=0 && <>msg:{resultTitle}</>}
            </li>

            <li>
                {resultcode!=0 && <p>token : {token}</p>}
            </li>
        </ul>
        </>
    );

}

export default Login;

```

</div></details>

## API問い合わせ時に認証トークンを含めるように仕様変更
トークン取得後はブラウザに保管したトークンをAPIでの問い合わせ時には含めるように仕様を変更します。
具体的には、api.tsのリクエスト時にトークンが存在するかどうかを確認し、存在する場合は問い合わせ時に含めるためのコードを追加します


```diff_typescript:api.ts

import axios, { AxiosResponse } from "axios";
import { Task } from "../models/Task";

axios.defaults.baseURL = "https://localhost:5001"; 


+ axios.interceptors.request.use(config => {
+     const token = window.localStorage.getItem('tasket_jwt_token');
+     if(token) config.headers!.Authorization = `Bearer ${token}`
+     return config;
+ })

const Tasks = {
    index: () => axios.get<Task[]>(`/task`).then((response: AxiosResponse<Task[]>)=>response.data),
    details: (id:string) => axios.get<Task>(`/task/${id}`).then((response: AxiosResponse<Task>)=>response.data),
    create: (task:Task) => axios.post<Task>(`/task/create`, task).then((response: AxiosResponse<Task>)=>response.data),
    update: (task:Task) => axios.post<Task>(`/task/update`, task).then((response: AxiosResponse<Task>)=>response.data),
    delete:(id:string) => axios.post<void>(`/task/delete/${id}`),
}


const api = {
    Tasks,
}

export default api;
```


## サーバーサイドに認証制御を追加

これまでの改修でサーバーサイドでは認証されたユーザーからのアクセスかどうかを把握する機能は作られていますが、認証によって動作の分岐などは起こっていない(あらゆる操作を許している)ので、今回コントローラーの一部の動作に制限を入れてみます。

具体的には、タスクごとの詳細情報を取得する関数を認証時のみ動作する様に変更します。



```diff_c_sharp:
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;


        {
            return await _context.t_tasks.ToListAsync();
        }
+        [Authorize]
        [HttpGet("{id}")]
        public async Task<ActionResult<t_task>> Details(Guid id)
        {
```


この状態でサーバーサイド・クライアントサイドともに再起動を行うと、以下の様になります



## 動作させてみる


<iframe width="560" height="315" src="https://www.youtube.com/embed/fWCLDZlPc40" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

今回は以上です。続きは[次回](https://qiita.com/mosuma/items/cf50dc4136fe06233412)です



TAG:V000-16-00