🗂

ASP.NET Core + React + Typescriptで認証付きWebアプリのひな型を作る⑥jwtトークン機能追加

2022/05/17に公開約8,300字

⑤で作った画面をよく見ると、wheatherforcastのデータが認証前から表示されていることが分かります。
(認証後も表示されますが、今の時点ではwheatherforcast apiにアクセスする際に認証情報を付加していないので、認証情報抜きでAPIが機能してしまっているという意味では同じです)



理由は単純で、wheatherforcastが認証を必要にしていないためです。
認証の有無にかかわらずアクセスしてきた場合データを返す仕様になっているので、認証前の状態でアクセスしてもデータを返してしまいます。

これを変更して、認証前はデータを表示しない、認証後のみデータを表示する。という仕様にします。

具体的には、コントローラーの頭に「[Authorize]」を付けます

WeatherForecastController.cs
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace server_app.Controllers
{
+    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    
※WeatherForecastController.cs全体
WeatherForecastController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace server_app.Controllers
{
    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

この状態で実行してみると、認証情報がないのでAPIはJSONを返さなくなります。
(画面表示はデータを取得するまではloadingと表示する仕様になっているので、永遠にloadingになっていますが、コンソールを見るとwheatherforcastが401(認証未)エラーを出していることが分かります)



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

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

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('react_dotnet_skelton_jwt_token', content.token);

        }

    }
※Login.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'},
            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('react_dotnet_skelton_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;

次に、認証時に取得したjwtトークンをweatherforecastに添付して問い合わせます。

WeatherForecast.tsx
  const populateWeatherData = async () => {
-    const response = await fetch('https://localhost:5001/weatherforecast');
+    const response = await fetch('https://localhost:5001/weatherforecast', {
+      headers: {
+        Authorization: `Bearer +${window.localStorage.getItem('react_dotnet_skelton_jwt_token')}`,
+      }
+    });
+    const data = await response.json();
+    setState({ forecasts: data, loading: false });
+  };
※WeatherForecast.tsx全体
※WeatherForecast.tsx全体
import React, { FC, useEffect, useState } from 'react';

type Forecast = {
  date: Date;
  temperatureC: number;
  temperatureF: number;
  summary: string;
};

type State = {
  forecasts: Forecast[];
  loading: boolean;
};

export const WeatherForecast: FC = () => {
  const [state, setState] = useState<State>({ forecasts: [], loading: true });

  useEffect(() => {
    populateWeatherData();
  }, []);

  const populateWeatherData = async () => {
    const response = await fetch('https://localhost:5001/weatherforecast', {
      headers: {
        Authorization: `Bearer ${window.localStorage.getItem('react_dotnet_skelton_jwt_token')}`,
      }
    });
    const data = await response.json();
    setState({ forecasts: data, loading: false });
  };

  const renderForecastsTable = (forecasts: Forecast[]) => {
    return (
      <table className="table table-striped" aria-labelledby="tabelLabel">
        <thead>
          <tr>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
          </tr>
        </thead>
        <tbody>
          {forecasts.map((forecast) => (
            <tr key={forecast.date.toString()}>
              <td>{forecast.date.toString()}</td>
              <td>{forecast.temperatureC}</td>
              <td>{forecast.temperatureF}</td>
              <td>{forecast.summary}</td>
            </tr>
          ))}
        </tbody>
      </table>
    );
  };

  let contents = state.loading ? (
    <p>
      <em>Loading...</em>
    </p>
  ) : (
    renderForecastsTable(state.forecasts)
  );

  return (
    <div>
      <h1 id="tabelLabel">Weather forecast</h1>
      <p>This component demonstrates fetching data from the server.</p>
      {contents}
    </div>
  );
};

この状態で実行することで、ログイン後にブラウザを更新するとwheatherforcastが表示されるようになります。



Discussion

ログインするとコメントできます