React×Spring Bootで認証・認可を実装した話
要約
この記事では、ReactとSpring Bootを使った認証・認可の設定方法について解説します。特に、フロントエンドで認証後にユーザー情報をバックエンドから受け取り、特定のページにアクセスできるようにする方法を説明します。
環境
今回使用した環境は以下の通りです。
・React: 18.3.1
・React Router DOM: 6.24.1
・Spring Boot: 3.3.1
・Spring Security: 6.3.1
知識のおさらい
認証(Authentication)とは?
認証は、ユーザーが誰であるかを確認するプロセスです。通常、ユーザー名とパスワードを使用してログインし、その資格情報を検証することで認証が行われます。
認可(Authorization)とは?
認可は、認証されたユーザーが特定のリソースや操作にアクセスできるかどうかを決定するプロセスです。認可は、ユーザーが特定の役割(例:管理者、一般ユーザー)を持っているかどうかに基づいて行われます。
現状の課題
現在、/loginページが存在し、/adminには認証ガードが設定されています。しかし、バックエンドで認証されたユーザー情報がフロントエンドに返されないため、/adminページへの遷移がうまくいっていません。
やりたいこと
フロントエンドで/adminにアクセスする際、/loginでユーザー認証を行った後に/adminページに遷移できるように、認可の設定を行います。また、認証されたユーザー情報をバックエンドからフロントエンドに返す必要があります。
実装方法
以下は、ReactとSpring Bootで認証・認可を実装するための具体的な手順です。
React側の設定
・App.tsx
import React from "react";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import Admin from "./components/Admin/Admin";
import Home from "./components/Home/Home";
import Login from "./components/Login/Login";
import PrivateRoute from "./components/PrivateRoute"; // 作成したPrivateRoute.tsx
const App: React.FC = () => {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route
path="/admin"
element={
<PrivateRoute>
<Admin />
</PrivateRoute>
}
/>
</Routes>
</Router>
);
};
export default App;
・PrivateRoute.tsx
import React from "react";
import { Navigate } from "react-router-dom";
const PrivateRoute: React.FC<{ children: JSX.Element }> = ({ children }) => {
const isAuthenticated = localStorage.getItem("isAuthenticated") === "true";
return isAuthenticated ? children : <Navigate to="/" />;
};
export default PrivateRoute;
2.Spring Boot側の設定
package com.sample;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors
.configurationSource(request -> {
var corsConfiguration = new org.springframework.web.cors.CorsConfiguration();
corsConfiguration.setAllowedOrigins(List.of("http://localhost:3000"));
corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowedHeaders(List.of("*"));
return corsConfiguration;
}))
.csrf(csrf -> csrf
.csrfTokenRepository(csrfTokenRepository())
.ignoringRequestMatchers("/csrf")
.ignoringRequestMatchers("/h2-console/**"))
.formLogin(form -> form
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler())
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.permitAll())
.logout(logout -> logout
.logoutSuccessUrl("/")
.permitAll())
.authorizeHttpRequests(authz -> authz
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.requestMatchers("/", "/api/user", "/csrf").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated())
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.sameOrigin()));
return http.build();
}
@Bean
public CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setParameterName("_csrf");
repository.setHeaderName("X-CSRF-TOKEN");
return repository;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return (request, response, authentication) -> {
HttpSession session = request.getSession(false);
if (session != null) {
CsrfToken csrfToken = (CsrfToken) session.getAttribute(CsrfToken.class.getName());
if (csrfToken != null) {
logger.info("CSRF Token from session: {}", csrfToken.getToken());
} else {
logger.warn("No CSRF Token found in session");
}
} else {
logger.warn("No session found");
}
// ユーザー情報を含むJSONレスポンスを返す
String username = authentication.getName();
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
response.getWriter().write("{\"status\": \"success\", \"username\": \"" + username + "\"}");
response.getWriter().flush();
};
}
}
説明
上記の設定では、Spring Securityの標準的なログインフォームを使用し、認証成功時にauthenticationSuccessHandler()メソッドが呼び出されるようにしています。このメソッドは、認証されたユーザーの情報を含むJSONレスポンスを返します。
このユーザー情報をフロントエンドで受け取り、セッションに保存することで、認証後に特定のページにアクセスできるようになります。具体的には、PrivateRouteコンポーネントがセッション情報を確認し、認証済みの場合にのみ子コンポーネントを表示するようになっています。
まとめ
ReactとSpring Bootを使った認証・認可の設定方法について説明しました。この記事を参考にすることで、フロントエンドとバックエンドの認証・認可の連携をスムーズに実装できるようになります。是非試してみてください。
この記事が気に入ったらサポートをしてみませんか?