最近よく聞く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を発行し返却
③認証失敗時はログイン画面へリダイレクト
④ホーム画面ではヘッダーとメインコンテンツを分けて表示
画面イメージ
◇ログイン画面
◇ホーム画面
page1,page2で画面を切り替えることができます。
Reactファイル構成
重要なファイル
多いですが、以下のファイルです。
◇ルータ
・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は1つでid="root"
の部分を動的に変更して画面を表示します。
<!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に呼び出してます。
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
を呼び出してます。
import {Routers} from './Routers'
function App() {
return (
<div className="App">
<Routers />
</div>
);
}
export default App;
各画面へのルートを作成しています。
① '/'
Login画面
②'/auth'
でログイン処理
③ログイン処理が成功すれば'/main'
へ遷移
④入れ子になっている部分はホーム画面で解説
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>
);
}
ユーザ名、パスワードを入力後送信ボタン押下で検証。
照合できればホーム画面へ。
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を使用してレンダーの結果が画面に反映された後に動作するようにします。
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で行っております。
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に表示されます。
import { Outlet } from 'react-router-dom';
import { Header } from "../component/Header";
export const Main = () => {
return (
<div>
<Header />
<Outlet />
</div>
);
}
コンポーネント
Link
でRouters.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>
);
}
export const Page1 = () => {
return (
<div>
Page1
</div>
);
}
export const Page2 = () => {
return (
<div>
Page2
</div>
);
}
追記
React側ソースコード漏れがあり追加いたしました。
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ファイル構成
SpringBootソースコード
Springの設定ファイル。ログイン時セキュリティを動かすURL、成功、失敗処理を記述しています。
ローカルで動かす場合は必要ない記述も入ってます。
(時間があるときにサーバから画面にJSまるごと送る方法も書きます。)
@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"
にアクセスがあるとこのフィルターを通ります
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'でログイン成功とみなしてます。
@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を返却してます。
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)を返却しています。
public class FailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
}