【SpringBoot&React】ログイン認証～ホーム画面表示

最近よく聞くReactを勉強して、ログイン認証～ホーム画面表示までなんとか出来たので個人的に見直し用に纏めておきます。Reactは始めたばかりなので間違いもあると思います。

## はじめに
React、SpringBootの環境作成については割愛してます。
バージョンは以下の通りです。
react: 18.1.0
react-router-dom:6.3.0
axios:0.27.2

react-router-dom、axiosは個別にインストールが必要です。
`npm install react-router-dom`
`npm install axios`

## 仕様について
①Basic認証でログイン
②認証成功時はJSESSIONIDを発行し返却
③認証失敗時はログイン画面へリダイレクト
④ホーム画面ではヘッダーとメインコンテンツを分けて表示

## 画面イメージ
### ◇ログイン画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1234354/5a5b0277-3712-0b47-bcfd-fdf7bbfe30be.png)


### ◇ホーム画面
page1,page2で画面を切り替えることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1234354/4872b9c7-3a28-4d2b-1b1d-78a2c153b23d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1234354/22740c13-783c-4e05-cfc5-cc22da3d72a2.png)


## Reactファイル構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1234354/c621136e-9192-af81-6ab9-ddf08f4865d8.png)



### 重要なファイル
多いですが、以下のファイルです。


◇ルータ
・Router.js

◇画面
・index.html
・index.js
・App.js
・Login.js
・Main.js
・Page1.js
・Page2.js
・Header.js
・PrivateRouteMain.js

◇API処理
・auth.js
・useAuth.sh

◇設定ファイル
・package.json

## Reactソースコード
HTMLは１つで`id="root"`の部分を動的に変更して画面を表示します。
```html:index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport"
          content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>React Router Example</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
```

こちらのjsからHTMLの`id="root"`を読み取り仮想DOMを構築していきます。
次にApp.jsに呼び出してます。
```js:index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

```

`Routers`を呼び出してます。
```js:App.js
import {Routers} from './Routers'

function App() {
  return (
    <div className="App">
      <Routers />
    </div>
  );
}

export default App;

```
各画面へのルートを作成しています。
① `'/'`Login画面
②`'/auth'`でログイン処理
③ログイン処理が成功すれば`'/main'`へ遷移
④入れ子になっている部分はホーム画面で解説
```js:Routers.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Login } from "./pages/Login";
import { Page2 } from './pages/Page2';
import { Page1 } from './pages/Page1';
import { PrivateRouteMain } from './component/PrivateRouteMain';
import { Main } from './pages/Main';


export const Routers = () => {
  return (
    <div>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Login />} />
          <Route path="/auth" element={<PrivateRouteMain />} />
          <Route path="/main" element={<Main />} >
            <Route path="/main/page1" element={<Page1 />} />
            <Route path="/main/page2" element={<Page2 />} />
          </Route>
        </Routes>
      </BrowserRouter>
    </div>
  );
}
```

ユーザ名、パスワードを入力後送信ボタン押下で検証。
照合できればホーム画面へ。
```js:Login.js
import { useState } from "react";
import {useNavigate  } from 'react-router-dom';


export const Login = () => {
const navigate = useNavigate()

  const [form, setForm] = useState({name: '', pass: ''});
  const handleChange = (input) => e => {
    setForm({...form, [input] : e.target.value});
  };

  return (
	
   <div>
      <h2>ログイン</h2>
	  USER
	  <input type="text" value={form.name} onChange={handleChange('name')} />
	  <br /><br />
	  PASSWORD
	  <input type="text" value={form.pass} onChange={handleChange('pass')} />
	  <br /><br />
      <button onClick={() => navigate('/auth',{state:{user:form.name,pass:form.pass},replace : true})}>送信</button>
	</div>

  );
}
```
useEffectを使用してレンダーの結果が画面に反映された後に動作するようにします。
```js:useAuth.js
import { useState, useEffect } from "react";
import { getAuth } from "../api/auth";
import { useNavigate } from 'react-router-dom';

export const useAuth = (user, pass) => {
	const navigate = useNavigate();
	const [status,setStatus] = useState();
	useEffect(() => {
		getAuth(user, pass).then((res) => {
			setStatus(res.status);
			navigate('/main')
		}).catch((err) => {
			setStatus(err.response.status);
			navigate('/');
		});
	}, []);
	return status;
}
```

API接続はaxiosで行っております。
```js:auth.js
import axios from "axios";

const authorizationURL = "/api/auth";
export const getAuth =  async(user,pass) => {
	const response = await axios.get(authorizationURL,{
		auth: {username:user,password:pass}
	});
	return response;
}
```

Header,Outletはコンポーネントを分けております。
Routers.jsで入れ子になった部分がOutletに表示されます。
```js:Main.js
import { Outlet } from 'react-router-dom';
import { Header } from "../component/Header";

export const Main = () => {
  return (
    <div>
        <Header />
        <Outlet />
    </div>
  );
}
```

### コンポーネント

`Link`でRouters.jsの入れ子部分のパスを指定してます。

```js:Header.js
import {Link } from 'react-router-dom';

export const Header = () => {
    return (
        <div>
            <div>
                <Link to="/main/page1">page1</Link>
            
                <Link to="/main/page2">page2</Link>
            </div>
        </div>
    );
}
```
```js:Page1.js
export const Page1 = () => {
  return (
    <div>
        Page1
    </div>
  );
}
```
```js:Page2.js
export const Page2 = () => {
  return (
    <div>
        Page2
    </div>
  );
}

```

## 追記
React側ソースコード漏れがあり追加いたしました。
```js:PrivateRouteMain.js
import { useState } from "react";
import { useAuth } from "../hooks/useAuth"
import { useNavigate,useLocation } from 'react-router-dom';
import { Main } from "../pages/Main";

export const PrivateRouteMain = () => {
  const { state } = useLocation();  
  useAuth(state['user'], state['pass']);
}
```


## SpringBootファイル構成
以下のファイルは全て重要なファイルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1234354/7ce54621-c638-dc47-381e-323f39655f97.png)

## SpringBootソースコード

Springの設定ファイル。ログイン時セキュリティを動かすURL、成功、失敗処理を記述しています。
ローカルで動かす場合は必要ない記述も入ってます。
（時間があるときにサーバから画面にJSまるごと送る方法も書きます。）
```java:WebSecurityConfigurerAdapter.java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Configuration
	protected static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter {

		@Autowired
		private WebAuthenticationProvider webAuthenticationProvider;

		@Override
		public void configure(AuthenticationManagerBuilder auth) throws Exception {
			auth.authenticationProvider(webAuthenticationProvider);
		}
	}

	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/static/css/**", "/static/img/**", "/static/js/**");
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		WebUsernamePasswordAuthenticationFilter filter = new WebUsernamePasswordAuthenticationFilter();
		// 独自フィルター作成
		filter.setAuthenticationManager(authenticationManager());
		filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/auth", "GET")); // ログイン時URL
		filter.setAuthenticationSuccessHandler(successHandler()); // 成功時
		filter.setAuthenticationFailureHandler(failureHandler()); // 失敗時
		http.authorizeRequests().antMatchers("/index").permitAll().anyRequest().authenticated();
		http.addFilter(filter);
	}

	@Bean
	public SuccessHandler successHandler() {
		return new SuccessHandler();
	}
	
	@Bean
	public FailureHandler failureHandler() {
		return new FailureHandler();
	}	
}
```

`"/api/auth"`にアクセスがあるとこのフィルターを通ります
```java:WebUsernamePasswordAuthenticationFilter.java
public class WebUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	@Override
	public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
			throws AuthenticationException {
		
		String authorization = req.getHeader("Authorization");
		String userAndPass = new String(Base64.decodeBase64(authorization.substring("Basic".length())));
		String user = userAndPass.substring(0,userAndPass.indexOf(":"));
		String password = userAndPass.substring((userAndPass.indexOf(":")+1));

		if(user == null) {
			user = "";
		}
		
		if(password == null) {
			password = "";
		}
	
        // トークンの作成
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user, password);
        
        setDetails(req, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
	}
}

```

プロバイダーで検証を行う。
今回はテストのためuser:'user' & password:'pass'でログイン成功とみなしてます。
```java:WebAuthenticationProvider.java
@Configuration
@EnableWebSecurity
public class WebAuthenticationProvider implements AuthenticationProvider {

	@Override
	public Authentication authenticate(Authentication auth) throws AuthenticationException {
		String user = auth.getPrincipal().toString();
		String password = auth.getCredentials().toString();
		
		if (ObjectUtils.isEmpty(user)) {
			throw new AuthenticationCredentialsNotFoundException("ユーザー名もしくはパスワードに誤りがあります。");
		}

		if (ObjectUtils.isEmpty(password)) {
			throw new AuthenticationCredentialsNotFoundException("ユーザー名もしくはパスワードに誤りがあります。");
		}
		
		if (!user.equals("user")) {
			throw new AuthenticationCredentialsNotFoundException("ユーザー名もしくはパスワードに誤りがあります。");
		}

		if (!password.equals("pass")) {
			throw new AuthenticationCredentialsNotFoundException("ユーザー名もしくはパスワードに誤りがあります。");
		}
		
        Collection<GrantedAuthority> authorityList = new ArrayList<>();
        authorityList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

		// トークンを返却
		return new UsernamePasswordAuthenticationToken(user, password,authorityList);
	}

	@Override
	public boolean supports(Class<?> token) {
		return UsernamePasswordAuthenticationToken.class.isAssignableFrom(token);
	}
}
```

成功時はレスポンスとしてステータス：OK(200) ＆ JSESSIONIDを返却してます。
```java:SuccessHandler.java
public class SuccessHandler implements  AuthenticationSuccessHandler {
	
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
    	response.setStatus(HttpStatus.OK.value());
    }
}
```
失敗時はステータス：UNAUTHORIZED（403）を返却しています。
```java:FailureHandler.java
public class FailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
    	response.setStatus(HttpStatus.UNAUTHORIZED.value());
    }
}
```